swiftswiftuiicloudcloudkitcloudkit-sharing

Using UIApplicationDelegateAdaptor to get callbacks from userDidAcceptCloudKitShareWith not working


I'm trying to get notified when userDidAcceptCloudKitShareWith gets called. Traditionally this was called in the App Delegate but since I am building an iOS 14+ using App as my root object. I couldn't find any documentation out yet as far as how to add userDidAcceptCloudKitShareWith to my App class, so I am using UIApplicationDelegateAdaptor to use an App Delegate class, however it doesn't seem like userDidAcceptCloudKitShareWith is ever getting called?

import SwiftUI
import CloudKit

// Our observable object class
class ShareDataStore: ObservableObject {
    
    static let shared = ShareDataStore() 
    @Published var didRecieveShare = false
    @Published var shareInfo = ""
}

@main
struct SocialTestAppApp: App {
    
    
    @StateObject var shareDataStore = ShareDataStore.shared 
    
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(shareDataStore)
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    
    let container = CKContainer(identifier: "iCloud.com.TestApp")
    
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("did finish launching called")
        return true
    }
    
    
    func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
        print("delegate callback called!! ")
        acceptShare(metadata: cloudKitShareMetadata) { result in
            switch result {
            case .success(let recordID):
                print("successful share!")
                ShareDataStore.shared.didRecieveShare = true
                ShareDataStore.shared.shareInfo = recordID.recordName
            case .failure(let error):
                print("failure in share = \(error)")
            }
        }    }
    
    func acceptShare(metadata: CKShare.Metadata,
                     completion: @escaping (Result<CKRecord.ID, Error>) -> Void) {
        
        // Create a reference to the share's container so the operation
        // executes in the correct context.
        let container = CKContainer(identifier: metadata.containerIdentifier)
        
        // Create the operation using the metadata the caller provides.
        let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
        
        var rootRecordID: CKRecord.ID!
        // If CloudKit accepts the share, cache the root record's ID.
        // The completion closure handles any errors.
        operation.perShareCompletionBlock = { metadata, share, error in
            if let _ = share, error == nil {
                rootRecordID = metadata.rootRecordID
            }
        }
        
        // If the operation fails, return the error to the caller.
        // Otherwise, return the record ID of the share's root record.
        operation.acceptSharesCompletionBlock = { error in
            if let error = error {
                completion(.failure(error))
            } else {
                completion(.success(rootRecordID))
            }
        }
        
        // Set an appropriate QoS and add the operation to the
        // container's queue to execute it.
        operation.qualityOfService = .utility
        container.add(operation)
    }
    
    
}

Updated based on Asperi's Answer:

import SwiftUI
import CloudKit

class ShareDataStore: ObservableObject {
    
    static let shared = ShareDataStore() 
    
    @Published var didRecieveShare = false
    @Published var shareInfo = ""
}

@main
struct athlyticSocialTestAppApp: App {
    
    
    @StateObject var shareDataStore = ShareDataStore.shared 
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    let sceneDelegate = MySceneDelegate()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(shareDataStore)
                 .withHostingWindow { window in
                 sceneDelegate.originalDelegate = window.windowScene.delegate
                 window.windowScene.delegate = sceneDelegate
              }
        }
        
    }
    
}


class MySceneDelegate: NSObject, UIWindowSceneDelegate {

    let container = CKContainer(identifier: "iCloud.com...")
    
     var originalDelegate: UIWindowSceneDelegate?

        var window: UIWindow?

        func sceneWillEnterForeground(_ scene: UIScene) {
            print("scene is active")
        }

        func sceneWillResignActive(_ scene: UIScene) {
            print("scene will resign active")
        }
        
    
      // forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
   func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    originalDelegate?.scene!(scene, willConnectTo: session, options: connectionOptions)
   }

        func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {

            print("delegate callback called!! ")
            acceptShare(metadata: cloudKitShareMetadata) { result in
                switch result {
                case .success(let recordID):
                    print("successful share!")
                    ShareDataStore.shared.didRecieveShare = true
                    ShareDataStore.shared.shareInfo = recordID.recordName
                case .failure(let error):
                    print("failure in share = \(error)")
                }
            }

        }

}


extension View {
    func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
        self.background(HostingWindowFinder(callback: callback))
    }
}

struct HostingWindowFinder: UIViewRepresentable {
    var callback: (UIWindow?) -> ()

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

Solution

  • In Scene-based application the userDidAcceptCloudKitShareWith callback is posted to Scene delegate, but in SwiftUI 2.0 App-based application the scene delegate is used by SwiftUI itself to provide scenePhase events, but does not provide native way to handle topic callback.

    The possible approach to solve this is to find a window and inject own scene delegate wrapper, which will handle userDidAcceptCloudKitShareWith and forward others to original SwiftUI delegate (to keep standard SwiftUI events working).

    Here is a couple of demo snapshots based on https://stackoverflow.com/a/63276688/12299030 window access (however you can use any other preferable way to get window)

    @main
    struct athlyticSocialTestAppApp: App {
        @StateObject var shareDataStore = ShareDataStore.shared 
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        let sceneDelegate = MySceneDelegate()
        
        var body: some Scene {
            WindowGroup {
                ContentView().environmentObject(shareDataStore)
                  .withHostingWindow { window in
                     sceneDelegate.originalDelegate = window?.windowScene.delegate
                     window?.windowScene.delegate = sceneDelegate
                  }
            }
        }
    }
    
    class MySceneDelegate : NSObject, UIWindowSceneDelegate {
       var originalDelegate: UISceneDelegate?
    
       func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
    
           // your code here
       }
    
       // forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
       func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
           originalDelegate?.scene(scene, willConnectTo: session, options: connectionOptions)
       }
    }