9

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 )

smr
  • 890
  • 7
  • 25

2 Answers2

8

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)
        }
    }
}
Matt Corey
  • 96
  • 3
  • Sorry for the delay, Matt. I reconfigured your solution to use `UICloudSharingController`'s `preparationHandler` initializer so it would create and send the share properly. Your solution does solve my issues #1 and #2. I think #3 was just me not configuring the controller properly or noticing the error from the `CKModifyRecordsOperation`. By the way, this also works fine as a nav bar button, although you might have to fiddle with the sizing a bit. Thanks! – smr Jan 02 '20 at 14:19
  • What is `CloudManager` here? My build fails because of it's undefined. I feel that I should replace it with something else but couldn't figure out. – Murat Çorlu Jan 14 '20 at 22:44
  • 1
    Sorry - that’s just a utility class that I use to track certain cloud-based functions and data. In this case, I was using that to get an instance of CKContainer that I had cached. I’ve updated the example to remove that class, to eliminate confusion. – Matt Corey Jan 15 '20 at 23:24
  • The button only works when it is a navigation bar item – Richard Witherspoon May 25 '20 at 23:37
  • If you create a random frame for the source view such as (x: 0, y:0, height: infinity, width: infinity) you can get it to show up on the screen. However doing this or by using the navigation bar, you you can't share the CloudKit invite. – Richard Witherspoon May 25 '20 at 23:48
  • @RichardWitherspoon could you elaborate a bit on `Pre-Create the CKShare record here, and assign to parent.share`. What did you do here? I'm currently using `NSPersistentCloudKitContainer` so all this stuff normally gets handled automagically for me. – goddamnyouryan Mar 25 '21 at 02:02
  • @MattCorey can you add what type ObjectToShare is that your answer uses? – GarySabo May 12 '21 at 02:10
  • Thank you! You saved my day! With a little rework I manage to make it work for my project. Thank you! – DungeonDev Mar 03 '22 at 14:45
1

Maybe just use rootViewController.

let window = UIApplication.shared.windows.filter { type(of: $0) == UIWindow.self }.first
window?.rootViewController?.present(sharingController, animated: true)
Steve Ham
  • 3,067
  • 1
  • 29
  • 36
  • Just tried it. By using a UIKit button so you can have a proper `sourceView` for the popover, you can present the sharing controller. However, still no sharing interface (the modal attaching the url to a message or email). Also, after the first attempt, the app locks up. The primary view fades into the background waiting for the sheet to be placed in front, but the sheet never displays. Had the same problem with the UIViewControllerRepresentable approach. – smr Dec 26 '19 at 15:06
  • I’m getting the same behavior. – Richard Witherspoon May 25 '20 at 23:49