В SwiftUI List View refre sh срабатывает всякий раз, когда базовый источник данных списка обновляется из представления, расположенного далеко в иерархии - PullRequest
2 голосов
/ 02 августа 2020

Я пытаюсь написать «Приложение для одного просмотра» в SwiftUI. Основная конструкция очень проста. У меня есть список элементов (например, расходы), которые я показываю в главном представлении в NavigationView -> List.

Исходный код представления списка

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

Каждая строка item - это NavigationLink, которая открывает Expense в режиме только для чтения, показывая все атрибуты Expense Item.

В правом верхнем углу есть кнопка «Добавить», позволяющая пользователю добавить новую статью расходов в список. 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, чтобы пользователь мог редактировать «Расходы». Я создал режим редактирования, который запускается как лист из ReadOnly View при нажатии кнопки «Редактировать».

Редактировать код просмотра

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

Скриншоты

image image image image

Проблема

Я ожидаю, что когда пользователь закончит редактирование, нажав Готово При нажатии кнопки Лист должен вернуться на экран ReadOnly, поскольку именно здесь пользователь нажал кнопку «Изменить». Но поскольку я изменяю источник данных ListView при нажатии кнопки «Готово», то ListView воссоздается / обновляется. Таким образом, вместо того, чтобы лист EditView возвращался в представление ReadOnly, ListView отображается при нажатии кнопки «Готово».

Поскольку мой код изменяет источник данных представления, которое сейчас недоступно для пользователя, поэтому ниже исключение также генерируется

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 refre sh, но то, что я не мог понять, это правильный шаблон для редактирования модели, а также не вызывать refre ListView sh для срабатывания, когда у нас есть промежуточный экран между ними, например, представление списка -> только для чтения -> вид редактирования.

Что предлагается для обработки этого случая?

...