You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中修改数组单个元素触发整行按钮变色问题求助

解决List中点击单个按钮触发整行颜色变化的问题

你遇到的问题是:在WordleRowView的预览中,点击单个字母按钮可以正常切换对应位置的颜色;但在WordleView的List组件中展示时,点击某行的任意一个按钮,该行所有按钮都会出现颜色变化的异常,尽管打印的colors数组值是正确的。

问题代码

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])
    }
}

解决方案

方案1:替换List为ScrollView+VStack(快速验证)

List的视图重用机制可能导致状态渲染异常,先替换为ScrollView嵌套VStack验证问题是否消失:

// 修改WordleView.swift的body部分
var body: some View {
    NavigationView {
        VStack {
            ScrollView {
                VStack(spacing: 8) {
                    ForEach(board.rows) { wordleRow in
                        WordleRowView(wordleRow: wordleRow)
                    }
                }
                .padding()
            }
            Button("Solve") {
                print(board.rows[0].colors)
            }
        }
    }
}

方案2:改用值类型管理行状态(推荐)

将WordleRow从ObservableObject的class改为Identifiable的struct,利用SwiftUI值类型的状态管理机制,避免引用类型带来的视图刷新问题:

修改WordleView.swift
import SwiftUI

struct WordleRow: Identifiable {
    let id = UUID()
    var word: String
    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 {
    @Binding var wordleRow: WordleRow
    
    var body: some View {
        HStack {
            LetterBox(wordleRow: $wordleRow, idx: 0)
            LetterBox(wordleRow: $wordleRow, idx: 1)
            LetterBox(wordleRow: $wordleRow, idx: 2)
            LetterBox(wordleRow: $wordleRow, idx: 3)
            LetterBox(wordleRow: $wordleRow, idx: 4)
        }
        .padding(.horizontal)
        .frame(maxWidth: .infinity)
        .aspectRatio(contentMode: .fit)
    }
}

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

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

原因分析

  1. List的视图重用:SwiftUI的List会复用行视图优化性能,结合引用类型的状态管理时,容易出现状态绑定混乱,导致整行颜色异常;而预览中没有List的重用机制,因此表现正常。
  2. 引用类型的更新检测:WordleRow作为class是引用类型,修改内部colors数组时,SwiftUI无法精准检测单个元素的变化,会触发整行视图强制刷新,放大了异常表现。改用值类型struct后,状态变化会触发精确的视图更新,彻底解决此类问题。

内容的提问来源于stack exchange,提问作者ceru123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 17:40:55