Hello everyone, and happy holidays! We’ve been trying to think up some gifts for all of you brilliant mobile developers. But just what is it that you want? 🤔
To build great apps, of course! 🙌
And the best way for us to help you do that is to build great products. So this December, we’ve got a new tutorial, for those of you who haven’t gotten started with our stuff, and want to become even more productive next year. The best part is, you don’t even have to wait for any particular holiday. It’s starting right now!
This 4-part Swift tutorial will teach you how to make a fairly simple app that tracks Santa’s location as he flies around the world on Christmas Eve. It’s a great introduction to the two products we make: The Realm Mobile Database for data persistence & handling, and the Realm Mobile Platform for syncing. Even if you’ve worked with Realm before, there’s definitely some new information in here for you.
We’ll be building this app:
For part 1, we’re going to be doing a lot of setup, then make sure that everything is correct by plotting Santa’s current location on the map. I’ve divided this part into 8 steps, so feel free to do some, then come back to it later.
In future parts, we’ll show his route and update the other data on the screen. One part of this series will be about the Realm Object Server, so definitely keep tuning in to learn more about the newest piece of the platform. With that preview done, let’s get started!
1. Make a new Xcode project.
- Use the “Single View Application” template, and name it “Santa Tracker” (or whatever you want, but I’m calling mine “Santa Tracker”).
- Be sure to set “Language” to “Swift” and “Devices” to just “iPhone”. We obviously won’t need Core Data, so you can uncheck that box.
2. Set up your storyboard.
- This app only has one view controller, so the easiest thing to do is to download the storyboard I’ve prepared for you and copy the view controller from there. You can replace the file
Main.storyboard
, or copy the vew controller from within Interface Builder. - If you copy within IB, you’ll need to set the view controller as “Is Initial View Controller” in the right-hand panel. You’ll know you’re good to go when you see an arrow coming out of nowhere and pointing into the left side of you view controller.
- Feel free to make changes! If you’ve got design ideas, who am I to stop you? 🎨 (But I’m not here to debug Auto Layout, so my use layout if you’re not comfortable with that.)
- The two images at the bottom won’t load until you add the right images to your project. You can grab the images here. Just add them to your
Assets.xcassets
file and then set the image views’ images to the right things. You can figure out which one goes where. - If you copied my storyboard, you’ll need to change your view controller’s class name. Open the file
ViewController.swift
and change the class name toSantaTrackerViewController
. You can also change the file name if you like, but it’s not required. (But I did because then they match and that’s always nice.)
3. Link MapKit.
- As you may already know if you’ve worked with maps, forgetting to link MapKit (like I did) will crash your app on launch. So embarrassing!
- Open your project settings, go to the “General” tab (it’s probably the one you land on), and scroll down to the “Linked Frameworks and Libraries” section all the way at the bottom. Use that to add
MapKit.framework
.
4. Add Realm to your project.
- We’ll be adding Realm Swift using CocoaPods. If you don’t have CocoaPods installed yet, take a minute to install it. If you want to use another installation method, you can see how here, but CocoaPods is the easiest.
- Now close that Xcode window! We’ll be making a new workspace that we’ll need to use.
- Open Terminal (or your favorite substitute) and navigate to your project directory. This is the folder that contains the
.xcodeproj
file. - Create a Podfile by executing
pod init
, then open it and addpod 'RealmSwift', '2.1.0'
under the line that says# Pods for Santa Tracker
. Save and close. - Back in Terminal, call
pod install
.- If this is your first time using CocoaPods, this can take a while. Get some tea. 🍵
- If you see a bunch of red text about being
Unable to satisfy the following requirements
, your Podspec repo is out of date and you need to update it withpod repo update
first (which will also take a while 🍵). - When you see
Pod installation complete!
, you’re done!
- As CocoaPods notes, from now on you’ll need to open the
.xcworkspace
file, NOT the.xcodeproj
file, or you won’t have access to any of your Pods.
5. Run the app (just to make sure everything is good so far).
- Open the
.xcworkspace
file, and hit Run (a.k.a., play button). ▶️ - If the simulator opens to your app, hooray! Everything is set up correctly! 🎉
- If it doesn’t run, now’s the time to debug that. Make sure you did all of the steps above. Read through once just to make sure! Let me know on Twitter if you need help, but I have faith in you to debug a little.
6. Set up outlets.
Set up the following outlets in SantaTrackerViewController
:
@IBOutlet private weak var timeRemainingLabel: UILabel!
@IBOutlet private weak var mapView: MKMapView!
@IBOutlet private weak var activityLabel: UILabel!
@IBOutlet private weak var temperatureLabel: UILabel!
@IBOutlet private weak var presentsRemainingLabel: UILabel!
You can figure out what goes where! (It’s the labels where the text will change [mostly numbers], and the one map.)
You’ll also need to import MapKit
at the top for the MKMapView
.
7. Set up basic data models.
A word before we get started with our model classes: You must, I repeat absolutely must, name your properties and models exactly what I name them. If you don’t, your app will crash when it tries to connect to the Realm Object Server in a later part of the tutorial. Then you will be sad.
Don’t be sad. Name your properties the way I do.
-
Let’s start our data model for Santa. We’ll build on this as we go along, so let’s start with just what we need to show a location. Make a new blank Swift file for Santa called, of course,
Santa.swift
. We’ll have just oneSanta
(duh) object ever, but we’ll still need a class so that data can be synced.class Santa: Object { dynamic var currentLocation: Location? }
(Don’t forget to
import RealmSwift
in all of these model files.) So we have acurrentLocation
property that will tell us where Santa is at the current time, which is of typeLocation?
. “What’s aLocation
?”, I hear you (and the compiler) asking. -
Make another Swift file for that, called
Location.swift
.class Location: Object { dynamic var latitude: Double = 0.0 dynamic var longitude: Double = 0.0 convenience init(latitude: Double, longitude: Double) { self.init() self.latitude = latitude self.longitude = longitude } }
This will represent a location in a sync-able format. It’s pretty simple, just a
latitude
andlongitude
, plus a convenience initializer for later. -
So that should make
Santa.swift
compile, but let’s add one convenience to Santa before we go. Notice howcurrentLocation
is optional? I don’t want to deal with unwrapping all over the place, especially when I know of a great default location for Santa. AmendSanta.swift
to look like this:class Santa: Object { private dynamic var _currentLocation: Location? var currentLocation: Location { get { // If we don't know where Santa is, he's probably still at home // I hear GPS isn't great up there return _currentLocation ?? Location(latitude: 90, longitude: 180) } set { _currentLocation = newValue } } override static func ignoredProperties() -> [String] { return ["currentLocation"] } }
The API for
Santa
hasn’t changed at all: still just onecurrentLocation
property, but now it’s non-optional!👏 Per the Realm Swift docs,Object
properties (i.e., other Realm objects) “must be optional”, so we can’t provide a default value and make it non-optional in the standard Swift way. Instead, we’ve made the true variableprivate
, and made our public variable behave the way we want. Then to make sure Realm doesn’t think it needs to persist this, we’ll tell Realm to ignore it. (Realm ignores read-only properties, but we’ve added a setter so that doesn’t help us.) -
As a convenience to ourselves while testing, let’s make some demo data:
extension Santa { static func test() -> Santa { let santa = Santa() santa.currentLocation = Location(latitude: 37.7749, longitude: -122.4194) return santa } }
I made mine say Santa is in San Francisco, but you can point him to your city if you get the coordinates.
Sidebar: Don’t mix up latitude and longitude, as I did many (many [many]) times while writing this tutorial. Here’s a helpful diagram! If the pin isn’t showing up where you want it to, check that first.
8. Show Santa’s location on the map!
-
Since the map is a defined piece of functionality, let’s make a class to handle all of that. Make a new Swift file called
MapManager.swift
:class MapManager: NSObject { private let santaAnnotation = MKPointAnnotation() init(mapView: MKMapView) { santaAnnotation.title = "🎅" super.init() mapView.addAnnotation(self.santaAnnotation) } func update(with santa: Santa) { // Update the map to show Santa's new location } }
You’ll need to
import MapKit
so the compiler knows about the map classes. TheMapManager
will be in charge of anMKPointAnnotation
, which represents a single point on a map and will draw itself using the familiar pin UI. When we create aMapManager
, we’ll create the annotation and add it to the map. -
So how do we tell the pin where to go, and how do we update it? It’s not the
MapManager
’s responsibility to know when it needs to update, so someone will tell us when to do that, and give us aSanta
to use. All we have to do then is update the annotation’scoordinate
property to the new location.But we have some problems here. Santa’s location is a
Location
, butMKPointAnnotation
expects aCLLocationCoordinate2D
. And a more subtle bug: Ifupdate(with:)
gets called on a thread other than the main thread, this will crash, because changing the location causes calls to UIKit. (This threading issue won’t bite us this time, but it will later in this series if we aren’t thoughtful now.)First things first: Converting
Location
s toCLLocationCoordinate2D
s is pretty easy. Let’s add an extension toLocation
to let us do that:private extension Location { var clLocationCoordinate2D: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } }
I’ve named it in the style of
UIColor.cgColor
, and all it does is copy over the two numbers. Easy! Be sure to add it inMapManager.swift
, since that’s the only place where that extension is needed, and it means you don’t have toimport CoreLocation
inLocation.swift
.The threading problem is also easy to fix: Just do the
coordinate
change insideDispatch.main.async
.Let’s put those together to write
update(with:)
:func update(with santa: Santa) { let santaLocation = santa.currentLocation.clLocationCoordinate2D DispatchQueue.main.async { self.santaAnnotation.coordinate = santaLocation } }
Since we can’t access the
Santa
on a thread other than the one is was created on (per the docs), we can’t pass it directly onto the main thread. So we just grab the data we need and pass it over. -
Alright, so now we can tell the manager to put Santa the map, so let’s do it! Update
SantaTrackerViewController
:// Has to be implicitly unwrapped // Needs the reference to the map view private var mapManager: MapManager! override func viewDidLoad() { 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 { // Update the map mapManager.update(with: santa) } }
Since we’re working with our Realm models now, you’ll need to
import RealmSwift
at the top of the file. So here we set up the map manager, which unfortunately has to be implicitly unwrapped, becuase it needs the reference to the map view. (I know this could be architected differently for now to remove this problem, but we’ll need this in a later part of this tutorial and it’s easier to minimize refactoring since I have psychic knowledge of what the future will hold. 🔮)Then we open Realm, set up the test data if it’s not already set up, and pass the first (and only)
Santa
along to update our map. We’ll improve the error handling around Realm in later parts, so don’t worry about it being a little clunky for now.Some things that might cause crashes at this point (at least the ones I ran into):
- Have you
import
ed the right frameworks in the right files? - Did you connect all the
@IBOutlet
s? - Do you have all the code from above? Especially if you retyped things, it’s easy to miss things.
Once you get your app to run and a pin appears where you wanted it to go, congratulations! You’re done (with part 1)! Get some eggnog (or whatever festive holiday treat you like) and I’ll see you back here next week, when we’ll plot Santa’s entire route.
- Have you
Check out part 2, available now! Get it wherever gifts are sold!
Receive news and updates from Realm straight to your inbox