SwiftUI 列表选择始终为零

时间:2021-02-09 22:31:23

标签: swift xcode macos 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())
    }
}

此视图为整个 NavigationView 创建侧边栏菜单。

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

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

用户设置如下所示:

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")
        }
    }
}

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

2 个答案:

答案 0 :(得分:4)

一些建议:

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

代码中的逻辑错误:

  1. 您正在使用特定 UUID 将 Week 存储在您的用户默认值中。但是,您在每次启动时动态重新生成 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())
    }
}
  1. UserSettings.init 后,如果之前保存过这些周,则会加载这些周(保证 ID 相同)
  2. $selectedWeek 上使用组合而不是 didSet。我只存储 ID,因为存储整个 Week 结构似乎没有意义,但您可以更改它
  3. 我为 NavigationLinkisActive 属性创建了动态绑定 -- 如果存储的 selectedWeekNavigationLink 的周 ID 相同,则链接处于活动状态.
  4. 除此之外,它与您的代码基本相同。我不在 selection 上使用 List,只是在 isActive
  5. 上使用 NavigationLink
  6. 如果您执行了 WeekonMove,我没有再次实现存储 onDelete,因此您必须实现它。

答案 1 :(得分:0)

遇到这样的情况,即多项目选择在 macOS 上不起作用。这是我认为正在发生的事情以及如何解决它并使其正常工作

背景

因此,在 macOS 上,嵌入在列表中的导航链接在详细视图中呈现其目的地(无论如何默认情况下)。例如

struct ContentView: View {
    let beatles = ["John", "Paul", "Ringo", "George", "Pete"]
    @State var listSelection = Set<String>()

    var body: some View {
        NavigationView {
            List(beatles, id: \.self, selection: $listSelection) { name in
                NavigationLink(name) {
                    Text("Some details about \(name)")
                }
            }
        }
    }
}

像这样渲染

enter image description here

问题

当使用 NavigationLinks 时,不可能在侧边栏中选择多个项目(至少从 Xcode 13 beta4 开始)。

...但如果只使用 Text 元素而没有任何 NavigationLink 嵌入,则效果很好。

发生了什么

详细视图一次只能显示一个 NavigationLink 视图,在代码中的某处(可能是 NavigationView)有一段代码通过踩踏多项选择并将其设置为强制执行该合规性零,例如

let selectionBinding = Binding {
    backingVal
} set: { newVal in
    guard newVal <= 1 else {
        backingVal = nil
        return
    }
    backingVal = newVal
}

据我所知,在这些情况下会发生什么并没有定义。对于某些视图,例如 TextField,它与它的原始数据源不同步(对于 more),而对于其他视图,因为在这里它尊重它。

解决方法/修复

要启用多项选择,请使用 Zstack 将 NavigationLinks 隐藏在基于文本的列表后面,并以编程方式驱动 NavigationLinks。例如

struct ContentView: View {
    let beatles = ["John", "Paul", "Ringo", "George", "Pete"]
    @State var listSelection = Set<String>()

    var detailShow: String? {
        return listSelection.count == 1 ? listSelection.first
            : listSelection.count > 1 ? "tooMany"
            : nil
    }

    var body: some View {
        NavigationView {
            /// Workaround for inability to select multiple NavigationLinks in a List on macOS
            ZStack {
                /// Hidden List with programatically driven NavigationLinks
                List {
                    ForEach(beatles, id: \.self) { name in
                        NavigationLink(
                            "",
                            tag: name,
                            selection: .constant(detailShow)
                        ) {
                            Text("Some details on \(name)")
                        }
                    }
                    NavigationLink(
                        "",
                        tag: "tooMany",
                        selection: .constant(detailShow)
                    ) {
                        Text("Too many selected")
                    }
                }
                .opacity(0.0)

                /// List that is selected from
                List(beatles, id: \.self, selection: $listSelection) { name in
                    Text(name)
                }
            }
        }
    }
}

玩得开心。