Search code examples
iosswiftrealm

Inconsistent realm data between threads


I have a case where I need to populate realm with a series of objects upon user request. To populate realm I make a network call then write the objects to realm. When the write is complete, the background thread invokes a callback that switches to the main thread to process the results. I would much prefer to use realm notifications but this is a very specific use case and notifications are not an option.

I can't share my full project but I was able to reproduce this issue with a sample project. Here is my data model.

class Owner: Object {
    @objc dynamic var uuid: String = ""
    @objc dynamic var name: String = ""

    convenience init(uuid: String, name: String) {
        self.init()
        self.uuid = uuid
        self.name = name
    }

    func fetchDogs() -> Results<Dog>? {
        return realm?.objects(Dog.self).filter("ownerID == %@", uuid)
    }

    override class func primaryKey() -> String? {
        return "uuid"
    }
}

class Dog: Object {
    @objc dynamic var uuid: String = ""
    @objc dynamic var name: String = ""
    @objc dynamic var ownerID: String = ""

    func fetchOwner() -> Owner? {
        return realm?.object(ofType: Owner.self, forPrimaryKey: ownerID)
    }

    convenience init(uuid: String, name: String, ownerID: String) {
        self.init()
        self.uuid = uuid
        self.name = name
        self.ownerID = ownerID
    }

    override class func primaryKey() -> String? {
        return "uuid"
    }
}

I realize that a List would be appropriate for the Owner -> Dog relationship but that's not an option in this project.

In the sample project there are two buttons that we care about. One to add some Dog objects to realm and associate them with the Owner UUID. The other button deletes all of the Dog objects that belong to the owner.

The basic flow is like this:

  • Delete all of the owner's dogs
  • Create a new set of dogs on a background thread and add them to realm.
  • Invoke a callback
  • Switch back to the main thread
  • Print all of the owner's dogs

Nine times out of ten the dogs are printed out successfully. However, sometimes realm indicates that there are no dogs associated with the current owner. What could be the reason for this? My view controller logic is below.

EDIT: The last thing I should mention is that this issue is only only reproducible on a physical device. I can't reproduce it on the simulator. That makes me wonder if this is a realm bug.

// MARK: - Props and view controller life cycle

class ViewController: UIViewController {

    var owner: Owner?
    let ownerUUID = UUID().uuidString

    override func viewDidLoad() {
        super.viewDidLoad()
        owner = makeOwner()
    }
}

// MARK: - Action methods

extension ViewController {

    @IBAction func onDeletePressed(_ sender: UIButton) {
        print("Deleting...")
        deleteDogs()
    }

    @IBAction func onRefreshPressed(_ sender: UIButton) {
        print("Creating...")
        createDogs {
            DispatchQueue.main.async {
                self.printDogs()
            }
        }
    }

    @IBAction func onPrintPressed(_ sender: UIButton) {
        printDogs()
    }
}

// MARK: - Helper methods

extension ViewController {

    private func makeOwner() -> Owner? {
        let realm = try! Realm()
        let owner = Owner(uuid: ownerUUID, name: "Bob")
        try! realm.write {
            realm.add(owner)
        }
        return owner
    }

    private func deleteDogs() {
        guard let dogs = owner?.fetchDogs() else { return }
        let realm = try! Realm()
        try! realm.write {
            realm.delete(dogs)
        }
    }

    private func createDogs(completion: @escaping () -> Void) {
        DispatchQueue(label: "create.dogs.background").async {
            autoreleasepool {
                let realm = try! Realm()
                let dogs = [
                    Dog(uuid: UUID().uuidString, name: "Fido", ownerID: self.ownerUUID),
                    Dog(uuid: UUID().uuidString, name: "Billy", ownerID: self.ownerUUID),
                    Dog(uuid: UUID().uuidString, name: "Sally", ownerID: self.ownerUUID),
                ]
                try! realm.write {
                    realm.add(dogs)
                }
            }
            completion()
        }
    }

    private func printDogs() {
        guard let dogs = owner?.fetchDogs() else { return }
        print("Dogs: \(dogs)")
    }
}

Solution

  • It appears the the dogs are created on a separate thread; a 'background thread' and because of that it won't have a run loop - so the two threads are looking at different states of the data.

    When creating objects on a thread with no runloop, Realm.refresh() must be called manually in order to advance the transaction to the most recent state.

    Check out the documentation on Seeing changes from other threads