Search code examples
swiftswiftuiswiftui-foreach

Using a ForEach loop to show the key and value of dictionary items in a Form view in SwiftUI


I'm trying to create what I thought would be a simple bit of code to show the key of a dictionary item and implement a Stepper or a Picker to modify the key's value in a single line within a Form view.

At the end of the day, all I'm trying to accomplish is showing the user a list of words they've entered, along with an associated number value that they can change.

Almost every method to solution my problem resulted in absolutely blowing up (crashing) my canvas because evidently my attempts at solutioning this are just that wrong.

Anyway, below are some examples of ways I've been trying to solve my problem. Note that I've also tried many types of views, like Text with the word, and a picker for the associated Integer. I just figure for the sake of demonstrating the problem, using a Stepper for each example might be good enough.

struct Likenesses {
    let word: String
    var likeness: Int = 0
}

struct ContentView: View {
    
    @State private var words: [String] = []
    @State private var likeness: [Int] = []
    @State private var wordLikeness: [String:Int] = [:]
    @State private var StructLikeness: [Likenesses] = []
    
    var body: some View {
        Form {
            
            // Dict Method
            Section {
                ForEach(Array(wordLikeness.keys), id: \.self) { key in
                    Stepper("\(key)", value: $wordLikeness[key], in: 0...8, step: 1)
                }
            }
            
            // Arrays method
            Section {
                ForEach(0...words.count, id: \.self) { i in
                    Stepper("\(word[i])", value: $likeness[i], in: 0...8, step: 1)
                }
            }
            
            // Struct method
            Section {
                ForEach(StructLikeness, id: \.self) { s in
                    /*
                    Yes, I know structs are not references and pass copies around, this was another issue I was going to have to solve eventually, but for now, this gets the idea of what I was trying to solve with a struct
                    */
                    Stepper("\(s.word)", value: $s.likeness, in: 0...8, step: 1)
                }
            }
        }
    }
}

Solution

  • Dictionary elements don't have a particular order, so you need to decide on an order first, before you use them in ForEach. For example, if you want to sort by the keys,

    @State private var likenesses: [String: Int] = ["Foo": 3]
    
    Form {
        Section {
            ForEach(likenesses.keys.sorted(), id: \.self) { key in
                Stepper("\(key): \(likenesses[key]!)", value: Binding($likenesses[key])!, in: 0...8, step: 1)
            }
        }
    }
    

    I would prefer passing an array of structs instead. Create the binding when creating the ForEach, not when creating the steppers.

    @State private var likenesses: [Likenesses] = [...]
    
    Form {
        Section {
            ForEach($likenesses) { s in
                Stepper("\(s.wrappedValue.word): \(s.wrappedValue.likeness)", value: s.likeness, in: 0...8, step: 1)
            }
        }
    }
    

    Note that the struct should conform to Identifiable:

    struct Likenesses: Identifiable {
        let word: String
        var likeness: Int = 0
        
        // here I'm using word as the id
        var id: String { word }
    }