SwiftUI学习->视图标识 (View Identity)

889 阅读4分钟

说明

视图标识 (Identity) - 标识在应用程序的多次更新过程中视图元素,决定是否重新生成视图元素,在 SwiftUI 中分两种方式来定义 Identity:

  • 声明式 Identity,一般是在 View 上添加一个 id(_:) 修饰器或者在数据驱动列表控件中显示声明 Identifier,如 ForEach、List
  • 结构性 Identity,是 SwiftUI 根据 View 的类型和层级结构来动态识别,虽然这种 Identity 不需要开发者指定,但也需要开发者清晰的将 View 的层级结构描述出来,方便 SwiftUI 内部识别。

声明式 Identity

  • 显式标识 View。 通过下面的例子能更好的理解,例如在这个救援犬列表里,用 dogTagID KeyPath 获取相应属性,在参数里指定到 id 上,就是在显式声明 View 的 Identity。这样就能标识出每条数据对应的展示视图。一旦列表数据发生变化, SwiftUI 可以根据这些 ID 来判断,哪些视图需要新生成,哪些视图重复使用,只需要执行动画。

fac1e38bb248310249e9b83ed89f3886.png

结构性 Identity

  • 不显式声明 Identity这并不意味着这些 View 根本没有 Identity,也就是说每个 View 都有一个 Identity,即使它不是显式声明出来的。在这种情况下 SwiftUI 内部会对没有显式 Identity 的 View 根据它的描述层级结构生成一种隐式的 Identity,就叫做结构性 Identity 方式一:如果利用分支语句来描述 SwiftUI 的 View,就会在不同的条件下生成不同的结构性 Identity。在 SwiftUI 内部会生成不同的视图元素实例,它们是不连续的,这也就解释了在不同状态下界面切换的时候,为什么只会有淡入淡出的过渡效果。

805f5db1472438ad9d9ec690a8346278.png 方式二:我们只用一个 PawView 自定义View,在这个自定义的 View 的 Modifier 上利用三目运算的方式来动态改变需要变化部分的数值,当在不同状态之间发生界面切换的时候,由于始终是一个视图元素,所以就会执行平滑的滑动动画。

b6e4c269c1be40a6a2c8a7254c867e73.png 综上所述,在使用结构性 Identity 的时候,第二种描述 View 的方式是更好的选择。应该尽量避免切换 Identity,这样做会给动画和性能都带来良好的效果,也有利于维持视图的生命周期和数据状态。

ViewBuilder

SwiftUI body 计算属性需要一个明确一致的返回类型,但 if else 条件判断使得返回类型不一致了,会引起编译失败。这时候 SwiftUI 引入了一个的黑魔法 - ViewBuilder (默认是附加在 body 计算属性上的,不需要开发者单独指定) 。ViewBuilder 帮助 SwiftUI 把各种条件判断,封装成 _ConditionalContent 的数据结构。但为了区分在不同分支下的类型不同,用泛型来进行了区分,这样即保证了返回数据的一致性,又保证了 SwiftUI 内部可以通过泛型识别出不同分支下结构性 Identity。

危险的 AnyView

a4b114506b29759629cf463edea4c767.png 上图是一个使用 AnyView 的示例代码。在这个自定义 View 中,为了保证最终返回一个明确一致的数据类型,每个分支都用一个 AnyView 包裹起来。由于 AnyView 隐藏了所包装视图的类型,让 SwiftUI 无法在条件判断中识别出结构性 Identity,在 SwiftUI 眼里,它看到的都是一些擦除类型的 AnyView,更要命的是,这段代码阅读起来特别困难。
那么,接下来让我们用正确的方式来重构这段代码:

  • 第一步:消除 AnyView 包裹,把内部具体的 View 类型暴露出来
  • 第二步:去掉 所有的 return 关键字
  • 第三步:在方法上添加 @ViewBuilder 标识,保证最终返回的是一个明确的类型,编译通过
  • 第四步:由于我们只是在 dog 的 breed 状态之间来回判断,那么把 if else 改为 switch case 会更合适 重构后,最终代码和 View 层级结构如下图:

d04631329296a4fd910f1671b86624c7.png 一般情况下,还是尽量避免使用 AnyView,因为 AnyView 有如下缺陷:

  • 代码难于阅读
  • 由于擦除了所有的 View 类型,无法在编译的过程中给出相应的提示
  • 可能会导致不必要的性能损失