CloudKit not working on actual iOS device

I've developed an app that contains an inbox that displays message from a CloudKit container. Works perfectly on simulator. Once I tried to run it on a phone..in Xcode debug environment and TestFlight it is unable to complete any transactions with production database. I'm running out of ideas. So far I have tried:

  • Verify settings between debug and release in Signing & Capabilities
  • Add CloudKit.framework to Framework, Libraries, and Embedded Content
  • Verify record and key names
  • verify .entitlements files

Please help!

Answered by Jduffy187 in 815792022

I'm updating this to help others that get stuck in the same or similar way. This issue was born out of a misunderstanding of how the Private database works. I thought that as the developer I could drop a record in the Private folder in Cloudkit Console and have it show up in every users Private Folder.

This is not how Cloudkit works. The Private Database is only accessible to device that the user is signed into. There is simply no way to do what I was trying to do. I've gone a different direction now. Hope this helps.

I've continued to troubleshoot this issue over the last week and change. My subscription does not register (cannot see it on production Subscriptions page on Cloudkit Console) and my fetch data (add(CKDatabaseOperation) returns no data.

Neither operation returns any error messages. It's like the query finds no matches, but I've reviewed everything repeatedly and the field names are correct. All fields are indexed.

I've verified the Security Roles. The whole thing works flawlessly on the simulator for development and production schemas, but does nothing with testing on a real device from Xcode or TestFlight.

Here is the source for my custom app delegate for my subscription

import SwiftUI
import UserNotifications
import CloudKit

class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNotificationCenterDelegate {
    @Published var isNotificationPresent: Bool = false
    @Published var notificationTapped: Bool = false
    
    var app: iLS_ToolkitApp?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        //application.registerForRemoteNotifications()
        if getIcloudStatus() {
            // Request permission from user to send notification
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { authorized, error in
              if authorized {
                DispatchQueue.main.async(execute: {
                  application.registerForRemoteNotifications()
                })
              }
                else {
                    print("Request Error: \(error?.localizedDescription ?? "Not Returned")")
                }
            })
            UNUserNotificationCenter.current().delegate = self
        } else {
            print("AppDeleagte iCloud not avaliable")
        }
        
        return true
    }
    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        //Get Device ID for Testing
        let stringToken = deviceToken.map { String(format: "%02.2hhx", $0)}.joined()
        print("Device Token: \(stringToken)")
        
        let subscription = CKQuerySubscription(recordType: "news", predicate: NSPredicate(value: true), subscriptionID: "news_feed", options: .firesOnRecordCreation)
        
        let notification = CKSubscription.NotificationInfo()
        notification.titleLocalizationKey = "%1$@"
        notification.titleLocalizationArgs = ["title"]
        notification.alertLocalizationKey = "%1$@"
        notification.alertLocalizationArgs = ["message"]
        notification.soundName = "default"
        notification.shouldSendContentAvailable = true
        
        subscription.notificationInfo = notification
        
        CKContainer.default().publicCloudDatabase.save(subscription) { _, error in
            if let error {
                print("Subscription Error: \(error)")
            }
        }
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError: Error) {
        print("Registration Failure \(Error.self)")
    }
}

extension AppDelegate {
    
    //Function is called when user taps the notification
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
        print("Notification Recieved")
        notificationTapped = true
    }
    
    //Function that is called when a Push notification is recieved
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        isNotificationPresent = true
        return [.banner, .list, .badge, .sound]
    }
    
    func getIcloudStatus() -> Bool {
        CKContainer.default().accountStatus { returnedStatus, returnedError in
            DispatchQueue.main.async {
                switch returnedStatus {
                case .available:
                    print("AppDelegate Cloudkit is available")
                default :
                    print("AppDelegate Cloudkit is not available")
                    return
                }
            }
        }
        return true
    }
}

The source for my class to read the messages will be in the next reply

class CloudKitRecieveMessages: ObservableObject {
    @Published var processedMessages: [News] = []
    @Published var unreadCount: Int = 0
    @Published var totalCount: Int = 0
    
    init() {
        fetchNews()
    }
    
    func fetchNews() {
        let predicate = NSPredicate(value: true)
        
        let query = CKQuery(recordType: "news", predicate: predicate)
        //query.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)]
        
        let queryOperation = CKQueryOperation(query: query)
        var tempNews: [News] = []
        
        //This property is defined before addOperation and data becomes available after
        //execution is async
        queryOperation.recordMatchedBlock = { returnedRecordId, returnedResult in
            print("in MatchBlock")
            switch returnedResult {
            case .success(let record):
                guard let title = record["title"] as? String else { print("title")
                    return }
                
                guard let message = record["message"] as? String else { print("message")
                    return }
                
                guard let deleted = record["deleted"] as? String else { print("deleted")
                    return }
                
                guard let unread = record["unread"] as? String else { print("unread")
                    return }
                
                guard let creation = record.creationDate else { print("creation")
                    return }
                
                tempNews.append(News(title: title, message: message, deleted: deleted, date: self.getDateFromCreationTime(d: creation), unRead: unread, record: record, creationDate: creation, time: self.getTimeFromCreationDate(d: creation)))
                
            case .failure(let error):
                print("MatchedBlock Error: \(error)")
            }
        }
        //This property is defined before addOperation and data becomes available after
        //execution is async
        queryOperation.queryResultBlock = { [weak self] returnedResult in
            DispatchQueue.main.async {
                print("result block: \(returnedResult) and \(tempNews)")
                self?.processedMessages.removeAll()
                self?.processedMessages.append(contentsOf: tempNews)
                self?.processedMessages.sort{
                    $0.creationDate < $1.creationDate
                }
                self?.getUnreadCount()
                self?.getTotalCount()
            }
        }
        addOperation(operation: queryOperation)
    }
    
    func addOperation(operation: CKDatabaseOperation) {
        CKContainer.default().publicCloudDatabase.add(operation)
    }
}
Accepted Answer

I'm updating this to help others that get stuck in the same or similar way. This issue was born out of a misunderstanding of how the Private database works. I thought that as the developer I could drop a record in the Private folder in Cloudkit Console and have it show up in every users Private Folder.

This is not how Cloudkit works. The Private Database is only accessible to device that the user is signed into. There is simply no way to do what I was trying to do. I've gone a different direction now. Hope this helps.

CloudKit not working on actual iOS device
 
 
Q