Track Santa With Realm: Part 4

Alright everyone. It’s almost the big day! And that means we need to wrap this app up if we’re gonna make it in time. So in this final part of the tutorial, we’ll put the finishing touches on the app: the weather where Santa is, and how long until he arrives.

I’m personally excited about the weather data, because it was my first experience developing for the Realm Object Server! I’m not a very experienced JavaScript developer, but the API was close enough to Realm Swift that I stumbled through alright. All to say, it’s neat stuff, and surprisingly easy to make something cool.

So let’s take a look at how the whole system works: how the app will pass data up to the server, how the server will act on that, and how the server can pass data back. Then Santa’s arrival time will be a bonus at the end!

1. Passing data from the app to the server

The whole setup is pretty easy once you have your model set up, so let’s start there. Make a new file called (creatively) Weather.swift:

class Weather: Object {
    dynamic var location: Location?
    dynamic var lastUpdatedDate: Date = Date()

    private dynamic var _loadingStatus: Int = 0
    private dynamic var _temperature: Double = .nan
    private dynamic var _condition: Int = 0
    var loadingStatus: LoadingStatus {
        let condition = Condition(rawValue: _condition)!
        return LoadingStatus(serverStatus: _loadingStatus,
                             temperature: _temperature,
                             condition: condition)
    }
    
    convenience init(location: Location) {
        self.init()
        self.location = location
    }
    
    func addObserver(_ observer: NSObject) {
        addObserver(observer, forKeyPath: #keyPath(Weather._loadingStatus), options: .initial, context: nil)
    }
    
    func removeObserver(_ observer: NSObject) {
        removeObserver(observer, forKeyPath: #keyPath(Weather._loadingStatus))
    }
}

We start with a location (marked as optional, per the docs), which of course we would need to get weather info! Then just for our bookkeeping we’ll also note the last time this weather was updated, since old weather data isn’t very useful. The default value for that is of course the Date initializer, since it just makes a reference to the time it (and therefore the Weather object) was created.

After that, things get less clear. We have three properties: a loading status, a temperature, and a condition (like rainy, clear, etc.). But they’re all private! And then there’s a crazy computed property below that. For this class, the temperature and condition are undefined if the data’s not loaded. But rather than simply document that and hope everyone follows the guidelines, I’ve made the API enforce this: The only way to get the temperature and condition are to get the public loadingStatus enum, and the only enum case that will expose that information will be the loaded case. Before we get to that enum, we’ll finish up here by noting the convenience for making new Weathers from Locations, and an observation just like the one we used on Santa.

Get more development news like this

Now onto this strange LoadingStatus enum. Under the Weather class, add the definition:

extension Weather {
    enum LoadingStatus {
        case uploading
        case processing
        case complete(temperature: Double, condition: Condition)

        fileprivate init(serverStatus: Int, temperature: Double, condition: Condition) {
            switch serverStatus {
            case 0:
                self = .uploading
            case 1:
                self = .processing
            case 2:
                self = .complete(temperature: temperature, condition: condition)
            default:
                fatalError("Unknown loading status: \(serverStatus)")
            }
        }
    }
}

We’ll put this enum inside of Weather, since its API is tied to Weather’s needs and it wouldn’t be useful anywhere else. You can see the three cases. uploading means we’ve made in on the device but the server hasn’t gotten to it yet, and processing means the server is working on it. But complete brings along the important data, since that case means we’ve gotten the data from the server. We have a secret initializer that Weather can call in its computed property, but no one else can use. It requires the temperature and condition, but doesn’t use them unless the status code (since we can’t store enums directly) says the data is loaded. The initializer API is a bit odd to be sure, but we’re only using it at one call site and I didn’t want to have to do all this in the computed property.

Now to finish off with those Conditions:

extension Weather {
    enum Condition: Int {
        case unknown = 0
        case clearDay, clearNight, rain, snow, sleet, wind, fog, cloudy, partlyCloudyDay, partlyCloudyNight
    }
}

I left this in Weather.swift, since it has no real logic. Weather party all in one file! 🌤🎊

I put this enum inside Weather for the same reason I put the last one in here. The list of conditions I’ve used here come straight from the Dark Sky API, since that’s the weather API we’ll be using and I have no desire to reinvent the wheel. Then the initializer just creates a Condition based on the string in the API response, nothing magical here.

Now, before we can start using this, we’ll need to make a small change to the way we work with Realms, and therefore to our SantaRealmManager. Right now, we just have one Realm, which makes everything super simple. But now we’re going to have two Realms: one for Santa’s data (we know this one pretty well), and another for the weather. Fortunately, the only difference between them is their sync URL, and then we just have to remember to use the right one. Modify SantaRealmManager like so:

class SantaRealmManager {
    // Properties & logIn
    
    // We removed realm()
    
    func santaRealm() -> Realm? {
        return realm(for: user, at: santaRealmURL)
    }
    
    func weatherRealm() -> Realm? {
        return realm(for: user, at: weatherRealmURL)
    }
    
    private func realm(for user: SyncUser?, at syncServerURL: URL) -> Realm? {
        guard let user = user else {
            return nil
        }
        
        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
    }
}

realm() became santaRealm(), and we added weatherRealm(). I’m sure you can figure out how to use those! 😜 Then I went ahead and extracted the common code into a helper function. Now I’m sure you’ve noticed that santaRealmURL and weatherRealmURL are new (the compiler’s dislike of them probably clued you in), so rename syncServerURL to santaRealmURL and add weatherRealmURL under it, which points to realm://162.243.150.99:9080/santa-weather (same as Santa, just with -weather tacked onto the end).

Now we can use this all! Head back to SantaTrackerViewController and fix the old reference to realm(), since we just removed that function.

Anywho, using the Weather objects is easy. We just have to create one, then watch it for changes. We’ll make a new one in update(with:), since that’s when we get Santa’s latest location. Add to the bottom of that function:

private func update(with santa: Santa) {
	// Santa part that we had before

    guard let weatherRealm = realmManager.weatherRealm() else {
        return
    }
    weather?.removeObserver(self)
    let weatherLocation = Location(latitude: santa.currentLocation.longitude, longitude: santa.currentLocation.longitude)
    let newWeather = Weather(location: weatherLocation)
    try? weatherRealm.write {
        weatherRealm.add(newWeather)
    }
    newWeather.addObserver(self)
    weather = newWeather
}

(FYI, we’ll add this in a second, but weather will be our strong reference that lets KVO work again.)

This is a straightforward process. First, we stop observing the old value, so it can’t update the UI for us any more. Then we make the new Weather, save it to the weatherRealm (which syncs it and gets the server to work), start observing it, and finally save the reference. One trick that we need to do here is to copy the data out of Santa’s currentLocation and into a brand-new Location, otherwise Realm think we’re trying to move objects between Realms and that’s a big no-no (a.k.a., an exception).

Let’s add that strong reference:

private var weather: Weather?

Now that we’re observing something else via KVO, we’ll need to update our KVO listener:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let santa = object as? Santa {
        update(with: santa)
    } else if let weather = object as? Weather {
        update(weather)
    } else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    }
}

