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)
}
}
}
}
}
}
Related
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
I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}
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 have a view model that's fetching some data from my API, and when I get data back, I'm setting it both in Realm and on two MutableProperties on the view model
userService.getBrowseData().on(
failed: { error in
// TODO: Should call a closure the VC passes in here with an error to show
}, value: { banners, lists in
browseStoreService.store(banners: banners, lists: lists)
self.banners.value = banners
self.lists.value = lists
}
).startWithCompleted { self.dataLoaded.value = true }
In my view controller, I'm getting a crash at the line where I instantiate and set a ListViewViewModel
disposable += viewModel?.dataLoaded.producer
.observe(on: UIScheduler())
.startWithValues { loaded in
if loaded {
guard let viewModel = self.viewModel else { return }
self.carousel?.reloadData()
for list in viewModel.lists.value {
let view = ListView()
view.viewModel = ListViewViewModel(list: list)
view.listViewDelegate = self
view.listItemViewDelegate = self
scrollingStackView.addArrangedSubview(view: view)
constrain(view) { view in
view.width == view.superview!.width
view.left == view.superview!.left
view.right == view.superview!.right
}
}
}
}
The strange thing to me is that I'm just setting the data in realm, I'm not doing any additional fetch to get the data out of realm which would make sense to cause a crash, especially if the write hadn't finished.
In this case, I'm taking an array of banners and an array of lists in my response from the API, sending them into my storeService to set them in realm and assigning those same arrays to their respective MutableProperties.
Any idea what could be going here that would be causing this crash?
Edit:
The store function looks like this:
func store(banners: [Banner], lists: [List]) {
let realm = try! Realm()
do {
try realm.write {
realm.add(banners, update: true)
realm.add(lists, update: true)
}
} catch {
return
}
realm.refresh()
}
and both models have a primary key of id
Iam using cloudkit to read and write in public database - in my application user suppose to be able to upload files and write records to database and other users to read it
I am using cloudkit however as far as I know to write to the public database user has to login with icloud account however apple does not allow this in production so how to solve this - how can I give the users a right to write to DB
let Container = CKContainer.default()
let database = Container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "NewCode", predicate: predicate) //Photos is table name in cloudkit server
//-------Fetch the data---------------------
database.perform(query, inZoneWith: nil,
completionHandler: ({results, error in
DispatchQueue.main.async() { //Disp
if (error != nil) {
DispatchQueue.main.async() {
}
}
else { //a
if results!.count > 0 {
print("count = \(results!.count)")
record = results![0]
currentRecord = record
newcode = currentRecord?.object(forKey: "Code") as! String
newcodevalue = Int(newcode)!
newcodevalue = newcodevalue + 10
print("new code is = \(newcodevalue)")
myCodeStrinValue = String(newcodevalue)
print("new code string = \(newcodevalue)")
record?.setObject(myCodeStrinValue as CKRecordValue?,forKey: "Code")
database.save(record!, completionHandler: //save
({returnRecord, error in
if let err = error {
DispatchQueue.main.async() {
}
} else {
DispatchQueue.main.async() {
print("NewCode Table updated successfully")
// passing the code value we fetched above to the second viewcontroller "Uploadphotoviewcontroller" to be saved with the uploaded photo when saved button on the second controller clicked. remember you have to set an ID for the second view controller "Uploadphotoviewcontroller" to be used here when passing the value to it (set ID in the attributes right panel
// Instantiate SecondViewController
let UploadPhotoviewcontroller = self.storyboard?.instantiateViewController(withIdentifier:
"UploadPhotoviewcontroller") as! UploadPhotoviewcontroller
// Set the code value got from the DB above to the variable "myCodeValue" (this variable declared in the second view controller that will receive the value passed from here "Uploadphotoviewcontroller"
// add alpha numeric value to the code to make it more complicated for secuirty reasons
let CodeLetter = self.randomAlphaNumericString (length: 3)
UploadPhotoviewcontroller.myCodeValue = CodeLetter + myCodeStrinValue
UploadPhotoviewcontroller.codeOnly = myCodeStrinValue
// Take user to SecondViewController and accordingly remember not create graphical sague way link in the main storyboard to avoid reload of the view controller - remember to set ID attribute to UploadPhotoviewcontroller to use it here
self.navigationController?.pushViewController(UploadPhotoviewcontroller, animated: true)
}
}
})) //save
}
else { //G
DispatchQueue.main.async()
{
}
} //G
} //a
} //Disp
}))