3lvis / DATAStack

100% Swift Simple Boilerplate Free Core Data Stack. NSPersistentContainer
Other
214 stars 44 forks source link

Data is never hitting persistent store #48

Closed justinmakaila closed 8 years ago

justinmakaila commented 8 years ago

I'm not sure what's going on, but I've been blocked by this issue for >= 48 hours. It appears as if none of the insertions on any context are making it to the persistent store coordinator. I've tried everything from changing the mergePolicy, to manually observing contexts. For whatever reason, changes made in a background context, even after invoking persistWithCompletion, never update the main context, even after explicit fetches until the application is killed and restarted.

I've tweaked the DemoSwift.ViewController to demonstrate the issue:

import UIKit
import CoreData
import DATASource

class ViewController: UITableViewController {

    var dataStack: DATAStack

    var _backgroundContext: NSManagedObjectContext

    lazy var dataSource: DATASource = {
        let request: NSFetchRequest = NSFetchRequest(entityName: "User")
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]

        let dataSource = DATASource(tableView: self.tableView, cellIdentifier: "Cell", fetchRequest: request, mainContext: self.dataStack.mainContext, configuration: { cell, item, indexPath in
            if let name = item.valueForKey("name") as? String, createdDate = item.valueForKey("createdDate") as? NSDate, score = item.valueForKey("score") as? Int {
                cell.textLabel?.text =  name + " - " + String(score)
            }
        })

