Presenting UIDocumentInteractionController with UIViewControllerRepresentable in SwiftUI - ios

I'm creating a new iOS app using SwiftUI where ever possible. However, I want to be able to generate a PDF with some data.
In a similar project without swiftUI I can do this
let docController = UIDocumentInteractionController.init(url: "PATH_TO_FILE")
docController.delegate = self
self.dismiss(animated: false, completion: {
docController.presentPreview(animated: true)
})
and as long as somewhere else in the view controller I have this:
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
I'm good to go.
What I can't work out is how to apply this to a UIViewControllerRepresentable and have it working in SwiftUI. Should my UIViewControllerRepresentable be aiming to be a UIViewController? How do I then set the delegate and presentPreview? Will this overlay any view and display full screen over my SwiftUI app as it does for my standard iOS app?
Thanks

Here is possible approach to integrate UIDocumentInteractionController for usage from SwiftUI view.
Full-module code. Tested with Xcode 11.2 / iOS 13.2
import SwiftUI
import UIKit
struct DocumentPreview: UIViewControllerRepresentable {
private var isActive: Binding<Bool>
private let viewController = UIViewController()
private let docController: UIDocumentInteractionController
init(_ isActive: Binding<Bool>, url: URL) {
self.isActive = isActive
self.docController = UIDocumentInteractionController(url: url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPreview>) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<DocumentPreview>) {
if self.isActive.wrappedValue && docController.delegate == nil { // to not show twice
docController.delegate = context.coordinator
self.docController.presentPreview(animated: true)
}
}
func makeCoordinator() -> Coordintor {
return Coordintor(owner: self)
}
final class Coordintor: NSObject, UIDocumentInteractionControllerDelegate { // works as delegate
let owner: DocumentPreview
init(owner: DocumentPreview) {
self.owner = owner
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return owner.viewController
}
func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
controller.delegate = nil // done, so unlink self
owner.isActive.wrappedValue = false // notify external about done
}
}
}
// Demo of possible usage
struct DemoPDFPreview: View {
#State private var showPreview = false // state activating preview
var body: some View {
VStack {
Button("Show Preview") { self.showPreview = true }
.background(DocumentPreview($showPreview, // no matter where it is, because no content
url: Bundle.main.url(forResource: "example", withExtension: "pdf")!))
}
}
}
struct DemoPDFPreview_Previews: PreviewProvider {
static var previews: some View {
DemoPDFPreview()
}
}

I ended up doing something like the following as I wasn't able to get this working reliably with UIViewControllerRepresentable and the above answer. You might need to edit / extend this for your usecase.
class DocumentController: NSObject, ObservableObject, UIDocumentInteractionControllerDelegate {
let controller = UIDocumentInteractionController()
func presentDocument(url: URL) {
controller.delegate = self
controller.url = url
controller.presentPreview(animated: true)
}
func documentInteractionControllerViewControllerForPreview(_: UIDocumentInteractionController) -> UIViewController {
return UIApplication.shared.windows.first!.rootViewController!
}
}
Usage:
struct DocumentView: View {
#StateObject var documentController = DocumentController()
var body: some View {
Button(action: {
documentController.presentDocument(url: ...)
}, label: {
Text("Show Doc")
})
}
}

Using QLPreviewController
I know the question is about UIDocumentInteractionController, but if you want to present a PDF file (for example), you can use a QLPreviewController.
Local file
Presenting a local file:
import SwiftUI
struct DocView: View {
#State private var buttonPressed: Bool = false
var body: some View {
Button {
buttonPressed = true
} label: {
Text("Show PDF file")
}
.sheet(isPresented: $buttonPressed) {
let localURL = Bundle.main.url(forResource: "Example", withExtension: "pdf")!
PreviewController(url: localURL)
}
}
}
Remote file
Please see this gist if you need to present a remote file.
PreviewController
The UIViewControllerRepresentable for QLPreviewController.
import QuickLook
import SwiftUI
struct PreviewController: UIViewControllerRepresentable {
#Environment(\.dismiss) private var dismiss
let url: URL
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done, target: context.coordinator,
action: #selector(context.coordinator.dismiss)
)
let navigationController = UINavigationController(rootViewController: controller)
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: QLPreviewControllerDataSource {
let parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(
_ controller: QLPreviewController,
previewItemAt index: Int
) -> QLPreviewItem {
return parent.url as NSURL
}
#objc func dismiss() {
parent.dismiss()
}
}
}

