Chapter 5: Roll the Dice! – Basic data flow concepts in SwiftUI

What you’ll learn in this chapter:

  • Understanding the basic data flow concept used in SwiftUI
  • Getting familiar with @State property wrappers
  • Handling user input in SwiftUI apps

What we’ll create 🚀

Let’s continue our SwiftUI journey by building a simple, virtual dice. The app shows a button on which the user can tap to roll the dice.

By building this app we will get familiar with a very important topic for app development with SwiftUI: Handling user input, managing data flow in SwiftUI apps and using property wrappers. Let’s get started!

Setting up our project 👷‍♂️

As always, we start by setting up a new Xcode project. Create a new Xcode project, select “App” under the “iOS” tab. Name the project – for instance – “RollTheDice!” and click on “Next”.

After we created our project, we need to import several icon files. Six icons for showing each side of the dice and one “?”-icon that shows up when the user hasn’t rolled the dice once.

Download the icon files and drag them into your Assets.xcassets folder.



The icon image files we will use for our app

Building the UI 👨‍🎨

As you saw in the preview above, our app basically consists of two elements: An Image view for showing the result after the user rolled the dice and a Button that can be used to roll the dice.

Let’s start by setting up the Image view. Replace the default Text view inside the ContentView.swift file with an Image view and remove the .padding modifier. For testing purposes, initialize the “1“-side icon file.

struct ContentView: View {
    var body: some View {
        Image("1")
    }
}

To frame our Image properly, we apply the same modifiers as we did for the category cells of our Food Delivery App in the last chapter.

Image("1")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(width: 100, height: 100)
    .clipped()

Below our Image, we want to insert a Button. To do this, wrap the Image into a VStack. We want the Button to contain a white Text view an orange background.

VStack {
    Image("1")
        //...
    Button(action: {
        print("Dice rolled.")
    }) {
        Text("Roll the dice")
            .frame(width: 240, height: 75)
            .foregroundColor(.white)
            .font(.headline)
            .background(Color.orange)
            .cornerRadius(20)
    }
}

Next, outsource the Text view inside the Button by CMD-clicking on it and selecting “Extract Subview”. Name the extracted subview RollButtonContent.

Now we can insert a Spacer view between the Image and the Button view to push both views along the x-axis. Additionally, we apply some upper padding to the Image view.

VStack {
    Image("1")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 100, height: 100)
        .clipped()
        .padding(.top, 250)
    Spacer()
    Button(action: {
        print("Dice rolled.")
    }) {
        RollButtonContent()
    }
}

We also want to increase the distance between the Button and the lower edge of the device’s screen. Therefore, we add some padding to it as well.

Button(action: {
    print("Dice rolled.")
}) {
    RollButtonContent()
}
    .padding(.bottom, 40)

That’s it! As you just saw, it’s surprisingly simple it is to compose user interfaces using SwiftUI!

Our finished interface

Next, we are going to implement our app’s logic.

Getting familiar with @State properties

Inside the Image view, we just initialized the “one“-side of the dice. Of course, we want to use a dynamic value instead of a static one for our Image view. Therefore, we declare a rolledNumber variable above our ContentView’s body and assign the value 1 to it. We can then use this variable for our Image by using string interpolation like this:

struct ContentView: View {
    
    var rolledNumber = 1
    
    var body: some View {
        VStack {
            Image("\(rolledNumber)")
                //...
            Spacer()
            //...
        }
    }
}

When the user taps on the Button, we want to generate a random Integer value ranging from one to six. Therefore, we write the following statement inside the Button’s action closure:

Button(action: {
    let randomNumber = Int.random(in: 1 ..< 7)
}) {
    RollButtonContent()
}

Next, we want to assign the randomNumber to our rolledNumber and use it for loading a new Image view.

We could do this now, but note the following: Whenever the user taps on the Button, we could assign the randomNumber to the rolledNumber variable. But this wouldn’t tell the ContentView to use this new value and update its body with eventually showing us an updated Image view. This means that the rolledNumber would be assigned to a new, random value when the user taps on the Button but the ContentView wouldn’t notice and therefore wouldn’t use this new value for loading a new Image view.

So, how can force the ContentView view to refresh when the user taps on the Button and a new, random value gets assigned to the rolledNumber property?

For exactly this purpose, SwiftUI provides @State properties to us. These @-keywords (yes, there are more of them!), are called property wrappers. Property wrappers in SwiftUI equip variables with a specific logic depending on the type of the used property wrapper. This logic is the core of the data flow concept used in SwiftUI, so understanding property wrappers is really important for developing SwiftUI apps.

