I'm trying to use UIVideoEditorController in SwiftUI, but I'm having an issue where upon tapping Save, it doesn't trigger the delegate method to be called with the edited video path, and it also dismisses all full screen covers behind the UIVideoEditorController until it's back to the root view controller. How can I resolve this?
Here's my UIViewControllerRepresentable version of the UIVideoEditorController:
struct VideoEditor: UIViewControllerRepresentable {
let source: URL
let maxSeconds: TimeInterval
let onSaved: (URL) -> Void
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> UIVideoEditorController {
let editor = UIVideoEditorController()
editor.videoPath = source.absoluteString
editor.videoMaximumDuration = maxSeconds
editor.videoQuality = .typeHigh
editor.delegate = context.coordinator
return editor
}
func updateUIViewController(_ uiViewController: UIVideoEditorController, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(videoEditor: self) }
class Coordinator: NSObject, UINavigationControllerDelegate, UIVideoEditorControllerDelegate {
private let videoEditor: VideoEditor
init(videoEditor: VideoEditor) {
self.videoEditor = videoEditor
}
func videoEditorController(
_ editor: UIVideoEditorController,
didSaveEditedVideoToPath editedVideoPath: String
) {
let url = URL(fileURLWithPath: editedVideoPath)
videoEditor.onSaved(url)
videoEditor.dismiss()
}
}
}
And here's how I use it:
struct ContentView: View {
let videoToEdit: URL
@State private var presentingFirstCover = false
@State private var presentingEditor = false
var body: some View {
VStack {
Button("Present first cover") { presentingFirstCover = true }
}
.fullScreenCover(isPresented: $presentingFirstCover) {
Button("Present editor") { presentingEditor = true }
.fullScreenCover(isPresented: $presentingEditor) {
VideoEditor(source: fullVideo, maxSeconds: 20) {
print("saved edited video to: \($0)")
}
}
}
}
I figured it out! It turns out the problems were:
source.path
, not source.absoluteString
), which led to an issue where instead of saving the trimmed video, it would cancel instead.videoEditorControllerDidCancel
method of the UIVideoEditorControllerDelegate
, where I needed to dismiss the the view controller myself. Without defining this method, the UIVideoEditorController had no way to dismiss itself other than dismissing back to the root, so that's what it did.Here's my final working solution:
import SwiftUI
struct VideoEditor: UIViewControllerRepresentable {
let source: URL
let maxSeconds: TimeInterval
let onSaved: (URL) -> Void
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> UIVideoEditorController {
let editor = UIVideoEditorController()
editor.videoPath = source.path
editor.videoMaximumDuration = maxSeconds
editor.videoQuality = .typeHigh
editor.delegate = context.coordinator
return editor
}
func updateUIViewController(_ uiViewController: UIVideoEditorController, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(videoEditor: self) }
class Coordinator: NSObject, UINavigationControllerDelegate, UIVideoEditorControllerDelegate {
private let videoEditor: VideoEditor
private var savedEditedVideo = false
init(videoEditor: VideoEditor) {
self.videoEditor = videoEditor
}
func videoEditorController(
_ editor: UIVideoEditorController,
didSaveEditedVideoToPath editedVideoPath: String
) {
defer { videoEditor.dismiss() }
// For some reason, this delegate method is always called twice.
// So, we must make sure we only call onSaved the first time this delegate method is called.
if savedEditedVideo { return }
let urlSavedTo = URL(fileURLWithPath: editedVideoPath)
videoEditor.onSaved(urlSavedTo)
savedEditedVideo = true
}
func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) {
videoEditor.dismiss()
}
func videoEditorController(
_ editor: UIVideoEditorController, didFailWithError error: any Error
) {
print("Failed to edit video \(videoEditor.source), with error \(error)")
videoEditor.dismiss()
}
}
}
Despite all this effort, I unfortunately was not able to use my solution because the quality of the exported video was not high enough, even when set to .typeHigh
(it's only like 480p).
Instead, I had to use the UIImagePickerController
to select the video directly from the camera roll and trim it, since with that view controller you are able to set videoExportPreset
to AVAssetExportPresetPassthrough
to allow it to stay in HD quality if the selected video is HD.