Track Santa with Realm: Part 3

Alright team. It’s the week you’ve all been waiting for. It’s time to connect to the Realm Object Server, and have data automagically sync!

As a way to prove to ourselves that we’ve correctly connected, we’ll map Santa’s route across the globe (which we know we didn’t put there, since there’s obviously no way we could know that ahead of time).

1. Prepare our app for connecting to the Realm Object Server.

Things will be easier if we make some small changes now, then we’ll have a better architecture for using the full platform.

  1. For starters, we can delete the lines where we add our test Santa data to Realm. Before we do that, run your app one time to make sure the test data is saved to Realm, so we can still test our app before connecting to the server. We’ll delete the test data later.

    Once your app shows the test data, go to SantaTrackerViewController and delete this part:

    // This is in viewDidLoad(), FYI
    if santas.isEmpty {
        try? realm.write {
            realm.add(Santa.test())
        }
    }

    Now we won’t clutter up Realm with both the server’s data and our test data accidentally.

  2. Now let’s create a class to help with the server connection. Instead of calling Realm() directly to get our Realm, which would give us an un-synced Realm, we’ll ask this class to give us the synced Realm. The new class will be responsible for remembering the authentication settings and applying them appropriately.

    Create a new file called SantaRealmManager.swift for our class SantaRealmManager (or whatever you like, that’s just what I named mine), and add this code:

    class SantaRealmManager {
        func realm() -> Realm {
            return try! Realm()
        }
    }

    (Don’t forget to import RealmSwift.) Right now, this just gives us the same Realm we’ve been using, but it will let us change to a synced Realm without having to change the call sites. I know this seems pointless for now (because it is, you’re not wrong there), but trust me, you’ll want this setup in a few minutes.

    As a side note, I’ve made this a class rather than a struct because it makes having multiple references easier. This object will eventually hold state related to being logged in. If this were a struct, I’d have to remember to log in every time I got a new reference to it, which would be kind of annoying (and very easy to forgot to do). With a class, I just need to log in once, and if I gave someone else a reference, they don’t need to log in again. The shared state is OK by me because logging in only works one way, one time, and no other state is mutated. (I won’t be making this class thread-safe, but that wouldn’t be that hard either.) In any event, this app won’t be creating multiple references to any SantaRealmManagers, but it’s good to think about this in case I ever wanted to expand this app.

    Anywho, back to the action! Let’s change our view controller to use this class:

    class SantaTrackerViewController {
        // Other properties go up here
           
        private let realmManager = SantaRealmManager()
           
        // Functions down here
    }

    Then in both places where we get a reference to Realm (one is in viewDidLoad(), the other is in the deinit), replace try! Realm() with realmManager.realm().

    Alright, now we’re ready!

2. Connect to the Realm Object Server.

