Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Realm notification token on background thread

Tags:

ios

swift

realm

I was trying to fetch realm data on the background thread and add a notification block (iOS, Swift).

Basic example:

    func initNotificationToken() {

        DispatchQueue.global(qos: .background).async {
          let realm = try! Realm()
          results = self.getRealmResults()

        notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in

          switch changes {
           case .initial:
            self?.initializeDataSource()
            break
          case .update(_, let deletions, let insertions, let modifications):
           self?.updateDataSource(deletions: deletions, insertions: insertions, modifications: modifications)
           break
          case .error(let error):
            fatalError("\(error)")
            break
          }
        }
      }
    }

    func initializeDataSource() {
          // process the result set data 

         DispatchQueue.main.async(execute: { () -> Void in
            // update UI
         })
    }

    func updateDataSource(deletions: [Int], insertions: [Int], modifications: [Int]) {
          // process the changes in the result set data 

         DispatchQueue.main.async(execute: { () -> Void in
            // update UI
         })
    }

When doing this I get

'Can only add notification blocks from within runloops'

I have to do some more extensive processing with the returned data and would like to only go back to the main thread when updating the UI after the processing is done.

Another way would probably to re-fetch the data after any update on the background thread and then do the processing, but it feels like avoidable overhead.

Any suggestions on the best practice to solve this?

like image 253
Nechriz Avatar asked Jan 24 '17 03:01

Nechriz


1 Answers

To add a notification on a background thread you have to manually run a run loop on that thread and add the notification from within a callout from that run loop:

class Stuff {
    var token: NotificationToken? = nil
    var notificationRunLoop: CFRunLoop? = nil

    func initNotificationToken() {
        DispatchQueue.global(qos: .background).async {
            // Capture a reference to the runloop so that we can stop running it later
            notificationRunLoop = CFRunLoopGetCurrent()

            CFRunLoopPerformBlock(notificationRunLoop, CFRunLoopMode.defaultMode.rawValue) {
                let realm = try! Realm()
                results = self.getRealmResults()

                // Add the notification from within a block executed by the
                // runloop so that Realm can verify that there is actually a
                // runloop running on the current thread
                token = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
                    // ...
                }
            }

            // Run the runloop on this thread until we tell it to stop
            CFRunLoopRun()
        }
    }

    deinit {
        token?.stop()
        if let runloop = notificationRunLoop {
            CFRunLoopStop(runloop)
        }
    }
}

GCD does not use a run loop on its worker threads, so anything based on dispatching blocks to the current thread's run loop (such as Realm's notifications) will never get called. To avoid having notifications silently fail to do anything Realm tries to check for this, which unfortunately requires the awakward PerformBlock dance.

like image 154
Thomas Goyne Avatar answered Oct 05 '22 09:10

Thomas Goyne