Same formula as before, just make sure it’s the right type and pass it into an update, which looks like this:

private func update(with weather: Weather) {
    let temperatureText: String
    let conditionIcon: UIImage
    switch weather.loadingStatus {
    case .uploading:
        temperatureText = "??"
        conditionIcon = Weather.Condition.unknown.icon
    case .processing:
        temperatureText = "..."
        conditionIcon = Weather.Condition.unknown.icon
    case .complete(temperature: let temperature, condition: let condition):
        temperatureText = "\(temperature)°C"
        conditionIcon = condition.icon
    }
    DispatchQueue.main.async {
        self.temperatureLabel.text = temperatureText
        self.conditionIconView.image = conditionIcon
    }
}

Thanks overloading! In here, we figure out what the right info is to put in the UI. We use a switch of course, and figure out the right text and weather icon.

For the text, I’ve chosen to use ?? for .uploading and ... for .processing (I imagine it as the server thinking deeply), but you’re free to use whatever you like! If we have the data, we show it! The temperature is in Celsius (sorry America, our temperature system is not widely shared), so we’ll note that for the confused Americans (“It’s HOW COLD?!”). I leave localizing the temperature as an exercise for the reader.

The icons will come from the Conditions:

extension Weather {
    enum Condition {
    	// Cases
    
        var icon: UIImage {
            return UIImage(named: "condition-\(rawValue)")!
        }
    }
}

I’m cheating here a little bit by not giving the icons more descriptive names, but I didn’t want a whole other switch statement, and I know how they’ll be used. You can grab the icons here; they’ve got the right sizes and names so you can just drop them into your Assets.xcassets. Or feel free to make you own icons!

We’ll finish this up by adding an IBOutlet named conditionIconView that points to the UIImageView that currently has the little sun icon in it. Don’t forget to connect it in Interface Builder!

If you run this app now, you should be able to get weather for San Francisco! 🌉 If all you want to learn about is the mobile side, then skip ahead to step 3. For a peek behind the curtain, read on.

2. Handling the data on the server

This is the cool part. Where the magic happens. Granted, this example isn’t very exciting magic, but hey, it’s magic.

Server-side data access is only available through the Professional or Enterprise Editions of the Realm Mobile Platform. You can read more about those, or sign up for a free trial, on the Pricing page.

So I’m going to gloss right over setting up your Realm Object Server. You should check out the docs for info on how to do that. For this tutorial, I’m assuming you have it set up and everything is working.

We’ll open our index.js file with some basic setup:

'use strict';

var fs = require('fs');
var request = require('sync-request');
var Realm = require('realm');

var REALM_ACCESS_TOKEN = fs.readFileSync('/etc/realm/admin_token.base64','utf8');

// Dark Sky API key
var API_KEY = "DARK_SKY_KEY";

var SERVER_URL = 'realm://127.0.0.1:9080';

