Realm initial sync is working but updates are not?

Hi all,

I’m porting a Swift iOS demo app from ROS to MongoDB Realm. I have an accounts collection and I initialize a notification handler as follows:

notificationToken = accounts.observe { [weak self] (changes) in
    guard let accountsTableView = self?.accountsTableView else { return }
    switch changes {
    case .initial:
        accountsTableView.reloadData()
    case .update:
        accountsTableView.reloadData()
    case .error(let error):
        // An error occurred while opening the Realm file on the background worker thread
        fatalError("\(error)")
    }
}

Then initial sync works and my tableview gets loaded with the list of expected accounts. However, if I make a change to an account in the MongoDb Collection, then I do not get the update syncing with my IOS app (a breakpoint on the “.update” case is not hit)?

If I restart the app the change comes though with the initial sync.

The Realm server logs do however show the following entry:

> OK
> Dec 02 8:27:55+00:00
> 49ms
> SyncWrite

[5fc7133ea9536f9ade3901ea]

Source:

Write originated from MongoDB

Logs:

[ “Upload message contained 1 changeset(s)”, “Integrating upload required conflict resolution to be performed on 0 of the changesets”, “Latest server version is now 12” ]

Partition:

5fc6497676312e5697784cbb

Write Summary:

{ “Account”: { “replaced”: [ “5fc4e76983e79e3471f8d3c7” ] } }

And my app logs show the following:

2020-12-02 08:27:24.554687+0000 mongodb-realm-offline-banking[91583:9949465] Sync: Connection[1]: Session[1]: client_reset_config = false, Realm exists = true, async open = false, client reset = false

2020-12-02 08:27:24.634948+0000 mongodb-realm-offline-banking[91583:9949452] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed

2020-12-02 08:27:24.690874+0000 mongodb-realm-offline-banking[91583:9949465] Sync: Connection[1]: Connected to endpoint ‘3.210.32.164:443’ (from ‘192.168.1.141:62813’)

FWIW - The app was fully functioning with ROS. Obviously I’ve updated the SDK to the latest version in order to sync with MongoDB Realm, and the notification handler did not require any code changes.

Anyone have any ideas?

Hi, @Ross_Whitehead,
How do you store notificationToken value? By description it looks like it’s being deallocated right after the subscription.

Hi Pavel, thanks for the reply.
notificationToken is a class level field on AccountsViewController (The Accounts view contains the table). So should not be deallocated until the AccountsViewController is de-initialized.

Here’s the full code…

    import UIKit
    import RealmSwift

    class AccountsViewController: UIViewController {
        @IBOutlet weak var accountsTableView: UITableView!
        
        var accounts: Results<Account>
        var realm: Realm
        var notificationToken: NotificationToken? = nil
        let app = App(id: AppConfig.REALM_APP_ID)
        
        required init?(coder aDecoder: NSCoder) {
            let user = app.currentUser
            let ownerId = AppConfig.OWNER_ID
            
            var configuration = user?.configuration(partitionValue: ownerId)
            configuration?.objectTypes = [Account.self]
            
            self.realm = try! Realm(configuration: configuration!)
            self.accounts = realm.objects(Account.self)

            super.init(coder: aDecoder)
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            setUpRealmNotificationHandler()
        }
        
        override func viewWillAppear(_ animated: Bool) {
            self.parent?.title = "Your Accounts"
        }
        
        deinit {
            notificationToken!.invalidate()
        }
       
        fileprivate func setUpRealmNotificationHandler() {
            notificationToken = accounts.observe { [weak self] (changes) in
                guard let accountsTableView = self?.accountsTableView else { return }
                switch changes {
                case .initial:
                    accountsTableView.reloadData()
                case .update:
                    accountsTableView.reloadData()
                case .error(let error):
                    // An error occurred while opening the Realm file on the background worker thread
                    fatalError("\(error)")
                }
            }
        }
    }

    extension AccountsViewController: UITableViewDelegate, UITableViewDataSource {
        func tableView(_ tableView: UITableView,
                       shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
            return false
        }
        
        func numberOfSections(in tableView: UITableView) -> Int {
            return accounts.count
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 1
        }
        
        func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
            return 10
        }
        
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            let headerView = UIView()
            headerView.backgroundColor = UIColor.clear
            return headerView
        }
        
        func tableView(_ tableView: UITableView,
                       cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            let cell = tableView.dequeueReusableCell(withIdentifier: "AccountCell") as! AccountTableViewCell
            
            cell.selectionStyle = .none
            cell.backgroundColor = UIColor.white
            cell.layer.borderColor = UIColor.gray.cgColor
            cell.layer.borderWidth = 0.25
            cell.layer.cornerRadius = 2
            cell.clipsToBounds = true
            
            let account = accounts[indexPath.section]
            cell.populate(with: account)
            return cell
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let selectedAccount = accounts[indexPath.section]
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let controller = storyboard.instantiateViewController(withIdentifier: "transactionsViewController") as! TransactionsViewController
            controller.account = selectedAccount
            self.navigationController?.pushViewController(controller, animated: true)
        }
    }

If you’re using MongoDB Realm Sync (looks like you are), the way you’re connecting to Realm is an issue. It’s not done like it used to be - the first time you connect it needs to be async Open Realm

Also see Tip

To open a synced realm, call asyncOpen, passing in the user’s Configuration object

the code

