M6L6-7 : User Input Form

With lesson 6 of module 7 of the CWC+ iOS Databases course, we see how to capture user input.

Lesson 6

We have a new SwiftUI view for our input form:

import SwiftUI

struct AddRecipeView: View {
    @State private var name = ""
    @State private var summary = ""
    @State private var prepTime = ""
    @State private var cookTime = ""
    @State private var totalTime = ""
    @State private var servings = ""
    @State private var highlights = [String]()
    @State private var directions = [String]()
    
    var body: some View {
        ...
        VStack {
            AddMetaData(
                name: $name,
                summary: $summary,
                prepTime: $prepTime,
                prepTime: $cookTime,
                totalTime: $totalTime,
                servings: $servings
            )
            AddListData(list: $highlights, title: "Highlights", placeholderText: "Vegetarian")
            AddListData(list: $directions, title: "Directions", placeholderText: "Simmer for an hour")
        }                
        .padding(.horizontal)
    }
}

In our sub-view for the form, we have several TextFields bound to the properties sent from the previous call:

import SwiftUI

struct AddMetaData: View {
    @Binding var name: String
    @Binding var summary: String
    @Binding var prepTime: String
    @Binding var cookTime: String
    @Binding var totalTime: String
    @Binding var servings: String
    
    var body: some View {
        Group {
            HStack {
                Text("Name: ")
                    .fontWeight(.bold)
                TextField("Tuna Casserole", text: $name)
            }
            HStack {
                Text("Summary: ")
                    .fontWeight(.bold)
                TextField("A delicious meal for the whole family", text: $summary)
            }
            HStack {
                Text("PrepTime: ")
                    .fontWeight(.bold)
                TextField("1 hour", text: $prepTime)
            }
            HStack {
                Text("Cook Time: ")
                    .fontWeight(.bold)
                TextField("2 hours", text: $cookTime)
            }
            HStack {
                Text("Total Time: ")
                    .fontWeight(.bold)
                TextField("3 hours", text: $totalTime)
            }
            HStack {
                Text("Servings: ")
                    .fontWeight(.bold) 
                TextField("6", text: $servings)
            }
        }
    }
}

The main magic from Lesson 6 is adding to our list data

import SwiftUI

struct AddListData: View {
    @Binding var list: [String]
    @State private var item: String = ""
    var title: String
    var placeholderText: String
    var body: some View {
        VStack (alignment: .leading) {
            HStack {
                Text("\(title):")
                    .fontWeight(.bold)
                TextField(placeholderText, text: $item)
                Button("Add") {
                    if item.trimmingCharacters(in: .whitespacesAndNewlines) != "" {
                        list.append(item.trimmingCharacters(in: .whitespacesAndNewlines))
                        item = ""
                    }
                }
            }
            ForEach(list, id: \.self) { item in
                Text(item)
            }
        }
    }
}

Two little insights that came up with this lesson.

Firstly, we need to ensure we initialise the private state property. Originally, we had this:

@State private var item: String

And that threw up this error:

... AddRecipeView.swift:51:21: 'AddListData' initializer is inaccessible due to 'private' protection level

It’s pretty logical really, in that the property must be initiated within its “private” scope.

Secondly, we originally used a List() instead of ForEach(), re:

List(list, id: \.self) { item in
    Text(item)
}

We discovered that, although we are appending the data with list.append(), this wasn’t being reflected back to highlights and directions in our main view and so was not triggering a screen refresh, and the list was not shown on-screen.

Changing to ForEach() fixes that issue.

Lesson 7

This lesson continues in the same vein as the above, this time to allow the user to add ingredients to the form.

The main difference here is that we declare our binding property data type as our original Ingredient class (now called IngredientJSON), as that’s already set up from before.

struct AddIngredientData: View {
    
    @Binding var ingredients: [IngredientJSON]
    @State private var name = ""
    @State private var unit = ""
    @State private var num = ""
    @State private var denom = ""
    
    var body: some View {
        VStack (alignment: .leading) {
            Text("Ingredients:")
                .fontWeight(.bold)
                .padding(.top, 5)
            HStack {
                TextField("Sugar", text: $name)
                TextField("1", text: $num)
                    .frame(width:20)
                Text("/")
                TextField("2", text: $denom)
                    .frame(width:20)
                TextField("Grams", text: $unit)
                Button("Add") {
                    let cleanedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
                    let cleanedNum = num.trimmingCharacters(in: .whitespacesAndNewlines)
                    let cleanedDenom = denom.trimmingCharacters(in: .whitespacesAndNewlines)
                    let cleanedUnit = unit.trimmingCharacters(in: .whitespacesAndNewlines)
                    
                    if cleanedName == "" || cleanedNum == "" || cleanedDenom == "" || cleanedUnit == "" {
                        return
                    }
                    
                    let i = IngredientJSON()
                    i.id = UUID()
                    i.name = cleanedName
                    i.num = Int(cleanedNum) ?? 1
                    i.denom = Int(cleanedDenom) ?? 1
                    i.unit = cleanedUnit

                    ingredients.append(i)

                    name = ""
                    unit = ""
                    num = ""
                    denom = ""
                }
            }
            ForEach(ingredients) { ingredient in
                Text("\(ingredient.name), \(ingredient.num ?? 1)/\(ingredient.denom ?? 1) \(ingredient.unit ?? "")")
            }
        }
    }
}

And now we’re ready to move on to handling the image data. That’s going to be enlightening.