Post

Replies

Boosts

Views

Activity

Is it possible to use background thread on NSFetchedResultsController for heavy read operation to ensure UI responsiveness?
Apple has shown us how to perform heavy write operation using background thread (by using newBackgroundContext) in their official earth quake example - https://github.com/yccheok/earthquakes-WWDC20 But, what about heavy read operation? (millions of rows for stress test purpose) We would also like our app UI to be responsiveness, when we are launching the app for first time, and the app is reading a large amount of data from CoreData. The following is the code snippet which is using NSFetchedResultController. [UI is not responsiveness if there are a lot of rows] } [UI is not responsiveness if there are a lot of rows] We try to perform controller.performFetch() using background thread. Still, but not sure why, the UI is still not responsiveness. My guess is that, after NSFetchedResultsController is occupied UI main thread, to perform some time consuming I/O read operation. } [UI is responsive now. But the solution is incorrect...] I guess, we need to place NSFetchedresultController under background thread too. Hence, we do the following modification. } The UI is responsiveness during fetching, and data is able to be fetched and shown after some time. However, if we investigate further by using the launching argument com.apple.CoreData.ConcurrencyDebug 1 We will experience the following crash after controller.performFetch() finish executed. CoreData`+[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__]: May I know, is it ever possible to make UI responsive, when we are using NSFetchedResultController to load a large amount of data (few millions rows as per testing)? Can NSFetchedResultController ever operated under background thread?
0
0
746
Mar ’21
CoreData common practice - Do you usually have a struct based data class, as the bridge between your UI layer, and the CoreData data layer?
I was wondering, when you use CoreData, do you usually create another equivalent struct based data class, to compliment the NSManagedObject? The struct based data class, will act as the bridge, between UI layer, and CoreData data layer. For instance, I have the following CoreData model data class. @objc(NSTabInfo) public class NSTabInfo: NSManagedObject { } extension NSTabInfo { @nonobjc public class func fetchRequest() -> NSFetchRequest<NSTabInfo> { return NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo") } @NSManaged public var colorIndex: Int32 @NSManaged public var customColor: Int32 @NSManaged public var name: String? @NSManaged public var order: Int64 @NSManaged public var typeValue: Int32 @NSManaged public var syncedTimestamp: Int64 @NSManaged public var uuid: UUID } extension NSTabInfo : Identifiable { } We need a UI, to represent such model data object. Initially, we have the following UI class class TabInfoCell: UICollectionViewCell { private var tabInfo: NSTabInfo? func update(_ tabInfo: NSTabInfo) { self.tabInfo = tabInfo } } But, we just feel not comfortable with such design. Letting TabInfoCell (UI class) to hold NSTabInfo (CoreData model data class) doesn't feel right, as NSTabInfo contains CoreData's context. I do not feel comfortable, to expose CoreData's context to an UI. Will holding a class reference to NSTabInfo in UI, affect CoreData memory allocation/ deallocation strategy? Using weak reference might solve the issue. But, what should the UI do when the weak reference become nil? With such concern, We have the following design struct TabInfo { enum Kind: Int { case All = 0 case Calendar case Custom case Settings } let kind: Kind var name: String? var colorIndex: Int var customColor: Int var order: Int var syncedTimestamp: Int64 var uuid: UUID } @objc(NSTabInfo) public class NSTabInfo: NSManagedObject { convenience init(context: NSManagedObjectContext, tabInfo: TabInfo) { self.init(context: context) self.colorIndex = Int32(tabInfo.colorIndex) self.customColor = Int32(tabInfo.customColor) self.name = tabInfo.name self.order = Int64(tabInfo.order) self.typeValue = Int32(tabInfo.kind.rawValue) self.syncedTimestamp = tabInfo.syncedTimestamp self.uuid = tabInfo.uuid } func toTabInfo() -> TabInfo { return TabInfo( kind: TabInfo.Kind(rawValue: Int(self.typeValue))!, name: self.name, colorIndex: Int(self.colorIndex), customColor: Int(self.customColor), order: Int(self.order), syncedTimestamp: self.syncedTimestamp, uuid: uuid ) } } @nonobjc public class func fetchRequest() -> NSFetchRequest<NSTabInfo> { return NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo") } @NSManaged public var colorIndex: Int32 @NSManaged public var customColor: Int32 @NSManaged public var name: String? @NSManaged public var order: Int64 @NSManaged public var typeValue: Int32 @NSManaged public var syncedTimestamp: Int64 @NSManaged public var uuid: UUID } extension NSTabInfo : Identifiable { } Then, in our UI class, it looks like class TabInfoCell: UICollectionViewCell { private var tabInfo: TabInfo? func update(_ tabInfo: TabInfo) { self.tabInfo = tabInfo } } I was wondering, does such design philosophy make sense? Do you usually have a struct based data class, as the bridge between your UI layer, and the CoreData data layer? Thanks.
0
0
698
Jun ’21
Why creating index in CoreData will end up duplicated index (with different name) in SQLite file?
I was wondering did anyone of you come across this strange situation? I tried to index a property in entity. For instance, for entity named NSPlainNote, I try to create an index named index_plain_note_label, on property label. This is how it looks like. However, if I inspect the content of the generated SQLite file. It seems that 2 same indices with different name are created. CREATE INDEX Z_NSPlainNote_index_plain_note_label ON ZNSPLAINNOTE (ZLABEL COLLATE BINARY ASC) CREATE INDEX Z_NSPlainNote_label ON ZNSPLAINNOTE (ZLABEL COLLATE BINARY ASC) If we observe carefully, such duplication also happen in all other index with 1 property like NSPlainNote's order NSPlainNote's sticky NSTabInfo's order Such duplication does not happen on index with 2 properties, or index with unique constraint. May I know why it is so? Is there any step which I have done incorrectly?
0
0
509
Jul ’21
performBatchUpdates completion handler is not called when there is section operation involved
So far, here's are the code snippets that almost work for NSFetchedResultsController + UICollectionView, based on the information provided https://developer.apple.com/videos/play/wwdc2018/225 (Starting at 36:50) https://stackoverflow.com/questions/38597804/how-to-order-moves-inserts-deletes-and-updates-in-a-uicollectionview-performb/53271562#53271562 Please note that, there are 2 [BlockOperation], as reloadItems and moveItem doesn't play well within single performBatchUpdates. Based on the workaround proposed in the video, we have to call reloadItems in a separate performBatchUpdates. We also do not follow 100% methods (Perform reloadItems typed performBatchUpdates first, followed by insert/ move/ delete typed performBatchUpdates) proposed in the video. This is because we notice that it doesn't work well even for simple case. Some strange behaviour including reloadItems will cause duplicated cell UI to be shown on screen. The "almost" work method we found are Perform performBatchUpdates for insert, move and delete At completion handler of performBatchUpdates, perform another performBatchUpdates for reloadItems NSFetchedResultsController + UICollectionView integration private var blockOperations: [BlockOperation] = [] // reloadItems and moveItem do not play well together. We are using the following workaround proposed at // https://developer.apple.com/videos/play/wwdc2018/225/ private var blockUpdateOperations: [BlockOperation] = [] extension DashboardViewController: NSFetchedResultsControllerDelegate { func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { if type == NSFetchedResultsChangeType.insert { print(">> insert") blockOperations.append( BlockOperation(block: { [weak self] in if let self = self { self.collectionView!.insertItems(at: [newIndexPath!]) } }) ) } else if type == NSFetchedResultsChangeType.update { print(">> update") blockUpdateOperations.append( BlockOperation(block: { [weak self] in if let self = self, let indexPath = indexPath { self.collectionView.reloadItems(at: [indexPath]) } }) ) } else if type == NSFetchedResultsChangeType.move { print(">> move") blockOperations.append( BlockOperation(block: { [weak self] in if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath { self.collectionView.moveItem(at: indexPath, to: newIndexPath) } }) ) } else if type == NSFetchedResultsChangeType.delete { print(">> delete") blockOperations.append( BlockOperation(block: { [weak self] in if let self = self { self.collectionView!.deleteItems(at: [indexPath!]) } }) ) } } func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { if type == NSFetchedResultsChangeType.insert { print(">> section insert") blockOperations.append( BlockOperation(block: { [weak self] in if let self = self { self.collectionView!.insertSections(IndexSet(integer: sectionIndex)) } }) ) } else if type == NSFetchedResultsChangeType.update { print(">> section update") blockOperations.append( BlockOperation(block: { [weak self] in if let self = self { self.collectionView!.reloadSections(IndexSet(integer: sectionIndex)) } }) ) } else if type == NSFetchedResultsChangeType.delete { print(">> section delete") blockOperations.append( BlockOperation(block: { [weak self] in if let self = self { self.collectionView!.deleteSections(IndexSet(integer: sectionIndex)) } }) ) } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { if blockOperations.isEmpty { performBatchUpdatesForUpdateOperations() } else { collectionView.performBatchUpdates({ [weak self] () -> Void in guard let self = self else { return } for operation: BlockOperation in self.blockOperations { operation.start() } self.blockOperations.removeAll(keepingCapacity: false) }, completion: { [weak self] (finished) -> Void in print("blockOperations completed") guard let self = self else { return } self.performBatchUpdatesForUpdateOperations() }) } } private func performBatchUpdatesForUpdateOperations() { if blockUpdateOperations.isEmpty { return } collectionView.performBatchUpdates({ [weak self] () -> Void in guard let self = self else { return } for operation: BlockOperation in self.blockUpdateOperations { operation.start() } self.blockUpdateOperations.removeAll(keepingCapacity: false) }, completion: { [weak self] (finished) -> Void in print("blockUpdateOperations completed") guard let self = self else { return } }) } } The above way, works "almost" well when no "section" operations involved. If there is item moved operation between different section, the following logging will be printed blockOperations completed >> move blockOperations completed >> move blockOperations completed However, if there are item being moved and section being added/ removed, the following logging will be printed >> section delete >> move >> section insert >> move This means the completion handler block is not executed! Does anyone know why it is so, and how I can workaround with this issue? Thanks.
0
0
1.3k
Jul ’21
CoreData: Is it possible to have SQLite CASE WHEN...END logic in NSSortDescriptor?
Currently, I am porting several SQLite statement to CoreData. "SELECT id, \"order\" FROM plain_note WHERE archived = :archived AND trashed = :trashed " + "ORDER BY " + "CASE WHEN color_index = -1 THEN 12 + ABS(custom_color) ELSE " + "color_index END DESC, " + "\"order\" ASC" "SELECT id, \"order\" FROM plain_note WHERE archived = :archived AND trashed = :trashed " + "ORDER BY " + "title COLLATE NOCASE DESC, " + "CASE WHEN locked = 1 THEN NULL ELSE " + "CASE WHEN type = 0 THEN body ELSE searched_string END " + "END COLLATE NOCASE DESC, " + "\"order\" ASC" But, I am clueless on how I can make CoreData have the following CASE WHEN...END behavior, during sorting. CASE WHEN color_index = -1 THEN 12 + ABS(custom_color) ELSE color_index END DESC CASE WHEN locked = 1 THEN NULL ELSE CASE WHEN type = 0 THEN body ELSE searched_string END END COLLATE NOCASE DESC I was wondering, is there any possible technique I can use, so that NSSortDescriptor can have the similar behavior?
0
0
430
Sep ’21
Top status bar show/ hide animation is broken in iOS 15
I have the following top status bar hide/ show code snippet with animation. It works well under iOS 14.5 private var additionalHeight: CGFloat { if UIDevice.current.hasNotch { return 0 } else { return self.navigationController?.view.safeAreaInsets.top ?? 0 } } private var statusBarHidden: Bool = false { didSet { if statusBarHidden { self.navigationController?.additionalSafeAreaInsets.top = additionalHeight } else { self.navigationController?.additionalSafeAreaInsets.top = 0 } UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in self.setNeedsStatusBarAppearanceUpdate() } } } // https://medium.com/@badlands/unfortunately-this-also-applies-to-ipad-pro-which-have-non-zero-safeareainsets-e1aa0d002462 extension UIDevice { /// Returns 'true' if the current device has a notch var hasNotch: Bool { if #available(iOS 11.0, *) { // https://stackoverflow.com/a/57899013/72437 let keyWindow = UIWindow.key // Case 1: Portrait && top safe area inset >= 44 let case1 = !UIDevice.current.orientation.isLandscape && (keyWindow?.safeAreaInsets.top ?? 0) >= 44 // Case 2: Lanscape && left/right safe area inset > 0 let case2 = UIDevice.current.orientation.isLandscape && ((keyWindow?.safeAreaInsets.left ?? 0) > 0 || (keyWindow?.safeAreaInsets.right ?? 0) > 0) return case1 || case2 } else { // Fallback on earlier versions return false } } } iOS 14.5 However, when same code runs in iOS 15, the animation is broken. We use latest XCode 13.0 During hide, there is no more "slide up" hide animation. During show, it will try to "push down" the entire view controller. iOS 15 Does anyone has idea how to fix such animation in iOS 15, so that it behaves similar as iOS 14.5 ?
0
0
645
Oct ’21
Why I am getting Multithreading_Violation_AllThatIsLeftToUsIsHonor for this simplest NSAsynchronousFetchRequest use case?
I want to start learning to use NSAsynchronousFetchRequest by referring to  https://www.marcosantadev.com/coredata_crud_concurrency_swift_2/ I have the simplest use case of NSAsynchronousFetchRequest NSAsynchronousFetchRequest // Call from UI main thread func X() { let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote") let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { asynchronousFetchResult in guard let result = asynchronousFetchResult.finalResult as? [NSPlainNote] else { return } } let coreDataStack = CoreDataStack.INSTANCE // backgroundContext created via persistentContainer.newBackgroundContext() let backgroundContext = coreDataStack.backgroundContext backgroundContext.perform { do { try backgroundContext.execute(asynchronousFetchRequest) } catch let error { backgroundContext.rollback() error_log(error) } } } However, running the above code will get me the following error CoreData`+[NSManagedObjectContextMultithreading_Violation_AllThatIsLeftToUsIsHonor]: If I modify the code by using NSFetchRequest directly. NSFetchRequest // Call from UI main thread func X() { let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote") let coreDataStack = CoreDataStack.INSTANCE // backgroundContext created via persistentContainer.newBackgroundContext() let backgroundContext = coreDataStack.backgroundContext backgroundContext.perform { do { let nsPlainNotes = try fetchRequest.execute() } catch let error { backgroundContext.rollback() error_log(error) } } } Thing works fine. May I know, what's wrong with my NSAsynchronousFetchRequest version of code? This is my CoreDataStack.swift for reference purpose. CoreDataStack.swift import CoreData class CoreDataStack { static let INSTANCE = CoreDataStack() private init() { } private(set) lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "wenote") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { // This is a serious fatal error. We will just simply terminate the app, rather than using error_log. fatalError("Unresolved error \(error), \(error.userInfo)") } }) // So that when backgroundContext write to persistent store, container.viewContext will retrieve update from // persistent store. container.viewContext.automaticallyMergesChangesFromParent = true // TODO: Not sure these are required... // //container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy //container.viewContext.undoManager = nil //container.viewContext.shouldDeleteInaccessibleFaults = true return container }() private(set) lazy var backgroundContext: NSManagedObjectContext = { let backgroundContext = persistentContainer.newBackgroundContext() // Similar behavior as Android's Room OnConflictStrategy.REPLACE backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // TODO: Not sure these are required... //backgroundContext.undoManager = nil return backgroundContext }() } Additional information Do note that, in NSAsynchronousFetchRequest example, even if backgroundContext.perform is not used. // Call from UI main thread func X() { let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote") let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { asynchronousFetchResult in guard let result = asynchronousFetchResult.finalResult as? [NSPlainNote] else { return } } let coreDataStack = CoreDataStack.INSTANCE // backgroundContext created via persistentContainer.newBackgroundContext() let backgroundContext = coreDataStack.backgroundContext do { try backgroundContext.execute(asynchronousFetchRequest) } catch let error { backgroundContext.rollback() error_log(error) } } Same fatal error still occur. Please note that, this fatal error will only be triggered, by editing the schema with Arguments Passed On Launch -com.apple.CoreData.ConcurrencyDebug 1 I even try to execute some simple project from  https://github.com/abhishekbedi1432/Core-Data-Asynchronous-Fetching/tree/master which is using NSAsynchronousFetchRequest. If I do not enable -com.apple.CoreData.ConcurrencyDebug 1, the sample project from github able to perform asynchronous fetch without issue. However, once the -com.apple.CoreData.ConcurrencyDebug 1 is enabled, it will also be getting the same fatal error.
0
0
777
Dec ’21
Asynchronous read in CoreData - Difference in using newBackgroundContext + FetchRequest vs newBackgroundContext + NSAsynchronousFetchRequest?
It seems there are 2 ways to perform asynchronous read in CoreData, without blocking main thread UI. newBackgroundContext + FetchRequest Source : https://www.advancedswift.com/core-data-background-fetch-save-create/ // Create a new background managed object context let context = persistentContainer.newBackgroundContext() // If needed, ensure the background context stays // up to date with changes from the parent context.automaticallyMergesChangesFromParent = true // Perform operations on the background context // asynchronously context.perform { do { // Create a fetch request let fetchRequest: NSFetchRequest<CustomEntity> fetchRequest = CustomEntity.fetchRequest() fetchRequest.fetchLimit = 1 let objects = try context.fetch(fetchRequest) // Handle fetched objects } catch let error { // Handle error } } newBackgroundContext + NSAsynchronousFetchRequest Source: https://www.marcosantadev.com/coredata_crud_concurrency_swift_2/ let privateManagedObjectContext = persistentContainer.newBackgroundContext() // Creates a fetch request to get all the dogs saved let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Dog") // Creates `asynchronousFetchRequest` with the fetch request and the completion closure let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { asynchronousFetchResult in // Retrieves an array of dogs from the fetch result `finalResult` guard let result = asynchronousFetchResult.finalResult as? [Dog] else { return } // Dispatches to use the data in the main queue DispatchQueue.main.async { // Do something } } do { // Executes `asynchronousFetchRequest` try privateManagedObjectContext.execute(asynchronousFetchRequest) } catch let error { print("NSAsynchronousFetchRequest error: \(error)") } However, note that, the above code will unfortunately cause fatal error, if I were to enable flag -com.apple.CoreData.ConcurrencyDebug 1. So far, I do not have a good solution to such. For more detail, please refer to https://developer.apple.com/forums/thread/697718 ? May I know, what is the difference among newBackgroundContext + FetchRequest vs newBackgroundContext + NSAsynchronousFetchRequest? How should I choose one over another? Thank you.
0
0
1.6k
Dec ’21
Does anyone know where I can get an official StoreKit 1 example code from Apple?
We wish to implement in-app purchase for our app, which is currently targeting 14.3 The latest StoreKit 2, is only available for iOS 15 We wish to study how should we implement such, by looking at official code example of StoreKit 1. However, the current posted code at Apple site, is only available for StoreKit 2 - https://developer.apple.com/documentation/storekit/in-app_purchase/implementing_a_store_in_your_app_using_the_storekit_api Does anyone have any idea, where can I get the official StoreKit 1 code example, from Apple? Thanks.
0
0
311
Apr ’22
Is there a way to update supplementary view efficiently, analogy to update items efficiently using reconfigureItems?
In iOS15, we have an efficient way to update items cell, by using reconfigureItems. Here's the code snippet to perform such efficient update. Update items cell efficiently using reconfigureItems private func reconfigureRecordingRow(_ recording: Recording) { var snapshot = dataSource.snapshot() snapshot.reconfigureItems([recording]) dataSource.apply(snapshot) } private func makeDataSource() -> DataSource { let dataSource = DataSource( collectionView: collectionView, cellProvider: { [weak self] (collectionView, indexPath, anyHashable) -> UICollectionViewCell? in guard let self = self else { return nil } guard let recordingCell = collectionView.dequeueReusableCell( withReuseIdentifier: "recording", for: indexPath) as? RecordingCell else { return nil } When reconfigureRecordingRow is called, cellProvider's function will be executed. collectionView.dequeueReusableCell is able to re-use existing UICollectionViewCell, without constructing new UICollectionViewCell However, I was wondering, how can I achieve a similar efficiency, if I have a section, with header supplementary view, and without any item? For instance Not able to update supplementary view efficiently private func reloadAttachmentRow() { var snapshot = dataSource.snapshot() let sectionIdentifiers = snapshot.sectionIdentifiers if sectionIdentifiers.contains(.attachment) { snapshot.reloadSections([.attachment]) } else { snapshot.insertSections([.attachment], beforeSection: .title) } dataSource.apply(snapshot) } dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in guard let self = self else { return nil } if kind == UICollectionView.elementKindSectionHeader { let section = indexPath.section let sectionIdentifier = self.sectionIdentifier(section) switch sectionIdentifier { case .attachment: guard let collageViewHeader = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: "attachment", for: indexPath) as? CollageViewHeader else { return nil } When reloadSections is called, dataSource.supplementaryViewProvider will be executed. As per my testing, collectionView.dequeueReusableSupplementaryView will return a new instance of UICollectionReusableView each time. As a result, I can visually observe the entire section is "flickering", when reloadAttachmentRow is called. I was wondering, how can we update supplementary view efficiently?
0
0
931
May ’22
Is there a way to know a file is not found in iCloud Document by using NSMetadataQuery and NotificationCenter?
I'm using NSMetadataQuery and NotificationCenter, to perform file downloading from iCloud. Construct NSMetadataQuery with predicate NSPredicate(format: "%K == %@", NSMetadataItemFSNameKey, filename) Observe NSMetadataQueryDidUpdate & NSMetadataQueryDidFinishGathering using NotificationCenter. Check file status NSMetadataUbiquitousItemDownloadingStatusKey. If the file is up-to-date, copy the file to destination directory, and jump to step 6. Perform FileManager.default.startDownloadingUbiquitousItem Receive file downloading status in NSMetadataQueryDidUpdate callback. If the file is up-to-date, copy the file to destination directory, and jump to step 6. Perform cleanup by removing all observers. If file is not available in iCloud, no notification received. We wish, even if the file doesn't exist, we will still be notified, so that we have chance to perform cleanup (step 6) Here's the code snippet to perform iCloud download. DownloadManager.swift class DownloadManager { static let INSTANCE = DownloadManager() var downloaders = [iCloudDocumentDownloader]() private init() { } func append(filename: String, destinationDirectory: URL) { let downloader = iCloudDocumentDownloader(filename: filename, destinationDirectory: destinationDirectory) downloaders.append(downloader) } func removeAll(_ downloader: iCloudDocumentDownloader) { downloaders.removeAll{$0 === downloader} } } iCloudDocumentDownloader.swift class iCloudDocumentDownloader { private let filename: String private let destinationDirectory: URL private let metadataQuery = NSMetadataQuery() private static let operationQueue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.name = "com.yocto.wenote.operationQueueForiCloudDocument" operationQueue.maxConcurrentOperationCount = 1 operationQueue.qualityOfService = .userInitiated return operationQueue }() deinit { NotificationCenter.default.removeObserver(self) } private func bye() { DownloadManager.INSTANCE.removeAll(self) } init(filename: String, destinationDirectory: URL) { self.filename = filename self.destinationDirectory = destinationDirectory metadataQuery.operationQueue = iCloudDocumentDownloader.operationQueue metadataQuery.predicate = NSPredicate(format: "%K == %@", NSMetadataItemFSNameKey, filename) metadataQuery.searchScopes = [ NSMetadataQueryUbiquitousDocumentsScope ] NotificationCenter.default.addObserver(self, selector: #selector(didUpdate), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: metadataQuery) NotificationCenter.default.addObserver(self, selector: #selector(didFinishGathering), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: metadataQuery) metadataQuery.start() } @objc func didUpdate(_ notification: Notification) { guard let metadataQuery = notification.object as? NSMetadataQuery else { return } metadataQuery.enumerateResults { [weak self] (item: Any, index: Int, stop: UnsafeMutablePointer<ObjCBool>) in guard let self = self else { return } guard let metadataItem = item as? NSMetadataItem else { return } guard let status = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String else { return } guard let url = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL else { return } if status == NSMetadataUbiquitousItemDownloadingStatusCurrent { if !destinationDirectory.createCompleteDirectoryHierarchyIfDoesNotExist() { self.bye() // Early return. return } let destinationURL = destinationDirectory.appendingPathComponent(filename, isDirectory: false) do { try FileManager.default.copyItem(at: url, to: destinationURL) } catch { error_log(error) } self.bye() } else if let error = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingErrorKey) as? NSError { error_log(error) self.bye() } else { } } } @objc func didFinishGathering(_ notification: Notification) { guard let metadataQuery = notification.object as? NSMetadataQuery else { return } metadataQuery.enumerateResults { [weak self] (item: Any, index: Int, stop: UnsafeMutablePointer<ObjCBool>) in guard let self = self else { return } guard let metadataItem = item as? NSMetadataItem else { return } guard let status = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String else { return } guard let url = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL else { return } if status == NSMetadataUbiquitousItemDownloadingStatusCurrent { if !destinationDirectory.createCompleteDirectoryHierarchyIfDoesNotExist() { self.bye() // Early return. return } let destinationURL = destinationDirectory.appendingPathComponent(filename, isDirectory: false) do { try FileManager.default.copyItem(at: url, to: destinationURL) } catch { error_log(error) } self.bye() } else if let error = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingErrorKey) as? NSError { error_log(error) self.bye() } else { do { try FileManager.default.startDownloadingUbiquitousItem(at: url) } catch { error_log(error) self.bye() } } } } }
0
0
1.3k
Jun ’22
How should we handle history tracking transactions purging, if we allow users to enable/ disable CloudKit sync?
Due to privacy concern, I believe some users prefer their data not to sync with iCloud. Hence, we plan to provide a switch option within the app, so that user can choose, whether to sync their data with iCloud. Such feature can be achieved via the implementation suggested at https://developer.apple.com/forums/thread/118924?login=true However, once user chooses to disable sync with iCloud, we should not perform purging on history tracking transactions. Reason is that, if few months later/ few years later, user decides to turn on sync with iCloud again, lack of sufficient history tracking transactions, will cause sync with iCloud operation fail. This is causing a dilemma. If we never clean the history tracking transactions, will it cause disk full issue? If that is so, may I know, what is the correct way, to handle history tracking transactions purging, if we intent to provide CloutKit sync enable/ disable feature? Thank you.
0
1
559
Jun ’22
How should we handle history tracking transactions purging, if we allow users to enable/ disable CloudKit sync?
Due to privacy concern, we wish to provide a toggle switch, so that user has the freedom, to choose whether to sync CoreData with iCloud. We can implement such a feature by following suggestion from https://developer.apple.com/forums/thread/118924?login=true However, we should not perform history tracking transactions purging, once user disables CloudKit sync Reason is that, if few months later/ few years later, user decides to turn on CloudKit sync again, lack of sufficient history tracking transactions, will cause CloudKit sync fail. This is causing a dilemma. If we never clean the history tracking transactions, will it cause disk full issue? If that is so, may I know, what is the correct way to handle history tracking transactions purging, if we allow users to enable/ disable CloudKit sync? Thank you.
0
0
724
Jun ’22
The longer we use CloudKit enabled CoreData, will it take longer time to sync a fresh new device?
After using CloudKit enabled CoreData app for a while, I have the following observation. I have been actively use a CloudKit enabled CoreData app, for 2 weeks. Then, I get another fresh device. Once I hook up the fresh device to internet, based on the UI updating sequence, it seems like CoreData is replaying the entire transaction history, starting from 2 weeks ago till now. So, my questions is, if we are using the CloudKit enabled CoreData app for 5 years. Then, when I get a fresh new device, will the CoreData replaying the entire transaction history starting from 5 years ago? Isn't that is highly inefficient, and cause the new device sync extremely slow? Is that is so, how can we avoid such slowness, when user is using the app long time enough, and then decide to sync to a newly bought device? Thank you
0
1
627
Jul ’22
PHPickerConfiguration will produce wrong ordering sometimes during multi selection.
We notice PHPickerConfiguration will produce wrong ordering sometimes, during multi selection. Our app is targeting iOS 15, and the test is run on simulator iOS 15.5 Here the video to demonstrate the issue https://youtu.be/RrhqFuB2kqs Please note that, the standing cat picture suppose to be the 4th order. By looking at the console and UI outcome, it is mistakenly returned as 3rd order. Here's the code which show func chooseImage() { var configuration = PHPickerConfiguration() configuration.selectionLimit = 0 // https://developer.apple.com/documentation/photokit/phpickerconfiguration/selection configuration.selection = .ordered // GIF exclusion is still not supported - https://developer.apple.com/forums/thread/687415 configuration.filter = .images let picker = PHPickerViewController(configuration: configuration) picker.delegate = self present(picker, animated: true) } and here's the code which prints out the ordering of images. extension NewNoteViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) guard !results.isEmpty else { return } var displayErrorOnce = false for result in results { result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { [weak self] (url, error) in precondition(!Thread.isMainThread) guard let self = self else { return } guard let url = url else { return } print(">>>> url \(url)") // ... } } } } This problem happens randomly. May I know, is there any workaround for this issue? Thank you.
Topic: UI Frameworks SubTopic: UIKit Tags:
0
0
655
Aug ’22