Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI and UICloudSharingController hate each other

Tags:

ios

swiftui

I have a project using SwiftUI that requires CloudKit sharing, but I'm unable to get the UICloudSharingController to play nice in a SwiftUI environment.

First Problem

A straight-forward wrap of UICloudSharingController using UIViewControllerRepresentable yields an endless spinner (see this). As has been done for other system controllers like UIActivityViewController, I wrapped the UICloudSharingController in a containing UIViewController like this:

struct CloudSharingController: UIViewControllerRepresentable {
    @EnvironmentObject var store: CloudStore
    @Binding var isShowing: Bool

    func makeUIViewController(context: Context) -> CloudControllerHost {
        let host = CloudControllerHost()
        host.rootRecord = store.noteRecord
        host.container = store.container
        return host
    }

    func updateUIViewController(_ host: CloudControllerHost, context: Context) {
        if isShowing, host.isPresented == false {
            host.share()
        }
    }
}


final class CloudControllerHost: UIViewController {
    var rootRecord: CKRecord? = nil
    var container: CKContainer = .default()
    var isPresented = false

    func share() {
        let sharingController = shareController
        isPresented = true
        present(sharingController, animated: true, completion: nil)
    }

    lazy var shareController: UICloudSharingController = {
        let controller = UICloudSharingController { [weak self] controller, completion in
            guard let self = self else { return completion(nil, nil, CloudError.controllerInvalidated) }
            guard let record = self.rootRecord else { return completion(nil, nil, CloudError.missingNoteRecord) }

            let share = CKShare(rootRecord: record)
            let operation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: [])
            operation.modifyRecordsCompletionBlock = { saved, _, error in
                if let error = error {
                    return completion(nil, nil, error)
                }
                completion(share, self.container, nil)
            }
            self.container.privateCloudDatabase.add(operation)
        }
        controller.delegate = self
        controller.popoverPresentationController?.sourceView = self.view
        return controller
    }()
}

This allows the controller to come up normally, but...

Second Problem

Tap the close button or swipe to dismiss and the controller will disappear, but there's no notification that it's been dismissed. The SwiftUI view's @State property that initiated presenting the controller is still true. There's no obvious method to detect dismissal of the modal. After some experimenting, I discovered the presenting controller is the original UIHostingController created in the SceneDelegate. With some hackery, you can inject an object that is referenced in a UIHostingController subclass into the CloudSharingController. This will let you detect the dismissal and set the @State property to false. However, all nav bar buttons no longer function after dismissing so you could only ever tap this thing once. The rest of the scene is completely functional, but buttons in the nav bar don't respond.

Third Problem

Even if you could get the UICloudSharingController to present and dismiss normally, tapping on any of the sharing methods (Messages, Mail, etc) makes the controller disappear with no animation and the controller for the sharing URL doesn't come up. No crash or console messages--it just disappears.

Demo

I made a quick and dirty project on GitHub to demonstrate the issue: CloudKitSharing. It just creates a single String and a CKRecord to represent it using CloudKit. The interface displays the String (a UUID) with a single nav bar button to share it:

CloudKitSharingUI

The Plea

Is there any way to use UICloudSharingController in SwiftUI? Don't have the time to rebuild the project in UIKit or a custom sharing controller (I know--the price of being on the bleeding edge 💩)

like image 562
smr Avatar asked Dec 07 '19 22:12

smr


2 Answers

I got this working -- initially, I wrapped the UICloudSharingController in a UIViewControllerRepresentable, much like the link you provided (I referenced that while building it), and simply adding it to a SwiftUI .sheet() view. This worked on the iPhone, but it failed on the iPad, because it requires you to set the popoverPresentationController?.sourceView, and I didn't have one, given that I triggered the sheet with a SwiftUI Button.

Going back to the drawing board, I rebuilt the button itself as a UIViewRepresentable, and was able to present the view using the rootViewController trick that SeungUn Ham suggested here. All works, on both iPhone and iPad - at least in the simulator.

My button:

struct UIKitCloudKitSharingButton: UIViewRepresentable {
    typealias UIViewType = UIButton

    @ObservedObject
    var toShare: ObjectToShare
    @State
    var share: CKShare?

    func makeUIView(context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) -> UIButton {
        let button = UIButton()

        button.setImage(UIImage(systemName: "person.crop.circle.badge.plus"), for: .normal)
        button.addTarget(context.coordinator, action: #selector(context.coordinator.pressed(_:)), for: .touchUpInside)

        context.coordinator.button = button
        return button
    }

    func updateUIView(_ uiView: UIButton, context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) {

    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UICloudSharingControllerDelegate {
        var button: UIButton?

        func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
            //Handle some errors here.
        }

        func itemTitle(for csc: UICloudSharingController) -> String? {
            return parent.toShare.name
        }

        var parent: UIKitCloudKitSharingButton

        init(_ parent: UIKitCloudKitSharingButton) {
            self.parent = parent
        }

        @objc func pressed(_ sender: UIButton) {
            //Pre-Create the CKShare record here, and assign to parent.share...

            let sharingController = UICloudSharingController(share: share, container: myContainer)

            sharingController.delegate = self
            sharingController.availablePermissions = [.allowReadWrite]
            if let button = self.button {
                sharingController.popoverPresentationController?.sourceView = button
            }

            UIApplication.shared.windows.first?.rootViewController?.present(sharingController, animated: true)
        }
    }
}
like image 170
Matt Corey Avatar answered Nov 04 '22 03:11

Matt Corey


Maybe just use rootViewController.

let window = UIApplication.shared.windows.filter { type(of: $0) == UIWindow.self }.first
window?.rootViewController?.present(sharingController, animated: true)
like image 33
Steve Ham Avatar answered Nov 04 '22 03:11

Steve Ham