在SwiftUI中,只要从层次结构较远的视图更新列表的基础数据源,就会触发刷新列表视图

我正在尝试在SwiftUI中编写“单视图应用程序”。主要设计非常简单。我有一个要在NavigationView-> List中的主视图中显示的项目列表(例如Expense)。

列表视图源代码

    import SwiftUI

struct AmountBasedModifier : ViewModifier{
    var amount: Int
    func body(content: Content) -> some View {
        if amount <= 10{
            return content.foregroundColor(Color.green)
        }
        else if amount <= 100{
            return content.foregroundColor(Color.blue)
        }
        else {
            return content.foregroundColor(Color.red)
            
        }
    }
}

extension View {
    
    func amountBasedStyle(amount: Int) -> some View {
        self.modifier(AmountBasedModifier(amount: amount))
    }
}

struct ExpenseItem: Identifiable, Codable {
    var id = UUID()
    var name: String
    var type: String
    var amount: Int
    
    static var Empty: ExpenseItem{
        return ExpenseItem(name: "", type: "", amount: 0)
    }
}

class Expenses: ObservableObject {
    @Published var items = [ExpenseItem](){
        didSet{
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(items){
                UserDefaults.standard.set(data, forKey: "items")
            }
        }
    }
    
    init() {
        let decoder = JSONDecoder()
        
        if let data = UserDefaults.standard.data(forKey: "items"){
            if let items = try? decoder.decode([ExpenseItem].self, from: data){
                self.items = items
                return
            }
        }
        items = []
    }
}

struct ContentView: View {
    @ObservedObject var expenses = Expenses()
    @State private var isShowingAddNewItemView = false
    
    var body: some View {
        NavigationView{
            List{
                ForEach(self.expenses.items) { item in
                    NavigationLink(destination: ExpenseItemHost(item: item, expenses: self.expenses)){
                        HStack{
                            VStack(alignment: .leading){
                                Text(item.name)
                                    .font(.headline)
                                Text(item.type)
                                    .font(.subheadline)
                            }
                            Spacer()
                            Text("$\(item.amount)")
                                .amountBasedStyle(amount: item.amount)
                        }
                    }
                }.onDelete(perform: removeItems)
            }
            .navigationBarTitle("iExpense")
            .navigationBarItems(leading: EditButton(), trailing: Button(action:
                {
                    self.isShowingAddNewItemView.toggle()
            }, label: {
                Image(systemName: "plus")
            }))
                .sheet(isPresented: $isShowingAddNewItemView) {
                    AddNewExpense(expenses: self.expenses)
            }
        }
    }
    
    func removeItems(at offsets: IndexSet){
        self.expenses.items.remove(atOffsets: offsets)
    }
}

每个行项目都是NavigationLink,以只读模式打开费用,显示费用项目的所有属性。

右上角有一个添加按钮,可让用户在列表中添加新的费用项目。 AddNewExpenseView(显示为工作表)可以访问列表数据源。因此,每当用户添加新费用时,列表的数据源就会更新(通过添加新项目),然后将工作表销毁。

添加查看源代码

struct AddNewExpense: View {
    @ObservedObject var expenses: Expenses
    @Environment(\.presentationMode) var presentationMode
    
    @State private var name = ""
    @State private var type = "Personal"
    @State private var amount = ""
    @State private var isShowingAlert = false
    
    static private let expenseTypes = ["Personal", "Business"]
    
    var body: some View {
        NavigationView{
            Form{
                TextField("Name", text: $name)
                Picker("Expense Type", selection: $type) {
                    ForEach(Self.expenseTypes, id: \.self) {
                        Text($0)
                    }
                }
                TextField("Amount", text: $amount)
            }.navigationBarTitle("Add New Expense", displayMode: .inline)
                .navigationBarItems(trailing: Button(action: {
                    if let amount = Int(self.amount){
                        let expenseItem = ExpenseItem(name: self.name, type: self.type, amount: amount)
                        self.expenses.items.append(expenseItem)
                        self.presentationMode.wrappedValue.dismiss()
                    }else{
                        self.isShowingAlert.toggle()
                    }
                    
                }, label: {
                    Text("Save")
                }))
                .alert(isPresented: $isShowingAlert) {
                    Alert.init(title: Text("Invalid Amount"), message: Text("The amount should only be numbers and without decimals"), dismissButton: .default(Text("OK")))
            }
        }
    }
}

