iOS and macOS
By the end of this page your app shows the editor on a video, lets users add their own media through your picker, and exports the result.
Requirements
- iOS 16 or later, or macOS 13 or later.
- UIKit or AppKit. SwiftUI works through a small wrapper (below).
Install
The SDK has two modules: Nitrakit (projects, export, license, languages) and NitrakitUI
(the views).
If users pick from their photo library, add NSPhotoLibraryUsageDescription to your
Info.plist.
1. Install your license key Stable
The key unlocks full-length, watermark-free exports for your bundle ID. Install it once, early, for example in your app delegate. Without a key everything still works, so you can skip this while trying the SDK.
import Nitrakit
let status = setLicense(key: "nk1.…")
if !status.problems.isEmpty { print("Nitrakit:", status.problems) }
See License key.
2. Create a project Stable
An Editor holds one project and its undo history. Create it with your output size and frame
rate, then add media: adding a file makes it a source; placing the source on a track makes
it a clip the user sees in the timeline.
let editor = Editor(width: 1080, height: 1920, fpsNum: 30, fpsDen: 1)
let media = try editor.addMedia(id: assetId, file: pickedFileURL)
let track = try editor.apply(edit: .addTrack(kind: .visual, magnetic: true))
_ = try editor.apply(edit: .insertMedia(
track: track, source: media.source,
startUs: 0, sourceInUs: 0, durationUs: media.durationUs))
assetId is your app's id for the file, for example its key in your storage: the project
refers to media only by these ids. The SDK keeps its own copy of the file, so editing starts
right away. Times are microseconds.
3. Tell the SDK where media lives Stable
When a project is opened on a device that doesn't have its media yet, the SDK asks your app where each media id lives. Return a URL (it can be signed) and the SDK downloads it, showing the progress on the clips:
NitrakitMedia.resolve = { id in
.remote(try await api.signedURL(for: id)) // or .file(localURL)
}
Set it once, at start-up. Skip it if your projects never leave the device. See Projects and media.
4. Show the editor Stable
Two views make up the editor: NitrakitPreviewView plays the project and lets the user move
and resize layers; NitrakitTimelineView has the timeline, tools and panels. Give both the
same editor; they stay in sync with each other and with your code.
import NitrakitUI
let preview = NitrakitPreviewView(frame: .zero)
preview.editor = editor
let dock = NitrakitTimelineView(frame: .zero)
dock.editor = editor
Lay them out like any view: typically the preview on top and the editor (about 400 pt tall) below. Each starts drawing once it has a size. Create them in code; they don't load from storyboards.
SwiftUI: wrap each view in a UIViewRepresentable:
struct EditorPreview: UIViewRepresentable {
let editor: Editor
func makeUIView(context: Context) -> NitrakitPreviewView {
let view = NitrakitPreviewView(frame: .zero)
view.editor = editor
return view
}
func updateUIView(_ view: NitrakitPreviewView, context: Context) {}
}
5. Connect your pickers Stable
The editor never opens the photo library or a file browser itself. When the user taps
Add media, picks a font or a colour, it asks your app through onRequest, so these
screens look like the rest of your app and media can come from wherever you keep it.
Return the answer from your picker, .cancel if the user backed out, or nil to let the SDK
show its built-in picker:
dock.onRequest = { request in
switch request.kind {
case .addMedia, .addOverlay, .addAudio, .addSticker:
guard let file = await presentMediaPicker() else { return .cancel } // your picker
let id = startUpload(file) // your storage
let media = try? editor.addMedia(id: id, file: file)
return media.map { .source(id: $0.source) } ?? .cancel
default:
return nil // built-in picker
}
}
The closure is async and runs on the main actor, so you can present a picker and await
its result. The built-in pickers cover fonts, colours, text, text styles and filters on iOS;
adding media always needs yours. On macOS there are no built-in pickers.
See Your pickers for every request and its answer.
6. Save the project Stable
Projects save as JSON, with media referred to by your ids. Store it on the device or sync it to your backend, and reopen it anywhere:
let json = editor.toJson()
let reopened = try Editor(open: json)
reopened.loadMedia() // from the device, else through NitrakitMedia.resolve
To save as the user works, set a listener; it's called after every change. While your app
uploads a file, show the progress on its clips with editor.setMediaProgress(id:fraction:).
7. Export Stable
Export renders the project to an MP4 in the background while the user keeps editing. Progress and the result arrive on the export thread:
final class Progress: ExportListener {
func progress(fraction: Float, frames: UInt64, total: UInt64) {
DispatchQueue.main.async { /* update your progress bar */ }
}
func finished(result: ExportResult, message: String) {
// .done: the file is at your path. .cancelled or .failed: no file (message says why).
}
}
let job = editor.export(path: outputPath,
settings: editor.exportSettingsFor(shortSide: 1080),
listener: Progress())
// job.cancel()
See Export.
Playback
Control playback from your own buttons through the preview's player:
preview.player?.play()
preview.player?.pause()
preview.player?.seek(timeUs: 2_000_000)
preview.onPosition = { us in /* main thread */ }
The preview pauses itself when it leaves the window.
Good to know
- Errors: calls that can fail throw
EditorError, e.g..collision(a clip would overlap another) or.media(message:)(a file can't be read). - Name clashes: the SDK's functions (
setLicense,setLocale…) are free functions. Inside a type with methods of the same name, writeNitrakit.setLicense(key:).