Related

Use YPImagePikcer in swiftUI

I'm trying to use YPImagePicker in SwiftUI how can I do it??
https://github.com/Yummypets/YPImagePicker
how can I call this function in SwiftUI?
This can be done using UIViewControllerRepresentable like so...
struct PhotoPicker: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
var config = YPImagePickerConfiguration()
YPImagePickerConfiguration.shared = config
let picker = YPImagePicker(configuration: config)
picker.delegate = context.coordinator
picker.didFinishPicking { items, cancelled in
if !cancelled {
let images: [UIImage] = items.compactMap { item in
if case .photo(let photo) = item {
return photo.image
} else {
return nil
}
}
finishedPicking(images)
}
dismiss()
}
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate {
let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
}
}
and then call it like this...
Text("SomeView")
.sheet(isPresented: $showPhotoPicker) {
PhotoPicker()
}

Swift 5 - documentPicker not called after a file was selected

I'm working on a Swift 5 project and I need to implement a document picker for which users can select a music file to be processed. The document picker is shown in the interface, but the documentPicker function is not being called after a document was selected in the UI.
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack() {
Button("Select files to sync") {
ImportMenuController().selectFile()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ImportMenuController.swift called by ContentView.swift:
// ImportMenuController.swift
import Foundation
import UIKit
import UniformTypeIdentifiers
class ImportMenuController: UIViewController {
func selectFile() {
let importMenu = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.mp3], asCopy: true)
importMenu.delegate = self
importMenu.allowsMultipleSelection = true
importMenu.shouldShowFileExtensions = true
DispatchQueue.main.async {
self.getTopMostViewController()?.present(importMenu, animated: true, completion: nil)
}
}
func getTopMostViewController() -> UIViewController? {
var topMostViewController = UIApplication.shared.keyWindow?.rootViewController
while let presentedViewController = topMostViewController?.presentedViewController {
topMostViewController = presentedViewController
}
return topMostViewController
}
}
extension ImportMenuController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL){
print("documentPicker opened")
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
print("documentPickerWasCancelled opened")
}
}
I experimented with different ways of calling the documentPicker, but unfortunately, I still didn't manage to arrive in the documentPicker or the documentPickerWasCancelled function, so the print commands are never executed. Ultimately, I need to retrieve the fileUrl for the selected file to process it further. What am I doing wrong?
Reference
Current Apple Developer Documentation
You need to retain the document-picker delegate (in this case, the instance of ImportMenuController).
This fixes it for you.
let importer = ImportMenuController()
var body: some View {
ZStack() {
Button("Select files to sync") {
importer.selectFile()
}
}

iOS14 CNContactViewController not showing delete button issue

When I press Edit from contact card, my CNContactViewController is not showing the delete option in the bottom of the screen.
NB: the button remains shown for iOS 13.
import Foundation
import ContactsUI
import SwiftUI
struct CNContactViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = CNContactViewController
var contact: Binding<CNContact>
var presentingEditContact: Binding<Bool>
func makeCoordinator() -> CNContactViewControllerRepresentable.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CNContactViewControllerRepresentable>) -> CNContactViewControllerRepresentable.UIViewControllerType {
let controller = CNContactViewController(forNewContact: contact.wrappedValue)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: CNContactViewControllerRepresentable.UIViewControllerType, context: UIViewControllerRepresentableContext<CNContactViewControllerRepresentable>) {
//
}
// Nested coordinator class, the prefered way stated in SwiftUI documentation.
class Coordinator: NSObject, CNContactViewControllerDelegate {
var parent: CNContactViewControllerRepresentable
init(_ contactDetail: CNContactViewControllerRepresentable) {
self.parent = contactDetail
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
parent.contact.wrappedValue = contact ?? parent.contact.wrappedValue
parent.presentingEditContact.wrappedValue = false
}
func contactViewController(_ viewController: CNContactViewController, shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
return true
}
}
}
.sheet(isPresented: $viewModel.presentingEditContact) {
NavigationView {
if #available(iOS 14, *) {
return AnyView(CNContactViewControllerRepresentable(contact: self.$viewModel.contact, presentingEditContact: $viewModel.presentingEditContact)
.navigationBarTitle("Edit Contact")
.edgesIgnoringSafeArea(.top))
} else {
return AnyView(CNContactViewControllerRepresentable(contact: self.$viewModel.contact, presentingEditContact: $viewModel.presentingEditContact)
.edgesIgnoringSafeArea(.top))
}
}
}

