Swift: CloudKit subscription firesOnRecordCreation not called as expected - swift

This is my register for subscription method
public func RegisterForSubscription(completion: #escaping (Result<CKSubscription, Error>) -> Swift.Void) {
let subscription = CKQuerySubscription(recordType: Film.RecordType, predicate: NSPredicate(value: true), options: [.firesOnRecordCreation, .firesOnRecordDeletion, .firesOnRecordUpdate])
let notificationInfo = CKSubscription.NotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
database.save(subscription) { savedSubscription, error in
if let error = error {
print("Subscription error: \(error.localizedDescription)")
completion(.failure(error))
return
}
if let savedSubscription = savedSubscription {
completion(.success(savedSubscription))
return
}
}
}
Now all i care is .firesOnRecordCreation but for debugging purposes, i added all three options.
When I tried adding (save) a new record, it successfully added them to CloudKit. here's the method
public func save(film: Film, completion: #escaping (Result<CKRecord, Error>) -> Swift.Void) {
let filmRecord = film.toRecord()
let operation = CKModifyRecordsOperation(recordsToSave: [filmRecord], recordIDsToDelete: nil)
operation.qualityOfService = .userInitiated
operation.completionBlock = {
completion(.success(filmRecord))
}
database.add(operation)
}
and my .toRecord() method:
public func toRecord() -> CKRecord {
let record = self.record ?? CKRecord(recordType: Self.RecordType)
record[RecordKeys.id.rawValue] = id
record[RecordKeys.title.rawValue] = title
record[RecordKeys.filmDescription.rawValue] = filmDescription
record[RecordKeys.image.rawValue] = image
record[RecordKeys.director.rawValue] = director
record[RecordKeys.producer.rawValue] = producer
record[RecordKeys.releaseDate.rawValue] = releaseDate
record[RecordKeys.rtScore.rawValue] = rtScore
record[RecordKeys.imdbLink.rawValue] = imdbLink
record[RecordKeys.imdbScore.rawValue] = imdbScore
return record
}
Everything works. however, I wanted to register for subscription (silent notifications) to detect live changes when user add a new record (film).
The problem is, whenever i add via the method (calling the save method), it is not triggering the .firesOnRecordCreation option. However, the other two works. I updated one of the attributes via CloudKit dashboard and deleted them, it is called (on appDelegate's didReceiveRemoteNotification)
What am i doing wrong? When user added a new film from the method (not VIA dashboard) it should trigger it, but not in this case.
EDIT:
Adding via the dashboard TRIGGERS the susbcription. But NOT when using the method.

The subscription notification will not be received by the device that created, deleted or updated CKRecord.
Edit: Read here about handling subscription notifications.
You use multiple devices because a notification isn’t sent to the same device that originated the notification

Related

How to disable reopening documents when app started via launchIsDefaultUserInfoKey

On macOS unlike iOS, it appears if you want to disable reopening documents at launch, you need to rely on the application delegate notifications vs the newer methods - with an options argument, like on iOS:
applicationWillFinishLaunching(_:), here you want to instantiate your sub-classed document controller
// We need our own to reopen our "document" urls
_ = DocumentController.init()
applicationDidFinishLaunching(_:), here you want to inspect the supplied userInfo
if let info = note.userInfo{
if let launchURL = info[NSApplication.launchIsDefaultUserInfoKey] as? String {
Swift.print("launchIsDefaultUserInfoKey: notif \(launchURL)")
disableDocumentReOpening = launchURL.boolValue
}
if let notif = info[NSApplication.launchUserNotificationUserInfoKey] as? String {
Swift.print("applicationDidFinishLaunching: notif \(notif)")
disableDocumentReOpening = true
}
}
so when my document controller is called to do the doc restores, it would see this flag within the app delegate: var disableDocumentReOpening = false.
func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: #escaping (NSWindow?, Error?) -> Void) {
if (NSApp.delegate as! AppDelegate).disableDocumentReOpening {
completionHandler(nil, NSError.init(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) )
}
else
{
NSDocumentController.restoreWindow(withIdentifier: identifier, state: state, completionHandler: completionHandler)
}
}
but unfortunately, I have something wrong but what? Manually launching the app in debugger, it appears the document controller restore call is ahead of the app delegate's routine to inspect the userInfo.
I had read a SO post on this, showing an objective-c code snippet, but flag was local to the controller, but didn't understand how you could have a read/write class var - as I tried. Also didn't understand its use of a class function as doc says its an instance method, but trying that as well didn't work either.
What am I missing?

