I wasn't sure how to phrase the question. I know it's misleading.
Context:
I'm using NSPersistentCloudKitContainer for my public and private database. The CoreData model has two configurations: Local, Cloud. Local corresponds to the private CloudKit database and Cloud to the public CloudKit database. Both configurations contain all objects: List, ListItem, and Like.
List has a to-many relationship to ListItem (with inverse). ListItem has a to-many relationship to Like (with inverse).
The private database implementation is working as expected.
A user can create lists and list items. Each of these objects can be set as public or private using the attribute isPublic.
I have a singleton object that monitors changes to Lists and ListItems using the NSManagedObjectContextDidChange notification. If the user updates the isPublic attribute to true a copy of the List or ListItem is created in the public database. If the user sets isPublic to false, the List or ListItem has an isTrashed attribute that's set to true in the public database (not sure when to actually delete the records from the public database but, that's for another question).
The user can also view all ListItems in the public database using a NSFetchedResultsController to load data and for monitor changes.
Problem
I'm able to display all public ListItems using a UITableView and a NSFetchedResultsController. I have a custom UITableViewCell that displays the user who created the ListItem's username and likes like so:
I do this async call for every UITableViewCell. Is this correct? If so, I'll need to implement a caching mechanism.
Not only that, what if the async call fails? Now, I have a UITableView cell that has a blank username. Should I prefetch the username for all items in the NSFetchedResultsController beforehand? That could be thousands of objects that haven't even been displayed and increases loading time.
Is it correct to async fetch the User CKRecord of each ListItem or is there a better way to access the user who created the CKRecord?
Should I create a custom User NSManagedObject?
I'd also like to display the usernames of each user who liked the list item. Does this mean for every like I'll need to do the same process? That means I'll have an API call for each username of each list item and each like of each list item ((1 + x) * y API calls; x = number of likes, y = number of list items).
Say I'm displaying 100 ListItems with an average of 20 likes. That means I'll make:
(1 + 20) * 100 = 2100 API calls (without caching)
That hits the 40 requests/second limit with less than 2 list items.
If more information is needed or more code, let me know.
It seems like my best option is to create a custom User object in my CoreData model since the NSPersistentCloudKitContainer setup is fetching relationships for me.
Sample Code (UITableViewCell)
public func configure(listItem: ListItem, environment: Environment) {
self.listItem = listItem
self.environment = environment
self.userRecordId = self.environment?.coreDataStack.creatorUserRecordID(for: listItem)
self.fetchUserTask?.cancel()
self.fetchUserTask = Task {
guard let userRecordId = self.userRecordId,
let userRecord = try? await CKContainer.default().publicCloudDatabase.record(for: userRecordId) else {
self.userNameLabel.text = "I'm server poor. You can't view this information."
return
}
if !Task.isCancelled {
Task { @MainActor in
self.userRecord = userRecord
self.userNameLabel.text = self.userRecord?["username"]
}
}
}
self.contentTextView.text = listItem.title
self.dateCreatedLabel.text = DateFormatter.localizedString(
from: listItem.modifiedAt!,
dateStyle: .medium,
timeStyle: .short
)
let likes = self.listItem?.likes
self.likeButton.setTitle("\(likes?.count ?? 0)", for: .normal)
if let likes = likes?.allObjects as? [Like] {
for like in likes {
guard let userRecordId = self.environment?.coreDataStack.creatorUserRecordID(for: like) else {
self.likeButton.isEnabled = false
continue
}
if userRecordId.recordName == CKCurrentUserDefaultName {
self.likeButton.isEnabled = false
break
}
}
}
}
UI implementation issues
Performing data fetching and any datasource interaction should not be done from UITableViewCell
subclass, since this breaks MVC pattern.
Instead, implement datasource interaction and fetching in your UIViewController
subclass.
CloudKit issues
According to this CoreData implicitly updates changes from CloudKit when it is properly configured, so you don't have to fetch manually from CKDatabase
(but, probably this works only for private CloudKit database, see below).
According to documentation of NSPersistentCloudKitContainer, it
mirrors select persistent stores to a CloudKit private database.
Thus, you need to implement a persistence layer for the public database yourself. It has nothing to do with UI yet, it has to be implemented purely as a service that gets notified about changes and synchronizes the data with your core data stack. Only if done this way, your data will be fetched only when necessary anywhere in your app, in the most economic and consistent way that saves your requests quota. In other words, you should implement your own service that does the job of NSPersistentCloudKitContainer
for the public database.
CKDatabase
has a method to fetch records in batch:
public func fetch(withRecordIDs recordIDs: [CKRecord.ID], desiredKeys: [CKRecord.FieldKey]? = nil, completionHandler: (Result<[CKRecord.ID : Result<CKRecord, any Error>], any Error>) -> Void)
So, synchronization from CloudKit to CoreData can look like this:
class MyUserSyncService {
let coreDataStack: MyCoreDataStack
func syncAll() {
let allListItemsAndLikesCreatorUserRecordIDs = coreDataStack.allListItemsAndLikesUserRecordIDs()
coreDataStack.deleteAllUsers(notMatchingRecordIDs: allListItemsAndLikesCreatorUserRecordIDs)
sync(userRecordIDs: allListItemsAndLikesCreatorUserRecordIDs)
}
func sync(userRecordIDs: [CKRecord.ID]) {
CKContainer.default().publicCloudDatabase
.fetch(
withRecordIDs: userRecordIDs,
desiredKeys: ["username"]
) { result in
guard let successResult = try? result.get() else { /* log error */ return }
successResult.forEach {
guard let record = try? $0.value.get() else { /* log error */ return }
coreDataStack.createOrUpdateUser(
withRecordID: $0.key,
userName: record["username"]
)
}
}
}
}