【问题标题】:SwiftUI List selection always nilSwiftUI 列表选择始终为零
【发布时间】:2021-02-09 22:31:23
【问题描述】:

在我的 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
}

UserSettings 看起来像这样:

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 属性被执行,但变量始终为零。由于某种原因,所选列表值无法存储在已发布/可绑定变量中。 为什么 $settings.selectedWeek 总是为零?

【问题讨论】:

    标签: swift xcode macos swiftui


    【解决方案1】:

    几个建议:

    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. 我为NavigationLinks isActive 属性创建了一个动态绑定——如果存储的selectedWeekNavigationLink 的周ID 相同,则该链接处于活动状态。
    4. 除此之外,它与您的代码基本相同。我不在List 上使用selection,只是在NavigationLink 上使用isActive
    5. 如果您执行了onMoveonDelete,我没有再次执行存储Week,因此您必须执行该操作。

    【讨论】:

    • 谢谢,我对 SwiftUI 还没有那么丰富的经验。现在我知道存在错误以及如何搜索它,我在互联网上找到了很多相同的陈述。但在我刚刚找到 iOS 的示例等之前,我尝试过它的方式或类似的方式。尽管如此,由于某种原因,我的问题在我在这里发布后 1 分钟被否决了。在 SO 上提问太烦人了。人们请理解,可能已经有类似的问题或解释,但他们可能会例如。适用于 iOS,而 macOS 出现另一个问题。提问不是坏事……
    • 我在互联网上几乎找不到适用于 macOS 或多平台应用程序项目的 SwiftUI 示例、代码和资料。谷歌总是显示像 SO 和其他人一样的 ol 页面,它们要么为 iOS 做这些事情,要么你敢问一个非常可靠的问题。
    • 您能否进一步解释一下这个例子中cancellable的用法?我实现了唯一的 selectionBindingForId 并且它现在也可以正常工作。可取消的目的是什么?
    • 当然 — 当您使用 Combine 发布者时,您必须保留对它的引用。否则,它会立即超出范围,并且您不会继续获得更新。 cancellable 可以让你将它保存在内存中,获取更新,如果你需要取消它,如果你不手动取消它,它会在 deinit 时被取消,这样你就不会导致内存泄漏。
    • 谢谢,我将不得不仔细研究 Combine 和 cancellable,但这对我帮助很大。
    【解决方案2】:

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

    背景

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

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

    这样渲染

    问题

    使用 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 来解决这个问题,这可行,但过于复杂。

    相反,macOS 的惯用选项,如 Lost Moa blog post 所示,根本不使用 NaviationLink

    事实证明,只需将侧边栏和详细视图彼此相邻放置并使用绑定就足以让NavigationView 了解如何渲染并阻止它踩到多个项目选择。示例如下:

    struct ContentView: View {
        let beatles = ["John", "Paul", "Ringo", "George", "Pete"]
        @State var listSelection: Set<String> = []
    
        var body: some View {
            NavigationView {
                SideBar(items: beatles, selection: $listSelection)
                Detail(ids: listSelection)
            }
        }
    
        struct SideBar: View {
            let items: Array<String>
            @Binding var selection: Set<String>
    
            var body: some View {
                List(items, id: \.self, selection: $selection) { name in
                    Text(name)
                }
            }
        }
    
        struct Detail: View {
            let ids: Set<String>
    
            var detailsMsg: String {
                ids.count == 1 ? "Would show details for \(ids.first)"
                    : ids.count > 1 ? "Too many items selected"
                    : "Nothing selected"
            }
    
            var body: some View {
                Text(detailsMsg)
            }
        }
    }
    

    玩得开心。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-02
      • 2012-01-16
      相关资源
      最近更新 更多