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.
-
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.
-
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 classSantaRealmManager
(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
SantaRealmManager
s, 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 thedeinit
), replacetry! Realm()
withrealmManager.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!
-
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
forURL
, andRealmSwift
. -
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 onSyncUser
calls the completion closure with two optional parameters: auser
or anerror
. 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 ofSyncUser
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.
-
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 aRealm
.We see if we’re logged in first, just like in
logIn
. Calling this method when we’re not logged in gets you anil
. If we are logged in, then we use theuser
to get the right Realm. We construct aSyncConfiguration
, then feed that into aRealm.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 irresponsiblyfatalError
. But in a real app, don’t do that! -
Since we changed the behavior of the
SantaRealmManager
, we need to updateSantaTrackerViewController
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 ofviewDidLoad()
and all ofdeinit
: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 aList
orResults
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
Santa
s, 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. -
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.
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.
-
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 theSantaTrackerViewController
(the map manager couldn’t be initialized with the map view untilviewDidLoad
, but it would have to be initialized duringinit
if it weren’t optional, and that comes beforeviewDidLoad
). We’ll also be using anMKPolyline
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 intoCLLocationCoordinate2D
s, since that’s what anMKPolyline
needs. We pass that array over to the main thread (remember, we can’t pass theSanta
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?
-
You’re not crazy, and you haven’t done anything wrong! It’s just that
MKMapView
doesn’t know how to draw anMKPolyline
by default. We need to help it out! In order to do that, our map manager needs to conform to theMKMapViewDelegate
protocol and supply a renderer, which is the object that tells the map view how to draw the polyline. (This protocol conformance is whyMapManager
is anNSObject
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 anMKPolylineRenderer
(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
thedelegate
of theMKMapView
, or none of this matters. I just did that at the end ofMapManager.init(mapView:)
, but you could do it right after calling that inSantaTrackerViewController.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. 🎶
Receive news and updates from Realm straight to your inbox