        return dataSource
    }()

    init(dataStack: DATAStack) {
        self.dataStack = dataStack
        self.dataStack.mainContext.stalenessInterval = 0.0
        self._backgroundContext = dataStack.newBackgroundContext("_Background Context")
        self._backgroundContext.stalenessInterval = 0.0

        super.init(style: .Plain)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        self.tableView.dataSource = self.dataSource

        let backgroundButton = UIBarButtonItem(title: "Background", style: .Done, target: self, action: #selector(ViewController.createBackground))
        let customBackgroundButton = UIBarButtonItem(title: "Custom", style: .Done, target: self, action: #selector(ViewController.createCustomBackground))
        self.navigationItem.rightBarButtonItems = [customBackgroundButton, backgroundButton]

        let mainButton = UIBarButtonItem(title: "Main", style: .Done, target: self, action: #selector(ViewController.createMain))
        let dropAllButton = UIBarButtonItem(title: "Edit Main Object", style: .Done, target: self, action: #selector(ViewController.editMainObject))
        self.navigationItem.leftBarButtonItems = [dropAllButton, mainButton]
    }

    func createBackground() {
        self.dataStack.performInNewBackgroundContext { backgroundContext in
            let entity = NSEntityDescription.entityForName("User", inManagedObjectContext: backgroundContext)!
            let object = NSManagedObject(entity: entity, insertIntoManagedObjectContext: backgroundContext)
            object.setValue("Background", forKey: "name")
            object.setValue(NSDate(), forKey: "createdDate")
            try! backgroundContext.save()
        }
    }

    func createCustomBackground() {
        let backgroundContext = dataStack.newBackgroundContext("Sync Context")

        backgroundContext.performBlock {
            let entity = NSEntityDescription.entityForName("User", inManagedObjectContext: backgroundContext)!
            let object = NSManagedObject(entity: entity, insertIntoManagedObjectContext: backgroundContext)
            object.setValue(backgroundContext.name, forKey: "name")
            object.setValue(NSDate(), forKey: "createdDate")
            try! backgroundContext.save()
        }
    }

    func createMain() {
        let entity = NSEntityDescription.entityForName("User", inManagedObjectContext: self.dataStack.mainContext)!
        let object = NSManagedObject(entity: entity, insertIntoManagedObjectContext: self.dataStack.mainContext)
        object.setValue("Main", forKey: "name")
        object.setValue(1, forKey: "score")
        object.setValue(NSDate(), forKey: "createdDate")
        //try! self.dataStack.mainContext.save()
        self.dataStack.persistWithCompletion()
    }

    func editMainObject() {
        self.dataStack.performInNewBackgroundContext { _backgroundContext in
            _backgroundContext.stalenessInterval = 0.0
        //_backgroundContext.performBlock {
            let fetchRequest = NSFetchRequest(entityName: "User")
            fetchRequest.predicate = NSPredicate(format: "%K contains[c] %@", "name", "Main")

            let backgroundUsers = try! _backgroundContext.executeFetchRequest(fetchRequest) as! [NSManagedObject]

            var i: Int32 = 0
            for user in backgroundUsers {
                i += 1
                let number = NSNumber(int: (123 * i))
                user.setValue(number, forKey: "score")
            }

            try! _backgroundContext.save()
        }
        //}
    }
}

In this example, pressing the "Main" button will create a new User with a score of 0. That User will not be modified by the editMainObject function when the "Edit Main Object" button is pressed.

If the app is killed and restarted, the "Edit Main Object" button will cause the objects to be modified and updated.

I'm really not sure how to proceed, or if I'm doing something wrong.

3lvis commented 8 years ago

Hi Justin,

This looks really interesting. What were you trying to achieve when you got this bug?

justinmakaila commented 8 years ago

@3lvis So, here's my use case: In the application I'm building, a user can take a picture, which is transformed into a Photo entity on the main context. That photo entity is saved. When changes happen in a context (observing the notification that i put in my PR ;)) I kick of a process in a background context, which is managed by a SyncManager, but it's created from DATAStack (similar to the example above, where there is a _backgroundContext on the view controller). That context queries for all Photos matching a predicate, and then sends the work off onto a network request. When the request comes back, I merge the JSON into the model, and save the object on the background context.

What I Expected: I expected the changes made in the background context to be merged into the main context, and the updates to be available .

What Happens: The observer in DATAStack fires, "merging" the changes into the main context. However, calling save on the main context does not make the changes available in the main context, nor does persisting the stack with persistWithCompletion. If I query for Photos on the main context, only the unchanged version shows up. If the app is killed and relaunched, the changes are available on the main thread.

Any thoughts? I've been poking around the source a little more and playing with some of the NSManagedObjectContext configuration variables, but there's not much there.

justinmakaila commented 8 years ago

@3lvis I'm putting together a better example. I'll fork and link to it.

justinmakaila commented 8 years ago

https://github.com/justinmakaila/DATAStack.git

^ See the Swift example on master

justinmakaila commented 8 years ago

Two things to try in the example:

Example One:

  1. Create an object on the main context, and the background context
  2. Press the "Edit" button
  3. Check logs. It should read "Editing 1 users"
  4. Save
  5. Press the "Edit" button
  6. Check logs. It should read "Editing 2 Users"
  7. The UI should be out of sync, and the "Main" cell should not be updating

Example Two:

  1. Drop the existing collection (either programmatically or using the triple tap gesture I added and rebuilding).
  2. Edit the editUsers function to use dataStack.mainContext
  3. Create an object on the main context, and the background context
  4. Press the "Edit" button
  5. Check logs. It should read "Editing 2 users", and the UI should reflect the changes
  6. Save
  7. Press the "Edit" button
  8. Check logs. It should read "Editing 2 users", and the UI should reflect the changes
justinmakaila commented 8 years ago

Making the new background context a child of the parent makes both examples work just fine, regardless of queue.

3lvis commented 8 years ago

Thanks for explaining your issue and for taking the time to make an example to show your problem. There was people reporting that mutating data in the main thread is not working correctly, personally I think this is something that shouldn't even be allowed, blocking the main thread is 📛 you could drop a few frames because of it 😱 Anyway, I understand that even though is not recommended, some developers might still want to do it and as you mentioned there are a few fixes that could be applied to DATASource in order to improve mutating data in the main thread but I would prefer to keep the implementation of DATASource optimized for "best practices" and keep it lean instead.

Here's what other users reported: https://github.com/3lvis/DATAStack/issues/28

justinmakaila commented 8 years ago

@3lvis But the issue isn't with the data created on the main context, it's with the data that's mutated in the background, and saved to the persistent store. The main context is never updated about these changes, and it appears as if there's no way to retrieve them.

3lvis commented 8 years ago

I see, thanks for clarifying that, I'll have a look.

3lvis commented 8 years ago

It works.

import UIKit
import CoreData
import DATASource

class ViewController: UITableViewController {

    var dataStack: DATAStack

    lazy var dataSource: DATASource = {
        let request: NSFetchRequest = NSFetchRequest(entityName: "User")
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]

        let dataSource = DATASource(tableView: self.tableView, cellIdentifier: "Cell", fetchRequest: request, mainContext: self.dataStack.mainContext, configuration: { cell, item, indexPath in
            if let name = item.valueForKey("name") as? String, createdDate = item.valueForKey("createdDate") as? NSDate, score = item.valueForKey("score") as? NSNumber {
                cell.textLabel?.text =  name + " - " + score.description

            }
        })

        return dataSource
    }()

    init(dataStack: DATAStack) {
        self.dataStack = dataStack

        super.init(style: .Plain)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        self.tableView.dataSource = self.dataSource

        let backgroundButton = UIBarButtonItem(title: "Background", style: .Done, target: self, action: #selector(ViewController.createBackground))
        self.navigationItem.rightBarButtonItem = backgroundButton

        let editButton = UIBarButtonItem(title: "Edit", style: .Done, target: self, action: #selector(ViewController.editUsersInContext))
        self.navigationItem.leftBarButtonItem = editButton
    }

    func createBackground() {
        self.dataStack.performInNewBackgroundContext { _backgroundContext in
            let entity = NSEntityDescription.entityForName("User", inManagedObjectContext: _backgroundContext)!
            let object = NSManagedObject(entity: entity, insertIntoManagedObjectContext: _backgroundContext)
            object.setValue("Background", forKey: "name")
            object.setValue(NSDate(), forKey: "createdDate")
            object.setValue(0, forKey: "score")
            try! _backgroundContext.save()
            self.dataStack.persistWithCompletion()
        }
    }

    func editUsersInContext() {
        self.dataStack.performInNewBackgroundContext { backgroundContext in
            let fetchRequest = NSFetchRequest(entityName: "User")
            let users = try! backgroundContext.executeFetchRequest(fetchRequest)
            for user in users {
                let score = user.valueForKey("score") as! NSNumber
                let newScore = NSNumber(int: score.integerValue + 1)

                user.setValue(newScore, forKey: "score")
            }
            try! backgroundContext.save()
            self.dataStack.persistWithCompletion()
        }
    }
}
3lvis commented 8 years ago

Closing since sample above fixes the problem.