SwiftUI 学习:疑似 List Selection 的 Bug 的案例

最近忙完了几件事,圣诞假期也临近,终于有时间好好学习并折腾一下 SwiftUI。在学习和 SwiftUI 的过程中,我遇到并发现了不少 Bug,同时对它的局限性也有了更多的了解。本文即是以 List Selection 的一处 Bug 为例来展开。

12 月 22 日 20:30 分更新

在推友 @siuying 的思路的指导下,我终于发现了这个疑似 Bug 的问题,真的是我对 SwiftUI 的不熟悉索导致的。

NavigationLink 是一个动作,而不是一个状态,根据他提供的代码,macOS 下已经成功运行正常,不过 iOS 依然没办法达成选中的效果,还需要继续探索。

demo 目标

以下是用 SwiftUI 实现的一个非常简单的 demo,它用 List 排列几个 Task 类型,然后有一个Random Select 的按钮,可以随机选中列表里的项目。

这个 demo 在 macOS 和 iOS 下几乎是一样的代码,iOS 下只要注释掉 .listStyle(SidebarListStyle()) 这行就可以跑起来。

先放截图:

macOS 下的截图:

SwiftUI Test Selection-0.png

iPad 下的截图:

SwiftUI Test Selection-1.png

源代码:

import SwiftUI

struct Task: Codable, Identifiable, Hashable {

    var id: Int
    var name: String

}

struct ContentView: View {
    @State private var tasks: [Task] = [
        Task(id: 0, name: "Fix #1"),
        Task(id: 1, name: "Fix #2"),
        Task(id: 2, name: "Fix #3"),
        Task(id: 3, name: "Fix #4"),
        Task(id: 4, name: "Fix #5")
    ]
    @State private var selection: Task?

    var body: some View {
        NavigationView {
            List(selection: $selection) {
                ForEach(tasks, id: \.self) { task in
                    NavigationLink(destination: VStack {
                        Text(task.name)
                        Button("Random Select") {
                            self.selection = self.tasks.randomElement()
                        }
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)) {
                        VStack {
                            Text(task.name)
                        }
                    }
                }
            }
            .listStyle(SidebarListStyle())
            VStack {
                Text("Please select one task")
                Button("Random Select") {
                    self.selection = self.tasks.randomElement()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }

}

发现问题和 Bug 分析

但就是这样的一个小 demo,也有很严重的 Bug。先说 macOS 端:

macOS 版本,跑起来以后,直接点 Random Select,是可以正常工作的,你会发现 List 左侧选中的项目,在随机跳动。

但是,一旦你手动选择过一个项目后,再点 Random Select,就不管用了。

而 iPadOS 下面,无论怎么点,都是不管用的…

昨天遇到这个问题后,我一直在想,我哪里做错了,是不是有什么条件未考虑到。今天我再想想,抛开 SwiftUI 还不成熟的话题,这就是 SwiftUI 的局限性,或许,我要做的任务,根本就不应该来用 SwiftUI 来完成。

SwiftUI 的局限性是什么?

SwiftUI 是什么?它是一套跨平台的框架,我上述代码,也完美的验证了同样的代码,可以跑在 macOS 和 iOS 上,并且都非常 Native——除了行为没有达到我的预期。

作为同时写过 macOS 和 iOS App 的开发者,我思考这个问题出现的原因,首先就是 AppKit 和 UIKit 下 API 和预期行为的大不同。

假如我们分别用平台对应的 API 去达到「选择列表行」的效果的话,那么要用到的 API 分别是:

  • macOS:NSTableView 的 func selectRowIndexes(_ indexes: IndexSet, byExtendingSelection extend: Bool)
  • iOS:UITableView 的 func selectRow(at indexPath: IndexPath?, animated: Bool, scrollPosition: UITableView.ScrollPosition)

这两个 API,不仅名字不一样,参数数量不一样,连参数类型都不一样。通过各种排列组合,有太多调用方式和结果了。

我不知道 SwiftUI 底层是怎么实现的,但是据目前了解到的信息,除了底层绘制的相关 View,大多数 SwiftUI 的控件,都是去调用对应平台的原生控件的,就像 List 在 macOS 下是 NSTableView,在 iOS 下是 UITableView。

于是,要达到预期的 List 选中(selection)的效果,我可以想象 SwiftUI 需要处理大量的逻辑,来使这个功能工作——根据目前的情况,一个小 demo 就暴露了 SwiftUI 目前的问题和不足。也就是我说的局限型。

SwiftUI 目前的使用场景

根据我有限的对 SwiftUI 的了解和实践,我觉得当前的 SwiftUI 适合的应用场景是:

  • 没有复杂逻辑的使用原生界面的列表型交互;
  • 需要高度自定义的、使用自定义绘制和动画的界面;

尽管 SwiftUI 没有达成我简单的「随机选择」这么一个需求(我希望是我代码的问题),但是我依然相信 SwiftUI 会在特定场景下达到生产力的极大提升,只是目前真的很不成熟,Xcode 也比较让人失望。

接下去能期待的一件事情,就是 Swift 5.2 的发布,它将极大的改进 SwiftUI 在编码过程中的错误提示。希望能早点发布。

欢迎使用图拉鼎和他的团队开发的作品

One Switch - 多功能开关工具

常驻 macOS 菜单栏的开关工具,可以快速开关 AirPods、睡眠模式、切换黑暗模式等。

3 Comments

最近在自学 Apple 生态开发,面对 SwiftUI 进退两难了。看来目前还是只做了解,等待 SwiftUI 慢慢成长,就像 Swift,也许时间可以治愈一切,哈哈。

图大,可以告知您博客所用的技术栈是什么嘛?最近想要自己写个博客,而不是用现成的博客平台。

tualatrix

可以慢慢学 SwiftUI,做点简单的东西,不一定要做正式的 App 才上。

我的 Blog 主要是 Django 写的。

leox

你好 我正在开发一款Mac软件,使用到和您文中相同的 NavigationView { List { NavigationLink 的嵌套方式(类似Finder)。我想要知道当前选中的是哪个cell,请问博主知道如何获取吗?(即OC中didselectedRow:)onChange、selection暂时未能实现

Leave a Comment