如何在 iOS 應用程式中加入 Nitrakit 影片編輯器(UIKit 和 SwiftUI)
Add a video editor to your iOS app (UIKit and SwiftUI)
Nitrakit iOS 影片編輯 SDK 可加入 UIKit 或 SwiftUI 應用程式,透過共用的 Editor 專案連接預覽與時間軸視圖。SDK 最低支援 iOS 16 和 macOS 13,並可透過 Swift package 整合。macOS 版不提供字體、顏色或濾鏡選擇器,相關介面需由應用程式提供。
If you ship a Swift app, you already know where Apple's help stops.
UIVideoEditorController trims a clip. AVFoundation will compose, mix and encode whatever you describe to it. Neither
gives your users a timeline to drag clips on, a text tool, or music they can line up with a cut. This post walks through adding Nitrakit, an iOS video editor SDK, to a UIKit or SwiftUI app, and what changes when the same code runs on macOS. Every snippet is taken from the
iOS guide.
Where AVFoundation ends and the editor begins
Apple's frameworks are good at the media work: reading assets, composing tracks, exporting a file. What they leave to you is the product your users touch. A scrubbing timeline with snapping. Text they can pinch to size. Undo that covers every gesture. A preview that shows exactly what the export will contain. That surface is where in-house editors spend their time, and we've laid out the trade-off for decision-makers in Build or buy an in-app video editor.
Nitrakit fills that gap with two UIViews and a project object. You keep AVFoundation for whatever else your app already does with it, such as recording or playback elsewhere.
Add the iOS video editor SDK as a Swift package
There's nothing to sign up for before you install. In Xcode, open File → Add Package Dependencies, paste https://github.com/Nitrakit/nitrakit-swift, and tick both products for your app target: Nitrakit and NitrakitUI. Teams that manage dependencies in a manifest add this instead:
dependencies: [.package(url: "https://github.com/Nitrakit/nitrakit-swift", from: "0.2.0")],
// in your target:
.product(name: "NitrakitUI", package: "nitrakit-swift"),
The floor is iOS 16 or macOS 13. Builds and test runs need no key.
One Editor per project
Everything the user does lands in an Editor: the clips, the text, the undo stack. Give it the canvas size and frame rate you want to publish at, then place the first clip:
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))
Note the assetId. It's whatever key your backend or local store already uses for that file, and it's the only way the project names its media. That small choice is why a project can later reopen on a different device.
UIKit: two views, your layout
The editor arrives as a pair. NitrakitPreviewView shows the video and lets people grab and move layers on it; NitrakitTimelineView holds the timeline, the tool bar and its panels. Point both at the same Editor:
import NitrakitUI
let preview = NitrakitPreviewView(frame: .zero)
preview.editor = editor
let dock = NitrakitTimelineView(frame: .zero)
dock.editor = editor
From there they behave like any view you constrain in Auto Layout or a stack view. A common arrangement puts the preview above a dock roughly 400 points high. Build them in code, not in Interface Builder.
Splitting the editor this way pays off when a screen needs less than everything. A caption fixer could show the preview alone with your own transport buttons. An app with a toolbar of its own might show just the timeline. The layouts page shows each variant.
SwiftUI: wrap once, then compose
Both views are UIKit views, so SwiftUI needs a thin wrapper, written once. Each one goes into a UIViewRepresentable, and a plain SwiftUI View stacks them:
import SwiftUI
import Nitrakit
import NitrakitUI
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) {}
}
struct EditorDock: UIViewRepresentable {
let editor: Editor
var onRequest: (@MainActor (EditorRequest) async -> RequestResponse?)?
func makeUIView(context: Context) -> NitrakitTimelineView {
let view = NitrakitTimelineView(frame: .zero)
view.editor = editor
view.onRequest = onRequest
return view
}
func updateUIView(_ view: NitrakitTimelineView, context: Context) {}
}
struct EditorScreen: View {
let editor: Editor
var body: some View {
VStack(spacing: 0) {
EditorPreview(editor: editor)
EditorDock(editor: editor, onRequest: { request in nil }) // your pickers: step 5
.frame(height: 400)
}
.background(Color.black)
}
}
The one SwiftUI trap is identity. body runs often, so the Editor must live outside it: in your observable model, or in @State that you fill on appear. Push EditorScreen from a NavigationStack, present it as a sheet, or put it in a tab; it's an ordinary view to the rest of your hierarchy.
The same editor on the Mac
The package also builds for macOS 13 and later, with AppKit views. In a SwiftUI Mac target, swap UIViewRepresentable for NSViewRepresentable and keep the rest of the structure. One
difference to plan for: on macOS the SDK ships no built-in choosers for fonts, colours or filters, so your app supplies every one of those screens. In return, a Mac companion app can share the editing feature, and its projects, with your iOS code.
Photos access and your media library
Nitrakit never asks the user for anything by itself. It doesn't present the photo library, a document browser, or a font list unprompted. When someone taps Add media, the timeline view
calls your onRequest closure, and your app decides what appears. That might be [PhotosUI (https://developer.apple.com/documentation/photosui), your own catalogue of stock clips, or a list of the user's uploads from your server:
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
}
}
Because the closure is async and isolated to the main actor, presenting a sheet and awaiting the choice reads top to bottom, with no delegate dance. Returning nil hands fonts, colours, text styles and filters to the SDK's stock screens on iPhone and iPad, so a first build only has to wire up media. The full request list is on the
Pickers page.
On permissions: if your flow reads from the user's photo library, your Info.plist needs an NSPhotoLibraryUsageDescription string, as it would without Nitrakit. The SDK itself requests nothing.
Drafts that follow the user
A project serialises to JSON that refers to media by your ids, so your app decides where drafts live: on the device, in iCloud, or on your own backend.
let json = editor.toJson()
let reopened = try Editor(open: json)
reopened.loadMedia() // from the device, else through NitrakitMedia.resolve
Start a reel on the iPhone, finish it on the iPad. When a draft opens somewhere its files aren't, the SDK calls one async function you set at launch, and you answer with a URL (signed or not). Clips show download progress while they arrive:
NitrakitMedia.resolve = { id in
.remote(try await api.signedURL(for: id)) // or .file(localURL)
}
For autosave, register a change listener on the editor. Missing files and uploads in flight are covered in [Projects and media (https://docs.nitrakit.dev/concepts/projects-and-media/).
Rendering the final MP4
Rendering happens off the main thread, and the timeline stays usable while it runs. You pass a path and a listener:
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()
Callbacks arrive on the export thread, hence the hop to the main queue. Once .done comes back, the file is yours: hand it to PHPhotoLibrary, a share sheet, or your upload queue. The Export page lists the settings.
Users who speak on camera can also get captions timed word by word, from the speech-to-text service your app already uses. The text and captions guide shows how.
Match your app, including dark mode
Designers will want the editor to feel native to your product, not to ours. The theme is set on the dock:
dock.theme = EditorTheme(
background: "#101418",
bar: "#0b0e11",
accent: "#2ee6a6",
font: "Brand Sans",
clips: .init(video: "#3355ff", audio: "#18a058")
)
The editor doesn't watch the system appearance on its own. Read the trait collection when it changes and assign the light or dark theme you've defined; that keeps the decision with your design system. Tools can be hidden or added, icons replaced and labels translated, all at run time. The [Customisation docs (https://docs.nitrakit.dev/customization/) cover each.
Before you submit to App Review
Two things to sort out before a build goes to TestFlight for real users.
The license key. Development and testing run without one. For the release, the key is set once at launch, for example in the app delegate:
import NitrakitUI
let status = NitrakitLicense.set("nk2.…")
if !status.problems.isEmpty { print("Nitrakit:", status.problems) }
It's tied to your bundle IDs, which is why it isn't a secret and can sit in source control. If a bundle ID doesn't match, problems tells you. See the License key page.
Download size. The SDK adds one dynamic framework, and App Store thinning ships only the device slice. The App size page has the current figures and explains how App Store encryption affects them.
Where to go from here
The fastest check is to paste EditorScreen into a scratch SwiftUI project and point it at a clip on the simulator. If you'd rather click before you compile, the browser demo runs the same editor. Ready to wire it into your real app? The iOS guide goes step by step, and the
iOS overview sums it up for whoever signs off. Your Android team gets the same editor from the Android guide, and a React Native app can share one codebase: [see that post (https://nitrakit.dev/blog/react-native-video-editor/).
Shipping date in sight? Talk to us about a key. Stuck on something in Xcode? The Nitrakit Discord is where we answer.
來源:DEV Community · dev.to