Last year on the WWDC 2019 Apple has announced iOS 13, watchOS 6, macOS X Catalina and a lot more. Until now a lot has been said about all those shiny new things. A lot of people suggest the latest WWDC be an as big step forward as the one during which Swift was introduced.
To my mind, the biggest changes do affect watchOS. Let’s face it, SwiftUI is available only since iOS 13, watchOS 6 and macOS Catalina and most of the developers would like to preserve compatibility with the previous version or two at least. Moreover, SwiftUI is not quite mature yet. There are still many problems during development as well as quite poor flexibility of the new framework. This means that we won’t see many SwiftUI iOS or macOS apps in the near future.
However, SwiftUI is just brilliant for watchOS. First of all, we usually do not need much functionality on a watch. Furthermore, the level of customization of views in WatchKit is not as high as in UIKit so moving to SwiftUI is not that painful. Finally, even now (November 2019) there are very few apps in watchOS AppStore which means that there’s enough space for you to implement your ideas.
In this article, I’d like to take a quick look at two major features of new watchOS update and create a stand-alone watchOS app using SwiftUI.
Apple Watch is a great device that can currently track a lot of things: the distance you walk/run/swim, heart rate, standing hours, even volume level of the environment! But what about tracking something more enjoyable than sport? What about TV series for example? No kidding, it’s sometimes big trouble for me to remember which episode was the last and as soon as I watch series on different platforms I would like to have a third-party app to track this. Moreover, sometimes it’s just vital to remember which episode was last to avoid spoilers! :)
So let’s try to create a SeriesTracker app that will help you to remember at which season and episode you’ve left a particular TV series.
Let’s start with the project configuration. One should create a new Xcode project by choosing File:
> New -> Project than switch to the `watchOS` tab and select `Watch App`.
Then it’s important to choose SwiftUI for the User interface in the project configuration window.
Now we’ve got some basic project structure. Let’s clean it up a little by dividing all the files into different directories.
We won’t need Notifications, Complication, and Resources so far in this article. We may surely use Assets.xcassets to add some pretty application icons or complications though :)
Let’s start with the acknowledgment of what an app should be able to do. We’ll store and display a list of series and some basic information about them. We’ll also provide the possibility to update information, remove series from the app and add new.
First of all, we need to define a model.
The series structure contains id which is also required by the Identifiable protocol, name of the show, season and episode numbers and a bool value saying whether we’ve already finished watching it. Codable protocol is needed in order to store our shows in json format and Identifiable will soon be explained when it comes to the SwiftUI List structure.
Now we should create some kind of persistence manager — DataStorage.
This class will have a `shared` singleton. We’ll keep series in the array and store them in user defaults. Sure usually it’s a bad idea to store a big amount of data there. However, it’s just good enough for our example.
There are two unusual things about the DataStorage — ObservableObject and @Published. ObservableObject is a protocol an object has to conform to make itself available for SwiftUI to track and @Published annotation is a property-wrapper that makes the particular property of the object to be available for observing. We’ll take look at how it works later in this article.
Let’s also add a line of code to ExtensionDelegate to save our data to UserDefaults each time our app becomes inactive:
Now when we’ve done with the model let’s dive deeper into the presentation layer. There we have HostingController and ContentView so far. We’ll create our custom views and make one of them a subview of content view and then navigate to all the others. We will use HostingController as the only controller in the app and make our application something like a view-based.
Let’s start creating views from the most atomic ones. Our application will not be using any Internet connection so far so we won’t be able to download any pictures for our shows. However, we could still create a placeholder view with the first letter of the show’s title — LetterImageView:
Every SwiftUI View is a structure conforming to the View protocol. Unlike UIKit all the views are structures in SwiftUI. That leads to having fewer problems with inappropriate use of OOP principles and helps us to avoid memory-management mistakes (like retaining cycles for instance).
Only one stored property is needed by our view. It’s a character we want to display as the placeholder. Like every View, our LetterImageView must provide a computed property `body` that contains all the nested views. Most of the SwiftUI views have an initializer that has a closure parameter where we put all the nested views.
In this particular view, we’ve used ZStack which is used to overlay the views: a gray rectangle of fixed size and a label. Apart from ZStack, SwiftUI has also HStack and VStack (which are horizontal and vertical stacks). One should pay attention to that we wrap labels inside of `GeometryReader`. This is made in order to adjust the label’s font to the view’s size in case the whole view is scaled. Let’s also mark that we clip the stack to the shape of the circle. SwiftUI provides us with a variety of shapes we can clip our views to.
It looks quite declarative, doesn’t it? Sure and that is a huge advantage of SwiftUI. Another great thing about this framework is that code is now the only source-of-truth and there’s zero possibility that any configurations are overridden elsewhere as that could happen while using Storyboards.
We should also pay attention to the LetterImageView_Preview structure. It is used by the preview engine and should provide all the mocked data for the view.
Now we’ve got a placeholder:
Let’s create a view that will display all the information about the show — SeriesDetailedView. Here we’ll need a `storage` as @EnvironmentObject, `seriesId` as stored property. We’ll also need some utilities like computed `seriesIndex` and `currentSeries`.
@EnvironmentObject, as well as @ObservableObject and @State, are the object-wrappers provided by SwiftUI. If any of the @Published values of these objects are modified, changes are automatically tracked by the framework which invalidated view’s layout and launches UI reload. The main difference between these three annotations is that @EnvironmentObject is usually used for the objects shared for the whole application and provided by special method `func environmentObject < B > (_ bindable: B) -> some View where B: ObservableObject` of the View protocol; @ObservableObject is used for objects shared between a small number of views and provided via View’s initializer; @State is used for the objects that are used in the context of single View and also provided via initializer.
The `body` of the SeriesDetailView is quite straightforward so let’s only focus on some SwiftUI features that were not described yet.
As soon as all the UI is written in closures we can easily use conditional statements to include/exclude some views as well as make any parameters of UI configuration depend on the model. It’s easy to see this in `In progress` label configuration.
Based on whether the user has finished watching a particular show we display different text on the status label and set the text’s color. A great declarative and a quite convenient way to set up the UI.
Next, we are going to create a view for editing the current season and the episode of the show and also the `finishedWatching` property. That means that we have to navigate to the next screen somehow. In SwiftUI there’s a simple way to do that.
Here we create a NavigationLinkView and provide the destination view which is EditSeriesView in this case. We pass the seriesId and environment right in function parameter inline. We use trailing closure to specify the way NavigationLink should be drawn. In this case that would be a button `Update`.
Although the interface of EditSeriesView is very simple, there is one major point to describe. We’ll create another utility as a computed property. Binding is used to reference an object or structure we want to update with the use of UI Controls. One should pay attention to that we use special $-syntax to make Binding from our ObservableObject. Now if we pass this binding to Slider, it will automatically update the particular series in our EnvironmentObject which will surely update the UI and keep it up to date. Simply talking as soon as the user triggers the slider, the text of the label is automatically updated.
Here’s how it looks like on a device:
Now it’s time to create a list of all series in the app. SwiftUI List is a powerful analog of UITableViewController from UIKit. It’s the main element in our SeriesListView. Later we’ll make this view the main view of our app providing it as a subview of the ContentView.
Let’s dive a bit deeper into the implementation of this view. Here we provide @EnvironmentObject that can be treated as some kind of data source in this particular case. Moreover, we provide two @State properties. `showInProgressOnly` is used to filter away the shows that are currently not watched by the user. `newSeriesName` will soon be used to store the name of the newly added show.
Usually, we do not need to use ForEach for views in List. Passing datasource and closure with nested views to the list initializer is good enough if we have dynamic cells only. In this case, we have both — static cells with a toggle that controls filtering and a number of dynamic cells displaying the shows. It’s important to emphasize that our datasource (the array of Series) is Identifiable as soon as Series is Identifiable itself. That makes List or ForEach able to distinguish between different models and enables them to work with reusable views. If Series wasn’t Identifiable we would have to provide an additional parameter `id` and specify #keyPath for the object’s primary key. It’s also nice to see how easily we can filter objects with the use of a single conditional statement (line 26):
Here’s how our list looks like on the device.
Now we can display and modify our shows let’s give the user the ability to remove ones:
To do this we should only create a method in our view that removes value from datasource based on the IndexSet and specify this function in `onDelete` for the ForEach element. Yes, that easy!
Now let’s create an input for adding new series. We create a method that inserts a new value into the datasource and creates a TextField which will modify `newSeriesName` @State and call `addNewSeries` function on commit.
Now we can add a new TV series to our app.
Last but not least, most of the SwiftUI views are reusable all over the apple devices (watch, iPhone, Mac) and that is beautiful in case we want to develop a cross-platform application.
Although it’s true that SwiftUI is much, much more than these few things covered in this article, this tiny app is a brilliant example of how simple and straightforward the process of creating the stand-alone watchOS app is now. It truly looks like the beginning of the new era in mobile and wears software development.
The complete version of SeriesTracker can be found on the repo.
Originally published in Stfalcon.com.