如何使用 SwiftUI 重置子视图状态变量?

Posted

技术标签:

【中文标题】如何使用 SwiftUI 重置子视图状态变量?【英文标题】:How to reset child view state variable with SwiftUI? 【发布时间】:2020-07-21 10:36:04 【问题描述】:

我确定这很愚蠢,但是当另一个状态发生变化时,应该如何重置子视图的状态值?

例如下面的代码显示了2个文件夹,分别有2个和3个项目,可以编辑。

如果您选择第二个文件夹 (Work) 及其第三个项目 (Peter),然后选择第一个文件夹 (Home),则应用程序会崩溃,因为 selectedItemIndex 超出范围。

我尝试在视图初始化时“重置”状态值,但似乎像这样更改状态会触发“运行时:SwiftUI:在视图更新期间修改状态,这将导致未定义的行为。”警告。

init(items: Binding<[Item]>)   
    self._items = items  
    self._selectedItemIndex = State(wrappedValue: 0)  
  

这样做的正确方法是什么?谢谢!

代码如下:

AppDelegate.swift

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate 

    var window: NSWindow!


    func applicationDidFinishLaunching(_ aNotification: Notification) 
        // Create the SwiftUI view that provides the window contents.
        let store = ItemStore()
        let contentView = ContentView(store: store)

        // Create the window and set the content view. 
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    

    func applicationWillTerminate(_ aNotification: Notification) 
        // Insert code here to tear down your application
    

ContentView.swift

import SwiftUI