SwiftUI with UIViewControllerRepresentable

I am trying to use a UIViewController representable in a swiftUi project. Specifically I am trying to press one button (assetOne) that allows the EU to select a video and then press another button (assetTwo) and it allows the user to select another video. Then the user will have the option to merge the videos (with a third button). I assumed that I would need to use a Coordinator to accomplish this but after seeing a SO solution without it I tried to do it without one. But when I run my project the build is successful but when I click on any of the buttons from the content view I get the error message below. What am I doing wrong? Do I need a Coordinator and how do I incorporate it with my current configuration?
Warning: Attempt to present <UIImagePickerController: 0x7fa05f827600>
on <TempTest.MergeVideoViewController: 0x7fa05ed088c0> whose view is
not in the window hierarchy!
Content View:
import SwiftUI
struct ContentView: View {
let someView = ImagePicker()
var body: some View {
VStack {
Button(action: {
print("SwiftUI: assetOne button tapped")
// Call func in SomeView()
self.someView.assetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
//...
ImagePicker: UIViewControllerRepresentable
struct ImagePicker: UIViewControllerRepresentable{
let someView = MergeVideoViewController()
func makeUIViewController(context: Context) -> MergeVideoViewController {
someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
func assetOne() {
someView.loadAssetOne()
}
//...
}
My UIViewController class:
class MergeVideoViewController: UIViewController {
var firstAsset: AVAsset?
var secondAsset: AVAsset?
var audioAsset: AVAsset?
var loadingAssetOne = false
var activityMonitor: UIActivityIndicatorView!
func exportDidFinish(_ session: AVAssetExportSession) {
// Cleanup assets
activityMonitor.stopAnimating()
firstAsset = nil
secondAsset = nil
audioAsset = nil
//...
func loadAssetOne() {
// func loadAssetOne(_ sender: AnyObject) {
if savedPhotosAvailable() {
loadingAssetOne = true
VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)
}
}
//...
The ImagePicker is-a View, it should be somewhere in body.
Here is possible approach - the idea is to get controller reference back in SwiftUI and call its actions directly when needed.
struct ImagePicker: UIViewControllerRepresentable{
let configure: (MergeVideoViewController) -> ()
func makeUIViewController(context: Context) -> MergeVideoViewController {
let someView = MergeVideoViewController()
configure(someView)
return someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
}
struct ContentView: View {
#State private var controller: MergeVideoViewController?
var body: some View {
VStack {
ImagePicker {
self.controller = $0
}
Button(action: {
print("SwiftUI: assetOne button tapped")
self.controller?.loadAssetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
}
}

ReplayKit with SwiftUI

I want to record the screen with ReplayKit.
I have researched the method with UIKit. But my project used SwiftUI, so I want to use the ReplayKit to record the screen with SwiftUI.
How I record the screen with SwiftUI?
-
When I use the stopRecording function, the function will have previewViewController. But I cannot call present function to present previewViewController.
Note: This is not a very practical answer.
In most cases, "SwiftUI.View" runs on top of "UIHostingController".
You need to grab this to present the "RPPreviewViewController".
You can find one of them by following the "UIApplication".
let scene = UIApplication.shared.connectedScenes.first as! UIWindowScene
let viewController = scene.windows.last!.rootViewController
viewController.present(previewViewController, animated: true, completion:nil)
I have just open-sourced a simple ReplayKit application, which uses SwiftUI.
https://github.com/snakajima/ReplayStartUpKit
Please take a look at how it presents a RPBroadcastActivityViewController from SwiftUI.
It first stores the pointer to the controller in a property bavController, then set #Pubilshed property activePopup to initiate SwiftUI change.
RPBroadcastActivityViewController.load { controller, error in
if let controller = controller {
self.bavController = controller
controller.delegate = self
self.activePopup = .broadCast
}
}
In SwiftUI (MainUIView.swift), the following view is activated when the property activePopup becomes .broadCast.
.sheet(item: $state.activePopup) { item in
switch(item) {
case .broadCast:
BroadcastActivityController(controller: state.bavController!)
}
}
BroadcastActivityController is bit long because of a work-around for iPad, but it is just a wrapper of RPBroadcastActivityController.
struct BroadcastActivityController: UIViewControllerRepresentable {
let controller: RPBroadcastActivityViewController
func makeUIViewController(context: Context) -> RPBroadcastActivityViewController {
return controller
}
func updateUIViewController(_ uiViewController: RPBroadcastActivityViewController, context: Context) {
// Hack to work around iPad issue
if UIDevice.current.userInterfaceIdiom == .pad {
guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate,
let vc = sceneDelegate.uiWindow?.rootViewController,
let view = vc.view else {
print("somethign is really wrong")
return
}
controller.modalPresentationStyle = .popover
if let popover = controller.popoverPresentationController {
popover.sourceRect = CGRect(origin: .zero, size: CGSize(width: 10, height: 10))
popover.sourceView = view
popover.permittedArrowDirections = []
}
}
}
typealias UIViewControllerType = RPBroadcastActivityViewController
}
You need to do something very similar to previewViewController.
Try This
import SwiftUI
import ReplayKit
struct ContentView: View {
let recorder = RPScreenRecorder.shared()
#State var isBool = false
#State var rp: RPPreviewView!
#State var isRecording = false
#State var isShowPreviewVideo = false
var body: some View {
ZStack {
VStack {
Button(action: {
if !self.isRecording {
self.startRecord()
} else {
self.stopRecord()
}
}) {
Image(systemName: isRecording ? "stop.circle" : "video.circle")
.resizable()
.frame(width: 100, height: 100)
}
}
if isShowPreviewVideo {
rp
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
}
}
}
func startRecord() {
guard recorder.isAvailable else {
print("Recording is not available at this time.")
return
}
if !recorder.isRecording {
recorder.startRecording { (error) in
guard error == nil else {
print("There was an error starting the recording.")
return
}
print("Started Recording Successfully")
self.isRecording = true
}
}
}
func stopRecord() {
recorder.stopRecording { (preview, error) in
print("Stopped recording")
self.isRecording = false
guard let preview = preview else {
print("Preview controller is not available.")
return
}
self.rp = RPPreviewView(rpPreviewViewController: preview, isShow: self.$isShowPreviewVideo)
withAnimation {
self.isShowPreviewVideo = true
}
}
}
}
struct RPPreviewView: UIViewControllerRepresentable {
let rpPreviewViewController: RPPreviewViewController
#Binding var isShow: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> RPPreviewViewController {
rpPreviewViewController.previewControllerDelegate = context.coordinator
rpPreviewViewController.modalPresentationStyle = .fullScreen
return rpPreviewViewController
}
func updateUIViewController(_ uiViewController: RPPreviewViewController, context: Context) { }
class Coordinator: NSObject, RPPreviewViewControllerDelegate {
var parent: RPPreviewView
init(_ parent: RPPreviewView) {
self.parent = parent
}
func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
withAnimation {
parent.isShow = false
}
}
}
}

Resources