@State is probably the most frequently used property wrapper in SwiftUI. We’ll get to know the other property wrapper types throughout this book, but we’ll start with this, basic one.

You can easily declare a State by putting the @State keyword in front of a variable. Let’s do this with our rolledNumber property.

@State var rolledNumber = 1

Hint: State properties must always be related to a view in SwiftUI. Therefore, make sure you always declare them inside a View struct (but not inside the View’s body)!

Now we can assign our randomNumber to the rolledNumber State when the user taps on the Button.

Button(action: {
    let randomNumber = Int.random(in: 1 ..< 7)
    rolledNumber = randomNumber
}) {
    RollButtonContent()
}

But what exactly does a State property do? Well, with States you can read out and manipulate data just as you do it with regular variables in Swift. But the main difference is that every time the data assigned to a State changes, the related View gets refreshed.

Let’s explain it with our Roll The Dice! app. Every time the user taps on the Button, the rolledNumber property gets updated with a new, random Integer ranging from one to six. Because this property is a State, its related view, the ContentView, notices and rebuilds its body with eventually using the new rolledNumber value for showing initializing a new, updated Image view.

Try it out in the Live preview! When you tap on the Button, the rolledNumber State gets updated, which causes the whole view to refresh its body with eventually showing us a new side of the dice.

As said before, this topic is very important for mastering SwiftUI, so make sure you really understood it.

Okay, we’ve accomplished generating a random number and presenting the corresponding dice side when to user taps on the Button.

Next, we want our app to show the “?“-icon when the user launches the app and hasn’t tapped the Button once. 

We can achieve this by declaring another State property rolledIt and assign false to it by default. 

struct ContentView: View {
    
    @State var rolledNumber = 1
    @State var rolledIt = false
    
    var body: some View {
        //...
    }
}

When the user taps on the Button, we need to make sure that this property gets true.

Button(action: {
    let randomNumber = Int.random(in: 1 ..< 7)
    rolledNumber = randomNumber
    rolledIt = true
}) {
    RollButtonContent()
}

We can now initialize the right image based on the State’s value. When the rolledIt State is false (when the user has not tapped the button once) we want to use the “unknown“ icon file. When the Button is tapped, we want to present the dice side corresponding to the numberRolled property. We can implement this logic by inserting a conditional statement inside the Image view.

Image(rolledIt ? "\(rolledNumber)" : "unknown")

This statement says: “If rolledIt is true, load the right image file depending on the rolledNumber property. If it’s false, load the “unknown” icon.

You can try out this functionality by starting a Live preview again. When the app launches, the rolledIt property is false. Therefore, the Image view loads the “unknown“ side of the dice. When you tap the Button for the first time, the rolledIt State gets true and therefore causes the ContentView to rebuild its body while loading the right Image view representing the new, random value.

Hint: In this particular case it wouldn’t even be necessary to make the rolledIt property to be a State, because the ContentView gets rebuilt anyway due to the other State that gets updated when the user taps the Button.

Conclusion 🎊

Great! We have finished our “RollTheDice!“ app. We learned how State properties work in SwiftUI and how we can use them to react to user actions, in our example when the user taps the Button.

In the next chapter, we are going to create a more complex UI by building a login page!

7 replies on “Chapter 5: Roll the Dice! – Basic data flow concepts in SwiftUI”

I used SF Symbols instead of images…

Maybe a bit of fun for people to play with?

Image(systemName: rolledIt
? “die.face.\(rolledNumber).fill”
: “questionmark.square.fill”
)

FYI, the background of the provided dice icons are grey (on macOS 11.2), so it creates a big ugly grey area that overlays most of the button. 🙁 Also the dots are the same grey.

Going with Stupps’s solution.

FYI, it seems I was bit by the “buttons don’t work the same on macOS” issue, so apart from the grey part of the image, the rest is explained by the fact I wasn’t in iOS though I thought I’d started that way.

So, part user error. I’ll create a ButtonStyle for the macOS version.

Thank you!, I enjoyed creating this app so much. very well explained 🙂

I’ve got one question, what is the reason behind extracting Text View inside the button as a subview? .. in the FoodDelivery app, extracting subview helped with reusing the code. It was so clear to me why we did it, but in Roll the Dice app i can’t figure out why.

Leave a Reply

Your email address will not be published. Required fields are marked *