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 Drink
s. We can use the ForEach
view to turn each of these Drink
s 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)