Giving functionality to a plus button

Hi, I am new to swift and making apps and I’m trying to make my ‘plus’ button create a whole new stack in my code, so that when the plus button is pressed it sets of some code that inserts a new section. I’m not sure if im doing this the correct way but would like some guidance.

import SwiftUI

struct ContentView: View {
@State private var stepperValueTea = 0
@State private var stepperValueCoffee = 0
@State private var stepperValueMilo = 0

var body: some View {
    
    
    VStack {
        
        NavigationView {
            
            List {
                HStack {
                    EditButton()
                    
                    Image(systemName: "plus")
                        .frame(maxWidth: .infinity, alignment:.bottomTrailing)
                        .foregroundColor(.red)
                        
                }
                .font(.system(size: 25))
                
                
                HStack {
                    Text("Tea") 
                    
                    Stepper("", value: $stepperValueTea, in: 0...20)
                        
                    
                    Text("\(stepperValueTea)")
                        .bold()
                        .padding()
                        .foregroundColor(.red)
                        .font(.system(size: 40))
                    
                }
                
                
                HStack {
                    Text("Coffee")
                    
                    Stepper("", value: $stepperValueCoffee, in: 0...20)
                    
                    Text("\(stepperValueCoffee)")
                        .bold()
                        .padding()
                        .foregroundColor(.red)
                        .font(.system(size: 40))
                    
                    
                }
                
                
                
                HStack { 
                    Text("Milo")
                    
                    Stepper("", value: $stepperValueMilo, in: 0...20)
                    
                    Text("\(stepperValueMilo)")
                        .bold()
                        .padding()
                        .foregroundColor(.red)
                        .font(.system(size: 40))
                }
                
            }
            .navigationTitle("Visidrink")
            
        }
        
        
        
    }
    .font(.system(size: 23))
    
}

}

1 Like

This question is kinda more specific to SwiftUI than the Swift language itself. SwiftUI questions are usually better asked on Apple Developer Forums.

However, the first thing I would suggest is to pull the contents of this cell out into a reusable View struct.

struct DrinkCell: View {
    var name: String
    @Binding var quantity: Int

    var body: some View {
        HStack {
            Text(name)
            Stepper("", value: $quantity, in: 0...20)
            Text("\(quantity)")
                .bold()
                .padding()
                .foregroundColor(.red)
                .font(.system(size: 40))
        }
    }
}

Doing so means you don't need to copy the contents of the view for each cell. With this change, we can rewrite the main view as such:

struct ContentView: View {
    @State private var stepperValueTea = 0
    @State private var stepperValueCoffee = 0
    @State private var stepperValueMilo = 0

    var body: some View {
        NavigationView {
            List {
                HStack {
                    EditButton()
                    
                    Image(systemName: "plus")
                        .frame(maxWidth: .infinity, alignment:.bottomTrailing)
                        .foregroundColor(.red)
                        
                }
                .font(.system(size: 25))

                DrinkView(name: "Tea", quantity: $stepperValueTea)
                DrinkView(name: "Coffee", quantity: $stepperValueCoffee)
                DrinkView(name: "Milo", quantity: $stepperValueMilo)
            }
            .navigationTitle("Visidrink")
        }
        .font(.system(size: 23))
    }
}

You can see with this structure that the DrinkView can be made totally dynamic, so instead of stamping out one view in code for each kind of drink, let's make a struct that describes a drink:

struct Drink {
    var name: String
    var quantity: Int
}

And instead of storing 3 @State properties, we can instead store an Array of these Drinks. We can use the ForEach view to turn each of these Drinks into a View dynamically.

struct ContentView: View {
    @State private var drinks = [
        Drink(name: "Coffee", quantity: 0),
        Drink(name: "Tea", quantity: 0),
        Drink(name: "Milo", quantity: 0)
    ]

    var body: some View {
        NavigationView {
            List {
                HStack {
                    EditButton()
                    
                    Image(systemName: "plus")
                        .frame(maxWidth: .infinity, alignment:.bottomTrailing)
                        .foregroundColor(.red)
                        
                }
                .font(.system(size: 25))

                ForEach(drinks) { drink in
                    DrinkView(name: drink.name, quantity: $drink.quantity)
                }
            }
            .navigationTitle("Visidrink")

        }
        .font(.system(size: 23))
    }
}

This example won't compile yet, though! SwiftUI needs some way to distinguish one Drink value from another, which is where the Identifiable protocol comes in. We can make Drink conform to Identifiable by adding an id property that uniquely identifies one Drink apart from another. We can use a UUID for this, which creates a new unique value every time it's initialized.

struct Drink: Identifiable {
    var name: String
    var quantity: Int
    var id: UUID
}

Now we need to change the initializer of the Array of Drinks to this:

@State private var drinks = [
    Drink(name: "Coffee", quantity: 0, id: UUID()),
    Drink(name: "Tea", quantity: 0, id: UUID()),
    Drink(name: "Milo", quantity: 0, id: UUID())
]

Be careful with UUIDs like this though -- make sure if you modify one of these Drink values that you don't overwrite the UUID with another value, otherwise SwiftUI will think you deleted the drink and created a new one.

Anyway, now that Drink conforms to Identifiable, this will compile. This is the current ContentView.

