List 还是 LazyVStack:SwiftUI 中的惰性容器选择

发表于

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

在 SwiftUI 的世界里,ListLazyVStack 作为两大核心惰性容器,为开发者展示大量数据提供了强大的支持。然而,它们在某些场景下表现相似,常常让开发者在选择时感到困惑。本文旨在剖析这两个组件的特点、优势,以帮助你更好地作出选择。

注意:本文中提到的 LazyVStack 主要指代 ScrollViewLazyVStack 的组合使用,通常还会配合 ForEach 来动态提供数据。值得一提的是,本文讨论的 LazyVStack 特性大多也适用于 LazyHStack,部分适用于其他 Lazy 系列容器。我们将聚焦于宏观层面的比较和探讨,而非具体的 API 使用细节。

底层实现:不同的起源,迥异的架构

SwiftUI 的演进历程中,ListLazyVStack 扮演着不同的角色并拥有不同的定位。List 作为 SwiftUI 首个版本中的元老级惰性容器,不仅是当时唯一的官方惰性组件,还为后续众多惰性容器的数据加载流程奠定了基调。相比之下,LazyVStack 是在第二年才随其他惰性容器(如 LazyHStackLazyVGridLazyHGrid 等)一同亮相。尽管两者在某些场景下表现相似,但它们的底层架构却大相径庭。

List 本质上是苹果对 UIKit/AppKit 组件的巧妙封装。在 iOS 13 到 iOS 15 期间,它的底层依托于 UITableView;而从 iOS 16 开始,其实现基础转向了更为灵活的 UICollectionView。与之形成鲜明对比的是,LazyVStack 及其他 Lazy+ 系列容器均为 SwiftUI 的原生实现,它们的底层并不依赖于任何特定的 UIKit/AppKit 组件。

这种根本性的实现差异,不仅仅是技术细节上的区别,更是导致了两者在多个关键方面表现各异的根源。无论是性能表现、功能丰富度、定制灵活性,还是布局逻辑,ListLazyVStack 都因其独特的底层架构而呈现出截然不同的特性。理解这一点,对于开发者在选择和使用这两个组件时至关重要。

风格化与预设:功能丰富 vs. 简约灵活

LazyVStack 与其亲戚 VStack 一样,专注于布局的本质功能。作为 SwiftUI 中的纯粹布局容器,它严格遵循预设的布局规则,将子视图有序排列,而不会自作主张地添加额外效果。这种简约设计为开发者提供了最大的自由度。

相比之下,List 的设计哲学截然不同。它不仅是一个容器,更是一个功能丰富的 UI 组件。List 自带多种预设视觉模板,开发者可通过 listStyle 轻松切换不同风格。但 List 的优势不仅限于视觉效果,它还提供了一系列独特的交互能力:

  • 滑动操作(swipeActions)专属于 List 子视图
  • ForEachonDeleteonMove 功能仅在 List 中生效
  • 内置的编辑模式支持,让基于手势的项目重排成为可能,这在 SwiftUI 中独一无二

对于需要滑动菜单、类系统应用的删除和移动编辑功能的场景,List 无疑是最佳选择。它不仅代码简洁,还针对苹果全生态系统做了深度优化。此外,List 丰富的构造方法(包括与 ForEach 和数据绑定的集成)大大提升了开发的效率,尤其对于初学者或原型开发。

然而,List 的预设功能也带来了一定的局限性。尽管随着 SwiftUI 的更新,List 获得了更多预设模板,但截至 iOS 18,Apple 仍未开放 List 样式的完全定制能力。相比之下,LazyVStack 如同一张白纸,虽然没有预设模式,但为开发者的创意提供了无限可能。

总的来说,Apple 对这两个组件的定位截然不同:List 是一个具备默认风格和行为的多功能容器,而 LazyVStack 则是一个纯粹、灵活的布局工具。

布局与自定义:灵活性与约束的平衡

LazyVStack 作为 SwiftUI 的惰性容器,在布局方面有其独特之处,尤其是在处理子视图高度时。与 VStack 不同,LazyVStack 在子视图未指定明确高度时,会采用子视图的理想尺寸。这一特性在实际应用中体现得非常明显:

