我正在开发一个 iOS 应用程序,它列出了 NavigationStack 内列表中的项目。当用户单击某个项目时,它会显示其详细信息,如果他单击“编辑”按钮,他可以编辑其值(在另一个视图上)。更新任何值都会刷新详细视图和列表上的值。
一切正常,除了当我在版本视图上杀死应用程序时,然后,在下次执行应用程序时,NavigationStack 正确恢复状态并且用户被带回版本视图,但如果我然后更改项目名称并单击“保存”,更改将被保存,但不会在详细信息视图和列表视图中更新。就像绑定被破坏一样,但如果我返回到详细信息视图或列表并再次手动导航到版本视图,则绑定会再次工作,并且任何编辑的值都会显示在详细信息和列表视图上。
下面是我的代码的简化:
@Observable class Item : Hashable, Equatable, Codable {
var name:String
init(_ name: String) {
self.name = name
}
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.name == rhs.name
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
@Observable class NavPathStore {
private let savePath = URL.documentsDirectory.appending(path: "SavedPathStore")
public var path = NavigationPath() {
didSet {
save()
}
}
init() {
if let data = try? Data(contentsOf: savePath) {
if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
path = NavigationPath(decoded)
return
}
}
}
func save() {
guard let representation = path.codable else { return }
do {
let data = try JSONEncoder().encode(representation)
try data.write(to: savePath)
} catch {
print("Failed to save navigation data")
}
}
}
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State var navigation = NavPathStore()
@State var items: [Item] = [Item("First"), Item("Second")]
var body: some View {
NavigationStack(path: $navigation.path) {
List {
ForEach(items, id:\.name) { item in
NavigationLink(value: Navigation_DetailView(item: item)) {
Text("Item \(item.name)")
}
}
}
.navigationDestination(for: Navigation_DetailView.self) { nav in
DetailView(item: nav.item)
}
}
}
}
struct Navigation_DetailView: Hashable, Equatable, Codable {
var item: Item
}
struct Navigation_EditView: Hashable, Equatable, Codable {}
struct DetailView: View {
let item: Item
var body: some View {
Text("Details for item \(item.name)")
.toolbar {
NavigationLink("Edit", value: Navigation_EditView())
}
.navigationDestination(for: Navigation_EditView.self) { nav in
EditView(item: self.item)
}
}
}
struct EditView : View {
@Environment(\.dismiss) private var dismiss
let item: Item
@State private var name: String
init(item: Item) {
self.item = item
self.name = item.name
}
var body: some View {
VStack {
TextField("Name", text: $name)
.textFieldStyle(.roundedBorder)
.padding()
Button("Save") {
item.name = self.name
dismiss()
}
}
.navigationTitle("Item Edition")
}
}
我怀疑问题与我调用 NavigationLinks 的方式或使用 NavPathStore 类存储导航的方式有关。
应该如何实现导航才能不破坏绑定?
编辑:我编辑了问题以提供最小的可重现示例。 编辑 2:再次编辑以更好地阐明如何重现问题。
您在
@State
中初始化了 EditView.init
。您应该将 name
初始化为某个常量字符串,然后将其设置为 onAppear
中所需的值。请参阅此讨论。
@State private var name: String = ""
// ...
.onAppear {
name = item.name
}
现在更改已在
DetailView
中更新,但 ContentView
仍会显示项目的初始名称。这是因为导航路径中的 Item
对象是与此处完全不同的对象:
@State var items: [Item] = [Item("First"), Item("Second")]
当您解码存储的导航路径时,您间接创建了一个Item
对象,该对象存储在
Navigation_DetailView
中。在
ContentView
中,由于上面的行,您进一步创建了另外 2 个
Item
对象。您实际上从未将这两个
Item
对象保存在文件中。您只保存导航路径上的所有
Item
,这显然不会同时保存这两个项目。您需要彻底改变设计。以下是如何执行此操作的概述:
Item
更改为 SavedItemsStore 中的
Item
对象。您可以通过匹配
id
来找到彼此对应的项目,其中
id
是您将添加到
Item
的新属性。或者您可以只匹配他们的
name
。
Item
。
@Observable class Item : Identifiable, Hashable, Equatable, Codable {
var name:String
let id: UUID
init(_ name: String, _ id: UUID = UUID()) {
self.name = name
self.id = id
}
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.name == rhs.name && lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(id)
}
}
@Observable class Storage {
private let savePath = URL.documentsDirectory.appending(path: "SavedPathStore")
private let itemsSavePath = URL.documentsDirectory.appending(path: "SavedItemsStore")
var path = [Navigation]()
var items = [Item]()
init() {
print(savePath)
let decoder = JSONDecoder()
if let data = try? Data(contentsOf: itemsSavePath) {
if let decoded = try? decoder.decode([Item].self, from: data) {
items = decoded
}
} else {
items = [Item("First"), Item("Second")]
}
if let data = try? Data(contentsOf: savePath) {
if let decoded = try? decoder.decode([Navigation].self, from: data) {
path = decoded.compactMap {
switch $0 {
case .detail(let item):
items.first { $0.id == item.id }.map { .detail($0) }
case .edit(let item):
items.first { $0.id == item.id }.map { .edit($0) }
}
}
}
}
}
func save() {
do {
let data = try JSONEncoder().encode(path)
try data.write(to: savePath)
let itemsData = try JSONEncoder().encode(items)
try itemsData.write(to: itemsSavePath)
} catch {
print("Failed to save data")
}
}
}
struct ContentView: View {
@State var storage = Storage()
@State var items: [Item] = []
var body: some View {
NavigationStack(path: $storage.path) {
List {
ForEach(items, id:\.name) { item in
NavigationLink(value: Navigation.detail(item)) {
Text("Item \(item.name)")
}
}
}
.navigationDestination(for: Navigation.self) { nav in
switch nav {
case .detail(let item):
DetailView(item: item)
case .edit(let item):
EditView(item: item)
}
}
}
.onAppear {
items = storage.items
}
.onChange(of: storage.path) {
storage.save()
}
}
}
enum Navigation: Hashable, Codable {
case detail(Item)
case edit(Item)
}
struct DetailView: View {
let item: Item
var body: some View {
Text("Details for item \(item.name)")
.toolbar {
NavigationLink("Edit", value: Navigation.edit(item))
}
}
}
struct EditView : View {
@Environment(\.dismiss) private var dismiss
let item: Item
@State private var name: String = ""
init(item: Item) {
self.item = item
}
var body: some View {
VStack {
TextField("Name", text: $name)
.textFieldStyle(.roundedBorder)
.padding()
Button("Save") {
item.name = self.name
dismiss()
}
}
.navigationTitle("Item Edition")
.onAppear {
name = item.name
}
}
}