var NOTIFIER_PATH = "santa-weather";

// Statuses from the mobile app
var kUploadingStatus = 0;
var kProcessingStatus = 1;
var kCompleteStatus = 2;

...

Some libraries we’ll need (I’m using sync-request because it’s easier for a demo, but async would obviously be great in production), then some variables. We read the server’s access token right out of the file instead of copy-pasting, which would be easier to mess up. Then we have our Dark Sky API key (get your own if you want to run this), plus the sync server URL for Node to talk to, and the path we want to be notified about. We could use a regex, but this is simple. Last, we have the values from Weather.LoadingStatus, so we can use them by name.

Then I stole all these utility functions. We’ll only use isRealmObject and isObject for this, but copy-paste. ¯\_(ツ)_/¯

Next I have a simple function that acts kind of like a Weather.Condition initializer:

...

// Convert Dark Sky icon to correct integer
function weather_code(icon) {
    switch (icon) {
        case "clear-day":
            return 1;
        case "clear-night":
            return 2;
        case "rain":
            return 3;
        case "snow":
            return 4;
        case "sleet":
            return 5;
        case "wind":
            return 6;
        case "fog":
            return 7;
        case "cloudy":
            return 8;
        case "partly-cloudy-day":
            return 9;
        case "partly-cloudy-night":
            return 10;
        default:
            return 0;
    }
}

...

Building up to the main listener callback, I have a helper function that does all the API work:

...

function process_weather_request(request) {
    realm.write(function() {
        // Let the client know we are working on the request
        // This will be synced back immediately
        request._loadingStatus = kProcessingStatus;
    });

    // Double check on the location, better safe than sorry
    var location = unprocessedWeatherRequest.location
    if (!isRealmObject(location)) {
        return;
    }

    // Create our request URL
    var requestURL = "https://api.darksky.net/forecast/" + API_KEY + "/" + location.latitude + "," + location.longitude + "?exclude=minutely,hourly,daily,flags,alerts&units=si"
    // I'm excluding all the bonus info because I'm not using it
    // Declaring the units means they won't vary based on location

    // Nice easy HTTP request
    var res = request('GET', requestURL);
    var body = JSON.parse(res.getBody('utf8'));
    var responseData = body.responses[0];
    var error = body.error;
    if (isObject(error)) {
        // If there was a problem, just fail
        return;
    }
    else {
        // Get the response data we care about
        var currentTemperature = responseData.currently.temperature;
        var currentCondition = weather_code(responseData.currently.icon);
        realm.write(function() {
            // Save the data!
            unprocessedWeatherRequest._loadingStatus = kCompleteStatus;
            unprocessedWeatherRequest._currentTemperature = currentTemperature;
            unprocessedWeatherRequest._currentCondition = currentCondition;
        });
    }
}

...

I’ve put comments in, so you can follow along. This does 100% of the processing of a new weather request (I’m calling them “requests” on the server so that I remember what I’m doing with them). That’s pretty cool, huh?

Now we get to the change callback:

...

var change_notification_callback = function(change_event) {
    let realm = change_event.realm;
  
    // Get a list of all the new weather requests' indexes
    let changes = change_event.changes.Weather;
    let requestIndexes = changes.insertions;

    var requests = realm.objects("Weather");

    // Loop through every new weather request
    for (var i = 0; i < requestIndexes.length; i++) {
        let requestIndex = requestIndexes[i];
        let request = requests[requestIndex];
        // Double check that it's a Realm object and that it hasn't been handled yet
        if (!isRealmObject(request) || request._loadingStatus !== kUploadingStatus) {
            continue;
        }

        // Log the new request and send it off for processing
        console.log("New weather request received:" + change_event.path);
        process_weather_request(request);
    }
}

...

The change notification callback is the function that’s executed on every change, just like a notification block on the client side. The API inside is pretty different from Realm Swift’s, and takes advantage of some JavaScript language features.

I’ll wrap up index.js with the plumbing for event handling:

...

// Create the admin user
var admin_user = Realm.Sync.User.adminUser(REALM_ACCESS_TOKEN);

// Callback on Realm changes
Realm.Sync.addListener(SERVER_URL, admin_user, NOTIFIER_PATH, 'change', change_notification_callback);

console.log('Listening for Realm changes across: ' + NOTIFIER_PATH);

And that’s it! Since it’s all in one file, the boilerplate gets mixed in, but the whole file is under 100 LOC, which is pretty great for something so powerful and useful.

3. The finishing touches

The last thing I want to do with you is make the countdown at the top of the screen work. I won’t get into the code, since it’s not really related to Realm, but you can check it out here and insert it into your own code as you see fit. How to hook it up to the clock at the top is left as an exercise for you, dear reader. I have faith in you! 👍


As always, if you have thoughts, requests, suggestions, questions, or praise, please let us know on Twitter! I’ve had a great time working on this series, and we’re always looking for better ways to teach people about Realm. Maybe your idea will be the next hit! Until then, merry Christmas, happy holidays of every kind, and happy new year! 🎄


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.