My first Swift app is a photo library manager, and after rebuilding its Core Data guts half a dozen times, I am throwing up my hands and asking for help. For each photo, there are a few "layers" of work I need to accomplish before I can display it:
Create an Asset (NSManagedObject subclass) in Core Data for each photo in the library.
Do some work on each instance of Asset.
Use that work to create instances of Scan, another NSManagedObject class. These have to-many relationships to Assets.
Look over the Scans and use them to create AssetGroups (another NSManagedObject) in Core Data. Assets and AssetGroups have many-to-many relationships.
For each photo, each layer must complete before the next one starts. I can do multiple photos in parallel, but I also want to chunk up the work so it loads into the UI coherently.
I'm really having trouble making this work gracefully; I've built and rebuilt it a bunch of different ways. My current approach uses singleton subclasses of this Service, but as soon as I call save() on the first one, the work stops.
Service.swift
class Service: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var name: String
var predicate: NSPredicate
var minStatus: AssetStatus
var maxStatus: AssetStatus
internal let queue: DispatchQueue
internal let mainMOC = PersistenceController.shared.container.viewContext
internal let privateMOC = PersistenceController.shared.container.newBackgroundContext()
internal lazy var frc: NSFetchedResultsController<Asset> = {
let req = Asset.fetchRequest()
req.predicate = self.predicate
req.sortDescriptors = [NSSortDescriptor(key: #keyPath(Asset.creationDate), ascending: false)]
let frcc = NSFetchedResultsController(fetchRequest: req,
managedObjectContext: self.mainMOC,
sectionNameKeyPath: "creationDateKey",
cacheName: nil)
frcc.delegate = self
return frcc
}()
#Published var isUpdating = false
#Published var frcCount = 0
init(name: String, predicate: NSPredicate? = NSPredicate(value: true), minStatus: AssetStatus, maxStatus: AssetStatus) {
self.name = name
self.predicate = predicate!
self.minStatus = minStatus
self.maxStatus = maxStatus
self.queue = DispatchQueue(label: "com.Ladybird.Photos.\(name)", attributes: .concurrent)
super.init()
self.fetch()
self.checkDays()
}
private func fetch() {
do {
try self.frc.performFetch()
print("\(name): FRC fetch count: \(frc.fetchedObjects!.count)")
} catch {
print("\(name): Unable to perform fetch request")
print("\(error), \(error.localizedDescription)")
}
}
func savePrivate() {
self.privateMOC.perform {
do {
try self.privateMOC.save()
}
catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
func save() {
do {
try self.privateMOC.save()
self.mainMOC.performAndWait {
do {
try self.mainMOC.save()
} catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
func checkDays() {
// Iterate over days in the photo library
if self.isUpdating { return }
self.isUpdating = true
// self.updateCount = self.frcCount
var daysChecked = 0
var day = Date()
while day >= PhotoKitService.shared.oldestPhAssetDate() {
print("\(name) checkDay \(DateFormatters.shared.key.string(from: day))")
checkDay(day)
var dc = DateComponents()
dc.day = -1
daysChecked += 1
day = Calendar.current.date(byAdding: dc, to: day)!
if daysChecked % 100 == 0 {
DispatchQueue.main.async {
self.save()
}
}
}
self.save()
self.isUpdating = false
}
func checkDay(_ date: Date) {
let dateKey = DateFormatters.shared.key.string(from: date)
let req = Asset.fetchRequest()
req.predicate = NSPredicate(format: "creationDateKey == %#", dateKey)
guard let allAssetsForDateKey = try? self.mainMOC.fetch(req) else { return }
if allAssetsForDateKey.count == PhotoKitService.shared.phAssetsCount(dateKey: dateKey) {
if allAssetsForDateKey.allSatisfy({$0.assetStatusValue >= minStatus.rawValue && $0.assetStatusValue <= maxStatus.rawValue}) {
let frcAssetsForDateKey = self.frc.fetchedObjects!.filter({$0.creationDateKey! == dateKey})
if !frcAssetsForDateKey.isEmpty {
print("\(name): Day \(dateKey) ready for proccessing.")
for a in frcAssetsForDateKey {
self.handleAsset(a)
}
}
}
}
self.save()
}
// implemented by subclasses
func handleAsset(_ asset: Asset) -> Void { }
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.frcCount = self.frc.fetchedObjects?.count ?? 0
self.checkDays()
}
}
I have a subclass of this for each of the four steps above. I want the data to flow between them nicely, and in previous implementations it did, but I couldn't chunk it the way I wanted, and it crashed randomly. This feels more controllable, but it doesn't work: calling save() stops the iteration happening in checkDays(). I can solve that by wrapping save() in an async call like DispatchQueue.main.async(), but it has bad side effects — checkDays() getting called while it's already executing. I've also tried calling save() after each Asset is finished, which makes the data move between layers nicely, but is slow as hell.
So rather than stabbing in the dark, I thought I'd ask whether my strategy of "service layers" feels sensible to others who others who have dealt with this kind of problem. It'd also be helpful to hear if my implementation via this Service superclass makes sense.
What would be most helpful is to hear from those with experience how they would approach implementing a solution to this problem: consecutive steps, applied concurrently to multiple Core Data entities, all in the background. There are so many ways to solve pieces of this in Swift — async/await, Tasks & Actors, DispatchQueues, ManagedObjectContext.perform(), container.performBackgroundTask(), Operation… I've tried each of them to mixed success, and what I feel like I need here is a trail map to get out of the forest.
Thanks y'all
Related
I am trying to make a document based app in swiftUI with a custom UI. I want iCloud capabilities in my app. I am trying to use iCloud Document (No cloudKit) way for storing data on iCloud container. I am using UIDocument and it's working. It's storing data to iCloud and I am able to retrieve it back.
Now the thing is when I run the app on two devices (iphone and iPad) and make changes to a file on one device, the changes are not reflecting on the other device while the file or say app is open. I have to close the app and relaunch it to see the changes.
I know I have to implement NSMetadataQuery to achieve this but I am struggling with it. I don't know any objective-C. I have been searching on the internet for a good article but could not find any. Can you please tell how do I implement this feature in my app. I have attach the working code of UIDocument and my Model class.
Thank you in advance !
UIDocument
class NoteDocument: UIDocument {
var notes = [Note]()
override func load(fromContents contents: Any, ofType typeName: String?) throws {
if let contents = contents as? Data {
if let arr = try? PropertyListDecoder().decode([Note].self, from: contents) {
self.notes = arr
return
}
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -1, userInfo: nil)
}
override func contents(forType typeName: String) throws -> Any {
if let data = try? PropertyListEncoder().encode(self.notes) {
return data
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -2, userInfo: nil)
}
}
Model
class Model: ObservableObject {
var document: NoteDocument?
var documentURL: URL?
init() {
let fm = FileManager.default
let driveURL = fm.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
documentURL = driveURL?.appendingPathComponent("savefile.txt")
document = NoteDocument(fileURL: documentURL!)
}
func loadData(viewModel: ViewModel) {
let fm = FileManager.default
if fm.fileExists(atPath: (documentURL?.path)!) {
document?.open(completionHandler: { (success: Bool) -> Void in
if success {
viewModel.notes = self.document?.notes ?? [Note]()
print("File load successfull")
} else {
print("File load failed")
}
})
} else {
document?.save(to: documentURL!, for: .forCreating, completionHandler: { (success: Bool) -> Void in
if success {
print("File create successfull")
} else {
print("File create failed")
}
})
}
}
func saveData(_ notes: [Note]) {
document!.notes = notes
document?.save(to: documentURL!, for: .forOverwriting, completionHandler: { (success: Bool) -> Void in
if success {
print("File save successfull")
} else {
print("File save failed")
}
})
}
func autoSave(_ notes: [Note]) {
document!.notes = notes
document?.updateChangeCount(.done)
}
}
Note
class Note: Identifiable, Codable {
var id = UUID()
var title = ""
var text = ""
}
This is a complex topic. Apple do provide some sample swift code, the Document-Based App Programming Guide for iOS and iCloud Design Guide.
There is also some good third party guidance: Mastering the iCloud Document Store.
I would recommend reading the above, and then return to the NSMetaDataQuery API. NSMetaDataQuery has an initial gathering phase and a live-update phase. The later phase can remain in operation for the lifetime of your app, allowing you to be notified of new documents in your app's iCloud container.
Context:
App with all data in CloudKit
ViewController calls a query to load the data for a tableview
tableview crashes because the array of data for the tableview hasn't
come back from CK
I've researched semaphores and have it nearly
working But can't seem to figure out where to place the
semaphore.signal() to get the exact right behaviour
within viewDidLoad, I call the function:
Week.fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
and the function:
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.async {
completion(weeks)
//Option 1: putting semaphore.signal() here means it never completes
// beyond initialization of the week records
}
//Option 2: putting semaphore.signal() here means it completes after the
// initialization of the Week items, but before completion(weeks) is done
// so the array isn't initialized in the view controller in time. so the
// VC tries to use weeks and unwraps a nil.
semaphore.signal()
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
Note: I have tested that the weeks array within the view controller is properly set eventually - so it does seem to be purely a timing issue :)
I've tested placement of .signal() and if I put it within the 'DispatchQueue.main.async' block, it never gets triggered - probably because that block itself is waiting for the signal.
However if I put it anywhere else, then the viewcontroller picks up at that point and the completion(weeks) doesn't get called in time.
Maybe it is obvious - but as my first time working with semaphores - I'm struggling to figure it out!
Update 1: It works with DispatchQueue(label: "background")
I was able to get it working once I twigged that the semaphore.wait() was never going to get called with semaphore.signal() on the main thread.
So I changed it from:
DispatchQueue.main.async
to
DispatchQueue(label: "background").async and popped the semaphore.signal() inside and it did the trick
Comments/critiques welcome!
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
NSLog("inside fetchWeeks in Week ")
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue(label: "background").async {
completion(weeks)
semaphore.signal()
}
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
}
Update 2: Trying to avoid use of semaphores
Per comment thread - we shouldn't need to use semaphores with CloudKit - so it is likely that I'm doing something stupid :)
moving fetchWeeks() to the viewController to try to isolate the issue...but it still blows up as fetchWeeks() has't completed before the code tries to execute the line after and use the weeks array
my viewController:
class ChallengeDetailViewController: UIViewController {
#IBOutlet weak var rideTableView: UITableView!
//set by the inbound segue
var challenge: Challenge?
// set in fetchWeeks based on the challenge
var weeks: [Week]?
override func viewDidLoad() {
super.viewDidLoad()
rideTableView.dataSource = self
rideTableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
rideTableView.delegate = self
fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
//This is where it blows up as weeks is nil
weeks = weeks!.sorted(by: { $0.weekSequence < $1.weekSequence })
}
//moved this to the view controller
func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.sync {
completion(weeks)
}
}
Model.currentModel.publicDB.add(operation)
}
Once again: Never use semaphores with the CloudKit API.
First of all declare data source arrays always as non-optional empty arrays to get rid of unnecessary unwrapping the optional
var weeks = [Week]()
The mistake is that you don't use the fetched data at the right place.
As the closure is asynchronous you have to proceed inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks
self?.weeks = weeks.sorted(by: { $0.weekSequence < $1.weekSequence })
}
or simpler
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
}
And if you need to reload the table view do it also inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
self?.rideTableView.reloadData()
}
To do so you have to call completion on the main thread
DispatchQueue.main.async {
completion(weeks)
}
And finally delete the ugly semaphore!
let semaphore = DispatchSemaphore(value: 0)
...
semaphore.signal()
...
semaphore.wait()
I'm trying to get an example project using CoreData and QueryGenerationTokens working. The essence of the project is to be committing changes to a background context on a timer (emulating changes coming down from a server) that shouldn't be displayed until an action is taken on the UI (say, a button press).
Currently, I have changes being saved on the background context (an entity is being added every 5s and saved) and they are automatically coming into the view context (as expected, .automaticallyMergesChangesFromParent is set to true). Where things go wrong, I am pinning the view context before any of these changes happen to the current query generation token. I would expect the view to not update with the background items being added, but it is updating with them. So it seems the query generation tokens are having no effect?
Some of the possible issues I've thought of:
the only example I've found from Apple doesn't show them using it with a fetched results controller (I'm using #FetchRequest in SwiftUI, which I'm almost entirely certain is essentially the same), so that may have an effect?
.automaticallyMergeChangesFromParent shouldn't be used and I should try a merge policy, but that doesn't seem to work either and conceptually, it seems the query generation tokens should work with this and pin to the generation no matter the merging.
Code for view - handles loading data from view context
// Environment object before fetch request necessary
// Passed in wherever main view is instantiated through .environment()
#Environment(\.managedObjectContext) var managedObjectContext
// Acts as fetched results controller, loading data automatically into items upon the managedObjectContext updating
// ExampleCoreDataEntity.retrieveItemsFetchRequest() is an extension method on the entity to easily get a fetch request for the type with sorting
#FetchRequest(fetchRequest: ExampleCoreDataEntity.retrieveItemsFetchRequest()) var items: FetchedResults<ExampleCoreDataEntity>
var body: some View {
NavigationView {
// Button to refresh and bring in changes
Button(
action: {
do {
try self.managedObjectContext.setQueryGenerationFrom(.current)
self.managedObjectContext.refreshAllObjects()
} catch {
print(error.localizedDescription)
}
},
label: { Image(systemName: "arrow.clockwise") }
)
// Creates a table of items sorted by the entity itself (entities conform to Hashable)
List(self.items, id: \.self) { item in
Text(item.name ?? "")
}
}
}
Code in SceneDelegate (where a SwiftUI application starts up) where I also initialize what is needed for CoreData:
// Setup and pass in environment of managed object context to main view
// via extension on persistent container that sets up CoreData stack
let managedObjectContext = NSPersistentContainer.shared.viewContext
do {
try managedObjectContext.setQueryGenerationFrom(.current)
} catch {
print(error.localizedDescription)
}
let view = MainView().environment(\.managedObjectContext, managedObjectContext)
// Setup background adding
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(backgroundCode), userInfo: nil, repeats: true)
// Setup window and pass in main view
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)
Function adding data in the background:
#objc func backgroundCode() {
ExampleCoreDataEntity.create(names: ["background object"], in: backgroundContext, shouldSave: true)
}
Setup of NSPersistentContainer:
extension NSPersistentContainer {
private struct SharedContainerStorage {
static let container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Core_Data_Exploration")
container.loadPersistentStores { (description, error) in
guard error == nil else {
assertionFailure("CoreData: Unresolved error \(error!.localizedDescription)")
return
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
return container
}()
}
static var shared: NSPersistentContainer {
return SharedContainerStorage.container
}
}
Create/Read/Update/Delete functions on the entity:
extension ExampleCoreDataEntity {
static func retrieveItemsFetchRequest() -> NSFetchRequest<ExampleCoreDataEntity> {
let request: NSFetchRequest<ExampleCoreDataEntity> = ExampleCoreDataEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \ExampleCoreDataEntity.creationDate, ascending: false)]
return request
}
static func create(names: [String], in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
names.forEach { name in
let item = ExampleCoreDataEntity(context: context)
item.name = name
item.creationDate = Date()
item.identifier = UUID()
}
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
func delete(in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
let name = self.name ?? "an item"
context.delete(context.object(with: self.objectID))
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
}
The issue was container.viewContext.automaticallyMergesChangesFromParent = true
That property cannot be set to true while working with query generation tokens. I came back to this issue and found this in the header of NSManagedObjectContext documented above automaticallyMergesChangesFromParent:
Setting this property to YES when the context is pinned to a non-current query generation is not supported.
The general flow of getting it to work is the following:
setting the query generation token to .current
calling .refreshAllObjects() on the view context
calling .performFetch() on the fetched results controller
This last part goes against the code I put in the original question which used #FetchRequest - currently, I can't figure out a way that doesn't seem extremely hacky to make it manually refetch. To get around this, I made an intermediate store class containing a FetchedResultsController that adopts its delegate protocol. That store also adopts ObservableObject which allows a SwiftUI view to listen to its changes when calling objectWillChange.send() within the ObservableObject adopting store.
In the documentation you linked to in the question you will see it says:
"Calling save(), reset(), mergeChangesFromContextDidSaveNotification:, or mergeChangesFromRemoteContextSave(:intoContexts:) on any pinned context will automatically advance it to the most recent version for the operation and then reset its query generation to currentQueryGenerationToken."
The reason you are seeing the changes from the background save is automaticallyMergesChangesFromParent is just convenience for mergeChangesFromContextDidSaveNotification so your generation is advancing.
FYI here is another sample project uses query generations - Synchronizing a Local Store to the Cloud
And here is the relevant code:
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
A class to set up the Core Data stack, observe Core Data notifications, process persistent history, and deduplicate tags.
*/
import Foundation
import CoreData
// MARK: - Core Data Stack
/**
Core Data stack setup including history processing.
*/
class CoreDataStack {
/**
A persistent container that can load cloud-backed and non-cloud stores.
*/
lazy var persistentContainer: NSPersistentContainer = {
// Create a container that can load CloudKit-backed stores
let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container)
return container
}()
/**
Track the last history token processed for a store, and write its value to file.
The historyQueue reads the token when executing operations, and updates it after processing is complete.
*/
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken,
let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
do {
try data.write(to: tokenFile)
} catch {
print("###\(#function): Failed to write token data. Error = \(error)")
}
}
}
/**
The file URL for persisting the persistent history token.
*/
private lazy var tokenFile: URL = {
let url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create persistent container URL. Error = \(error)")
}
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
/**
An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
*/
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
/**
The URL of the thumbnail folder.
*/
static var attachmentFolder: URL = {
var url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
url = url.appendingPathComponent("attachments", isDirectory: true)
// Create it if it doesn’t exist.
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create thumbnail folder URL: \(error)")
}
}
return url
}()
init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
print("###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
}
}
// MARK: - Notifications
extension CoreDataStack {
/**
Handle remote store change notifications (.NSPersistentStoreRemoteChange).
*/
#objc
func storeRemoteChange(_ notification: Notification) {
print("###\(#function): Merging changes from the other persistent store coordinator.")
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
}
/**
Custom notifications in this sample.
*/
extension Notification.Name {
static let didFindRelevantTransactions = Notification.Name("didFindRelevantTransactions")
}
// MARK: - Persistent history processing
extension CoreDataStack {
/**
Process persistent history, posting any relevant transactions to the current view.
*/
func processPersistentHistory() {
let taskContext = persistentContainer.newBackgroundContext()
taskContext.performAndWait {
// Fetch history received from outside the app since the last token
let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
historyFetchRequest.predicate = NSPredicate(format: "author != %#", appTransactionAuthorName)
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
request.fetchRequest = historyFetchRequest
let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty
else { return }
// Post transactions relevant to the current view.
DispatchQueue.main.async {
NotificationCenter.default.post(name: .didFindRelevantTransactions, object: self, userInfo: ["transactions": transactions])
}
// Deduplicate the new tags.
var newTagObjectIDs = [NSManagedObjectID]()
let tagEntityName = Tag.entity().name
for transaction in transactions where transaction.changes != nil {
for change in transaction.changes!
where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
newTagObjectIDs.append(change.changedObjectID)
}
}
if !newTagObjectIDs.isEmpty {
deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
}
// Update the history token using the last transaction.
lastHistoryToken = transactions.last!.token
}
}
}
// MARK: - Deduplicate tags
extension CoreDataStack {
/**
Deduplicate tags with the same name by processing the persistent history, one tag at a time, on the historyQueue.
All peers should eventually reach the same result with no coordination or communication.
*/
private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {
// Make any store changes on a background context
let taskContext = persistentContainer.backgroundContext()
// Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won’t block the main queue.
taskContext.performAndWait {
tagObjectIDs.forEach { tagObjectID in
self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
}
// Save the background context to trigger a notification and merge the result into the viewContext.
taskContext.save(with: .deduplicate)
}
}
/**
Deduplicate a single tag.
*/
private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
guard let tag = performingContext.object(with: tagObjectID) as? Tag,
let tagName = tag.name else {
fatalError("###\(#function): Failed to retrieve a valid tag with ID: \(tagObjectID)")
}
// Fetch all tags with the same name, sorted by uuid
let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: Schema.Tag.uuid.rawValue, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "\(Schema.Tag.name.rawValue) == %#", tagName)
// Return if there are no duplicates.
guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
return
}
print("###\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
// Pick the first tag as the winner.
let winner = duplicatedTags.first!
duplicatedTags.removeFirst()
remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
}
/**
Remove duplicate tags from their respective posts, replacing them with the winner.
*/
private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
duplicatedTags.forEach { tag in
defer { performingContext.delete(tag) }
guard let posts = tag.posts else { return }
for case let post as Post in posts {
if let mutableTags: NSMutableSet = post.tags?.mutableCopy() as? NSMutableSet {
if mutableTags.contains(tag) {
mutableTags.remove(tag)
mutableTags.add(winner)
}
}
}
}
}
}
I am using Swift 4 to build a single view iOS 11 application that has a UITableViewController that is also defined as a delegate for a NSFetchedResultsController.
class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate {
var container:NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
var frc : NSFetchedResultsController<Student>?
override func viewDidLoad() {
container?.performBackgroundTask { context in
// adds 100 dummy records in background
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
} // end of background inserting.
// now defining frc:
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch() // this works and I get no errors
tableView.reloadData()
frc.delegate = self
} // end of frc definition
}
}
If I add one row of Student using the viewContext, the frc will fire the required methods to show it in the tableView. However, the 100 dummy rows are not shown. In fact, If I try to tell the tableview to reload after the insertion is done, my app starts to behave weirdly and becomes buggy, and does not do what it should do (i.e: does not delete rows, does not edit, etc).
But If I restart my app, without calling the dummy insertion, I can see the 100 rows inserted from the previous run.
The only problem is that I can't call tableView.reloadData() from the background thread, so I tried to do this:
// after printing the count, I did this:
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData() // causes UI to behave weirdly
}
then I tried to call viewContext.perform to reload the table view in the proper thread
func viewDidLoad() {
// code for inserting 100 dummy rows in background thread
// code for defining frc and setting self as delegate
if let context = container?.viewContext {
context.perform { [weak self] in
self?.tableView.reloadData() // that also causes UI to behave weirdly
}
}
}
How can tell my tableview to reload and display the 100 dummy rows in a thread-safe manner?
override func viewDidLoad() {
super.viewDidLoad()
//Always need your delegate for the UI to be set before calling the UI's delegate functions.
frc.delegate = self
//First we can grab any already stored values.
goFetch()
//This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
container?.performBackgroundTask { context in
//We are in a different queue than the main queue, hence "backgroundTask".
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
//Now that we are done saving its ok to fetch again.
goFetch()
}
//goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
//The reason it works if you restart the app because that data you didn't let finish saving is persisted
//So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.
}
func goFetch() {
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch()
//Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
//To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
}
}
After a lot of reading about the NSFetchedResultsController and the NSPersistentContainer and finally finding an important piece of information here at SO I think I have a working example.
My code is slightly different since I used a project I had for this. Anyway here is what I did:
In my view controller I had a property for my container
private var persistentContainer = NSPersistentContainer(name: coreDataModelName)
And in viewDidLoad I loaded the persistent store and created my 100 records.
persistentContainer.loadPersistentStores { persistentStoreDescription, error in
if let error = error {
print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
} else {
self.createFakeNotes() // Here 100 elements get created
DispatchQueue.main.async {
self.setupView() // other stuff, not relevant
self.fetchNotes() // fetch using fetch result controller
self.tableView.reloadData()
}
}
}
Below is createFakeNotes() where I use a separate context for inserting the elements in a background thread, this code is pretty much taken from Apple's Core Data programming guide but to make the UI being updated I needed to set automaticallyMergesChangesFromParent to true which I found out in this SO answer
I also delete old notes first to make the testing easier.
private func createFakeNotes() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
do {
try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
} catch {
print("Delete error [\(error)]")
return
}
let privateContext = persistentContainer.newBackgroundContext()
privateContext.automaticallyMergesChangesFromParent = true //Important!!!
privateContext.perform {
let createDate = Date()
for i in 1...100 {
let note = Note(context: privateContext)
note.title = String(format: "Title %2d", i)
note.contents = "Content"
note.createdAt = createDate
note.updatedAt = createDate
}
do {
try privateContext.save()
do {
try self.persistentContainer.viewContext.save()
} catch {
print("Fail saving main context [\(error.localizedDescription)")
}
} catch {
print("Fail saving private context [\(error.localizedDescription)")
}
}
}
You should fetch your data by calling it from viewwillappear and then try to reload your tableview.
override func viewWillAppear(_ animated: Bool) {
getdata()
tableView.reloadData()
}
func getdata() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do{
persons = try context.fetch(Person.fetchRequest())
}
catch {
print("fetching failed")
}
}
I wrote a function to fetch database in CoreData. this function will take a closure and run performBackgroundTask to fetch the data. Then, passing the result to the closure to run.
I wrote static properties in AppDelegate for me to access viewContext easily:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static var persistentContainer: NSPersistentContainer {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer
}
static var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
// ...
}
The following is the function(not method) I wrote which crashed by using context:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? context.fetch(fetchRequest) else { // crash
return
}
context.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
but if i replace context with AppDelegate.viewContext, the function won't crash:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? AppDelegate.viewContext.fetch(fetchRequest) else { // crash
return
}
AppDelegate.viewContext.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
what is exactly going on?
thanks.
Here are some issues:
performBackgroundTask is already on the right thread for the context so there is no reason to call context.performAndWait and may lead to a deadlock or a crash.
The items fetched or created in a performBackgroundTask cannot leave that block under any circumstances. The context will be destroyed at the end of the block and the managedObjects will crash when it tries to access its context
Managing core-data thread safety can be difficult and I have found it a generally good practice to never pass or return managed objects to functions, unless the context of the object is explicit and clear. This is not an unbreakable rule, but I think it is a good rule of thumb when making your APIs.
performBackgroundTask is generally used for updates to core data. If you are only doing fetches you should use the viewContext. Doing a fetch on the background only to pass it to the main thread is generally a waste.
While in a performBackgroundTask block you cannot access the viewContext - neither for reading or for writing. If you do the app can crash any at time with confusing crash reports, even at a later time when you are not violating thread safety.
I don't know what the predicates that you are creating look like, but I have a strong feeling that they are wrong. This would cause a crash when fetching.
Overall I think that the function you created has little value. If all it is doing is a fetch then you should simply create the predicate and sort descriptors and fetch on the viewContext. If you insist on keeping the function, then remove the performBackgroundTask, fetch using the viewContext, return the results(instead of a callback) and only call it from the main thread.