VideoEditorKit is a package-first iOS video editor built with SwiftUI,
Observation, AVFoundation, PhotosUI, Core Image, and Swift Testing.
It provides a ready-to-present editor for local videos, plus a small set of public configuration, persistence, transcription, canvas, and export APIs.
- iOS 18.6+ app target
- Swift 6
- Xcode with Swift Package Manager
- iPhone UI today; iPad support is not the current contract
- Local video editing from a file URL or async import resolver
- Trim, playback speed, crop presets, canvas zoom/pan, explicit rotation, mirror
- One recorded audio track mixed with the source audio
- Brightness, contrast, and saturation adjustments
- Frame/background styling
- Optional transcript generation and editable caption overlays
- Manual save that creates an edited copy while preserving the original video
- Saved preview metadata and thumbnail generated from the rendered saved copy
- Save button stays available after load; without unsaved changes it only closes the editor
- Export to
.mp4withoriginal,low,medium, andhighquality choices - Optional host-configured image watermark applied only during export
- Reusable export-quality sheet for list and share flows outside the editor
| Editor | Crop |
|---|---|
| Presets | Adjusts |
|---|---|
| Audio | Speed | Export |
|---|---|---|
Add the package in Xcode:
https://github.com/didisouzacosta/VideoEditorKit.git
Or add it to Package.swift:
.package(
url: "https://github.com/didisouzacosta/VideoEditorKit.git",
branch: "main"
)Then link the VideoEditorKit product to your app target.
import SwiftUI
import VideoEditorKit
struct EditorHostView: View {
@State private var configuration = VideoEditingConfiguration.initial
@State private var savedEditedVideoURL: URL?
@State private var exportedVideoURL: URL?
let sourceVideoURL: URL
var body: some View {
VideoEditorView(
"Editor",
sourceVideoURL: sourceVideoURL,
editingConfiguration: configuration,
configuration: .allToolsEnabled,
onSavedVideo: { savedVideo in
configuration = savedVideo.editingConfiguration
savedEditedVideoURL = savedVideo.url
},
onExportedVideoURL: { url in
exportedVideoURL = url
}
)
}
}Use this prompt when asking an AI coding assistant to integrate
VideoEditorKit into an iOS host app:
Integrate VideoEditorKit into this iOS SwiftUI app.
Requirements:
- Add the Swift Package dependency from https://github.com/didisouzacosta/VideoEditorKit.git.
- Present VideoEditorView from a local playable source video URL.
- Keep three files separate: the original source video, the saved edited copy,
and any exported/share output.
- Store SavedVideo.url, SavedVideo.originalVideoURL,
SavedVideo.editingConfiguration, SavedVideo.thumbnailData, and
SavedVideo.metadata when onSavedVideo is called.
- Reopen saved projects with VideoEditorSession using the original source URL,
the saved VideoEditingConfiguration, the saved ExportedVideo metadata as
preparedOriginalExportVideo, and the same saved configuration as
preparedOriginalExportEditingConfiguration.
- Use onExportedVideoURL only for explicit export/share flows.
- Do not implement or call onSaveStateChanged; use only onSavedVideo for manual
save persistence.
- Do not pass onDismissed unless the host needs a close event. If it is passed,
treat it only as dismissal notification and never as editing state.
- Do not overwrite the original video with saved or exported files.
- Keep .original export available.
- If source preparation is asynchronous, use VideoEditorSession.Source.importedFile
and resolve it to a local file URL before editing.
Expected SwiftUI shape:
- A host view owns the selected source URL and optional saved project state.
- VideoEditorView receives the latest VideoEditingConfiguration.
- onSavedVideo persists the edited copy, thumbnail, metadata, and configuration.
- onExportedVideoURL starts the app's share/export UI with the exported URL.
- Existing saved-project list thumbnails should come from SavedVideo.thumbnailData
or from the saved edited copy, not from the original source video.
- Pass a local playable file URL into the editor.
- Keep the original video, saved edited copy, and exported output as separate files.
- Persist
SavedVideo.urlandSavedVideo.editingConfigurationafteronSavedVideo. - Use
onDismissedonly as a close event; it does not publish editing state. - Use
onExportedVideoURLonly for explicit export/share output. - Do not overwrite the original source video with a saved or exported video.
- Do not block
.original; original export is always available.
The save button is available once the editor content is loaded. If the current
edit has unsaved changes, tapping save renders a new edited copy and then calls
onSavedVideo. If there are no unsaved changes, tapping save only dismisses the
editor and does not emit another SavedVideo.
SavedVideo contains:
url: the rendered edited copyoriginalVideoURL: the preserved source videoeditingConfiguration: the snapshot that produced the saved copythumbnailData: a JPEG thumbnail generated from the saved copymetadata:ExportedVideometadata loaded from the saved copy
Manual save renders with the native save profile. When a canvas/crop preset, social destination, or freeform crop is active, the saved video uses that canvas size, normalized to even pixels for encoder compatibility. With the original canvas, the saved video keeps the source presentation size. The saved preview metadata and thumbnail are loaded from the rendered copy, so project lists can display the same positioning and preset framing that the editor saved.
onSaveStateChanged and VideoEditorSaveState are no longer part of the public
save contract. Hosts should rely on onSavedVideo for persisted edits.
onDismissed is optional and should only be provided when the host needs to
react to closure.
Use VideoEditorSession when reopening saved projects or resolving the source
asynchronously:
let session = VideoEditorSession(
source: .fileURL(originalVideoURL),
editingConfiguration: savedConfiguration,
preparedOriginalExportVideo: savedEditedVideoMetadata,
preparedOriginalExportEditingConfiguration: savedConfiguration
)Use .importedFile only when a local URL must be produced asynchronously before
the editor can load the video. During that bootstrap, the editor shows the
loader only; it does not briefly display the navigation title before content is
loaded.
Use videoExportSheet when a host screen needs the same export-quality picker
outside VideoEditorView, such as a saved-video list share button. The modifier
uses VideoEditorConfiguration.exportQualities, including blocked qualities,
then renders the selected quality before returning an ExportedVideo.
@State private var exportingProject: Project?
@State private var sharedVideoURL: URL?
var body: some View {
ProjectsList(
onShare: { project in
exportingProject = project
}
)
.videoExportSheet(
item: $exportingProject,
configuration: .allToolsEnabled,
request: { project in
VideoExportSheetRequest(
id: project.id.uuidString,
sourceVideoURL: project.originalVideoURL,
editingConfiguration: project.editingConfiguration ?? .initial,
preparedOriginalExportVideo: project.preparedOriginalExportVideo
)
},
onExported: { exportedVideo, _ in
sharedVideoURL = exportedVideo.url
}
)
}Configure a host-owned image watermark through VideoEditorConfiguration:
let logoImage = UIImage(named: "Logo")
let configuration = VideoEditorConfiguration(
watermark: logoImage.map {
VideoWatermarkConfiguration(
image: $0,
position: .bottomTrailing
)
}
)Watermarks are export-only. They are not shown in preview, are not included in
manual saves, and are not persisted in VideoEditingConfiguration. Pass
watermark: nil to disable watermarking, such as for paid plans. The image is
rendered at its UIImage.size without scaling; if it is larger than the export
frame, it remains anchored to the selected corner and may be clipped.
Transcription is optional. Enable it by passing a transcription configuration:
let configuration = VideoEditorConfiguration(
tools: ToolAvailability.enabled(ToolEnum.all),
exportQualities: ExportQualityAvailability.allEnabled,
transcription: .openAIWhisper(
apiKey: resolvedAPIKey(),
preferredLocale: "en"
)
)For custom backends, implement VideoTranscriptionProvider and return ordered
segments with word timings whenever possible.
- Editor entry:
VideoEditorView,VideoEditorSession,VideoEditorCallbacks - Host policy:
VideoEditorConfiguration,ToolAvailability,ExportQualityAvailability - Persistence:
VideoEditingConfiguration,SavedVideo - Export:
VideoQuality,ExportedVideo,VideoExportSheetRequest - Transcription:
VideoTranscriptionProvider,Transcript*,Transcription* - Canvas/crop:
VideoCanvas*,VideoCrop*
See also:
- Docs/FEATURES.md
- Docs/ARCHITECTURE.md
- Docs/VALIDATION.md
- Sources/VideoEditorKit/VideoEditorKit.docc/VideoEditorKit.md
Package.swift
Sources/VideoEditorKit/
Tests/VideoEditorKitTests/
Example/VideoEditor/
Example/VideoEditorTests/
Example/VideoEditor.xcworkspace
Use iOS Simulator validation:
scripts/format-swift.sh
scripts/test-ios.shTargeted validation:
xcodebuild \
-workspace Example/VideoEditor.xcworkspace \
-scheme VideoEditorKit-Package \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 17' \
test
xcodebuild \
-workspace Example/VideoEditor.xcworkspace \
-scheme VideoEditor \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 17' \
test