CKShare - Failed to modify some records error - CloudKit

I'm trying to share a record with other users in CloudKit but I keep getting an error. When I tap one of the items/records on the table I'm presented with the UICloudSharingController and I can see the iMessage app icon, but when I tap on it I get an error and the UICloudSharingController disappears, the funny thing is that even after the error I can still continue using the app.
Here is what I have.
Code
var items = [CKRecord]()
var itemName: String?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
let share = CKShare(rootRecord: item)
if let itemName = item.object(forKey: "name") as? String {
self.itemName = item.object(forKey: "name") as? String
share[CKShareTitleKey] = "Sharing \(itemName)" as CKRecordValue?
} else {
share[CKShareTitleKey] = "" as CKRecordValue?
self.itemName = "item"
}
share[CKShareTypeKey] = "bundle.Identifier.Here" as CKRecordValue
prepareToShare(share: share, record: item)
}
private func prepareToShare(share: CKShare, record: CKRecord){
let sharingViewController = UICloudSharingController(preparationHandler: {(UICloudSharingController, handler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modRecordsList = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modRecordsList.modifyRecordsCompletionBlock = {
(record, recordID, error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modRecordsList)
})
sharingViewController.delegate = self
sharingViewController.availablePermissions = [.allowPrivate]
self.navigationController?.present(sharingViewController, animated:true, completion:nil)
}
// Delegate Methods:
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("saved successfully")
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("failed to save: \(error.localizedDescription)")// the error is generated in this method
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return nil //You can set a hero image in your share sheet. Nil uses the default.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return self.itemName
}
ERROR
Failed to modify some records
Here is what I see...
Any idea what could be wrong?
EDIT:
By the way, the error is generated in the cloudSharingController failedToSaveShareWithError method.
Looks like you're trying to share in the default zone which isn't allowed. From the docs here
Sharing is only supported in zones with the
CKRecordZoneCapabilitySharing capability. The default zone does not
support sharing.
So you should set up a custom zone in your private database, and save your share and records there.
Possibly it is from the way you're trying to instantiate the UICloudSharingController? I cribbed my directly from the docs and it works:
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
If that's not the problem it's something with either one or both of the records themselves. If you upload the record without trying to share it, does it work?
EDIT TO ADD:
What is the CKShareTypeKey? I don't use that in my app. Also I set my system fields differently:
share?[CKShare.SystemFieldKey.title] = "Something"
Try to add this to your info.plist
<key>CKSharingSupported</key>
<true/>

Set Message Body of email with data from Firebase database in swift

I am building a purchase order app and I have a table view that shows the current purchase order. I would like to be able to send an email with the data of that purchase order to the supplier.
I have everything working fine but I am unable to set the message body to data from firebase.
My firebase database looks like this and I would like to extract the name, info, quantity and price of each items in the list and populate the email with this data.
and this is how I am saving the data
func saveItemInBackground(shoppingItem: ShoppingItem, completion: #escaping (_ error: Error?) -> Void) {
let ref = firebase.child(kSHOPPINGITEM).child(shoppingItem.shoppingListId).childByAutoId()
shoppingItem.shoppingItemId = ref.key
ref.setValue(dictionaryFromItem(item: shoppingItem)) { (error, ref) -> Void in
completion(error)
}
}
I am using this code to present the email.
func configureMailController() -> MFMailComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
firebase.child(kSUPPLIEREMAIL).child(FUser.currentId()).child("SupplierEmail").observe(.value, with: {
snapshot in
if snapshot.exists() {
let toEmail = snapshot.value
mailComposerVC.setToRecipients(["\(toEmail as! String)"])
mailComposerVC.setSubject("New EDM Pro Purchase Order")
mailComposerVC.setMessageBody("", isHTML: false)
} else {
self.showMailError()
}
})
return mailComposerVC
}
and was wondering if i could user a function to set the email body and then call the function in the line mailComposerVC.setMessageBody("", isHTML: false)
Alternatively if this is not possible I could create a pdf of the tableview and attach it to the email. I am not sure how this could be done.
Any help on this would be amazing!

