Hi again! 👋
If you followed last week’s tutorial (and you should have, or this one’s not gonna make much sense), your app should look like this when you launch it:
(I put my test pin on San Francisco in step 7.4 last week, so your pin might be somewhere else if you changed that. I’m also in SF, so your map may show something different. I really have no idea how the simulator picks the starting region.)
That’s a lovely app, but it doesn’t exactly do much. In fact, the only thing that’s not hard-coded is the pin. So let’s dive in and change that!
This week, we’ll be setting up all of our outlets to use data from Realm. This means we’ll also be making more data models, which will perfectly situate us to connect to the Realm Object Server next week!
1. Make more models.
For the rest of the app to work, we’ll need to model the rest of our data. You can see that we’ll need some way to represent:
- The time until Santa arrives at the current location
- What Santa is doing at the current time
- What the weather is like where Santa is
- How many presents Santa has left to deliver
Let’s take those one at a time.
The time until Santa arrives at the current location
Instead of asking the server for this, we can calculate it on the device, as long as we know when Santa will be making his stops. Fortunately, the server we’ll connect to next week has just this information! (Isn’t it convenient how that worked out?) So let’s add that info to our Santa
class:
class Santa: Object {
// Current location stuff from last week is up here
let route = List<Stop>()
// ignoredProperties is down here
}
A route is basically just a list of where Santa will be and when (i.e., a stop). The docs tell us to model lists as List
s (so well named!), and to declare them with this syntax. The contents will automatically update as we change them. Let’s make the compiler happy by defining a Stop
in a new file:
class Stop: Object {
dynamic var location: Location?
dynamic var time: Date = Date(timeIntervalSinceReferenceDate: 0)
convenience init(location: Location, time: Date) {
self.init()
self.location = location
self.time = time
}
}
As I said before, a stop is just somewhere that Santa will be, and when he’ll be there. Santa keeps a very tight schedule of course, and is never late or early. He’s had hundreds of years to get this down to a science, so we can trust that if our data says he’ll be somewhere, he will be.
We reuse the Location
class from last week, and add a Date
to it to mark the time. Note that we can use the new Swift 3 Foundation classes!
Then we have convenience initializer, which I forgot to talk about last week. Per the docs, custom initializers on our Realm objects must be marked convenience
and call self.init()
(not super
, like you might otherwise do) before doing anything else, due to Swift’s more limited introspection capabilities.
We’ll worry about the algorithm that deteremines arrival time later, but trust me, it’s cool! And I’m not just saying that because I wrote it.
What Santa is doing at the current time
You can see at the bottom of the map that we can report on what Santa’s up to at the moment you open the app. Maybe he’s flying around, or delivering presents, or even talking to Mrs. Claus! Now I don’t know about you, but that sounds like a Swift enum to me. Unfortunately, those can’t be directly represented in Realm. But fortunately they can be represented by their raw values!
Let’s make a new file for our Activity
enum:
enum Activity: Int {
case unknown = 0
case flying
case deliveringPresents
case tendingToReindeer
case eatingCookies
case callingMrsClaus
}
As you can see, this enum is backed by Int
s, which are representable in Realm. We could also have used String
s, but Int
s are smaller and we’ll only be using the enum anyway. I assign the first case to 0
, just to make double sure that the values are known (the Swift compiler will make each successive case 1 greater if you don’t give them explicit values). This makes it easy to share this data model with other platforms, since we can all agree on the meanings of the numbers.
We could define the text snippets for each of these activities elsewhere, like in the view controller, or have some dedicated object to translate them, but I like to keep simple things simple, and just add that right to this enum in an extension:
extension Activity: CustomStringConvertible {
var description: String {
switch self {
case .unknown:
return "❔ We're not sure what Santa's up to right now…"
case .callingMrsClaus:
return "📞 Santa is talking to Mrs. Claus on the phone!"
case .deliveringPresents:
return "🎁 Santa is delivering presents right now!"
case .eatingCookies:
return "🍪 Santa is having a snack of milk and cookies."
case .flying:
return "🚀 Santa is flying to the next house."
case .tendingToReindeer:
return "𐂂 Santa is taking care of his reindeer."
}
}
}
In a real app, these should be localized, but your humble author only speaks English fluently. 😓 You can of course translate them, or really edit them however you like. Make these say whatever you want!
Now to add them to Santa
:
class Santa: Object {
// Current location and route up here
private dynamic var _activity: Int = 0
var activity: Activity {
get {
return Activity(rawValue: _activity)!
}
set {
_activity = newValue.rawValue
}
}
// ignoredProperties down here
}
Our public API is what Swift developers would expect: a property called activity
of type Activity
, which is an enum. In order to save that with Realm (and sync it later on), we have to back that with something Realm can save, so we pick an Int
, since that’s the type we picked to back our enum with. So we have a private variable called _activity
, and the public enum basically forwards to it. Now we have a Realm object with an enum property!
The last trick with this is that like last week, we’ve created a read-write property, so we need to tell Realm not to try to persist it. We’ll add activity
to the list of ignored properties:
class Santa: Object {
// Properties are all up here
// We defined this function last week, so use that
override static func ignoredProperties() -> [String] {
// Just add "activity" to this array
return ["currentLocation", "activity"]
}
}
What the weather is like where Santa is
This one is special, because we’ll be using it to learn how to work with the Realm Object Server. For now, we won’t worry about it, and we’ll circle back to it in a later week.
How many presents Santa has left to deliver
This is by far the most straightforward property. It’s just an Int
!
class Santa: Object {
// Complicated properties up here
dynamic var presentsRemaining: Int = 0
// New ignoredProperties down here
}
Make sure it’s dynamic
of course, but other than that, this is much simpler.
Update the test data
Now that we have more properties, we should update our test data:
extension Santa {
static func test() -> Santa {
let santa = Santa()
santa.currentLocation = Location(latitude: 37.7749, longitude: -122.4194)
santa.activity = .deliveringPresents
santa.presentsRemaining = 42
return santa
}
}
You can pick whatever test data you like! Just something to make sure your UI updates later.
Make sure your models compile by running your app. Because we changed a lot about the data models, Realm should throw an exception that says you need to migrate. Just delete the app from your simulator (the same way you’d delete it on a device, click and hold) and run your app again. The UI won’t be any different since we haven’t written that code yet, but just running the app successfully means that Realm has validated your new models and likes them.
2. Drive the UI with Realm data.
Last week, we made the app drop the pin based on Santa’s location when the app launched. This week, we’re going to augment that in two ways: We’re going to add in more outlets, and we’re going to make the UI react to data changes.
-
First things first, let’s connect more outlets. In
SantaTrackerViewController
, define anupdate
function that takes in aSanta
and updates the UI:class SantaTrackerViewController: UIViewController { // Properties // viewDidLoad private func update(with santa: Santa) { mapManager.update(with: santa) let activity = santa.activity.description let presentsRemaining = "\(santa.presentsRemaining)" DispatchQueue.main.async { self.activityLabel.text = activity self.presentsRemainingLabel.text = presentsRemaining } } }
First, we forward this message to the map manager we wrote last week, so it can update the map. Then we put the activity and the presents remaining in their labels. Like last time, this
update(with:)
can be called off the main thread, which means these UIKit calls will blow up. 💥 Same solution as last time: Explicitly dispatch to the main thread.We’ll handle the arrival time next week, when we can get Santa’s route from the server, and the weather the week after, when we learn about the Realm Object Server in more depth. For now, this will be enough!
-
Now let’s make this react to data changes. Realm is designed for you to use reactive patterns, which means that the data drives the app. Realm offers a few ways to do this, which you can read about in the docs, but the one we’ll focus on now is key-value observing, or KVO, since that’s currently the only one that works for single objects. (A new API based on the collection notifications is due in early 2017.) Since the KVO API can be kinda clunky, I’ve written a simple wrapper:
class Santa: Object { // All of the existing code // We'll need to save these, or notifications won't be sent private var observerTokens = [NSObject: NotificationToken]() // This sets up observations to each of Santa's properties, plus properties of those func addObserver(_ observer: NSObject) { // Add a typical KVO observer to all the properties // One of these needs to generate the initial call, could be any of them addObserver(observer, forKeyPath: #keyPath(Santa._currentLocation), options: .initial, context: nil) // Want to make sure we're observing the location's properties in case someone changes one manually addObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.latitude), options: [], context: nil) addObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.longitude), options: [], context: nil) addObserver(observer, forKeyPath: #keyPath(Santa._activity), options: [], context: nil) addObserver(observer, forKeyPath: #keyPath(Santa.presentsRemaining), options: [], context: nil) observerTokens[observer] = route.addNotificationBlock { // self owns this route, so it will always outlive this closure [unowned self, weak observer] changes in switch changes { case .initial: // Fake a KVO call, just to keep things simple observer?.observeValue(forKeyPath: "route", of: self, change: nil, context: nil) case .update: observer?.observeValue(forKeyPath: "route", of: self, change: nil, context: nil) case .error: fatalError("Couldn't update Santa's info") } } } func removeObserver(_ observer: NSObject) { observerTokens[observer]?.stop() observerTokens.removeValue(forKey: observer) removeObserver(observer, forKeyPath: #keyPath(Santa._currentLocation)) removeObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.latitude)) removeObserver(observer, forKeyPath: #keyPath(Santa._currentLocation.longitude)) removeObserver(observer, forKeyPath: #keyPath(Santa._activity)) removeObserver(observer, forKeyPath: #keyPath(Santa.presentsRemaining)) } }
This wrapper does a few things that make it simpler to use. First, it removes some extra parameters from the regular KVO function that we won’t use anyway. Second, it automatically signs up the observer for all properties, and uses the private versions that we know support KVO. As a bonus, it uses the new
#keyPath
keyword, which lets the compiler check the paths so we know we didn’t mistype them. Third, it brings route observation into the same code path by redirecting Realm collection notifications into KVO notifications. -
To make the UI reactive, all we have to do is use this wrapper! Let’s modify the end of
viewDidLoad()
inSantaTrackerViewController
:override func viewDidLoad() { // We already had all this code super.viewDidLoad() // Set up the map manager mapManager = MapManager(mapView: mapView) // Find the Santa data in Realm let realm = try! Realm() let santas = realm.objects(Santa.self) // Set up the test Santa if he's not already there if santas.isEmpty { try? realm.write { realm.add(Santa.test()) } } // Be responsible in unwrapping! if let santa = santas.first { // There used to be a call to mapManager in here, but not any more! santa.addObserver(self) } }
All we changed here is that we’re not going to call
update(with:)
on the map manager any more; we’ll let our reactive change handler do that.Don’t forget to remove the observer when you’re done! (In this demo app, we won’t ever be done listening, but it’s something you should be doing in your apps, and I want to set a good example.)
deinit { let realm = try! Realm() let santas = realm.objects(Santa.self) if let santa = santas.first { santa.removeObserver(self) } }
So we have our observation set up, and we remember to take it down when we’re done. But…where is our change handler? We should probably write that. Below
viewDidLoad()
, add a new function: the 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 { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } }
Thankfully, we’re using KVO in a very simple way, so we don’t have to worry about most of the parameters. All we do is make sure that we’re dealing with an update from a
Santa
(better safe than sorry). If we are, we feed it intoupdate(with:)
and update our UI. If we’re not, we pass the notification up the chain tosuper
, per Apple’s docs.Alright, now run your app! You should see the UI change with your test data. If so, congratulations! Your data models are done, and your UI is reactive to your data changing. Your app is now ready for next week, when we’ll connect to the Realm Object Server. The nice part about the way the whole system was designed is that you really won’t have to change very much code at all. With the reactive setup we just built, changes from the server will generate the same notifications as local changes, so we won’t have to worry about handling incoming data in a special way.
Anyway, enough about that, you’ll see next week. For now, great job, and good luck with all your gift shopping! 🎁
Receive news and updates from Realm straight to your inbox