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

SwiftUI中TextField绑定状态无法更新问题求助

问题:SwiftUI中EditExpenseView的TextField无法更新状态

运行应用(非预览模式)时,TextField无法更新状态。已将StateObject(ViewModel实例)传入EnvironmentObject,在视图中遍历模型数组,将迭代元素以Binding形式传入另一个视图绑定到TextField供用户编辑,但跳转至EditExpenseView后,TextField无法编辑。

注意:

  • 若将TextField移至ExpenseList视图,绑定编辑功能正常,曾以为是List遍历不可变集合导致问题。
  • 使用索引通过$expenses[index]传递数组Binding,仅用于获取用户要编辑的列表项索引,避免访问不可变集合。

Expense Model

struct Expense: Equatable, Identifiable, Codable {
    
    init(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus, budgetId: UUID?) {
        self.date = date
        self.description = description
        self.amount = amount
        self.type = type
        self.status = status
        self.budgetId = budgetId
    }
    
    static func == (lhs: Expense, rhs: Expense) -> Bool {
        lhs.id == rhs.id
    }
    
    var id: UUID = UUID()
    var date: Date
    var description: String
    var amount: Decimal
    var type: ExpenseType
    var status: ExpenseStatus
    var budgetId: UUID?
}

ExpenseViewModel

class ExpenseViewModel: ObservableObject, Identifiable {
    
    @Published var expenses: [Expense] = []
    
    func insertExpense(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus) -> Void {
        expenses.insert(Expense(date: date, description: description, amount: amount, type: type, status: status, budgetId: nil), at:0)
    }
    
    func remove(_ expense: Expense) {
        expenses.removeAll(where: {$0.id == expense.id})
    }
    
}

App入口

import SwiftUI

@main
struct iBudgeteerApp: App {
    
    @StateObject private var expenses = ExpenseViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(expenses)
        }
    }
}

ContentView

struct ContentView: View {
    
    @EnvironmentObject private var model: ExpenseViewModel
    
    private static let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
    
    var body: some View {
        NavigationStack {
            VStack {
                Button("Add Row") {
                    model.insertExpense(date: Date(), description: "Groceries", amount: 29.94, type: .Expense, status: .Cleared)
                }
                ExpenseList(expenses: $model.expenses)
            }
        }
    }
}

ExpenseList视图

struct ExpenseList: View {
    
    @Binding var expenses: [Expense]
    
    var formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
    
    var body: some View {
        
        
        List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
            index in
            
            HStack {
                
                Text("\(index + 1).").padding(.trailing)
                
                VStack(alignment: .leading) {
                    
                    HStack {
                        Text(expenses[index].date.formatted(date:.numeric, time: .omitted))
                        Spacer()
                        Text(expenses[index].description)
                    }
                    
                    HStack {
                        Text(expenses[index].description)
                        Spacer()
                        Text("\(expenses[index].amount as NSNumber, formatter: formatter)")
                            .foregroundColor( expenses[index].type == .Expense ? .red : .green)
                        Image(systemName: expenses[index].type == .Expense ? "arrow.down" : "arrow.up").foregroundColor( expenses[index].type == .Expense ? .red : .green)
                    }.padding(.top, 1)
                }
                .swipeActions(edge: .trailing) {
                    Button(role: .destructive, action: { expenses.remove(at: index) } ) {
                        Label("Delete", systemImage: "trash")
                    }
                    .tint(.gray)
                }
                .swipeActions() {
                    NavigationLink {
                        EditExpenseView(expense: self.$expenses[index])
                    } label: {
                        Label("Edit", systemImage: "slider.horizontal.3")
                    }
                    .tint(.yellow)
            }
            }
        }
    }
    
}

EditExpenseView视图

struct EditExpenseView: View {
    
    @Binding var expense: Expense
    
    var formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
    

    var body: some View {
        
        Form {
            
            Section(header: Text("Editing: \(expense.description)")) {
                VStack {
                    DatePicker(
                        "Date",
                        selection: $expense.date,
                        displayedComponents: [.date]
                    )
                    HStack {
                        Text("Name")
                        Spacer()
                        TextField("description",text: $expense.description)
                            .fixedSize().multilineTextAlignment(.trailing)
                    }
                    HStack {
                        Text("Amount")
                        Spacer()
                        TextField("0.00", value: $expense.amount, formatter: formatter).fixedSize()
                    }
                    
                    Picker("Status", selection: $expense.status) {
                        ForEach(ExpenseStatus.allCases, id: \.self) {
                            status in
                            Text("\(status.rawValue)")
                        }
                    }
                    Picker("Type", selection: $expense.type) {
                        ForEach(ExpenseType.allCases, id: \.self) {
                            type in
                            Text("\(type.rawValue)")
                        }
                    }
                }
            }
        }
        
    }
}

更新测试结果

可正常运行的代码:

List ($expenses) { $expense in
    NavigationLink(expense.description) {
        EditExpenseView(expense: $expense)
    }         
}

ForEach($expenses) { $expense in
    NavigationLink(expense.description) {
        EditExpenseView(expense: $expense)
    }
}

无法正常运行的代码:

