SwiftUI:如何用可变数据实现列表中的NavigationLinks?

sg3maiej  于 2023-03-07  发布在  Swift
关注(0)|答案(2)|浏览(243)

我想在名称的SwiftUI中构建一个简单的List,当点击时,导航到允许修改这些名称的详细视图。
当我使用@State数组为List提供名称,并试图将数组中元素的绑定传递给细节视图时,对细节视图中绑定对象的任何修改都将导致不仅细节视图的重绘,而且包含List的屏幕外视图也将重绘。
一般来说,问题似乎是不可能改变父视图中的数据,这在post on the Apple Developer Forums中有描述。
post with a similar title on this subject here at StackOverflowblog posts试图解决这个问题,等等,但是我还没有找到一个好的通用解决方案或设计模式来处理看起来相当常见的用例。
Apple's WWDC 2022 video "The SwiftUI Cookbook for Navigation"完全避开了数据变化的问题,并且只显示了使用静态数据的应用程序的示例(即它们不修改任何食谱)。
下面是我为演示这个问题而编写的一些代码:

//
//  ContentView.swift
//

import SwiftUI

struct Item: Hashable, Identifiable {
    let id = UUID()
    var text: String
}

struct ContentView: View {
    @State var items = [Item(text: "foo bar"), Item(text: "biz boz") ]

    var body: some View {
        NavigationStack {
            let _ = { print("render NavigationStack") }()

            List(items) { item in
                let _ = { print("render List item") }()

                NavigationLink(item.text, value: item.id)
            }
            .navigationDestination(for: UUID.self) { id in
                if let index = items.firstIndex(where: {$0.id == id}) {
                    let _ = { print("render TextField") }()

                    VStack {
                        TextField("Value", text: $items[index].text)
                            .multilineTextAlignment(.center)
                        Spacer()
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

这里有一段视频展示了这个问题:

请注意,当在文本字段中键入字符时,光标总是跳到字段的末尾,这是因为修改文本时,TextField的父视图将重新呈现,因此整个详细视图将重新呈现。
我特别想完成的是将父视图的重绘与细节视图中的数据修改分开。如果父视图在屏幕外根本不重绘,那就太理想了,但我仍然需要父视图中的列表根据细节视图中的修改更新其文本。如果可能的话,我还想向细节视图传递一个绑定。但在这一点上,任何功能性的变通方案都会令人满意。

k97glaaz

k97glaaz1#

若要在局部视图中修改父视图的状态数据时分开重绘父视图,应使用目标数据的副本进行操作,并在需要时手动应用更改。
例如,你可以创建一个ItemEditingView,它有自己的text状态,并与你的Item示例绑定,然后我们可以实现一个自定义工具栏按钮(“Back”,“保存”等),允许我们只在按下时应用更改。

struct ItemEditingView: View {
    @Binding var item: Item
    @State var text: String
    
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var backButton: some View {
        Button(action: {
            // Apply changes
            item.text = text
            
            // Dismiss
            self.presentationMode.wrappedValue.dismiss()
        }, label: {
            Image(systemName: "chevron.backward")
                .fontWeight(.semibold)
            Text("Back")
        })
    }
    
    var body: some View {
        VStack {
            TextField("Text", text: $text)
                .multilineTextAlignment(.center)
            Spacer()
        }
        .navigationBarItems(leading: backButton)
        .navigationBarBackButtonHidden()
    }
}

struct ContentView: View {
    @State var items = [Item(text: "foo bar"), Item(text: "biz boz")]
    
    var body: some View {
        NavigationStack {
            List(items.indices, id: \.self) { i in
                NavigationLink(destination: ItemEditingView(item: $items[i], text: items[i].text)) {
                    Text(items[i].text)
                }
            }
        }
    }
}
j2qf4p5b

j2qf4p5b2#

所提出的答案绝对是好的,但是让我使用MVVM设计模式提出一个更完整的答案。

首先,你需要一个模型来描述你的数据,有些语言称之为data-class,但是在Swift中,这通常被描述为一个struct

//
//  ItemModel.swift
//  Design Pattern
//
//  Created by Allan Garcia on 04/03/23.
//

import Foundation

struct Item: Hashable, Identifiable {
    let id = UUID()
    var text: String
}

然后,在MVVM模式中,您有一个"中间人",负责存储、管理、保存数据、检测变化,并通过无线电调谐到称为ObservableObject的"站点",让视图重新绘制自己。
这是本案例的一个示例:

//
//  ItemStorageViewModel.swift
//  Design Pattern
//
//  Created by Allan Garcia on 04/03/23.
//

import SwiftUI

class ItemStorageViewModel: ObservableObject {
    
    @Published var items: [Item]
    
    init() {
        items = ItemStorageViewModel.addSomeExampleItems()
    }
    
    static func addSomeExampleItems() -> [Item] {
        let item1 = Item(text: "foo")
        let item2 = Item(text: "bar")
        return [item1, item2]
    }
    
    func save(_ item: Item) -> Void {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index] = item
        } else {
            print("Couldn't find the index.")
        }
    }
}

有两项职责:首先,将Item数组的更改发布到世界上,无论谁是该 * view-model * 的示例,这都是使用 * decorator***@Published意图来完成的,当有人想要更改其值时,意图将其保存在存储"var items"中。
看起来描述这个变量更合适的方法是使用
private(set)**visibility,但是我没有理会。

    • static func**只是一个辅助函数,可以很好地处理Xcode中的预览。

然后,您希望在某个视图中显示此存储的列表...
现在,您不需要NavigationLink(项目. self,项目:item),因为这是一个Struct,当你有几个模型要显示在同一个List中时,你可以使用这里提供的更简单的Struct NavigationLink(destination:label)。
对于目的地,您将提供视图,您要显示的所有必要参数.
在标签中只显示列表中显示的内容。
就像这样:

//
//  ItemListView.swift
//  Design Pattern
//
//  Created by Allan Garcia on 04/03/23.
//

import SwiftUI

struct ItemListView: View {
    
    @ObservedObject private var viewModel = ItemStorageViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.items) { item in
                NavigationLink {
                    ItemEditItemView(viewModel: viewModel, editingItem: item)
                } label: {
                    Text(item.text)
                }
            }
            .navigationTitle("Items")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ItemListView()
    }
}

最后但并非最不重要的是,这是编辑你的模型的一个项目并调用适当的Intent到视图模型中的模拟视图,这可以被定制为比这看起来更好。

//
//  ItemEditItemView.swift
//  Design Pattern
//
//  Created by Allan Garcia on 04/03/23.
//

import SwiftUI

struct ItemEditItemView: View {
        
    @ObservedObject var viewModel: ItemStorageViewModel
    
    @State var editingItem: Item
    
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    var body: some View {
        VStack {
            TextField("Edit text:", text: $editingItem.text)
                .padding()
            HStack {
                Button {
                    self.viewModel.save(editingItem)
                    
                    self.presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Save")
                        .padding()
                        .bold()
                }
                Button {
                    self.presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Cancel")
                        .padding()
                        .foregroundColor(.red)
                }
            }
            Spacer()
//            Text("Debug EditingText")
//                .foregroundColor(.red)
//                .bold()
//            Text("Editing text is: \(editingItem.text)")
        }
        .navigationTitle("Editing Item")
        .navigationViewStyle(.stack)
    }
}

struct ItemEditItemView_Previews: PreviewProvider {
    static var previews: some View {
        let vc = ItemStorageViewModel()
        
        ItemEditItemView(viewModel: vc, editingItem: vc.items.first!)
    }
}

希望这能为这里已经提到的答案增加一些价值。

相关问题