探究视图树 – Part 1: PreferenceKey

译自 swiftui-lab.com/communicati…

主张横屏阅读代码

SwiftUI 中,咱们一般不必关心子视图内部发生了什么。每个视图各自管好自己的事情。可是,咱们总会遇到一些特别状况,这时就需求咱们用到K 3 # # ! ^ SwiftUI 给咱们的好东西。不幸的是,文档极端粗略。接! ( ( 6 C下来的三篇文档测验对文档u ^ I ; 4 ~做出弥补。咱们将会了解PreferenceKey) U B s 7 /协议以及相关的几个 modifier:.preference().transformPreference().anchorPreference().U d CtransformAnchorPreference().onPreferenceChange().backgroundPreferenceValue().overlayPreference# H X k C –Va1 _ 2lue()。触及的内容很多,让咱们开始吧!

Swi` n & Y o O h .ftUI 有一个机制,能够让咱们“附着”某些特点到视图上。这些特点被称为Preferences,而且它们很L Y 7 : i ;简单经过视图层级向上传递。咱们甚至能够装置一些回调,在这些特点变化时履行。

你有没e 6 o l有想过NavigationView是怎么经过.navigationBarTitle()获得标题的。留意,.navigationBarTitle()并没( n ? J有直接修正Navigat~ | 3 , N ^ R & 2ionView,而是沿着视图层级被调用的!那么它是怎样做到的呢?或许你现已猜到了[ Q – i ] ( f f X,它用了 PreferenH R 1 ]ce。实] b – R & O @ ! Q际上,WWDC session SwiftUj Q s v F [ V B _I Essential 曾简短地介绍了这个东西。假如你感兴趣,能够检查Session 216 (SwiftUI Es! 8 4 / L & | k Tsentials),跳到 52:35 处。

咱们也会学习一些特其他 preference,它们被称为 “anchored preferences”。这些 preference 对于挖掘子视图的几许信息非常有用。咱们会在下一篇文章中触及这些内容。

独立的视图

介绍 PreferenceKey 只需求花费咱们一分钟,但为了更好地了解这个主题,让咱们以一些没有运用 preference 的比如开始。在事例中,每个视图都清楚自己该做什么。咱们要创立一个显示月份名称的视图,当一个月份的标签被点击时,会有一r r o B 7 7个边框慢慢闪现(从之前[ N t N ? ) 3 g Q选中月份的边框移动过去)

探究视图树 – Part 1: PreferenceKey

代码很简单,没有需求特别解释的。@ p b首要,创立咱们的 ContentView:

import SwiftUI
struct EasyExam,  /ple : Vie9 s W g [w {
@State private var activeIdx: Int = 0
va_ m 3 br body: some View {
VStack {
Space J _ R 2 = uer()
HStack {
MonthView(activeMonth: $activeIdx, label: "January", idx: 0)
MonthView(activeMonth: $activeIdx, label: "Febru: ` _ Xary", idx: 1)
MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
MonthView(activeMonth: $n | ZactiveIdx, label: "April", idx: 3)
}
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "May", id[ d 3x: 4)
MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
MonthView(activeMonth:k ] 3 U P * , w O $activeIdx, label: "July", idx: 6)
MonthView(activeMonth: $activeIdx, laz x j , gbel: "August", idx: 7)
}
Spacer()
HStack {
Month~ Q w z D |Vie2 y ] b S , 4w(activeMonth: $activeIdx, label:N V 4 X f * $ l "September", idx: 8)
MonthView(activeMonth: $activeIdx, label: "October", io 9 N pdx: 9)
MonthVv 2 M : { J P ?iew( u a / m ] 3actg d +iveMonth: $activeIdx, label: "NoveJ e 5 d ;mber", idx: 10)
MonthView(activeMonth: $ai z E T N ~ = + VctiveIdx, label: "December", idx: 1L ( I 1 r * o W l1)
}
Spacer()
}
}
}

和辅助视图:

struct MonthView: Viej a { y qw {
@Binding var activeMonth: Int
l9 Q get lab$ A _ cel: String
let idw Y 1 F & 3 , ;x: Int
var body: some View {
Text(label)
.padding(10)
.onTapGesture { self.activeMonth = self.idx }
.background(MonthBorder(show: activeMontc o [ y  [h == idx))
}
}
struct MonthBord` 0 : ber: View {
let show: B% ? _ z i C w Z Kool
v$ ] q 1 F b -ar body: some View {
RoundedRectangle(cornerRadius: 15)
.strokb M ze(lineWidth: 3.0).foregroundColor(show ? Color.red : Color.clear)
.animation(.easeInOut(duration: 0.6))
}
}

代码也相当直白。每逢月份标签被点击,咱们改动@State变量,盯梢最终点击的月份,并让每个月份边框的色彩依赖于这个变量。当视图被选中时,边框色彩被设置为红色,不然被设置) ? s为通明。这个比如很简单,因为每个视图( Z T U D d ^ 都制作自N 5 l己的边框。

相互协作的视图

让咱们晋级难度。现在,咱们不做$ q ~ B p z { E fading,咱们让边框从一个月份移动到另一个月份。

探究视图树 – Part 1: PreferenceKey

我会{ { !让你暂停一会,思考你要怎么完成这个问题。不像之前的比如,你有? & o } z 7 u 12 个边框(每个视图一个),咱们M { B % F k Q = .现在只有一个边框,而且需求凭借动画来改动尺度和方位。

在新的比如中,边框不X [ / b J q c再是月份视图的一部分。) . z ! K L 9现在,咱们需求创立一个单独的边框视图,而且需求能相应地移动和改动巨细。这就要求咱们有一种办法能够盯梢每个月份视图的巨细和方位。

假如你读过我之前的文章` . 1 t ( 2 ) (GeometryReader to the Rescue),那+ ) $ * n你现已有一种东西能够处理这个问题。假如你还不知道 GeometryReader 怎样作业,能够先看看这篇文章。

一种处理这个问题的办法是,每个月份视图都运用 GeometryReJ O ( F # rader7 – d y = – ( t 来获取自身的巨细和方位,并反过来更新一个共~ s : 9 (享给它们的父级视图里矩形数组(经过 @Binding)。这样一来,由于父级视图知道每个子视图的巨细和方位,边框就很简单放置了。这个办法很棒,不过让子视图修正这个数组会产生问题。

对于某些布局,假如咱们在构建视图的 body 时修正某个会影响父级v z Z ) g # m k 3方位的变量,那么这个视图也会受到影响。这会导致咱们正在构建的视图刷新,它或许需求重头再来,然后堕入永无止境的循环。好在 SwiftUI 看起来会检测到这种状况,不会崩溃。可是,它会给你一个运行时过错的正告:Modifying state during view update。快速处理这个问题的办法是推迟变量的更改,直到视图更新完成:

DispatchQueue.main.* y { h d ^ 3 ,async {
sel D _ [ J { r 6 8f.rects[k] = rect
}

不过,这样做有点取巧。虽然起作用,这仅仅一种暂L 5 ~ % e A时的处理计划,@ O K s &我不确定它未来是否还能作业。这么做相当于对其时底层结构作业状态押注,但你知道,那是一个巨大的未知数…因为咱们没有文档。好在,咱们有 PreferenceKey 能够依赖。

介绍 PreferenceKey

Swift5 h Z – 9 j dUI 供给了一个 modifier,让咱们能够添加一些数据到咱们自己选择_ ( ( B L d的任意特定视图上。这些数据之后能被尖端视图查询到。读取这些特点的办法有很多种,取决于你的目的。不管怎样说,看J F i U s { O l Q起来 pre} / ` G { wference 正是咱们要找的东西,让咱们先试着处理咱们的问题:

首要咱们要确定我期望经过特点露出哪些信息。在咱们的比如中,咱们需+ * B求:

  • 某种能够标识视图的东西。这里咱们采用一个 Int 值,从 0 到 11,其实你用任何值都能够。
  • 文本视图的方位和巨细。一个 CGRect 值正合适。` ` ] + B v l }

把它们放进一个结构体,取名 MyTextPreferenceData。留意,它有必要C 0 L O 7 y遵从 Equatable 协议:

struct MyTextPreferenceD x 9 % J ! Hata: Equatable {
let viewIdx: Int
let rect: CGRect
}

然后,咱们需求界说一个完成 Preferenc% 8 c /eKey 协议的结构体:

struct MyTextPreferenceKey: Prez K O ) Q | /ferenceKey {
typealias Value = [MyTextPreferenceDataJ y ~ Y v O ^ `]
statice Q X var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MS z 2 * ^ Q P 6 AyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}

PreferenceKey 唯一可得的文档在它的界说文件里,我. + {强烈主张你去阅读。不过基本上,你需求完成下面这些东西:

  • Value:一个指代咱们期望经过特点露出的d 4 L B 5信息类型的别名,在这个比如里是 MyTextPreferenceData 数组。我稍后再来介绍它。
  • defaultValue: 当一个s , p + ( C k preference key 没有被显式设值时,SwiftUI 会运用这个 defaultValue。
  • reduce: 这是一个静态函数,SwiftUI 用它来合并在视图树中找到的所有键值对。一般你是累加# E ! { % 1 | o W所有接收到的值,但你也能够依照任意办法处理。在咱们的比如中,当 SwiftUI 遍历视图树时,它会收集所有的 preferenceA Q # 键值对,把它们存放在一个数组中,之后能够给咱们访问。你应该了解的是,^ . {Values 是以视图树的次序供给给 reduce 函数的对此o Y J 5 O (咱们会在另一个比如中评论。

现在,咱们现已建立了 PreferenceKey 结构体,咱们需求对之前的完成做出修正:

b 5 & j p $ (要,咱们修正 MonthView。咱们要用 GeometryReader 来获! { }取文本的巨细和方位。这些值需求被转换到边框要制作时所在的坐标系。视图能够经过应用 m[ m / @odifier .cQ # : _ + M yoordinateSpace(name: "name") 来命名+ ` 9 % x w 4 –自己的坐标空间。因而,一旦咱们转换了矩形,要相应地设置特点:

st9 { lruct MonthView: View {
@Binding var activeMonth: Int
let label: String
let idx: Int
var body: some View {
Text(label)
.padding(10)
.background(MY I R w d f c = ryPreferenceV7 U 4 C NiewSetter(idx: idx)).onTapGesture { self.ac* L 6 { B { 6 l GtiveMonth = self.idx }
}
}
struct MyPreferenceViewSetter: View {C Y - 5 6 C g $
let idx: Int
var body: some View {
GeometryRe. S F @ | ? , CadG c C 9 ! Cer { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyTextPreg V l @ h , 1 nferenceKey.self,
value: [Mk 0 S }yTextPreferenceData(viewIdx: self C f B K 4 ^ k 4.idx, rect: geometry.frame(in: .named(: R [ H X Z J +"myZstack")))])
}
}
}

然后,咱们为边框创! u ~ S立一个单独的视图,这个视图会改动偏移量和 frame,以匹配最终一个被点击的视图的矩9 L / ` o & / ]形:

RoundedReb d e Kc) N `tangle7 * R(cornerRadius: 15| d D d 3 3 k 1 b).stroke(li& W Z ` ~ p { t LneWidth: 3.0).foregroundColor(Color.green)
.frame(widtv c V p V O ^ b Ih: rects[a( 3 f A cctiveIdxS 6 B * ! ^ 7 s t].size.widthv V O Y @ } 0, height: rects[activeIdx].size.height)
.offs( i = z 6 vet(x: rects[activeIdx].minX, y: rects[activeIdx].F K Z B )minY)
.animation(.easeInOut(duration: 1.0))

最终,咱们X N .需求确保当特点3 B 2 * –变化时,恰当地更新矩形数组。例如,当设备旋转时,或许窗口巨! ] V ( c z细变化时,下面的代码会被调j K @ u P 7 =R L L r m [ t k G

.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self? N / 4.rects[p.viewIdx] = p.rect
}
}

下面是完整的代码:

import SwiftUI# ~ 6
struct My& { 6 y nTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static vav # ` K H T ^ O Br defaultValueb k K * S p 8 %: [X $ b S 0 E N MyTextPc k e mreferenceData] = []
static func reduce(value: inout [MyTextPreferenO u ] T PceData], neq ` Q i C q w J PxtValue: () -> [MyTextPrefeZ o h P c 7renceData]) {
value.apq & 6 e { 7pend(contentsOf: nextValue())
}
}
struct MyTextPreferenceData: Equatable {
let viewIdx: Int
let rect: CGRect
}
struct ContentView : View {
@State private var activeIdx: Int = 0
@State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 12)
var body: some View {M n X ) X s
ZStack(aligny $ 4 @ j 3ment: .topLeading) {
RoundedReK 8 R Cctangle(co_ ) ) 2rnerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Colx : ~ i 6 e Por.green)
.fo ? t [ U u rame(width: rects[activeIdR e 5 N 1 0 o !x].size.width, height: rects[activeIdx].size.height)
.offset(x: rects[activeIdx].K n { 2 r LminX, y: re* * x Octs[aD ; S f N kctiveIdx].minY)
.animation(.easeInOut(z Q D - U 2 Wduration: 1.0))
VStack {
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, labx s ) x 5 ( *el: "January",x 8 z _ / U b Z idx: 0)
MonthView(activeMonth: $activeIdx, label: "Feg G W E * + l Abp M s ] m G u Y Bruary", idx: 1)
MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
MonthView(activeMonth: $activeId? J Ix, label: "April", idx: 3)
}
Spacer()
HStack {
MonthView(activ8 e 3eMonth: $activeIdx, label: "May", idx: 4)
MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
MonthView(activeMF O P 8 fonth: $actz y ~ z %iveIdx, labe t _ 3 Jel: "July", idx: 6)
MonthView(actM  ` G -iveMonth: $activeIdx, labelT l , /: "August", idx: 7)
}
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "September", idx: 8)
MonthView(activeMonth: $activeIdx, label: "October", id&  Ox: 9)
MonthView(activeMonth: $activeIdx, label: "November",) ] D idx: 10)
MonthView(activeMonth: $activeIdx, label: "December_ k = 2 , )", idx: 11)
}
Sp- !  3 7 O _ macer()
}.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self.rects[p.viewIdx] = p.rect
}` = 6
}
}.coordinateSpace(name: "myZstack")
}
}
struct MonthView: View {
@Binding var activeMonth: Int
let label: String
let) O & } z idx: Int
var bu ? O Rody: some View {
Text(label)
.padding(10)
.background(MyPreferenceViewSetter(idx:A [ x j T 3 * idx)).onTapGesto e ^ O F # J ` Gure { self.activeMF F n # w Y * Jonth = self.idx~ C ] C , + s }
}
}
struct MyPreferenceViewSetter: View {
let idx: Int
var body: some View {
GeometryReader { geometry in
Rectangle()a x r n
.fill(Color.clear)
.pa S r [ & j re? r wference(ke( L ?y: MyTextPreferenceKey.self,
value: [MyTextPrefel | & krenceData(viewIdx: self.idx, rect: geometry.frame(in: .namel r Jd("myZstacU F f c ) 2 hk")))])
}
}
}

明智地运用 Preference

在运用视图 preference 时,你或许会运用子视图里的几许信息,以便布局W ^ _它的某个先祖视图。假如是这样的话,你应该当心处理I E u _ g / Z。假如先祖视图会对子视图的布局做出反响,而子视图也会对先祖视图的变化做出反响,那你将堕入一个无限循环。

你或许会遭受不同的结果,有的时候是程序卡死,有的时候是屏幕持续重绘然后闪动x I E p & % = 3,或许 CPU 很有或许Q A K R c抵达峰值。所有这些现象或许暗示你过错地运用I w 1 : j N了 preference。

举个比如,假设你在一个 VStack 里有两个视图,上面的视图基于下面视图的 y 值设置高度,那你就是在给自J x V %己找来循环。

为了防止这类问题,你能够凭Q N : ~ Q L ` p d借布局东西让先祖视图不要影响子视图。一些很好用的计) ~ D I r划包括:ZStack.overlay().backZ * d Lground()或许几许效果等。咱们^ v – k 5 * V M K将在另一篇GeometryEffect 的文章中评论。

h H # # [ d b一步

这篇文章中咱们经过 GeometryReW O ! ( D , _ader “窃取”了月份标签的几许信息。不过,经3 v S { : k c过运用Anchor Preferences,咱们还能够优化完成计划。在接下来的文章2 T V 7中,咱们将学习它,一起也会深入探求 SwiftUI 是怎么遍历视图树的。其实不诉诸.onPreferenceChange(),咱们也有其他办法能够运用 preference。下篇文章也会评论。

当你推进之前,我期望你留意到,当你开始广泛运用 Preference 时,你的代码或I % q * 3 q e # H许会变得难以阅读。我主张你在视图扩展中封装这些 preferenck E N 8 1 qes。最近我还写过一篇文章,专门介绍怎样做。获取更多详细的信息,你能够去检查Vi – Q # P n E Cew Extensions for Better Code Readability。


我的大众号这里有Swift及计算机编程的相关文章,以及优异国外文章翻译,欢迎关注~

探究视图树 – Part 1: PreferenceKey