Swift
struct ContentView: View {
  var body: some View {
    LazyVStack {
      Rectangle()
    }
  }
}

上述代码中,Rectangle 只会呈现为高度为 10 的矩形(Shape 的默认理想尺寸),而非在 VStack 中会出现的填满可用空间的矩形。

这种尺寸确定逻辑与 ScrollView 在可滚动方向上的布局方式一致,即使嵌套在 VStack 中,Rectangle 仍然保持 10 的高度:

Swift
ScrollView {
  VStack {
    Rectangle()
  }
}

深入了解 SwiftUI 布局尺寸确定机制,推荐阅读 SwiftUI 布局 —— 尺寸 一文。

LazyVStack 作为纯粹的布局容器,为开发者提供了极大的自由度,能够更精确地还原设计样式。相比之下,List 的呈现受多方面因素影响,包括选定的风格、所处的容器环境以及运行的系统平台。开发者对 List 的样式干预能力较为有限,其布局也更倾向于模式化。

调整 List 的外观和行为主要依赖于 Apple 提供的专用视图修饰器。虽然这些修饰器随 SwiftUI 版本更新而不断丰富,但在较早版本的 SwiftUI 中,某些复杂布局可能难以实现或需要采用变通方法。

由于底层实现的差异,List 在处理行高动态变化时存在局限性。例如:

Swift
struct ContentView: View {
  @State var high = false
  var body: some View {
    List{
      Toggle(isOn: $high.animation()){}
      Rectangle()
        .frame(height:high ? 200 : 100)
    }
  }
}

list-row-animation-issue

这种动态高度变化的场景,建议尽量避免在 List 中使用。

此外,List 对子视图(行)的转场动画类型也有所限制。对于需要特殊动画和转场效果的场景,LazyVStack 往往能提供更大的灵活性和更好的表现。

与其他容器的协同:上下文感知的优势

尽管一些开发者对 List 的实现颇有微词,认为它无法完全满足特定需求,进而考虑通过自行包装 UIKit/AppKit 组件来创建更贴合需求的替代品。这种做法虽然在某些情况下确实能够更好地满足个性化需求,但我们也不应忽视苹果为使 List 适配多平台和特殊需求所付出的巨大努力。

SwiftUI 中有一个尚未公开的重要机制——上下文感知能力。官方容器和组件能够智能感知自身所处的环境,并根据不同的上下文自动调整呈现样式。这一独特能力是 List 相较于 LazyVStack 的显著优势之一。

List 与导航容器的配合堪称完美:

  1. 支持侧边栏(Sidebar)等特殊显示模式
  2. 导航容器对 List 绑定的数据源和行标签提供了出色的支持
  3. 某些组件(如 NavigationLink)在 List 中呈现独特样式

这些特性不仅大幅减少了开发工作量,还能实现 LazyVStack 难以企及的交互体验。

想深入了解 List 与导航容器的协作,推荐阅读 SwiftUI 4.0 的全新导航系统在 SwiftUI 中创建自适应的程序化导航方案

然而,这种上下文感知和默认行为有时也会带来一些挑战:

  • 默认情况下,List 只会响应行视图中的一个 Button 元素的操作(可通过调整 buttonStyle 解决)
  • 在早期 SwiftUI 版本中,由于缺乏完善的程序化导航能力,控制 NavigationLink 样式颇具挑战性

总而言之,当开发者希望充分利用系统组件的自动感知能力,打造出与系统风格一致的界面时,List 无疑是更优秀的选择。它不仅能为用户提供熟悉的交互体验,还能让开发者在不同平台间实现更高的代码复用率。

滚动控制:原生与变通

近期 SwiftUI 的版本更新中,苹果大幅增强了滚动控制能力,引入了一系列新 API。然而,这些新功能主要针对 ScrollViewList 在这方面的发展相对滞后。目前,官方为 List 提供的滚动控制手段仍局限于 ScrollViewReader

