NavigationSplitView 和 @State 的奇怪行为

问题描述 投票:0回答:3

我的应用程序中有一个 NavigationSplitView,我的详细视图中有一个在 init 中创建的 @State 变量。

当我从侧边栏中选择某些内容并且详细信息视图呈现时,起初一切看起来都不错。但是当我在侧边栏上选择不同的项目时,@state 变量的内容不会重新创建。

使用调试器,我可以看到每次在侧边栏中选择一个新项目时都会调用详细视图的 init,并且可以看到创建了 @State 变量。但当它实际渲染时,@State 变量仍然包含先前选择的值。

我已将这个问题简化为一个测试用例,我将粘贴在下面。详细信息视图中的顶部文本是从侧边栏传入的变量,第二行文本由@State变量生成。预期的行为是,如果我选择“一”,详细信息视图将显示“一”和“名称是一”。如果我选择“二”,详细视图将显示“二”和“名称是二”。

相反,如果我先选择“one”,它会正确显示。但是当我选择“二”时,它显示“二”和“名字是一”。

请注意,如果我选择“二”作为启动应用程序后执行的第一件事,它会正确显示“二”和“名称是二”,但是当我接下来单击“一”时,它将显示“一”和“名字是两个”。所以状态变量被设置一次,然后就不会再改变,

这是示例代码和屏幕截图:

import SwiftUI

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

struct ContentView: View {
    
    @State private var selectedItem: Item.ID? = nil
    
    private let items = [Item(name: "one"), Item(name: "two"), Item(name: "three")]
    
    func itemForID(_ id: UUID?) -> Item? {
        guard let itemID = id else { return nil }
        return items.first(where: { item in
            item.id == itemID
        })
    }
    
    var body: some View {
        NavigationSplitView{
            List(selection: $selectedItem) {
                ForEach(items) { item in
                    Text(item.name)
                        .tag(item.id)
                }
            }
        } detail: {
            if let name = itemForID(selectedItem)?.name {
                DetailView(name: name)
            } else {
                Text("Select an item")
            }
        }
    }
}

struct DetailView: View {
    
    @State var detailItem: DetailItem
    
    var name: String
    
    init(name: String) {
        self.name = name
        _detailItem = State(wrappedValue: DetailItem(name: name))
    }
    
    var body: some View {
        VStack {
            Text(name)
            Text(detailItem.computedText)
        }
    }
}

struct DetailItem {
    let name: String
    
    var computedText: String {
        return "The name is \(name)"
    }
}

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

First selection, expected result Second selection, expected to see

swiftui
3个回答
2
投票

这与NavigationSplitView无关,而是与你如何初始化@State属性有关。

根据@State上的Apple文档(https://developer.apple.com/documentation/swiftui/state):

不要在视图层次结构中实例化视图的位置初始化视图的状态属性,因为这可能与 SwiftUI 提供的存储管理发生冲突。

以及 init(wrappedValue:) 的文档 (https://developer.apple.com/documentation/swiftui/state/wrappedvalue):

不要直接调用此初始化程序。相反,使用 State 属性声明一个属性,并提供一个初始值:

@State private var isPlaying: Bool = false

根据我的理解,如果你强制在视图初始化中初始化状态,它将在视图的整个生命周期中持续存在,并且它的后续更改不会对视图产生任何影响。

Apple文档中推荐的方法是在父视图中创建结构体并将其传递给子视图,如果需要更改子视图中的结构体,请使用@Binding来允许读写访问。

如果你想忽略文档并强制它工作,你可以给你的

id
一个
DetailView
,强制它在项目 ID 更改时刷新视图:

var body: some View {
    NavigationSplitView{
        List(selection: $selectedItem) {
            ForEach(items) { item in
                Text(item.name)
                    .tag(item.id)
            }
        }
    } detail: {
        if let name = itemForID(selectedItem)?.name {
            DetailView(name: name)
                .id(selectedItem) // Fresh @State for every item
        } else {
            Text("Select an item")
        }
    }
}

2
投票

问题。将detailItem 作为@State 的目的是什么?如果删除@State,这个测试用例就可以工作。

compatedText 的方式会随着时间而改变吗?

struct DetailView: View {
    
//    @State var detailItem: DetailItem
    var detailItem: DetailItem
    
    var name: String
    
    init(name: String) {
        self.name = name
//        _detailItem = State(wrappedValue: DetailItem(name: name))
        detailItem = DetailItem(name: name)
    }
    
    var body: some View {
        VStack {
            Text(name)
            Text(detailItem.computedText)
        }
    }
}

1
投票

你的 Item 结构不好,如果名称是唯一的,它应该是:

struct Item: Identifiable {
    var id: String { name }
    let name: String
}

否则:

struct Item: Identifiable {
    let id = UUID()
    let name: String
}
© www.soinside.com 2019 - 2024. All rights reserved.