SwiftUI has been loved ❤️ by the iOS developers, for its simple and declarative syntax. Except that not many people use it in production 😞. The minimum iOS 13 requirement, and the potential disruptive syntax change in the near future, all scared developers away.
We all know the best way to learn, is to get your hands dirty; and I believe getting your hands dirty in the face of the market, is even better. So I took the challenge to build a heart rate monitor app with SwiftUI, and published it in the App Store. Hard things have been learnt, and I’ll share them in the series of articles.
This article (Part 1) focuses on the fun part: building the UI for the app.
When blood flows from your heart to your finger, more light is absorbed by blood; when blood flows away from your finger, less light is absorbed. This enabled us to use iPhone camera to measure heart rates.
A typical measurement process will experience these screens on UI:
1. The app will start in a ready-to measure state.
2. When user taps the Start button, the app turn on the flashlight, and wait for user to put their fingers on the camera.
3. When the user’s finger is on the camera, the measuring process starts, and there’s a ring showing the progress and current measurement.
4. After the measurement finishes, we present a UI to summarize the result.
5. Some errors will be rendered differently.
The above-mentioned major screens are represented by a MeasurementState enum.
With the enum defined, the states used to render the UI are enclosed in a MeasurementService. This class will be coordinating camera readings, heart rate calculations, and update these published states; UI will rely on these states for rendering.
We use a ZStack to render these screens. Why not using modals? Because it’s very hard to get fullscreen modals rendered in SwiftUI (pitfall #1), there’s nothing like UIModalPresentationStyle.fullScreen in SwiftUI, using a ZStack gives you full control over how you want to present and render screens. As you can see below, we use measurementService.state, to determine whether to display MeasurementView and SuccessView.
What is this NavigationConfigurator thing doing? Customizing navigation bar is already hard in UIKit (if you want translucency, getting rid of a default gradient or shadow or separator); customizing nav bar in SwiftUI is even harder (pitfall #2). The following code shows what’s inside NavigationConfigurator: it exposes a chance for the call site to configure the nav bar, through UIKit API.
The Fun Part: BpmView
The most fun part in SwiftUI, is building cool UI and animations in no time. For example, the BpmView renders the current bpm reading, and has a smooth ring progress animation.
To build this UI, we place a RingView in the bottom of the ZStack, and place all other elements on the bottom, with the help of a VStack.
Note that you can’t have have more than 10 children in a SwiftUI VStack (pitfall #3), so be mindful of these small limitations here and there.
RingView consists of two RingShape, the second being overlaid on top of the first one.
The RingShape is an animatable Shape struct.
Now we have all the components in place, let’s preview different variants of this BpmView. Not bad!
Part 1 Summary
In this part, we went through how to create a SwiftUI app in production, which is multi-screen with animatable components. We also went through several tricky part in the current state of SwiftUI:
#1. Render fullscreen UI without modal presentation..
#2. Customize nav bar by hooking into UIKit API.
#3. Don’t put more than 10 children in VStack/HStack/ZStack.
In the next part, I’ll talk about how to have MeasurementService connect with an Objc library, and actually take heart rate measurements, and feed the results onto the UI.