How to update data on cloudKit without creating a new record?

Here is what I am trying to do. I have a simple journaling app with two views: a tableView that lists the titles of the entries and a viewController that has a text field for a title, and a textView for the text body (and a save button to save to cloudKit). On the viewController, I hit save and the record is saved to cloudKit and also added to the tableView successfully. This is all good.
I want to be able to edit/update the journal entry. But when I go back into the journal entry, change it in any way, then hit save again, the app returns to the tableView controller with an updated entry, but cloudKit creates a NEW entry separate from the one I wanted to edit. Then when I reload the app, my fetchRecords function fetches any extra records cloudKit has created.
Question: How do I edit/update an existing journal entry without creating a new entry in cloudKit?
Let me know if you need something else to further clarify my question.
Thanks!
Here are my cloudKit functions:
import Foundation
import CloudKit
class CloudKitManager {
let privateDB = CKContainer.default().publicCloudDatabase //Since this is a journaling app, we'll make it private.
func fetchRecordsWith(type: String, completion: #escaping ((_ records: [CKRecord]?, _ error: Error?) -> Void)) {
let predicate = NSPredicate(value: true) // Like saying I want everything returned to me with the recordType: type. This isn't a good idea if you have a massive app like instagram because you don't want all posts ever made to be loaded, just some from that day and from your friends or something.
let query = CKQuery(recordType: type, predicate: predicate)
privateDB.perform(query, inZoneWith: nil, completionHandler: completion) //Allows us to handle the completion in the EntryController to maintain proper MVC.
}
func save(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
modify(records: records, perRecordCompletion: perRecordCompletion, completion: completion )
}
func modify(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.savePolicy = .ifServerRecordUnchanged //This is what updates certain changes within a record.
operation.queuePriority = .high
operation.qualityOfService = .userInteractive
operation.perRecordCompletionBlock = perRecordCompletion
operation.modifyRecordsCompletionBlock = { (records, _, error) in
completion?(records, error)
}
privateDB.add(operation) //This is what actually saves your data to the database on cloudkit. When there is an operation, you need to add it.
}
}
This is my model controller where my cloudKit functions are being used:
import Foundation
import CloudKit
let entriesWereSetNotification = Notification.Name("entriesWereSet")
class EntryController {
private static let EntriesKey = "entries"
static let shared = EntryController()
let cloudKitManager = CloudKitManager()
init() {
loadFromPersistentStorage()
}
func addEntryWith(title: String, text: String) {
let entry = Entry(title: title, text: text)
entries.append(entry)
saveToPersistentStorage()
}
func remove(entry: Entry) {
if let entryIndex = entries.index(of: entry) {
entries.remove(at: entryIndex)
}
saveToPersistentStorage()
}
func update(entry: Entry, with title: String, text: String) {
entry.title = title
entry.text = text
saveToPersistentStorage()
}
// MARK: Private
private func loadFromPersistentStorage() {
cloudKitManager.fetchRecordsWith(type: Entry.TypeKey) { (records, error) in
if let error = error {
print(error.localizedDescription)
}
guard let records = records else { return } //Make sure there are records.
let entries = records.flatMap({Entry(cloudKitRecord: $0)})
self.entries = entries //This is connected to the private(set) property "entries"
}
}
private func saveToPersistentStorage() {
let entryRecords = self.entries.map({$0.cloudKitRecord})
cloudKitManager.save(records: entryRecords, perRecordCompletion: nil) { (records, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
} else {
print("Successfully saved records to cloudKit")
}
}
}
// MARK: Properties
private(set) var entries = [Entry]() {
didSet {
DispatchQueue.main.async {
NotificationCenter.default.post(name: entriesWereSetNotification, object: nil)
}
}
}
}
Here's a couple threads that might be helpful.
If you were caching the data locally you would use the encodesystemfields method to create a new CKRecord that will update an existing one on the server.
How (and when) do I use iCloud's encodeSystemFields method on CKRecord?
It doesn't appear you are caching locally. I don't have experience doing it without using encodesystemfields, but it looks like you have to pull the record down and save it back in the completion handler of the convenience method:
Trying to modify ckrecord in swift