费用明细(只读)查看源代码

struct ExpenseItemView: View {
    var item: ExpenseItem
    
    var body: some View {
        List{
            Section{
                Text("Name")
                    .font(.headline)
                Text(item.name)
            }
            
            Section{
                Text("Expense Type")
                    .font(.headline)
                Text(item.type)
            }
            
            Section{
                Text("Amount")
                    .font(.headline)
                Text("$\(item.amount)")
            }
        }.listStyle(GroupedListStyle())
        .navigationBarTitle(Text("Expense Details"), displayMode: .inline)
    }
}

到目前为止,一切都很好。然后,我想到在ExpenseItem View屏幕上添加一个Edit按钮,以便用户可以编辑Expense。我创建了一个编辑视图,当单击“编辑”按钮时,该视图将从“只读”视图作为工作表启动。

编辑查看代码

struct ExpenseItemHost: View {
    @State var isShowingEditSheet = false
    @State var item: ExpenseItem
    @State var itemUnderEdit = ExpenseItem.Empty
    
    var expenses: Expenses
    
    var body: some View {
        VStack{
            ExpenseItemView(item: self.item)
        }
        .navigationBarItems(trailing: Button("Edit")
        {
            self.isShowingEditSheet.toggle()
        })
        .sheet(isPresented: $isShowingEditSheet) {
            EditExpenseItemView(item: self.$itemUnderEdit)
                .onAppear(){
                    self.itemUnderEdit = self.item
            }
            .onDisappear(){
                
//TO DO: Handle the logic where save is done when user has explicitly pressed "Done" button.  `//Presently it is saving even if Cancel button is clicked`
                if let indexAt = self.expenses.items.firstIndex( where: { listItem in
                    return self.item.id == listItem.id
                }){
                    self.expenses.items.remove(at: indexAt)
                }
                
                self.item = self.itemUnderEdit
                self.expenses.items.append(self.item)
            }
        }
    }
}


struct EditExpenseItemView: View {
    @Environment(\.presentationMode) var presentationMode
    
    @Binding var item: ExpenseItem
    static private let expenseTypes = ["Personal", "Business"]
    
    var body: some View {
        NavigationView{

            Form{
                TextField("Name", text: self.$item.name)
                Picker("Expense Type", selection: self.$item.type) {
                    ForEach(Self.expenseTypes, id: \.self) {
                        Text($0)
                    }
                }
                TextField("Amount", value: self.$item.amount, formatter: NumberFormatter())
            }

            .navigationBarTitle(Text(""), displayMode: .inline)
            .navigationBarItems(leading: Button("Cancel"){
                self.presentationMode.wrappedValue.dismiss()
            }, trailing: Button("Done"){
                self.presentationMode.wrappedValue.dismiss()
            })
        }
    }
}

屏幕截图

问题

我希望当用户通过按“完成”按钮完成编辑后,表格应返回到“只读”屏幕,因为这是用户单击“编辑”按钮的地方。但是由于单击“完成”按钮时我正在修改ListView的数据源,因此ListView会被重新创建/刷新。因此,单击“完成”按钮后,将显示ListView,而不是EditView表返回到ReadOnly视图。

由于我的代码正在更改视图的数据源,该视图现在用户无法访问,因此下面的异常也会生成

2020-08-02 19:30:11.561793+0530 iExpense[91373:6737004] [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7f9a8b021800; baseClass = UITableView; frame = (0 0; 414 896); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x6000010a1110>; layer = <CALayer: 0x600001e8c0e0>; contentOffset: {0, -140}; contentSize: {414, 220}; adjustedContentInset: {140, 0, 34, 0}; dataSource: <_TtGC7SwiftUIP13$7fff2c9a5ad419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x7f9a8a5073f0>>

我可以理解为什么触发了ListView刷新,但是我无法弄清楚的是编辑模型的正确模式,并且当我们在中间有中间屏幕时,即List View-> ReadOnly-> Edit,不会导致触发ListView刷新。视图。

对这个案件有什么建议?