struct ContentView: View {
    @State private var drinks = [
        Drink(name: "Coffee", quantity: 0, id: UUID()),
        Drink(name: "Tea", quantity: 0, id: UUID()),
        Drink(name: "Milo", quantity: 0, id: UUID())
    ]

    var body: some View {
        NavigationView {
            List {
                HStack {
                    EditButton()
                    
                    Image(systemName: "plus")
                        .frame(maxWidth: .infinity, alignment:.bottomTrailing)
                        .foregroundColor(.red)
                        
                }
                .font(.system(size: 25))

                ForEach(drinks) { drink in
                    DrinkView(name: drink.name, quantity: $drink.quantity)
                }
            }
            .navigationTitle("Visidrink")

        }
        .font(.system(size: 23))
    }
}

Now the last thing to do is to append a new value to the drinks array in response to the + being tapped. To do that, we'll need to make it a Button and have that append a value to drinks.

Button {
    let newDrink = Drink(name: "Water", quantity: 0, id: UUID())
    drinks.append(newDrink)
} label: {
    Image(systemName: "plus")
        .foregroundColor(.red)
}
.frame(maxWidth: .infinity, alignment:.bottomTrailing)

When you tap this button, you'll notice that the list row appears with no animation, but if you wrap the append in withAnimation { ... } then List will pick an appropriate insertion animation for you automatically. SwiftUI can animate most changes to view inputs with the withAnimation function or using the .animation(_:value:) modifier.

Button {
    let newDrink = Drink(name: "Water", quantity: 0, id: UUID())
    withAnimation {
        drinks.append(newDrink)
    }
} label: {
    Image(systemName: "plus")
        .foregroundColor(.red)
}
.frame(maxWidth: .infinity, alignment:.bottomTrailing)

Now, to put it all together:

struct ContentView: View {
    @State private var drinks = [
        Drink(name: "Coffee", quantity: 0, id: UUID()),
        Drink(name: "Tea", quantity: 0, id: UUID()),
        Drink(name: "Milo", quantity: 0, id: UUID())
    ]

    var body: some View {
        NavigationView {
            List {
                HStack {
                    EditButton()
                    
                   Button {
                       let newDrink = Drink(name: "Water", quantity: 0, id: UUID())
                        withAnimation {
                            drinks.append(newDrink)
                       }
                   } label: {
                        Image(systemName: "plus")
                           .foregroundColor(.red)
                   }
                   .frame(maxWidth: .infinity, alignment:.bottomTrailing)
                        
                }
                .font(.system(size: 25))

                ForEach(drinks) { drink in
                    DrinkView(name: drink.name, quantity: $drink.quantity)
                }
            }
            .navigationTitle("Visidrink")

        }
        .font(.system(size: 23))
    }
}

Now, for one more bit of feedback: I'd highly recommend avoiding .font(.system(size: ...)) -- iOS has built-in support for fonts that resize based on the user's preferred font size. There are several semantic sizes to choose from, I think instead of .font(.system(size: 25)), you might want to use .font(.title3) or .font(.headline)

5 Likes

Wow, thank you so much Harlan, I will digest this information and incorporate it into my app. I’ve learnt a lot from this, still a long way to go though. Any great videos to watch on YouTube that could help me with my progress with SwiftUI for iPad developing?

I think the resources I tend to recommend most are Data Essentials in SwiftUI from WWDC 2021:

And Demystify SwiftUI, also from WWDC 2021:

Both of these are going to go a long way to developing the mental model to how to best think in SwiftUI terms.

2 Likes

Hi,

I’m having some difficulties after I have edited my code to your suggestions.

struct Drink: Identifiable {
    var name: String
    @Binding var quantity: Int
    var id: UUID
}


struct DrinkView: View {
    var name: String
    @Binding var quantity: Int
    
    var body: some View {
        HStack {
            Text(name)
            Stepper("", value: $quantity, in: 0...20)
            Text("\(quantity)")
                .bold()
                .padding()
                .foregroundColor(.red)
                .font(.system(size: 40))
        }
    }
}

struct ContentView: View {
    
    @State private var drinks = [
        Drink(name: "Coffee", quantity: 0, id: UUID()),
        Drink(name: "Tea", quantity: 0, id: UUID()),
        Drink(name: "Milo", quantity: 0, id: UUID())
    ]
    
    var body: some View {
        
            NavigationView {
                List {
                    HStack {
                        
                        EditButton()
                        
                        Button {
                            let newDrink = Drink(name: "Water", quantity: 0, id: UUID())
                            withAnimation {
                                drinks.append(newDrink)
                            }
                        } label: {
                            Image(systemName: "plus")
                                .foregroundColor(.red)
                        }
                        .frame(maxWidth: .infinity, alignment:.bottomTrailing)
                        
                    }
                    .font(.system(size: 25))
                    
                    ForEach(drinks) { drink in
                        DrinkView(name: drinks.name, quantity: $drinks.quantity)
                    }
                }
                .navigationTitle("Visidrink")
        }
        .font(.title3)
    }
}


It doesn’t seem to work

Can you describe the problem you're having? One thing I edited in my post is the Drink struct should not have @Binding on its quantity property (typo, apologies) -- that might solve the issue you're seeing.

Nothing happens when I click the plus button.

Sorry for the late reply here. That screenshot doesn't include the code for the button in question -- did you ever end up figuring out the disconnect?