NEW: Join my free 100 Days of SwiftUI challenge today! >>

Taking basic order details

Paul Hudson    @twostraws   

The first step in this project will be to create an ordering screen that takes the basic details of an order: how many cupcakes they want, what kind they want, and whether there are any special customizations.

Before we get into the UI, we need to start by defining the data model. Previously we’ve used @State for simple value types and @ObservedObject for reference types, and we’ve looked at how it’s possible to have an ObservableObject class containing structs inside it so that we get the benefits of both.

Here we’re going to take a different solution: we’re going to have a single class that stores all our data, which will be passed from screen to screen. This means all screens in our app share the same data, which will work really well as you’ll see.

For now this class won’t need many properties:

  • The type of cakes, plus a static array of all possible options.
  • How many cakes the user wants to order.
  • Whether the user wants to make special requests, which will show or hide extra options in our UI.
  • Whether the user wants extra frosting on their cakes.
  • Whether the user wants to add sprinkles on their cakes.

Each of those need to update the UI when changed, which means we need to mark them with @Published and make the whole class conform to ObservableObject.

So, please make a new Swift file called Order.swift and give it this code:

class Order: ObservableObject {
    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]

    @Published var type = 0
    @Published var quantity = 3

    @Published var specialRequestEnabled = false
    @Published var extraFrosting = false
    @Published var addSprinkles = false
}

We can now create a single instance of that inside ContentView by adding this property:

@ObservedObject var order = Order()

That’s the only place the order will be created – every other screen in our app will be passed that property so they all work with the same data.

We’re going to build the UI for this screen in three sections, starting with cupcake type and quantity. This first section will show a picker letting users choose from Vanilla, Strawberry, Chocolate and Rainbow cakes, then a stepper with the range 3 through 20 to choose the amount. All that will be wrapped inside a form, which is itself inside a navigation view so we can set a title.

Put this into the body of ContentView now:

NavigationView {
    Form {
        Section {
            Picker("Select your cake type", selection: $order.type) {
                ForEach(0..<Order.types.count) {
                    Text(Order.types[$0])
                }
            }

            Stepper(value: $order.quantity, in: 3...20) {
                Text("Number of cakes: \(order.quantity)")
            }
        }
    }
    .navigationBarTitle("Cupcake Corner")
}

Before we add the second and third sections, I’d like you to build and run the code, then try it out. Our two form fields should work fine, but you might – might – see a regular annoying message in Xcode: “ForEach<Range, Int, Text> count (4) != its initial count (1)”.

I say might because this feels like another SwiftUI bug: even though our ForEach is using fixed data (i.e., the number of items in the Order.types array), SwiftUI seems to be having a hard time working with it. If you don’t see the error you have nothing to worry about, but if you do see it then I want to show you how to resolve it: we need to give it an explicit identifier.

So, modify your ForEach to this:

ForEach(0..<Order.types.count, id: \.self) {

That clears up the problem entirely, but honestly I’m hoping this error will just go away in a future SwiftUI update.

The second section of our form will hold three toggle switches bound to specialRequestEnabled, extraFrosting, and addSprinkles respectively. However, the second and third switches should only be visible when the first one is enabled, so we’ll wrap then in a condition.

Add this second section now:

Section {
    Toggle(isOn: $order.specialRequestEnabled.animation()) {
        Text("Any special requests?")
    }

    if order.specialRequestEnabled {
        Toggle(isOn: $order.extraFrosting) {
            Text("Add extra frosting")
        }

        Toggle(isOn: $order.addSprinkles) {
            Text("Add extra sprinkles")
        }
    }
}

Go ahead and run the app again, and try it out – notice how I bound the first toggle with an animation() modifier attached, so that the second and third toggles slide in and out smoothly.

However, there’s another bug, and this time it’s one of our own making: if we enable special requests then enable one or both of “extra frosting” and “extra sprinkles”, then disable the special requests, our previous special request selection stays active. This means if we re-enable special requests, the previous special requests are still active.

This kind of problem isn’t hard to work around if every layer of your code is aware of it – if the app, your server, your database, and so on are all programmed to ignore the values of extraFrosting and addSprinkles when specialRequestEnabled is set to false. However, a better idea – a safer idea – is to make sure that both extraFrosting and addSprinkles are reset to false when specialRequestEnabled is set to false.

We can make this happen by adding a didSet property observer to specialRequestEnabled. Add this now:

@Published var specialRequestEnabled = false {
    didSet {
        if specialRequestEnabled == false {
            extraFrosting = false
            addSprinkles = false
        }
    }
}

Our third section is the easiest, because it’s just going to be a NavigationLink pointing to the next screen. We don’t have a second screen, but we can add it quickly enough: create a new SwiftUI view called “AddressView”, and give it an order observed object property like this:

struct AddressView: View {
    @ObservedObject var order: Order

    var body: some View {
        Text("Hello World")
    }
}

struct AddressView_Previews: PreviewProvider {
    static var previews: some View {
        AddressView(order: Order())
    }
}

We’ll make that more useful shortly, but for now it means we can return to ContentView.swift and add the final section for our form. This will create a NavigationLink that points to an AddressView, passing in the current order object.

Please add this final section now:

Section {
    NavigationLink(destination: AddressView(order: order)) {
        Text("Delivery details")
    }
}

That completes our first screen, so give it a try one last time before we move on – you should be able to select your cake type, choose a quantity, and toggle all the switches just fine.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

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: 5.0/5