Stop an export, a load or a save that your app no longer needs, or give it a deadline, by cancelling the Swift task that runs it.
Exports, loads, downloads and saves can keep the engine busy long after you start them. When a user closes a screen or picks another file, your app no longer needs that work. You cancel a call by cancelling the Swift task that awaits it. The engine then ends the call, stops its work and frees what it holds.
This guide covers how to cancel a call, how to start the next call and which calls you can cancel. It also shows how to give a call a deadline, how to stop a video export from its progress and what the engine keeps after a cancel.
Cancelling a Call#
Start the call in a Task and keep the task. Call cancel() on the task to stop the call. Here we start a PNG
export of a page that holds an image block, and cancel the export at once.
let exportTask = Task { try await engine.block.export(page, mimeType: .png)}exportTask.cancel()
do { let blob = try await exportTask.value print("The export ended before the cancel with \(blob.count) bytes")} catch is CancellationError { print("The export stopped")}The task throws after the cancel, so the code after the call does not run. A cancelled call throws CancellationError,
and the loop over an export stream, such as a video or audio export, ends without an error. The example catches
CancellationError and lets every other error through.
The task in the example has not started when cancel() runs. It still starts the export, which then ends at once.
Check Task.isCancelled before a call when no work must start. A task that has already finished ignores the
cancel, and value returns its result.
You can call cancel() from any thread, because the cancel reaches the engine on the main actor by itself. A
SwiftUI .task modifier cancels its task when the view goes away, so the calls that the task awaits stop with it.
Starting the Next Call#
After the task throws, the engine is free for the next export or load. Start it after the catch or in a new task.
Here we export the same page again, this time without a cancel.
let blob = try await engine.block.export(page, mimeType: .png)print("The next export made a PNG file of \(blob.count) bytes")Each image or PDF export runs on a worker engine of its own. The cancel releases the worker of the cancelled export, and the next export starts a new worker.
Calls That Can Be Cancelled#
Each of these calls stops its engine work when you cancel the task that awaits it.
- Exports:
engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)for images and PDF,engine.block.export(_:to:mimeType:options:onPreExport:uriResolver:onProgress:)for a PDF file,engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:onData:)for PDF chunks,engine.block.exportWithColorMask(_:mimeType:maskColorR:maskColorG:maskColorB:options:onPreExport:uriResolver:),engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:),engine.block.exportAudio(_:mimeType:options:), and their overloads that export several blocks - Scene and block loads:
engine.scene.load(from:overrideEditorConfig:waitForResources:onProgress:)with a URL,engine.scene.load(from:overrideEditorConfig:waitForResources:)with a string andwaitForResources,engine.scene.loadArchive(from:overrideEditorConfig:waitForResources:onProgress:),engine.scene.applyTemplate(from:)with a URL,engine.block.load(from:),engine.block.loadArchive(from:) - New scenes from media:
engine.scene.create(fromImage:dpi:pixelScaleFactor:sceneLayout:),engine.scene.create(fromVideo:) - Downloads for a block or an asset:
engine.block.createCaptionsFromURI(_:),engine.block.addImageFileURIToSourceSet(_:property:uri:),engine.block.addVideoFileURIToSourceSet(_:property:uri:),engine.block.forceLoadAVResource(_:),engine.block.forceLoadResources(_:),engine.asset.addLocalAssetSourceFromJSON(_:matcher:)with a URL,engine.asset.apply(sourceID:assetResult:),engine.asset.apply(sourceID:assetResult:placement:),engine.asset.defaultApplyAsset(assetResult:),engine.asset.defaultApplyAsset(assetResult:placement:) - Lookups:
engine.editor.getMIMEType(url:),engine.editor.getFontMetrics(fontFileURI:),engine.block.getDominantColors(_:options:) - Color profiles:
engine.scene.setCMYKProfile(from:)with a URL,engine.editor.loadCMYKProfile() - Saves:
engine.scene.saveToArchive(options:),engine.block.saveToArchive(blocks:), and the saves to a stringengine.scene.saveToString(allowedResourceSchemes:onDisallowedResourceScheme:),engine.scene.saveToString(options:)andengine.block.saveToString(blocks:allowedResourceSchemes:onDisallowedResourceScheme:)while they wait foronDisallowedResourceScheme
Giving a Call a Deadline#
No engine call takes a deadline. Run the call and a Task.sleep(nanoseconds:) in a task group, take the result
that comes first, and cancel the other task. Here we give the load of the scene file at sceneURL ten
seconds.
let loadedInTime = try await withThrowingTaskGroup(of: Bool.self) { group in group.addTask { try await engine.scene.load(from: sceneURL) return true } group.addTask { try await Task.sleep(nanoseconds: 10 * NSEC_PER_SEC) return false } let first = try await group.next() ?? false group.cancelAll() return first}print(loadedInTime ? "The scene loaded in time" : "The scene did not load within 10 seconds")When the sleep ends first, cancelAll() cancels the load, and the current scene stays. When the load ends first,
cancelAll() ends the sleep. A load that fails before the deadline throws its error from group.next(), and the
task group throws it again. From iOS 16, Mac Catalyst 16 and macOS 13 on, Task.sleep(for: .seconds(10)) gives the
same deadline.
Cancelling a Video Export From Its Progress#
engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:) returns an AsyncThrowingStream of
VideoExport events. Leave the loop over the stream to stop the export. A return or a break ends the
iteration, and the export stops and releases its worker engine. Here we stop a video export that has rendered less
than half of its frames after one minute.
@MainActorprivate func exportVideoUnlessSlow(engine: Engine, page: DesignBlockID) async throws -> Blob? { let startedAt = Date() for try await event in try await engine.block.exportVideo(page, mimeType: .mp4) { switch event { case let .progress(renderedFrames, _, totalFrames): if Date().timeIntervalSince(startedAt) > 60, renderedFrames < totalFrames / 2 { return nil } case let .finished(video: video): return video } } return nil}A cancel of the task that runs the loop stops the export too. The loop then ends without an error, so the function
returns nil. The first reports also count the start-up time of the export, so a projection of the total time from
them would stop fast exports too. engine.block.exportAudio(_:mimeType:options:) returns a stream of
AudioExport events, and leaving its loop stops it in the same way.
What a Cancel Stops and What It Keeps#
A cancel stops the work of the call, but the engine keeps what other blocks and calls still need:
- A video export stops at once and releases its worker engine with its memory.
- An image or PDF export releases its worker engine.
- An audio export that mixes a page stops at once and drops its mix.
- An audio export that copies the audio of one block without the mixer finishes the copy in the background and drops it.
- A load stops its download, unless a block or another call needs the same file. The engine reads every video fill at each update, so the download of a video fill goes on.
load(from:)andloadArchive(from:)keep the current scene. WithwaitForResourcesthe new scene replaces it before the wait for its resources, so a cancel during that wait keeps the new scene.load(from:)with a string andwaitForResourcesloads the scene at once, so a cancel ends only the wait.applyTemplate(from:),create(fromImage:dpi:pixelScaleFactor:sceneLayout:)andcreate(fromVideo:)keep the current scene.loadCMYKProfile()ends only the wait, because the document keeps its profile loaded.- A cancelled
setCMYKProfile(from:)gives back its claim, so an assignment that it replaced still takes effect. - An answer of
onDisallowedResourceSchemeafter the cancel changes nothing. apply(sourceID:assetResult:)stops the download that the default apply waits for, so it creates no block. An asset source with its own apply function keeps running in your code. A block that it creates stays, and the call does not return it.getDominantColors(_:options:)waits while a resource of the block loads, so a cancel ends that wait.
Troubleshooting#
- The task returns a result although you cancelled it: The call ended before the cancel. A task that has
finished ignores
cancel(). - The call starts although you cancelled the task first: A cancelled task still starts its call, which then
ends at once. Check
Task.isCancelledbefore the call. - The download of a cancelled load goes on: Another block or another call needs the same file.
- A video export goes on after you release the engine: The export runs on a worker engine of its own. Cancel the task that runs the loop over the stream, or leave the loop.
API Reference#
These are the methods this guide calls. The section Calls That Can Be Cancelled lists every method that stops when you cancel its task.
| Method | Category | Purpose |
|---|---|---|
engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:) |
Block | Export a block as an image or a PDF |
engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:) |
Block | Export a page as a video |
engine.scene.load(from:overrideEditorConfig:waitForResources:onProgress:) |
Scene | Load a scene or an archive from a URL |
Next Steps#
- Export Overview - Export designs and videos
- Resources - Learn how the engine loads files
- Scenes - Load and save scenes
- Error Catalog - Look up engine error codes