The API is pretty straightforward. First we create credentials, then use that to log in and get a user, then use that user to get the right Realm. Three easy steps!

  1. Fortunately, we made a class for this all to live in! It’s our formerly useless friend, SantaRealmManager. Let’s start by defining some variables we’ll need.

    class SantaRealmManager {
        private let username = "santatracker@realm.io"
        private let password = "h0h0h0"
           
        private let authServerURL = URL(string: "http://162.243.150.99:9080")!
        private let syncServerURL = URL(string: "realm://162.243.150.99:9080/santa")!
    
        // Our existing method is down here
    }

    First up we have a login that I’ve set up with the right permissions. (No, you can’t use your own, since the server doesn’t know who you are and what permissions you’d need.) FYI, this account has read-only access to this data, so make sure you’re not trying to write data, since that won’t work. Thanks Access Control!

    There’s also two URLs: one we’ll use to log in, and one we’ll point our synced Realm at. If you’re getting weird errors, make sure you’ve imported Foundation for URL, and RealmSwift.

  2. Alright, let’s logIn! Here’s the method:

    class SantaRealmManager {
      // Properties
      private var user: SyncUser?
         
      func logIn(completion: ((Void)->Void)? = nil) {
            guard user == nil else {
                completion?()
                return
            }
               
          let credentials = SyncCredentials.usernamePassword(username: username, password: password)
          SyncUser.logIn(with: credentials, server: authServerURL) {
              user, error in
              if let user = user {
                  self.user = user
                  DispatchQueue.main.async {
                      completion?()
                  }
              } else if let error = error {
                  fatalError("Could not log in: \(error)")
              }
          }
      }
         
      // realm()
    }

    If we’ve already logged in, we’ll have a user (as you’ll see in a second), so we can just let the caller know that we’re good to go by calling the completion closure, then bail! If we’re not logged in, then we…log in. Of course.

    We start by building a SyncCredentials object using the username and password I gave you. Realm Object Server also supports Google, Facebook, and iCloud (on Apple platforms) out of the box, and you can build your own authentication mechanism if you want to hook it up to something else. Check out the docs for more info on authenticating with Realm Object Server.

    Whatever way you generate credentials, you use them plus the authentication URL of your server to log in. As you can see from the variables we got above, the authentication URL and the sync URL aren’t the same! (Easy trap to fall into, that I did fall into. Learn from my mistakes.) Authentication uses HTTP, and sync URLs must use the realm:// protocol.

    The logIn method on SyncUser calls the completion closure with two optional parameters: a user or an error. If logging in worked, we save the user to the new property at the top, and call the completion closure if we were given one. I’ve chosen to just always throw things back onto the main thread, so the threading implementation of SyncUser doesn’t leak out of this class. In a more complex app, you might want a more robust threading model where you do work on background threads.

    If logging in failed, just fatalError. ¯\_(ツ)_/¯ It’s a demo app. In a real app, you should probably handle that?

    Now that we’re logged in, we can use that to supply the synced Realm, rather than the non-synced Realm we’re vending now.

  3. Let’s update our realm() method with our new sync magic:

    class SantaRealmManager {
        // Properties
           
        // logIn
           
        func realm() -> Realm? {
            if let user = user {
                let syncConfig = SyncConfiguration(user: user, realmURL: syncServerURL)
                let config = Realm.Configuration(syncConfiguration: syncConfig)
                guard let realm = try? Realm(configuration: config) else {
                    fatalError("Could not load Realm")
                }
                return realm
            } else {
                return nil
            }
        }
    }

    The first thing you’ll notice is that the signature has changed. We used to always give back a Realm, so it wasn’t optional. Now, we might not be logged in when someone calls this, so we might not be able to give out a Realm.

    We see if we’re logged in first, just like in logIn. Calling this method when we’re not logged in gets you a nil. If we are logged in, then we use the user to get the right Realm. We construct a SyncConfiguration, then feed that into a Realm.Configuration, which lets us get the Realm.

    We responibly guard on the off chance that we can’t get the Realm (probably due to iOS running out of memory), then immediately irresponsibly fatalError. But in a real app, don’t do that!

  4. Since we changed the behavior of the SantaRealmManager, we need to update SantaTrackerViewController accordingly. If you go back to that file, the compiler will already be telling you one change you need to make. Replace the bottom half of viewDidLoad() and all of deinit:

    class SantaTrackerViewController {
        // More properties
       	
        private let realmManager = SantaRealmManager()
        // We need this if Santa hasn't been downloaded
        private var notificationToken: NotificationToken?
        // We have to keep a strong reference to Santa for KVO to work
        private var santa: Santa?
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Set up the map manager
            mapManager = MapManager(mapView: mapView)
    
            // Find the Santa data in Realm
            realmManager.logIn {
                // Be responsible in unwrapping!
                if let realm = self.realmManager.realm() {
                    let santas = realm.objects(Santa.self)
    
                    // Has Santa's info already been downloaded?
                    if let santa = santas.first {
                        // Yep, so just use it
                        self.santa = santa
                        santa.addObserver(self)
                    } else {
                        // Not yet, so get notified when it has been
                        self.notificationToken = santas.addNotificationBlock {
                            _ in
                            let santas = realm.objects(Santa.self)
                            if let santa = santas.first {
                                self.notificationToken?.stop()
                                self.notificationToken = nil
                                self.santa = santa
                                santa.addObserver(self)
                            }
                        }
                    }
                }
            }
        }
           
        deinit {
            santa?.removeObserver(self)
        }
    
        // More methods
    }

    You can see we’re using the new API we just wrote, along with a few other key changes, but let’s talk about our SantaRealmManager API first. We log in, then when that’s done (i.e., when the completion closure is run), we get a reference to the synced Realm (safely, of course!).

    When I was first writing this app, I tried to just grab the first Santa and use it, like we had last week. But now with syncing involed, we might not have that info synced from the server yet (like if this is the first time we’ve run this app). In that case, we never set up the Santa observer because there’s no Santa to observe! If we were observing a List or Results property, this wouldn’t be an issue, but with a single object, it can get a bit tricky.

    So the first thing we do with the reference to the Realm is to check if we already have Santa’s data. If we do, we observe it just like we did before, though this time we remember to keep a strong reference to it to KVO will work (which was an error on my part last time to not do so 😅). If we don’t have the data yet, we set up a notification block on all the Santas, which will tell us when one gets added to our local Realm. Once we get that, we turn off the notification and do the same stuff we did before.

    In deinit, we just make sure that we remove our observation, using the reference to Santa that we already have.

  5. Before you can run the app, you’ll need to turn on Keychain Sharing in Capabilities. In the left sidebar, select your Xcode proejct (topmost file, with a blue icon), select your app target under Targets in the left sidebar inside the main window (Xcode’s UI gets confusing), go to the Capabilities tab across the top, then find Keychain Sharing in that list and turn it on.

    Enabling Keychain Sharing

    I’m pretty sure you need a developer account to use this feature, but please let me know if that’s not true!

Alright kids, it’s time to sync some data! If you run your app, you should see Santa move to San Francisco (he’s hanging out at our office because it’s so awesome to work here, duh), and the number of presents should go to 1000.

If you’ve got that, then great! If not, try running the app again, check all the code above, and if you’re stuck, just ask me on Twitter.

