WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Cleaning up the user interface

Paul Hudson    @twostraws   

Although our app works right now, it’s not something you’d want to ship on the App Store – it has at least one major usability problem, and the design is… well… let’s say “substandard”.

Let’s look at the usability problem first, because it’s possible it hasn’t occurred to you. When you read Date.now it is automatically set to the current date and time. So, when we create our wakeUp property with a new date, the default wake up time will be whatever time it is right now.

Although the app needs to be able to handle any sort of times – we don’t want to exclude folks on night shift, for example – I think it’s safe to say that a default wake up time somewhere between 6am and 8am is going to be more useful to the vast majority of users.

To fix this we’re going to add a computed property to our ContentView struct that contains a Date value referencing 7am of the current day. This is surprisingly easy: we can just create a new DateComponents of our own, and use Calendar.current.date(from:) to convert those components into a full date.

So, add this property to ContentView now:

var defaultWakeTime: Date {
    var components = DateComponents()
    components.hour = 7
    components.minute = 0
    return Calendar.current.date(from: components) ?? .now

And now we can use that for the default value of wakeUp in place of Date.now:

@State private var wakeUp = defaultWakeTime

If you try compiling that code you’ll see it fails, and the reason is that we’re accessing one property from inside another – Swift doesn’t know which order the properties will be created in, so this isn’t allowed.

The fix here is simple: we can make defaultWakeTime a static variable, which means it belongs to the ContentView struct itself rather than a single instance of that struct. This in turn means defaultWakeTime can be read whenever we want, because it doesn’t rely on the existence of any other properties.

So, change the property definition to this:

static var defaultWakeTime: Date {

That fixes our usability problem, because the majority of users will find the default wake up time is close to what they want to choose.

As for our styling, this requires more effort. A simple change to make is to switch to a Form rather than a VStack. So, find this:

NavigationStack {
    VStack {

And replace it with this:

NavigationStack {
    Form {

That immediately makes the UI look better – we get a clearly segmented table of inputs, rather than some controls centered in a white space.

There’s still an annoyance in our form: every view inside the form is treated as a row in the list, when really all the text views form part of the same logical form section.

We could use Section views here, with our text views as titles – you’ll get to experiment with that in the challenges. Instead, we’re going to wrap each pair of text view and control with a VStack so they are seen as a single row each.

Go ahead and wrap each of the pairs in a VStack now, using .leading for the alignment and 0 for spacing. For example, you’d take these two views:

Text("Desired amount of sleep")

Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)

And wrap them in a VStack like this:

VStack(alignment: .leading, spacing: 0) {
    Text("Desired amount of sleep")

    Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)

Much better!

The last change we're going to make is small, but magical. Take a look at this code again, which shows how many cups of coffee the user has had:

Stepper("\(coffeeAmount) cup(s)", value: $coffeeAmount, in: 1...20)

Writing "cup(s)" works, but it's a bit lazy. Ideally we'd show "1 cup", but "2 cups", "3 cups", and so on – we'd have correct plurals here.

We could correct that with a ternary operator like this:

Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount) cup(s)", value: $coffeeAmount, in: 1...20)

But SwiftUI has an even better solution: it can handle the pluralization for us! Change your code to this:

Stepper("^[\(coffeeAmount) cup](inflect: true)", value: $coffeeAmount, in: 1...20)

This is odd syntax, I know, but it's actually a specialized form of Markdown, which is a common text-format. This syntax tells SwiftUI that the word "cup" needs to be inflected to match whatever is in the coffeeAmount variable, which in this case means it will automatically be converted from "cup" to "cups" as appropriate.

And now run the app one last time, because it’s done – good job!

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

Unknown user

You are not logged in

Log in or create account

Link copied to your pasteboard.