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()
}
}
}
}
}
Post
Replies
Boosts
Views
Activity
Every of our data row, contains an unique uuid column.
Previously, before adopting CloudKit, the uuid column has a unique constraint. This enables us to prevent data duplication.
Now, we start to integrate CloudKit, into our existing CoreData. Such unique constraint is removed. The following user flow, will cause data duplication.
Steps to cause data duplication when using CloudKit
Launch the app for the first time.
Since there is empty data, a pre-defined data with pre-defined uuid is generated.
The pre-defined data is sync to iCloud.
The app is uninstalled.
The app is re-installed.
Launch the app for the first time.
Since there is empty data, a pre-defined data with pre-defined uuid is generated.
Previous old pre-defined data from step 3, is sync to the device.
We are now having 2 pre-defined data with same uuid! :(
I was wondering, is there a way for us to prevent such duplication?
In step 8, we wish we have a way to execute such logic before written into CoreData
Check whether such uuid exists in CoreData. If not, write to CoreData.
If not, we will pick the one with latest update date, then overwrite
the existing data.
I once try to insert the above logic into https://developer.apple.com/documentation/coredata/nsmanagedobject/1506209-willsave . To prevent save, I am using self.managedObjectContext?.rollback(). But it just crash.
Do you have any idea, what are some reliable mechanism I can use, to prevent data duplication in CoreData CloudKit?
Additional info:
Before adopting CloudKit
We are using using the following CoreData stack
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
precondition(Thread.isMainThread)
let container = NSPersistentContainer(name: "***", managedObjectModel: NSManagedObjectModel.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
}()
Our CoreData data schema has
Unique constraint.
Deny deletion rule for relationship.
Not having default value for non-null field.
After adopting CloudKit
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
precondition(Thread.isMainThread)
let container = NSPersistentCloudKitContainer(name: "***", managedObjectModel: NSManagedObjectModel.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
}()
We change the CoreData data schema to
Not having unique constraint.
Nullify deletion rule for relationship.
Having default value for non-null field.
Based on a feedback of a Developer Technical Support engineer from https://developer.apple.com/forums/thread/699634?login=true , hen mentioned we can
Detecting Relevant Changes by Consuming Store Persistent History
Removing Duplicate Data
But, it isn't entirely clear on how it should be implemented, as the github link provided in broken.
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?
We start a voice recording via
self.avAudioRecorder = try AVAudioRecorder(
url: self.recordingFileUrl,
settings: settings
)
self.avAudioRecorder.record()
At certain point, we will stop the recording via
self.avAudioRecorder.stop()
I was wondering, is it safe to perform file copy on self.recordingFileUrl immediately, after self.avAudioRecorder.stop()?
Is all recording data has been flushed to self.recordingFileUrl and self.recordingFileUrl file is closed properly?
Since StoreKit2
Transaction.currentEntitlements
will able to return us user current owned purchased, during app startup.
If that is the case, is it still necessary for developer to provide a restore button?
If we still need to provide a restore button, what should the restore button do and what API should it call?
Thanks
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.
I still fail to understand, what is the problem NSPersistentHistoryTransaction is trying to solve, in the CoreDataCloudKitDemo WWDC 2019 "Using Core Data with CloudKit"
https://github.com/software123inc/CoreDataCloudKitDemo/blob/master/CoreDataCloudKitDemo/DataProvider/CoreDataStack.swift#L161
I want to see, what problem will occur, if processPersistentHistory is not executed.
By making the processPersistentHistory empty, I try to do the following testing.
Run 2 simulators simultaneously in the same machine.
Add an item to simulator A.
Since, there is no way for simulator B to receive push notification, I press the home button for simulator B.
In simulator B, I tap on the app icon to launch the app again.
In simulator B, I can observe controllerDidChangeContent is being called. My guess is that, because the backed SQLite is seamlessly updated by CloudKit background task, NSFetchedResultController will be notified the SQLite DB change, and subsequently update the UI. Check the "Download CloudKit Changes into Core Data" of https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/syncing_a_core_data_store_with_cloudkit
In simulator B, due to controllerDidChangeContent is being triggered correctly, I can observe the UI change perform by NSFetchResultController without issue.
Hence, I am not clear, on what problem processPersistentHistory is trying to solve in the demo code.
May I know what kind of test case I can perform, to understand the problem solved by processPersistentHistory?
Based on "Integrate Store Changes Relevant to the Current View"
https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/syncing_a_core_data_store_with_cloudkit
Your app receives remote change notifications when the local store updates from CloudKit. However, it’s unnecessary to update your UI in response to every notification, because some changes may not be relevant to the current view.
Analyze the persistent history to determine whether the changes are relevant to the current view before consuming them in the user interface. Inspect the details of each transaction, such as the entity name, its updated properties, and the type of change, to decide whether to act.
For more information about persistent history tracking, see Consuming Relevant Store Changes.
This part is getting confusing. Our NSFetchedResultController is receiving relevant entity change event due to SQLite, and subsequently able to update the UI correct. If that is so, why do we still need persistent history?
Is using CoreData + CloudKit to store text notes and iCloud Document to store note image attachments as image files a good design approach?
Currently, I have almost finished implementing the app to store text notes as Core Data and note image attachments as image files.
I like to provide iCloud storage support to the app.
I come across a few app examples
https://www.raywenderlich.com/13219461-getting-started-with-core-data-and-cloudkit
https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud
Both examples are using CoreData + CloudKit to store the image as SQLite blob binary data which CoreData will perform such task automatically)
I'm aware that when storing the binary image into CoreData, CoreData is smart enough to choose either to store it as a binary blob in SQLite, or an external file.
However, I am a little skeptical about such an approach
We are using Kingfisher for smooth image loading in the collection view. If the image data are not in a regular flat-file, how can we integrate CoreData's blob data with Kingfisher?
Storing images in CoreData seems like a black box. If something goes wrong in between, it is hard to debug where and how goes wrong.
We like to provide alternative cloud storage (using cloud S3 storage to store SQLite files and multiple images) for the app. So, saving the image as flat files will make such an effort easier.
Some related discussion on storing the image in DB vs flat files - https://stackoverflow.com/questions/3748/storing-images-in-db-yea-or-nay
I would prefer
Only use CoreData + CloudKit to store the text note and file path.
The image file will store in both the app folder and the iCloud document folder (so that it syncs seamlessly to iCloud). If the required images are not in the app folder (App uninstall, then re-install), the app will try to copy the image file from the iCloud document
I was wondering, anyone of you has tried such a design in your app? Do you find it is a good approach based on your experience?
Thanks.
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.
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.
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 ?
Currently, we try to place multiple UITextViews in UICollectionView.
To ensure UICollectionView's cell height, will adjust based on the dynamic content of UITextView, this is what we have done.
Disable scrolling in UITextView.
Use .estimated(CGFloat(44)) for UICollectionViewCompositionalLayout
Whenever there is text change, call collectionView.collectionViewLayout.invalidateLayout(). This is a critical step to ensure cell height will adjust accordingly.
However, calling collectionView.collectionViewLayout.invalidateLayout() does come with a side effect.
The current scroll position of UICollectionView will be reset, after calling collectionView.collectionViewLayout.invalidateLayout().
Does anyone know how can I
Prevent unwanted auto scroll position resetting?
UICollectionView will auto scroll to current cursor position, so that what is current being typed is visible to user?
The code to demonstrate this problem is as follow - https://github.com/yccheok/checklist-demo
Here's the code snippet, on what was happening as typing goes on
func textViewDidChange(_ checklistCell: ChecklistCell) {
//
// Critical code to ensure cell will resize based on cell content.
//
// (But comes with a side effect which will reset scroll position.)
self.collectionView.collectionViewLayout.invalidateLayout()
//
// Ensure our checklists data structure in sync with UI state.
//
guard let indexPath = collectionView.indexPath(for: checklistCell) else { return }
let item = indexPath.item
let text = checklistCell.textView.text
self.checklists[item].text = text
}
Side Note
Note, the closest solution we have came across is posted at https://medium.com/@georgetsifrikas/embedding-uitextview-inside-uitableviewcell-9a28794daf01
In UITableViewController, during text change, the author is using
DispatchQueue.main.async {
tableView?.beginUpdates()
tableView?.endUpdates()
}
It works well. But, what is the equivalent solution for UICollectionView?
We can't try out with self.collectionView.performBatchUpdates, as our solution is built around Diffable Data Source.
I have tried
DispatchQueue.main.async {
self.collectionView.collectionViewLayout.invalidateLayout()
}
That doesn't solve the problem either.
Thank you.
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?
Currently, we are using PHPickerViewController + CGImage for efficient memory usage consideration - https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/
However, we are getting "unsupported file format 'org.webmproject.webp'" error, while trying to save CGImage in webp format.
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard !results.isEmpty else { return }
for result in results {
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { (url, error) in
guard let url = url else { return }
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: 512
]
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) else { return }
let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
//
// No issue in dealing with UTType.jpeg.identifier and UTType.png.identifier.
// destUrl is type URL.
//
guard let destination = CGImageDestinationCreateWithURL(destUrl as CFURL, UTType.webP.identifier, 1, nil) else { return }
CGImageDestinationAddImage(destination, image!, nil)
CGImageDestinationFinalize(destination)
}
}
We face no issue in saving CGImage in UTType.jpeg.identifier format and UTType.png.identifier format.
May I know how can we save CGImage in webp format without issue? Thank you.
For PHPickerViewController, we know we can perform simple filtering by
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
But, how about we only want to show images with format JPG & PNG, but excluding GIF? This is because our app doesn't support GIF.
Is it possible to do so?