在 SwiftUI 中,每当列表的底层数据源从层次结构中较远的视图更新时,就会触发列表视图刷新

Ish*_*war 5 swiftui swiftui-list

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

列表查看源代码

    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)
    }
}
Run Code Online (Sandbox Code Playgroud)

每个行项目都是 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")))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

编辑查看代码

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()
            })
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

截图

问题

我希望当用户通过按“完成”按钮完成编辑时,工作表应返回到“只读”屏幕,因为这是用户单击“编辑”按钮的位置。但由于单击“完成”按钮时我正在修改 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>>
Run Code Online (Sandbox Code Playgroud)

我可以理解为什么 ListView 刷新被触发,但我无法弄清楚编辑模型的正确模式,以及当我们在中间屏幕之间(即列表视图 -> 只读 -> 编辑)时不会导致 ListView 刷新触发看法。

处理此案有何建议?