NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

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 create a new instance of Date 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) ?? Date()
}

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

@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:

NavigationView {
    VStack {

And replace it with this:

NavigationView {
    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.

If you prefer, you can get the old style back by specifically asking for the wheel picker to be used. We lost it when we moved to a Form, because DatePicker has a different style when used in forms, but we can get it back by using the modifier .datePickerStyle(WheelDatePickerStyle()).

So, modify your date picker code to this:

DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
    .labelsHidden()
    .datePickerStyle(WheelDatePickerStyle())

Tip: Wheel pickers are available only on iOS and watchOS, so if you plan to write SwiftUI code for macOS or tvOS you should avoid them.

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(value: $sleepAmount, in: 4...12, step: 0.25) {
    Text("\(sleepAmount, specifier: "%g") hours")
}

And wrap them in a VStack like this:

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

    Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
        Text("\(sleepAmount, specifier: "%g") hours")
    }
}

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

Hacking with Swift is sponsored by Instabug

SPONSORED Are you tired of wasting time debugging your Swift app? Instabug’s SDK is here to help you minimize debugging time by providing you with complete device details, network logs, and reproduction steps with every bug report. All data is attached automatically, and it only takes a line of code to setup. Start your free trial now and get 3 months off exclusively for the Hacking with Swift Community.

Start your free trial!

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

BUY OUR BOOKS
Buy Pro Swift 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 (Vapor Edition) 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 Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

Link copied to your pasteboard.