List($expenses) {
                $expense in
                
                    VStack(alignment: .leading) {
                        
                        HStack {
                            Text(expense.date.formatted(date:.numeric, time: .omitted))
                            Spacer()                        }
                        
                        HStack {
                            Text(expense.description)
                            Spacer()
                            Text("\(expense.amount as NSNumber, formatter: formatter)")
                                .foregroundColor( expense.type == .Expense ? .red : .green)
                            Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up").foregroundColor(expense.type == .Expense ? .red : .green)
                        }.padding(.top, 1)
                    }
                    .swipeActions(edge: .trailing) {
                        Button(role: .destructive, action: { //expenses.remove(expense)
                        } ) {
                            Label("Delete", systemImage: "trash")
                        }
                        .tint(.gray)
                    }
                    .swipeActions() {
                        
                        NavigationLink {
                            EditExpenseView(expense: $expense)
                        } label: {
                            Label("Edit", systemImage: "slider.horizontal.3")
                        }
                        .tint(.yellow)
                    }
               
               
}

问题原因及解决方法

核心原因

  1. 排序后数组索引绑定失效:
    原代码中使用expenses.sorted(by: {$0.date > $1.date}).indices遍历,sorted会返回新数组副本,此时$expenses[index]指向的是原数组的索引位置,和排序后数组的索引不匹配,导致Binding关联的元素错误,无法正确更新原数组数据。

  2. List内容结构破坏绑定链:
    使用List($expenses)绑定形式时,若在元素外层嵌套过多容器并附加swipeActions,会破坏List内部的绑定关联逻辑,导致Binding失效。

正确解决步骤

方法1:修复排序遍历的绑定问题

通过绑定的方式处理排序后的数组,确保每个元素的Binding关联原数组:

struct ExpenseList: View {
    @Binding var expenses: [Expense]
    
    var formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
    
    var body: some View {
        let sortedExpenses = Binding(
            get: { expenses.sorted(by: {$0.date > $1.date}) },
            set: { newValues in
                newValues.forEach { newValue in
                    if let index = expenses.firstIndex(where: {$0.id == newValue.id}) {
                        expenses[index] = newValue
                    }
                }
            }
        )
        
        List(Array(sortedExpenses.enumerated()), id: \.element.id) { index, $expense in
            HStack {
                Text("\(index + 1).").padding(.trailing)
                
                VStack(alignment: .leading) {
                    HStack {
                        Text(expense.date.formatted(date:.numeric, time: .omitted))
                        Spacer()
                        Text(expense.description)
                    }
                    
                    HStack {
                        Text(expense.description)
                        Spacer()
                        Text("\(expense.amount as NSNumber, formatter: formatter)")
                            .foregroundColor(expense.type == .Expense ? .red : .green)
                        Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up")
                            .foregroundColor(expense.type == .Expense ? .red : .green)
                    }.padding(.top, 1)
                }
                .swipeActions(edge: .trailing) {
                    Button(role: .destructive, action: {
                        expenses.removeAll(where: {$0.id == expense.id})
                    }) {
                        Label("Delete", systemImage: "trash")
                    }
                    .tint(.gray)
                }
                .swipeActions() {
                    NavigationLink {
                        EditExpenseView(expense: $expense)
                    } label: {
                        Label("Edit", systemImage: "slider.horizontal.3")
                    }
                    .tint(.yellow)
                }
            }
        }
    }
}

方法2:ForEach嵌套List(推荐)

用ForEach包裹排序后的绑定数组,确保每个元素的Binding正确关联原数组:

struct ExpenseList: View {
    @Binding var expenses: [Expense]
    
    var formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
    
    var body: some View {
        List {
            ForEach($expenses.sorted(by: {$0.wrappedValue.date > $1.wrappedValue.date}), id: \.wrappedValue.id) { $expense in
                HStack {
                    Text("\((expenses.firstIndex(where: {$0.id == expense.id})! + 1)).").padding(.trailing)
                    
                    VStack(alignment: .leading) {
                        HStack {
                            Text(expense.date.formatted(date:.numeric, time: .omitted))
                            Spacer()
                            Text(expense.description)
                        }
                        
                        HStack {
                            Text(expense.description)
                            Spacer()
                            Text("\(expense.amount as NSNumber, formatter: formatter)")
                                .foregroundColor(expense.type == .Expense ? .red : .green)
                            Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up")
                                .foregroundColor(expense.type == .Expense ? .red : .green)
                        }.padding(.top, 1)
                    }
                    .swipeActions(edge: .trailing) {
                        Button(role: .destructive, action: {
                            expenses.removeAll(where: {$0.id == expense.id})
                        }) {
                            Label("Delete", systemImage: "trash")
                        }
                        .tint(.gray)
                    }
                    .swipeActions() {
                        NavigationLink {
                            EditExpenseView(expense: $expense)
                        } label: {
                            Label("Edit", systemImage: "slider.horizontal.3")
                        }
                        .tint(.yellow)
                    }
                }
            }
        }
    }
}

关键要点

  • 避免直接遍历排序后数组的索引,排序会生成新数组,索引与原数组不匹配。
  • 使用ForEach($expenses)或绑定的排序数组,确保每个元素的Binding指向原数组中的正确对象。
  • 在List中使用swipeActions时,确保Binding元素直接关联到ForEach或List的绑定元素,不要破坏绑定链。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 21:55:22