Realm.asyncOpen(configuration: configuration) { result in
    switch result {

You’ve probably got this covered but also ensure the Account object contains both an _id primary key var as well as the partition key.

1 Like

Jay, thanks for the suggestions. I changed my code to use the asyncOpen method as you advised, but no luck. The notification handler is still not getting fired on updates.

And yes, I do have _id and a separate partition key in my objects - and the initial load works as expected.

Here’s the changed code:

class AccountsViewController: UIViewController {
    @IBOutlet weak var accountsTableView: UITableView!
    
    var accounts: Results<Account>? = nil
    var notificationToken: NotificationToken? = nil
    var app = App(id: AppConfig.REALM_APP_ID)
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad() {
        let user = app.currentUser
        let ownerId = AppConfig.OWNER_ID
        
        var configuration = user?.configuration(partitionValue: ownerId)
        configuration?.objectTypes = [Account.self, Transaction.self, PaymentRequestEvent.self]
        
        Realm.asyncOpen(configuration: configuration!) { result in
            switch result {
            case .failure(let error):
                print("Failed to open realm: \(error.localizedDescription)")
                fatalError("\(error)")
            case .success(let realm):
                print("Successfully opened realm: \(realm)")
                self.accounts = realm.objects(Account.self)
                self.setUpRealmNotificationHandler()
            }
        }
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        self.parent?.title = "Your Accounts"
    }
    
    deinit {
        notificationToken!.invalidate()
    }
    
    fileprivate func setUpRealmNotificationHandler() {
        notificationToken = accounts!.observe { [weak self] (changes) in
            guard let accountsTableView = self?.accountsTableView else { return }
            switch changes {
            case .initial:
                accountsTableView.reloadData()
            case .update:
                accountsTableView.reloadData()
            case .error(let error):
                // An error occurred while opening the Realm file on the background worker thread
                fatalError("\(error)")
            }
        }
    }
}

Did you set your viewController to be the tableView Delegate and Datasource?

override func viewDidLoad() {
   super.viewDidLoad()
   self.accountsTableView.delegate = self
   self.accountsTableView.dataSource = self
   setUpRealmNotificationHandler()
}

Hi Jay, yes I set these in the storyboard. And everything binds correctly with the initial sync of objects appearing in the table. After than when I make a server-side change the updates are not coming through. When I have a moment I’m going to get the realm ios tutorial code up-and-running and see if that reveals some answers. But currently snowed under with other work ATM.

A shot in the dark here but I think the issue may lie with how Realm is being accessed. Note that with Realm.asyncOpen:

The Realm passed to the publisher is confined to the callback queue as if Realm(configuration:queue:) was used.

So instead of using the realm returned within that call

Realm.asyncOpen(configuration: configuration!) { result in
            switch result {
            case .failure(let error):
                print("Failed to open realm: \(error.localizedDescription)")
                fatalError("\(error)")
            case .success(let realm): <- realm is on a background thread
               print("Successfully opened realm: \(realm)")
               self.accounts = realm.objects(Account.self)
               self.setUpRealmNotificationHandler()
           }
 }

try this instead

Realm.asyncOpen(configuration: configuration!) { result in
            switch result {
            case .failure(let error):
                print("Failed to open realm: \(error.localizedDescription)")
                fatalError("\(error)")
            case .success(let realm): <- realm is on a background thread
                self.configureRealm()
           }

func configureRealm() {
    let app = App(id: AppConfig.REALM_APP_ID)
    let user = app.currentUser
    let ownerId = AppConfig.OWNER_ID
    let config = user?.configuration(partitionValue: ownerId)
    let realm = try! Realm(configuration: config)

    self.accounts = realm.objects(Account.self)
    self.setUpRealmNotificationHandler()
    self.taskTableView.delegate = self
    self.taskTableView.dataSource = self
    DispatchQueue.main.async {
        self.accountsTableView.reloadData()
    }
}

Ok, this was annoying me so I downloaded and configured the ISO Swift Tutorial app. It has the same problem, which is: the change handler is not fired for modifications. However, it is being fired for deletions and insertions.

Hi Jay, sorry for the delay in trying this out. Other work + xmas.
Unfortunately, it still does not work. I’m going to give up on this for now. My company is going to do a Realm POC (rather than me just playing) in coordination with MongoDB professional services. Once this is happening I’ll get them to advise. And if do find a resolution I’ll report back.
Many Thanks, Ross

The task app is working for me. However, I don’t believe the downloadable git app includes the observer code. Did yours?

You may know this but the order in which the handler handles the events is important. I’ve goofed a couple of times and swapped things around and it appears one event or the other was not being called but they were, I just had them in the wrong order.

case .update(_, let deletions, let insertions, let modifications):

and

// Always apply updates in the following order: deletions, insertions, then modifications.
 // Handling insertions before deletions may result in unexpected behavior.
1 Like

@Ross_Whitehead The likely issue you are running into is that you are using Compass, 3T, or the Atlas collection viewer to make a modifications. This actually translates into delete and replace of that document instead of an actual modification which confuses the Realm’s notification system. We are looking to fix this in both places but for now you should be able to trigger a modification notification by using a mongo shell command or similar

1 Like

OK, that’s good to hear. I appreciate that everything is in beta, so a few “undocumented features” will need to be ironed out. Thanks Pavel, Jay and Ian for taking time to help out.