尽管如此,List 的底层实现基于成熟的 UIKit 组件,这为开发者提供了一条变通之路。通过使用如 SwiftUI-Introspect 等第三方库,开发者可以直接访问底层 UIKit 组件的 API,从而实现更多的控制手段。这种方法不仅适用于滚动控制,还能用于自定义显示样式。

即便如此,从 iOS 17 开始,ScrollViewLazyVStack 的组合在滚动控制方面的能力和便利性已经远超 List。考虑到 LazyVStack 在布局灵活性和动画支持上的固有优势,当需要实现精确的子视图滚动或基于子视图位置而呈现不同的视觉效果时,LazyVStack 无疑是更优选择。

想深入了解滚动控制 API 的最新发展,推荐阅读 深入了解 SwiftUI 5 中 ScrollView 的新功能SwiftUI 滚动控制 API 的发展历程与 WWDC 2024 的新亮点

性能:挑战与权衡

尽管 SwiftUI 已经问世六年,但在处理大数据集时,ListLazyVStack 的性能表现仍有待提升。即使面对中等规模的数据集,由于底层实现的差异,两者也呈现出不同的性能特征。

LazyVStack 本质上是一个具备惰性加载能力的 VStack,它维护着一个完整的容器高度(子视图高度总和加上间距)。为了实现惰性加载,它采用动态估算高度的方式,根据可视区域附近子视图的数量和高度来推算整体高度。这种机制带来了两个显著问题:

  1. 快速滚动到特定位置时,需要实例化并计算该位置之前所有子视图的高度(即评估它们的 body),这可能导致明显的性能下降。
  2. 当子视图高度差异较大时,快速滚动或大幅跳转可能因计算效率问题导致白屏现象(未能及时计算出所有必要子视图)。

相比之下,List 在 SwiftUI 层面并不维护完整内容高度的概念。在快速滚动或大范围跳转时,它会智能地选择必要的子视图进行实例化和高度计算,显著提高了滚动和跳转效率。

值得注意的是,在 List 中使用 id 修饰器可能会导致子视图失去惰性加载能力,而 LazyVStack 则不存在这个问题。因此,为 List 提供数据源时,建议让数据类型同时遵循 IdentifiableHashable 协议,避免使用 id 修饰器作为滚动控制的定位标签。

总体而言,在相同数据量下,List 通常比 LazyVStack 表现出更高的效率。

想深入了解性能优化策略,推荐阅读 几个在 SwiftUI 中使用惰性容器的技巧和注意事项优化在 SwiftUI List 中显示大数据集的响应效率

总结

“存在即合理”这一观点在 ListLazyVStack 的设计哲学中得到了充分体现。这两个组件各具特色,为 SwiftUI 开发者提供了不同的解决方案。在选择使用哪一个时,开发者需要全面考虑多个关键因素:

  1. 性能表现:特别应注意需要进行大范围跳转和子视图高低差较大的场景
  2. 滚动控制能力:精确度和子视图的位置感知能力
  3. 布局灵活性:对复杂 UI 设计的适应能力以及对动画和转场的兼容度
  4. 预设功能需求:是否需要利用系统提供的内置特性
  5. 跨平台兼容性:在不同 Apple 生态系统中的表现
  6. 与其他 SwiftUI 组件的协同能力:特别是在导航和数据绑定方面

没有一刀切的解决方案,最佳选择往往取决于具体的项目需求和开发场景。深入理解这两个组件的优缺点,有助于开发者在面对具体挑战时做出明智决策。

在实践中,可能会出现需要结合使用两者的情况,或者在不同页面中分别使用它们以发挥各自的长处。关键是要根据应用的具体需求、目标用户群、性能要求等因素,灵活选择和运用这些工具。

随着 SwiftUI 的不断发展,这些组件的能力和适用场景可能会发生变化。保持对新特性和最佳实践的关注,将有助于在 SwiftUI 开发中始终保持高效和创新。

每周一晚,与全球开发者同步,掌握 Swift & SwiftUI 最新动向
可随时退订,干净无 spam