UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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")
    .font(.headline)

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")
        .font(.headline)

    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!

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
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.