3. Mapping Santa’s Route

One of the things your app synced from the server was Santa’s planned route! It’s in his route property that we set up last week. Let’s map that, so we can see when he’ll be in your neck of the woods.

  1. We already have a class whose responsibility is handling the map (so convenient!), so we can add this code to it. First, we’ll need some new properties:

    class MapManager: NSObject {
        private let mapView: MKMapView
        private let santaAnnotation = MKPointAnnotation()
        private var routeOverlay: MKPolyline
    
        ...

    We’re about to need a reference to the MKMapView, which explains the implicitly unwrapped optional instance of this class in the SantaTrackerViewController (the map manager couldn’t be initialized with the map view until viewDidLoad, but it would have to be initialized during init if it weren’t optional, and that comes before viewDidLoad). We’ll also be using an MKPolyline to draw Santa’s route onto the map.

    Next comes the updated init(mapView:):

    ...
           
        init(mapView: MKMapView) {
            self.mapView = mapView
            santaAnnotation.title = "🎅"
            routeOverlay = MKPolyline(coordinates: [], count: 0)
            super.init()
            mapView.addAnnotation(self.santaAnnotation)
        }
           
        ...

    What you’d expect, just saving a reference to the map view and making an empty polyline. Otherwise the same as we had before.

    Now the exciting stuff: the new update(with:).

    ...
    
        func update(with santa: Santa) {
            let santaLocation = santa.currentLocation.clLocationCoordinate2D
            let coordinates: [CLLocationCoordinate2D] = santa.route.flatMap({ $0.location?.clLocationCoordinate2D })
            DispatchQueue.main.async {
                self.santaAnnotation.coordinate = santaLocation
                self.mapView.remove(self.routeOverlay)
                self.routeOverlay = MKPolyline(coordinates: coordinates, count: coordinates.count)
                self.mapView.add(self.routeOverlay)
            }
        }
    }

    The current location lines are the same as before. To plot Santa’s route, we map the locations from his route into CLLocationCoordinate2Ds, since that’s what an MKPolyline needs. We pass that array over to the main thread (remember, we can’t pass the Santa itself or its direct properties across threads). There, we remove the old polyline, since it’s outdated now, and replace it with the new points. Pretty straightforward, right?

    Now run your app! …You don’t see any difference? There’s no line zigzagging across the globe?

  2. You’re not crazy, and you haven’t done anything wrong! It’s just that MKMapView doesn’t know how to draw an MKPolyline by default. We need to help it out! In order to do that, our map manager needs to conform to the MKMapViewDelegate protocol and supply a renderer, which is the object that tells the map view how to draw the polyline. (This protocol conformance is why MapManager is an NSObject subclass, I forgot to explain that in part 1.)

    extension MapManager: MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            guard overlay is MKPolyline else {
                return MKOverlayRenderer(overlay: overlay)
            }
    
            let renderer = MKPolylineRenderer(overlay: overlay)
            renderer.strokeColor = .black
            renderer.lineWidth = 3
            renderer.lineDashPattern = [3, 6]
            return renderer
        }
    }

    We’re only implementing one method here, since all we want for now is to draw the route. We check that we’re working with an MKPolyLine, and if we’re not we just use the default renderer since we have to supply something. If it’s a polyline, we make an MKPolylineRenderer (can you guess what that does?), set a few properties that determine the line’s visuals, and return it. You can make the route look like whatever you want! I kept mine simple for now, but #yolo.

    The one other thing you’ll need to do is to actually make the MapManager the delegate of the MKMapView, or none of this matters. I just did that at the end of MapManager.init(mapView:), but you could do it right after calling that in SantaTrackerViewController.viewDidLoad() as well.

    Now run your app. You should see a crazy path going up and down across the continents. If you don’t, reread the above, make sure you set the delegate, and then ask on Twitter.

    A personal observation here: It would be awesome if this path could be interpolated, so the path looked nice and smooth on the map, but I was unable to find anything that looked like it would do that. If you know of something, hit me up!

And there you have it! You’ve got an app that syncs with a Realm Object Server and shows that data!

Those reactive patterns we’ve been following sure came in handy, didn’t they? Did you realize that the only real changes we had to make to support syncing data were to the way we get references to a Realm? We didn’t have to change anything at all about the flows by which UI updates could be called. That’s becuase the Realm Mobile Database has been specifically designed with this in mind: Changes can come from anywhere at any time, and you shouldn’t have to worry about that. You just use your database, and syncing just fits right into your existing patterns. That’s magic, yo. ✨


See ya next week for our final installation, where we’ll learn about event handling on the server, and add some polish to this app. Until then, stay warm/cool (depending on where you are), and sing all your favorite holiday songs. 🎶


Michael Helmbrecht

Michael Helmbrecht

Michael designs and builds things: apps, websites, jigsaw puzzles. He's strongest where disciplines meet, and is excited to bring odd ideas to the table. But mostly he's happy to exchange knowledge and ideas with people. Find him at your local meetup or ice cream shop, and trade puns.