swiftuiuikituiviewcontrollerrepresentableuivideoeditorcontroller

How to use UIVideoEditorController in SwiftUI?


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)")
        }
    }
  }
}

Solution

  • I figured it out! It turns out the problems were:

    1. I wasn't passing in the video path correctly (I needed to use source.path, not source.absoluteString), which led to an issue where instead of saving the trimmed video, it would cancel instead.
    2. The reason it was dismissing back to the root was because I hadn't implemented the 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.