"Realm accessed from incorrect thread" exception thrown from dispatch_group_notify block

I have read the other answers but couldn't find a suitable solution.
I have a product which is uploaded to server only if all IMAGES belonging to that product have finished uploading. The product's details (along with the images) are filled on view controller 1 and then he is taken to the next screen (view controller 2), regardless of whether all images have finished uploading or not. MY VC1 completes the product upload process like this.
let areAllImagesUploaded = RetailProductsService.sharedInstance.checkProductImageDependency(realm!, uuid: retailProduct.getClientId())
if areAllImagesUploaded {
uploadProductToServer(retailProduct)
} else {
do {
try realm = Realm()
RetailProductsService.sharedInstance.updateSyncStatusForSellerProduct(realm!, clientId: retailProduct.getClientId(), syncStatus: ProductSyncStatus.SYNC_FAILED)
let groupT = dispatch_group_create()
for sellerSKU in retailProduct.sellersSKUs {
for productImage in sellerSKU.productImages {
dispatch_group_enter(groupT)
let imageUploadInfo = ImageUploadInfo(productId: retailProduct.getClientId(), imageId: sellerSKU.getId(),imageData: productImage.imageData, uploadURL: ServerConfig.RETAIL_SERVER_UPLOAD_FILE_URL)
ImageUploadManager.sharedInstance.queueImageForUpload(imageUploadInfo, completion: { (success, error) -> Void in
dispatch_group_leave(groupT)
})
dispatch_group_notify(groupT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), { () -> Void in
//self.uploadProduct(retailProduct)
self.uploadProductToServer(retailProduct) // Fails here
})
}
}
} catch {
print("Error in saving product.")
}
}
I have marked the line on which I get this error. My app has moved to the next view controller while this function in view controller 1 continues uploading images and as soon as all images associated the product are uploaded to server, it tries to upload the product. However it fails with this exception.
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread'
Please help!
Realm Objects cannot be accessed from different threads. Your retailProduct is created or fetched from Realm storage by threadX by than you switch to some other thread (threadY) by invoking dispatch_group_notify. To fix the exception you might want to do something like this:
I assume that your retailProduct object is of type RetailProduct and has an id property used as primary key in Realm storage. Of course you can fetch you retailProduct by using another query than that.
let retailProductId = retailProduct.id
dispatch_group_notify(groupT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), { () -> Void in
// threadY executing this lines
if let retailProduct = realm.objectForPrimaryKey(RetailProduct.self, key: retailProductId){
self.uploadProductToServer(retailProduct)
}
})
We need to understand the fact Realm Objects cannot be accessed from different threads. What does this means and how to workout this issue.
First, realm objects cannot be access from different thread means, one instance of thread defined in one thread cannot be access from different thread. What we should do actually is we need to have different instance of realm instance for each thread.
For eg. let's look at following e.g. where we insert 50 records in database asynchronously in background thread upon button click and we add notification block in main thread to update the no of people in count label. Each thread (main and background ) have its own instance of realm object to access Realm Database. Because Realm Database is thread safe.
class Person: Object {
dynamic var name = ""
convenience init(_ name: String) {
self.init()
self.name = name
}
}
override func viewDidAppear(_ animated: Bool) {
let realmMain = try! Realm ()
self.people = realmMain.objects(Person.self)
self.notification = self.people?.addNotificationBlock{ [weak self] changes in
print("UI update needed")
guard let countLabel = self?.countLabel else {
return
}
countLabel.text = "Total People: \(String(describing: self?.people?.count))"
}
}
#IBAction func addHandler(_ sender: Any) {
print(#function)
let backgroundQueue = DispatchQueue(label: "com.app.queue",
qos: .background,
target: nil)
backgroundQueue.async {
print("Dispatched to background queue")
let realm = try! Realm()
try! realm.write {
for i in 1..<50 {
let name = String(format: "rajan-%d", i)
//print(#function, name)
realm.add(Person(name))
}
}
}
}