Ask Apple provides developers with an opportunity to directly communicate with Apple engineers outside of WWDC. This article summarizes some of the Q&A related to Core Data from this event, and adds a bit of personal insight. This is Part 2 of the series.
Q&A
Derived Attributes
Q: Hi, could you share more examples of syntax for “Derived Attributes” besides .@count? Thank you in advance.
A: There are some explanations in the documentation of NSDerivedAttributeDescription.
The value of a derived attribute is derived from one or more other attribute values. In simple terms, this means that when a managed object instance is created or modified, Core Data automatically generates a value for the derived attribute. The value is calculated based on a pre-set derived expression and other attribute values. For more details, please refer to the article How to Use Derived and Transient Attributes in Core Data.
Synchronization of Main App and Extension App Data
Q: I have a main application and an extension app, both of which read from the same Core Data database. However, when I make changes in the main app, my extension app does not see the changes until it is restarted. Should I simply solve this problem by calling NSManagedObjectContext.refreshAllObjects, or do I have to use the more difficult method of enabling history tracking, detecting remote changes, merging changes from transactions, and cleaning transaction history?
A: You should use the NSPersistentStoreRemoteChangeNotificationOptionKey option on the NSPersistentStore to enable remote change notifications. The Persistent History section of this method helps ensure that you do not repeatedly retrieve data from the database and only refresh when the data you need has changed.
This is another question about persistent history tracking. Apple should really provide clearer documentation for this feature. The Persistent History Tracking Kit can reduce your development workload.
How to update the Spotlight index of Core Data that has been deleted through the file system
Q: Is it possible to specify the storage location of the Spotlight index when indexing the content of Core Data? I have a document-based application, where some files and the sqlite file created by Core Data are packaged together. If a user deletes a document outside of the application, for example in Finder, I want the Spotlight index to be deleted with it. So, if the index can be stored in the package folder, it can solve this problem. Is there any way to handle this properly?
A: It sounds like a valuable feature suggestion, and we encourage you to submit a feedback request! Currently, calling APIs from the application is the only way to remove items from the index.
Currently, Spotlight cannot handle this situation. If a user deletes these documents through the file system (without going through the application), the only way to remove the corresponding index is for the application to know which document was deleted and then delete the index belonging to that document through CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers:). Otherwise, the indexes can only disappear from Spotlight automatically after they expire. Refer to Showing Core Data data from your app in Spotlight for more information.
How is the performance of @FetchRequest?
Q: Is @FetchRequest more performant than fetching data in the ViewModel’s initializer through a fetchRequest?
A: The cost of @FetchRequest depends on how much the results change after the initial data fetch, while the cost of manually refetching depends on the total number of results. @FetchRequest wraps an NSFetchedResultsController, which has no special logic of its own.
Ways to Retrieve Data
Q: I want to know which way is a better approach?
Load CoreData data all at once in the application and save it in a local variable
Use multiple FetchRequests
Currently, I am using UICalendarView in SwiftUI and retrieving data from CoreData. Can I load data for each date in the calendar through a fetchRequest in the calendarView(_:decorationFor:) method (referring to the second approach)? Or should I just use one fetchRequest and save the data locally, then access it through the above method (referring to the first approach)? I want to know what the best practice is here. Thank you!
A: Generally, different views frequently use different retrieval requests. For content such as date ranges, you may want to retrieve them in batches. Lengthy I/O will cause your view’s drawing to freeze. Short I/O will cause you to issue too many separate requests, which greatly reduces efficiency. The Core Data Performance tool in Instruments can help investigate what is the best solution for you.
UICalendarView is a control added in iOS 16. MultiDatePicker only implements part of its functionality. UICalendarView allows developers to add decoration for specific dates, and the usage can be found in the article Getting UIKit’s UICalendarView from iOS 16 fully functioning in a SwiftUI app.
Retrieving NSAttributedString
Q: I need to store NSAttributedString in a database and be able to search for any text within the attributed string. Is creating two separate attributes, one containing the plain text string and the other containing the attributed string as a Transformable data, the best method? Is there another way to reduce the amount of data stored without using two attributes?
A: You are using the currently recommended approach. Additionally, plain text attributes can be indexed by Spotlight, making them searchable by the system.
Generating plain text corresponding to the data for searching is a common approach. In some cases, even if the original content of the attribute is plain text, its search efficiency can be improved by generating a normalized version (a version that ignores case and diacritics).
Private Context
Q: How to configure the Core Data Stack so that users can continue to use the application while changes are being saved in the background?
A: NSPersistentContainer can meet your needs. You can use the viewContext to drive the UI interaction with users, and create a private context through the newBackgroundContext method to complete data saving on it. Please make sure to enable automatic merging changes on the viewContext so that changes on the backgroundContext can be automatically updated in the viewContext.
Whether you explicitly create a private context through newBackgroundContext or operate in a temporary private context through performBackgroundTask, you cannot use managed objects obtained from the viewContext in the private context. Managed objects are thread-bound. Even if they come from private contexts but belong to different contexts, they can only be used in their corresponding contexts. For more information, please refer to the article Several Tips on Core Data Concurrency Programming.
How to transition from UserDefaults to Core Data
Q: Currently, my app uses @AppStorage for data persistence. I have three main model objects that are stored on the current device. I want to switch to Core Data + CloudKit. How can I ensure that existing local @AppStorage data is safely transitioned to Core Data + CloudKit when existing users open the new app?
A: Upon launch, check if UserDefaults is not empty, then import into Core Data and delete local UserDefaults.
Asynchronous Saving
Q: Hi, is it necessary to use asynchronous when saving photo data to Core Data? Thanks!
A: Are you asking whether to use perform or performAndWait? I think it depends on your requirements and the desired UX experience.
perform and performAndWait correspond to asynchronous/synchronous operations within the context. For private contexts, even using performAndWait usually does not affect the UI.
Data Model Source Files (Class/Category/Manual)
Q: I would like some guidance on using Core Data model entity generation (Codegen). For example, when should I use Manual? I’m also unsure of the role of Category/Extension and how to choose between it and Class?
A: Most people use Class and add any custom methods they need in their own managed object extensions. However, in rare cases, such as when you need to add properties that must be declared in the class definition, you should use Category/Extension to give you control over the required class declaration.
In earlier versions of Xcode, using Class mode would generate two files, xxx+CoreDataClass.swift and xxx+CoreDataProperties.swift. xxx+CoreDataProperties.swift created the declarations for the entity’s properties through extensions, while xxx+CoreDataClass.swift was the class’s definition. Category/Extension mode only generated xxx+CoreDataProperties.swift, which means the user had to write the class definition themselves. However, in newer versions of Xcode (at least from version 13), there is no longer any difference between the two. Both will generate two files, and if the user adds custom properties to the class definition, Xcode will not overwrite them in the generated code. After generating the files, the Entity needs to be switched to Manual/None mode, otherwise Xcode will show a duplicate type declaration error (there is another Entity definition saved in the project), and if it still cannot be compiled, the build cache should be cleared.
Deleting Data Through CloudKit Dashboard
Q: I have a question related to syncing with Core Data and CloudKit. I noticed that when I delete a record from the CloudKit database using Safari client (through CloudKit Dashboard), the object still remains in the Core Data data store on the device. Is this intentional? How can I sync these changes between CloudKit manager and the device? Thank you!
A: It is not yet clear whether this workflow generates push notifications to NSPersistentCloudKitContainer. You should see the changes if you restart the application.
How to determine if synchronization is complete
Q: I am using NSPersistentCloudKitContainer and want to improve the user experience when downloading data from iCloud for the first time. Is there a way to tell the user when the data has finished syncing? I know about NSPersistentCloudKitContainer.eventChangedNotification, but it seems there is no real way to tell the app when syncing has completed.
A: Other devices can always make an infinite stream of new changes. The best you can do is to see which imports have been initiated and their completion status. You can file a feedback for an enhancement for an approximate answer.
Apple engineers did not give a positive answer to this question. The issue of sync progress has been raised multiple times at WWDC, developer forums, and this Ask Apple, but as of yet, there is no good solution. My suggestion is to give users a prompt in the app (especially during first launch) when syncing is in the import state (obtained through eventChangedNotification) by using dynamic elements such as ProgressView. The synchronization mechanism of Core Data with CloudKit is divided into multiple stages. That is to say, for the first synchronization, the import status is likely to appear multiple times (it is impossible to determine the end of the import by changes in import status). By providing import status prompts, you can alleviate user confusion to some extent. Additionally, you can consider using CloudKit API to query the number of data records in the cloud and compare them with the number of records already synced to the local device to obtain an approximate sync progress (this method is only applicable to simple data models with less complex relationships).
Optional nature of entity properties
Q: The optional nature of entity properties in Core Data behaves unexpectedly. If I mark a property as optional, it should not have a default value, and the managed property should always be an optional property. If I mark it as non-optional, it should require a default value, and the managed property should always be non-optional. Can we expect such a correction in the future (at least in new projects)?
A: The optional nature of Core Data predates the existence of Swift, allowing properties to be temporarily invalid. For example, when you create a new object with a string property, the initial value (without a default value) is nil, which is fine until the object is validated (usually at save time). In the case of optional scalars, Core Data is limited by the type restrictions expressible in Objective-C (for example, there is no type like Int64, and optional types can only be expressed as NSNumber). Submitting a feedback report on this idea may be your best option.
The optional nature of entity properties can be a confusing area for beginners in Core Data. Even if you mark an attribute (such as a string) as non-optional (with a default value set) in the model editor, the return value when retrieving the property value from the managed object will still be of type Optional. For the above problem, the following solutions can be considered: 1. For some types of attributes, you can manually define (or modify the Xcode generated subclass source file) to change the type String? in the generated code to String; 2. Declare a computed property of a non-optional value, and process the optional value attribute value in it; 3. Convert the managed object instance as a whole to a value type that is more friendly to SwiftUI views.
Manually Sorting Data
Q: In my application, users can rearrange items in a table view by dragging and dropping. My data model has a userOrder attribute of type Int16. What is a good way to save the new order of the data after the table view rows have been reordered?
A: Instead of storing the first object with userOrder == 0, the second object with userOrder == 1, and the third object with userOrder == 2, perhaps modeling it as an ordered relationship is a better choice. Let Core Data do the work for you. To manage an ordered relationship, Core Data calculates an object’s index in UInt16 space, exactly halfway between the previous and next objects. When the integer space is exhausted, it jumps over an object in either direction and evenly redistributes those objects.
Unfortunately, ordered relationships cannot be used when Core Data cloud sync is enabled. In this case, the approach the asker is currently using is the correct choice.
Filtering Relationship Data
Q: I found that using @FetchRequest in SwiftUI is a great way to bind the user interface with Core Data. However, when trying to achieve the same seamless binding using relationships, I encountered a small issue. Since NSManagedObjects represent a one-to-many relationship in the form of an NSSet, I have to retrieve the “children” (data of the many side) in its own @FetchRequest, losing the benefits of Core Data relationship properties. What am I doing wrong?
A: This sounds similar to another issue where I suggested using predicates to filter objects that only have a certain relationship. I think the same approach should work for you? Let Core Data do the filtering work by building a predicate, like NSPredicate(format: "country = %@", country).
NSManagedObject conforms to the ObservableObject protocol, which means it will notify subscribers through Publishers when its property values change. Unfortunately, property value changes in relationship objects are not included in the monitored changes. Retrieving the relationship object list through predicates may be the best way for now. Additionally, Antoine van der Lee wrote an article on extending NSFetchedResultsController to observe relationship changes: NSFetchedResultsController extension to observe relationship changes.
How to reflect the change of ordered objects in persistent history tracking
Q: How does persistent history reflect the change of the order of objects in the “ordered” relationship? Does NSPersistentHistoryChange include the parent entity or child entity? What properties are included in updatedProperties?
A: For sorting changes, both sides of the relationship will be displayed as NSPersistentHistoryChange, and the relationship will be listed in updatedProperties.
Passing Managed Objects via navigationDestination
Q: I have a question related to SwiftUI’s navigationDestination(for: myCoreDataClass) that requires my NSManagedObjects to conform to the Codable protocol (presumably to persist the Path). I hand-coded the NSManagedObject code and implemented the Codable protocol to achieve this goal. Is there a better way to handle this? Thank you.
A: Codable cannot accurately encode objects in an object graph individually. Instead, you should create an encodable transform that suits the subset of data needed here. Perhaps URIRepresentation could be used.
When NSManagedObject includes relationships, encoding it is extremely difficult. The only requirement for incoming data in navigationDestination is to conform to the Hashable protocol, so passing in the URL corresponding to the managed object ID is likely the best choice (through objectID.uriRepresentation, the URL conforms to the Codable protocol, satisfying the requirement to persist the Path).
Summary:
In the summary of the two Q&A compilations, I neglected the questions that did not have a conclusion.
I hope this article can be helpful to you. You are also welcome to communicate with me through Twitter, Discord channel, or the message board of my blog.
Top comments (0)