arraysswiftswiftuiswiftui-list

SwiftUI changing one element in an array changes all elements in the array


I'm trying to change the color of a button when it's pressed. It works as intended in the preview of WordleRowView.swift but when I press a button in WordleView.swift, every button in that row changes color.

WordleView.swift

import SwiftUI

class WordleRow: ObservableObject, Identifiable {
    let id = UUID()
    @Published var word: String
    @Published var colors: [Color] = [.gray, .gray, .gray, .yellow, .green]
    
    init(_ word: String) {
        self.word = word
    }
}

class WordleBoard: ObservableObject {
    
    @Published var rows = [WordleRow]()
    
    init() {
        self.rows = [
            WordleRow("hello"),
            WordleRow("horse")
        ]
    }
}

struct WordleView: View {
    
    @ObservedObject var board = WordleBoard()
    
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(board.rows) { wordleRow in
                        WordleRowView(wordleRow: wordleRow)
                    }
                    .onDelete { board.rows.remove(atOffsets: $0) }
                    .listRowBackground(Color.clear)
                }
                Button("Solve") {
                    print(board.rows[0].colors)
                }
            }
        }
    }
}

#Preview {
    WordleView()
}

WordleRowView.swift

import SwiftUI

struct WordleRowView: View {
    @ObservedObject var wordleRow: WordleRow
    
    var body: some View {
        
        HStack {
            LetterBox(wr: wordleRow, idx: 0)
            LetterBox(wr: wordleRow, idx: 1)
            LetterBox(wr: wordleRow, idx: 2)
            LetterBox(wr: wordleRow, idx: 3)
            LetterBox(wr: wordleRow, idx: 4)
        }
        .padding(.horizontal)
        .frame(maxWidth: .infinity)
        .aspectRatio(contentMode: .fit)
    }
}

#Preview {
    WordleRowView(wordleRow: WordleRow("hello"))
}

struct LetterBox: View {
    @ObservedObject var wr: WordleRow
    
    let idx: Int
    
    var letter: String {
        String(wr.word[wr.word.index(wr.word.startIndex, offsetBy: idx)])
    }
    
    var body: some View {
        Button(letter) {
            switch wr.colors[idx] {
            case .gray:
                wr.colors[idx] = .yellow
            case .yellow:
                wr.colors[idx] = .green
            default:
                wr.colors[idx] = .gray
            }
            
            print(idx)
            print(wr.colors)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .aspectRatio(1, contentMode: .fit)
        .foregroundStyle(.white)
        .background(wr.colors[idx])
    }
}

I've tried using a dictionary instead of an array to store colors but the same problem occurs.


Solution

  • You can see from the console logs that the action for every button is firing when you use the WordleView preview, but not when you use the WordleRowView preview. The difference between these two is that you have a List in WordleView, and multiple buttons in a list row for some reason don't work properly unless you apply a style (see Buttons in SwiftUI List ForEach view trigger even when not "tapped"? for another example).

    Applying .buttonStyle(.plain) in LetterBox fixes your issue.