final class ItemStore: ObservableObject 
    @Published var data: [Folder] = [Folder(name: "Home",
                                            items: [Item(name: "Mark"), Item(name: "Vincent")]),
                                     Folder(name: "Work",
                                            items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]


struct Folder: Identifiable 
    var id = UUID()
    var name: String
    var items: [Item]


struct Item: Identifiable 
    static func == (lhs: Item, rhs: Item) -> Bool 
        return true
    
    
    var id = UUID()
    var name: String
    var content = Date().description
    
    init(name: String) 
        self.name = name
    


struct ContentView: View 
    @ObservedObject var store: ItemStore
    
    @State var selectedFolderIndex: Int?
    
    var body: some View 
        HSplitView 
            // FOLDERS
            List(selection: $selectedFolderIndex) 
                Section(header: Text("Groups")) 
                    ForEach(store.data.indexed(), id: \.1.id)  index, folder in
                        Text(folder.name).tag(index)
                    
                .collapsible(false)
            
            .listStyle(SidebarListStyle())
            
            // ITEMS
            if selectedFolderIndex != nil 
                ItemsView(items: $store.data[selectedFolderIndex!].items)
            
        
        .frame(minWidth: 800, maxWidth: .infinity, maxHeight: .infinity)
    



struct ItemsView: View 
    @Binding var items: [Item]
    @State var selectedItemIndex: Int?
    
    var body: some View 
        HSplitView 
            List(selection: $selectedItemIndex) 
                ForEach(items.indexed(), id: \.1.id)  index, item in
                    Text(item.name).tag(index)
                
            
            .frame(width: 300)
            
            if selectedItemIndex != nil 
                DetailView(item: $items[selectedItemIndex!])
                .padding()
                .frame(minWidth: 200, maxHeight: .infinity)
            
        
    
    
    init(items: Binding<[Item]>) 
        self._items = items
        self._selectedItemIndex = State(wrappedValue: 0)
    



struct DetailView: View 
    @Binding var item: Item
    
    var body: some View 
        VStack 
            TextField("", text: $item.name)
        
    


// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/

struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection 
    typealias Index = Base.Index
    typealias Element = (index: Index, element: Base.Element)

    let base: Base

    var startIndex: Index  base.startIndex 

    var endIndex: Index  base.endIndex 

    func index(after i: Index) -> Index 
        base.index(after: i)
    

    func index(before i: Index) -> Index 
        base.index(before: i)
    

    func index(_ i: Index, offsetBy distance: Int) -> Index 
        base.index(i, offsetBy: distance)
    

    subscript(position: Index) -> Element 
        (index: position, element: base[position])
    


extension RandomAccessCollection 
    func indexed() -> IndexedCollection<Self> 
        IndexedCollection(base: self)
    

【问题讨论】:

这能回答你的问题吗? Handling derived state in SwiftUI 我尝试将您的代码粘贴到 Xcode 11.4 中,但出现了很多编译器错误,因此很难进行试验。 IndexedCollection 上应该有类型参数吗?有些东西可能没有成为您粘贴到问题中的内容。例如,我看到Binding&lt;[Item]&gt; 变成了Binding 【参考方案1】:

感谢 @jordanpittman 提出修复建议:

ItemsView(items: $store.data[selectedFolderIndex!].items).id(selectedRowIndex)

来源:https://swiftui-lab.com/swiftui-id

【讨论】:

【参考方案2】:

ContentView.swift 的完全可玩示例草稿。在两种编辑模式(非活动/活动行选择)中使用它并适应您的需求。

import SwiftUI

struct ItemStore 
    var data: [Folder] = [Folder(name: "Home", items: [Item(name: "Mark"), Item(name: "Vincent")]),
                          Folder(name: "Work", items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]


struct Folder: Identifiable 
    var id = UUID()
    var name: String
    var items: [Item]


struct Item: Identifiable 

    var id = UUID()
    var name: String
    var content = Date().description



struct ContentView: View 
    @State var store: ItemStore

    @State var selectedFolderIndex: Int? = 0
    @State private var editMode = EditMode.inactive
    var body: some View 
        NavigationView 
            VStack 
                // FOLDERS

                List(selection: $selectedFolderIndex) 
                    Section(header: Text("Groups")) 
                        ForEach(store.data.indexed(), id: \.1.id)  index, folder in
                            HStack 
                                Text(folder.name).tag(index)
                                Spacer()
                            
                                .background(Color.white) //make the whole row tapable, not just the text
                            .frame(maxWidth: .infinity)
                            .multilineTextAlignment(.leading)
                            .onTapGesture 
                                self.selectedFolderIndex = index
                            
                        .onDelete(perform: delete)
                    
                
                .listStyle(GroupedListStyle())
                .id(selectedFolderIndex)
                // ITEMS
                if selectedFolderIndex != nil && (($store.data.wrappedValue.startIndex..<$store.data.wrappedValue.endIndex).contains(selectedFolderIndex!) )
                    ItemsView(items: $store.data[selectedFolderIndex!].items)
                
            
            .navigationBarTitle("Title")
            .navigationBarItems(trailing: EditButton())
            .environment(\.editMode, $editMode)
        
    

    func delete(at offsets: IndexSet) 
        $store.wrappedValue.data.remove(atOffsets: offsets) // Note projected value! `store.data.remove() will not modify SwiftUI on changes and it will crash because of invalid index.
    



struct ItemsView: View 
    @Binding var items: [Item]
    @State var selectedDetailIndex: Int?
    var body: some View 
        HStack 
            List(selection: $selectedDetailIndex) 
                ForEach(items.indexed(), id: \.1.id)  index, item in
                    Text(item.name).tag(index)
                    .onTapGesture 
                        self.selectedDetailIndex = index
                    
                

            

            if selectedDetailIndex != nil && (($items.wrappedValue.startIndex..<$items.wrappedValue.endIndex).contains(selectedDetailIndex!) )    
                DetailView(item: $items[selectedDetailIndex!])
                    .padding()
            
        

    




struct DetailView: View 
    @Binding var item: Item

    var body: some View 
        VStack 
            TextField("", text: $item.name)
        
    


// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/

struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection 
    typealias Index = Base.Index
    typealias Element = (index: Index, element: Base.Element)

    let base: Base

    var startIndex: Index  base.startIndex 

    var endIndex: Index  base.endIndex 

    func index(after i: Index) -> Index 
        base.index(after: i)
    

    func index(before i: Index) -> Index 
        base.index(before: i)
    

    func index(_ i: Index, offsetBy distance: Int) -> Index 
        base.index(i, offsetBy: distance)
    

    subscript(position: Index) -> Element 
        (index: position, element: base[position])
    


extension RandomAccessCollection 
    func indexed() -> IndexedCollection<Self> 
        IndexedCollection(base: self)
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView(store: ItemStore())
    

【讨论】:

以上是关于如何使用 SwiftUI 重置子视图状态变量?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 swiftui 中为子视图设置私有状态变量?

SwiftUI - 更新父视图的状态时如何保留子视图的状态?

如何在 SwiftUI 中禁用子视图上的特定状态更改动画

SwiftUI List 在任何视图更改时重置滚动

SwiftUI:如何更改子视图?

SwiftUI巧妙利用View可识别性完成复杂状态界面的重置