Hi all,
Recently, I notice that by using UICollectionViewCompositionalLayout, we can achieve same outcome as UICollectionViewFlowLayout, but with less code.
An advantage of using UICollectionViewCompositionalLayout is that, it requires less code, as shown in this discussion thread
https://stackoverflow.com/a/51231881/72437
I was wondering, in all our upcoming new code, should we always prefer UICollectionViewCompositionalLayout over UICollectionViewFlowLayout? As, it is more powerful and required less code to implement.
Is there any reason we want to use UICollectionViewFlowLayout in our new code?
Thanks.
Post
Replies
Boosts
Views
Activity
Most examples given are only capable to detect data movement, insertion, deletion, but not content modification.
class WiFiController {
		struct Network: Hashable {
				let name: String
				let identifier = UUID()
				func hash(into hasher: inout Hasher) {
						hasher.combine(identifier)
				}
				static func == (lhs: Network, rhs: Network) -> Bool {
						return lhs.identifier == rhs.identifier
				}
		}
This is because func == only compare identifier, but not content.
Even if I modify the class to
class WiFiController {
		struct Network: Hashable {
				let name: String
				let identifier = UUID()
				func hash(into hasher: inout Hasher) {
						hasher.combine(name, identifier)
				}
				static func == (lhs: Network, rhs: Network) -> Bool {
						return lhs.name == rhs.name && lhs.identifier == rhs.identifier
				}
		}
The outcome is not perfect still. As, it still fail to detect when an item is being modified and being moved at the same time.
Isn't a good Diff library should have the ability to
Compare identify to detect movement, insertion, deletion
Compare content to detect modification
Currently, func == can only either compare identify, or compare content, but not both!
I posted my finding here - https://stackoverflow.com/questions/64293965/is-it-possible-to-have-diffabledatasource-correctly-handle-item-move-item-modif
Is there any plan to further improve DiffableDataSource, so that we can
Compare identify to detect movement, insertion, deletion
Compare content to detect modification
Thanks.
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?
This problem has buzzed me quite a while. So far, I still haven't founded a good solution.
Currently, I have an entity with a Bool column named pinned. I use it as the sectionNameKeyPath for NSFetchedResultsController.
So, my UICollectionView will always have 1 sections (All pinned = true, or All pinned = false), or 2 sections (Some pinned = true, and some pinned = false)
When I toggle the pinned value from true to false, or false to true, I expect FRC shall fire a "move" callback. (Due to item has moved to a new section) This happen most of the time.
However, sometimes, randomly, instead of firing "move" callback, FRC will fire "update" callback.
When such incident happen, I will exam the content of fetchedResultsController.sections. Then, I will notice the entity item stays in wrong section.
My FRC looks pretty straightforward.
lazy var fetchedResultsController: NSFetchedResultsControllerNSPlainNote = {
// Create a fetch request for the Quake entity sorted by time.
let fetchRequest = NSFetchRequestNSPlainNote(entityName: "NSPlainNote")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "pinned", ascending: false)
]
// Create a fetched results controller and set its fetch request, context, and delegate.
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: CoreDataStack.INSTANCE.persistentContainer.viewContext,
sectionNameKeyPath: "pinned",
cacheName: nil
)
controller.delegate = fetchedResultsControllerDelegate
// Perform the fetch.
do {
try controller.performFetch()
} catch {
fatalError("Unresolved error \(error)")
}
return controller
}()
This is how I update the pinned column using background thread.
func updatePinned(_ objectID: NSManagedObjectID, _ pinned: Bool) {
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
backgroundContext.perform {
let nsPlainNote = try! backgroundContext.existingObject(with: objectID) as! NSPlainNote
nsPlainNote.pinned = pinned
RepositoryUtils.saveContextIfPossible(backgroundContext)
}
}
I am not really sure, whether this can caused by my background thread.
As, if I replace the backgroundContext with viewContext, I haven't observed the random problem so far.
But, even so, I am not confident to conclude using backgroundContext is the culprit to this problem.
The setup of my background thread is also pretty straightforward. I cannot see how it can went wrong.
class CoreDataStack {
public static let INSTANCE = CoreDataStack()
private init() {
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "***")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// TODO:
container.viewContext.automaticallyMergesChangesFromParent = true
//container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//container.viewContext.undoManager = nil
//container.viewContext.shouldDeleteInaccessibleFaults = true
return container
}()
lazy var backgroundContext: NSManagedObjectContext = {
let backgroundContext = persistentContainer.newBackgroundContext()
// TODO:
//backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//backgroundContext.undoManager = nil
return backgroundContext
}()
}
I was wondering, have anyone of you encounter similar problem when trying to utilize sectionNameKeyPath or FRC? Do you know what? Do you have any workaround/ solution for that?
Thank you!
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.
The following simple function will cause Xcode 12E262 to have "Abort: trap 6" during compilation.
import UIKit
import CoreData
class ViewController: UIViewController {
func xyz() {
let container = NSPersistentContainer(name: "xyz")
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "xyz")
let batchUpdateResult = try! container.viewContext.execute(batchUpdateRequest) as? NSBatchUpdateResult
guard let batchUpdateResult = batchUpdateResult else { return }
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
We will not observe "Abort: trap 6", if under Build Settings, we are using "Optimize for Speed" in Debug, instead of "No Optimization"
We can also avoid "Abort: trap 6", if we change the following code
guard let batchUpdateResult = batchUpdateResult else { return }
to
guard let batchUpdateResult2 = batchUpdateResult else { return }
May I know, why is it so?
A simpler code example to reproduce problem, without CoreData would be
import UIKit
class ViewController: UIViewController {
func getAny() throws -> Any? {
return nil
}
func xyz() {
let name = try! getAny() as? UIViewController
guard let name = name else { return }
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Introduction
I expect when I perform "update" on 1 of the CoreData entity content, corresponding UICollectionViewCell should updated seamlessly.
But, the thing doesn't work as expected.
After debugging, I notice that instead of updating visible UICollectionViewCell on screen, an invisible offscreen UICollectionViewCell has been created and all update operations are performed on the invisible UICollectionViewCell?!
Simple hookup between NSFetchedResultsController and Diffable Data Source
The hookup is pretty straightforward
extension ViewController: NSFetchedResultsControllerDelegate {
func controller(_ fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshotReference: NSDiffableDataSourceSnapshotReference) {
guard let dataSource = self.dataSource else {
return
}
let snapshot = snapshotReference as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
print("dataSource.apply")
dataSource.apply(snapshot, animatingDifferences: true) {
}
}
}
Simple data structure
import Foundation
import CoreData
extension NSTabInfo {
@nonobjc public class func fetchRequest() -> NSFetchRequest<NSTabInfo> {
return NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
}
@NSManaged public var name: String?
@NSManaged public var order: Int64
}
extension NSTabInfo : Identifiable {
}
Simple data source to update cell
private func initDataSource() {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { [weak self] (collectionView, indexPath, objectID) -> UICollectionViewCell? in
guard let self = self else { return nil }
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "cell",
for: indexPath) as? CollectionViewCell else {
return nil
}
guard let nsTabInfo = self.getNSTabInfo(indexPath) else { return nil }
cell.label.text = nsTabInfo.name
print("Memory for cell \(Unmanaged.passUnretained(cell).toOpaque())")
print("Content for cell \(cell.label.text)\n")
return cell
}
)
self.dataSource = dataSource
}
Updating code doesn't work
We perform update on the 1st cell using the following code
@IBAction func updateClicked(_ sender: Any) {
let backgroundContext = self.backgroundContext
backgroundContext.perform {
let fetchRequest = NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "order", ascending: true)
]
do {
let nsTabInfos = try fetchRequest.execute()
if !nsTabInfos.isEmpty {
// Perform update on the first cell
nsTabInfos[0].name = "\(Int(nsTabInfos[0].name!)! + 1)"
if backgroundContext.hasChanges {
try backgroundContext.save()
}
}
} catch {
print("\(error)")
}
}
}
Nothing is changed on the screen. However, if we look at the print output. We can see the initial content ("0") of the 1st cell is updated to "1". But, all of these are being done in an invisible same instance cell.
dataSource.apply
Memory for cell 0x0000000138705cd0
Content for cell Optional("1")
Memory for cell 0x0000000138705cd0
Content for cell Optional("1")
Memory for cell 0x0000000138705cd0
Content for cell Optional("2")
Memory for cell 0x0000000138705cd0
Content for cell Optional("3")
Ordering work without issue
Changing the ordering of the cell works as expected. The following is the code for charging ordering.
@IBAction func moveClicked(_ sender: Any) {
let backgroundContext = self.backgroundContext
backgroundContext.perform {
let fetchRequest = NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "order", ascending: true)
]
do {
let nsTabInfos = try fetchRequest.execute()
for (index, element) in nsTabInfos.reversed().enumerated() {
element.order = Int64(index)
}
if backgroundContext.hasChanges {
try backgroundContext.save()
}
} catch {
print("\(error)")
}
}
}
Do you have idea why such problem occur? I prefer not to have collectionView.reloadData as workaround, as it will create more issue (like resetting scroll position, cell press state, ...)
I posted a complete demo at https://github.com/yccheok/problem-update-frc-diffable
Thank you
Currently, I have the following core data backed collection view, which enable users to perform add/ delete/ modify/ reordering.
It will be much more convenient to achieve what I, if I let UICollectionViewCell, to hold its corresponding NSManagedObject. So that I can each perform add/ delete/ modify/ reordering.
I was wondering, should I mark the NSManagedObject as weak, to avoid from interferencing with how CoreData manage its memory?
Using weak
class TabInfoSettingsCell: UICollectionViewCell {
// NSManagedObject. Use weak, as we do not want to inteference with how CoreData manage its memory.
private weak var nsTabInfo : NSTabInfo?
Use strong
Or, such concern is invalid. We can simply use strong
class TabInfoSettingsCell: UICollectionViewCell {
// NSManagedObject.
private var nsTabInfo : NSTabInfo?
Use struct
Or, such concern is valid and using weak is unsafe (as it can become nil in some edge case?). We can use a struct, which contains everything including objectID, but excluding NSManagedObjectContext.
class TabInfoSettingsCell: UICollectionViewCell {
// struct. Contains everything including objectID, but excluding NSManagedObjectContext.
private var tabInfo : TabInfo?
This come with the cost of having to create a struct out from NSManagedObject each time, in cellForItemAt.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
tabInfoSettingsCell.update(nsTabInfo.toTabInfo())
...
}
May I know, which is the correct design, as far as safe memory management is concerned?
NSAsynchronousFetchRequest seems like an answer to read large amount of data from CoreData, without blocking main thread.
But, it is lacking some key features, provided by NSFetchedResultsController. The key features are :-
In NSFetchedResultsControllerDelegate didChange and controllerDidChangeContent, we automatically receive a complete change information of persistence store. This enables us to implement the animation of add/delete/move/modify in collection view.
Always listen to DB changes. Whenever there is changes being written into the persistence store, NSFetchedResultsControllerDelegate didChange and controllerDidChangeContent will be triggered automatically. This enables us to ensure our UI is in sync with DB.
But, NSAsynchronousFetchRequest doesn't come with NSAsynchronousFetchedResultsController :(
I was wondering, if you were using NSAsynchronousFetchRequest, how do you implement
Animation changes on collection view?
Always listen to DB change?
My initial thought for animation changes on collection view, it seems we can utilise UICollectionViewDiffableDataSource.
But, I notice it might be highly memory inefficient to do so. We need to keep ALL NSManagedObject in memory, fire all faults during comparison. It looks like we will run out of memory, if there are many rows.
May I know, how do you achieve the following features, if you ever apply NSAsynchronousFetchRequest in production app?
Animation changes on collection view?
Always listen to DB change?
Thanks.
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?
I am using "checkmark.circle.fill"
I wish to have a blue filled circle with the checkmark being solid black.
For the blue filled circle, I can choose the tinted color as blue.
But, the default SF Symbols "checkmark.circle.fill", its checkmark is transparent.
It looks as follow
So, I try to use "SF Symbols beta", to create custom icon.
During editing, the thing seems like what I want
Blue filled circle
Solid black color for checkmark
Then, I use "Export Symbol...", using Template Version 2.0 (XCode cannot recognise Template Version 3.0)
It seems like the checkmark fall back to transparent color.
When I import it to XCode
It seems like the "solid black color" on the checkmark is gone. When I use this custom icon in my app, it gave same result as "checkmark.circle.fill" (the checkmark still remain as transparent)
Doesn't anyone know how to resolve this issue? Thanks.
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.
Currently, if we perform multiple selection on PHPickerViewController, a tick icon is being used to indicate selection.
https://i.imgur.com/KCaSF9p.png
However, such information doesn't convey information for the selection ordering.
Is it every possible, to display the ordering number? This is an example from 3rd party library - https://github.com/mikaoj/BSImagePicker
As you can see, the 3rd party library is using 1,2,3... to convey selection ordering information
https://i.imgur.com/YoQVS4v.png
Can we achieve the same behavior in PHPickerViewController? We prefer to use PHPickerViewController, because it doesn't need to request permission to access photo library.
Thanks.
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?
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.