SwiftUI 列表选择始终为零

Cra*_*vid 3 macos xcode swift swiftui

在我的 macOS 应用程序项目中,我有一个 NavigationLinks 的 SwiftUI 列表视图,它使用来自项目数组的 foreach 循环构建:

struct MenuView: View {
    @EnvironmentObject var settings: UserSettings
    
    var body: some View {
        List(selection: $settings.selectedWeek) {
            ForEach(settings.weeks) { week in
                NavigationLink(
                    destination: WeekView(week: week)
                        .environmentObject(settings)
                    tag: week,
                    selection: $settings.selectedWeek)
                {
                    Image(systemName: "circle")
                    Text("\(week.name)")
                }
            }
            .onDelete { set in
                settings.weeks.remove(atOffsets: set)
            }
            .onMove { set, i in
                settings.weeks.move(fromOffsets: set, toOffset: i)
            }
        }
        .navigationTitle("Weekplans")
        .listStyle(SidebarListStyle())
    }
}
Run Code Online (Sandbox Code Playgroud)

该视图为整个导航视图创建侧边栏菜单。

在此列表视图中,我想将选择机制与 NavigationLink 中的标签一起使用。Week是一个自定义模型类:

struct Week: Identifiable, Hashable, Equatable {
    var id = UUID()
    var days: [Day] = []
    var name: String
}
Run Code Online (Sandbox Code Playgroud)

用户设置如下所示:

class UserSettings: ObservableObject {
    @Published var weeks: [Week] = [
        Week(name: "test week 1"),
        Week(name: "foobar"),
        Week(name: "hello world")
    ]
    
    @Published var selectedWeek: Week? = UserDefaults.standard.object(forKey: "week.selected") as? Week {
        didSet {
            var a = oldValue
            var b = selectedWeek
            UserDefaults.standard.set(selectedWeek, forKey: "week.selected")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我的目标是直接将列表选择的值存储在 UserDefaults 中。didSet 属性被执行,但变量始终为零。由于某种原因,所选列表值无法存储在已发布/可绑定变量中。为什么 $settings.selectedWeek 总是为零?

jn_*_*pdx 5

一些建议:

  1. SwiftUI(特别是在 macOS 上)的某些行为是不可靠/不可预测的List。其中之一是selection——有很多东西要么完全不起作用,要么充其量只是稍微有问题,但在同等的 iOS 代码中却可以正常工作。好消息是,NavigationLink它的isActive工作方式就像列表中的选择一样——我将在我的示例中使用它。
  2. @Published didSet 可能在某些情况下有效,但这是您不应该依赖的另一件事。属性包装器方面使其行为与可能的情况不同(搜索“@Published didSet”以查看处理它的合理数量的问题)。好消息是,您可以使用组合来重新创建行为并以更安全/更可靠的方式进行。

代码中存在逻辑错误:

  1. 您正在Week使用特定的 UUID 存储用户默认值。但是,您可以在每次启动时动态重新生成数组weeks,从而保证它们的 UUID 不同。如果您想在整个启动过程中保留一周的时间以及您的选择,则需要将它们存储起来。

这是一个工作示例,我将在下面指出一些事项:

import SwiftUI
import Combine

struct ContentView : View {
    var body: some View {
        NavigationView {
            MenuView().environmentObject(UserSettings())
        }
    }
}

class UserSettings: ObservableObject {
    @Published var weeks: [Week] = []
    
    @Published var selectedWeek: UUID? = nil
    
    private var cancellable : AnyCancellable?
    private var initialItems = [
        Week(name: "test week 1"),
        Week(name: "foobar"),
        Week(name: "hello world")
    ]
    
    init() {
        let decoder = PropertyListDecoder()
                
        if let data = UserDefaults.standard.data(forKey: "weeks") {
            weeks = (try? decoder.decode([Week].self, from: data)) ?? initialItems
        } else {
            weeks = initialItems
        }
        
        if let prevValue = UserDefaults.standard.string(forKey: "week.selected.id") {
            selectedWeek = UUID(uuidString: prevValue)
            print("Set selection to: \(prevValue)")
        }
        cancellable = $selectedWeek.sink {
            if let id = $0?.uuidString {
                UserDefaults.standard.set(id, forKey: "week.selected.id")
                let encoder = PropertyListEncoder()
                if let encoded = try? encoder.encode(self.weeks) {
                    UserDefaults.standard.set(encoded, forKey: "weeks")
                }
            }
        }
    }
    
    func selectionBindingForId(id: UUID) -> Binding<Bool> {
        Binding<Bool> { () -> Bool in
            self.selectedWeek == id
        } set: { (newValue) in
            if newValue {
                self.selectedWeek = id
            }
        }

    }
}

//Unknown what you have in here
struct Day : Equatable, Hashable, Codable {
    
}

struct Week: Identifiable, Hashable, Equatable, Codable {
    var id = UUID()
    var days: [Day] = []
    var name: String
}


struct WeekView : View {
    var week : Week
    
    var body: some View {
        Text("Week: \(week.name)")
    }
}

struct MenuView: View {
    @EnvironmentObject var settings: UserSettings
    
    var body: some View {
        List {
            ForEach(settings.weeks) { week in
                NavigationLink(
                    destination: WeekView(week: week)
                        .environmentObject(settings),
                    isActive: settings.selectionBindingForId(id: week.id)
                )
                {
                    Image(systemName: "circle")
                    Text("\(week.name)")
                }
            }
            .onDelete { set in
                settings.weeks.remove(atOffsets: set)
            }
            .onMove { set, i in
                settings.weeks.move(fromOffsets: set, toOffset: i)
            }
        }
        .navigationTitle("Weekplans")
        .listStyle(SidebarListStyle())
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 如果之前保存过,则在UserSettings.init几周内加载(保证相同的 ID)
  2. 使用“组合”$selectedWeek代替“ didSet.”。我只存储 ID,因为存储整个Week结构似乎有点毫无意义,但你可以改变它
  3. NavigationLink我为s属性创建了一个动态绑定- 如果存储的内容与 s 的周 ID相同,isActive则链接处于活动状态。selectedWeekNavigationLink
  4. 除此之外,它与您的代码基本相同。我不使用selectionon List,只isActive使用 onNavigationLink
  5. Week如果您执行了onMoveor ,我没有实现再次存储onDelete,因此您必须实现它。