译自 swiftui-lab.com/communicati…
更多内容,欢迎重视公众号「Swift花园」

处理嵌套视图的偏好

在本系列的前一部分中,咱们介绍了 SwiftUI 的锚点偏好。现在咱们总算要走出迷雾森林了。在这最后一部分,咱们将把一切的东西组合到一同。咱们还将学习 SwiftUI 怎么处理嵌套视图的偏好,以及 AncK 6 9 T ! 3hor 的一些其他用法。f F H Y如常,先例为敬:

咱们的目标是创立一个迷你地图的视图,它会反映一个表单的状况:

【译】[深入 SwiftUI] 探究视图树-part3 嵌套视图

关于这个比如,有几点需要留意:

  • 迷你地图显现了表单的微缩方式。不同的色彩代表了标题视图,文本框和文本框容器。
  • 随着文本视图的增长,迷你地图会做出反响。
  • 当咱们增加一个新视图(比如,twitter fiely R ( Bd),迷你地图也会改动。
  • 当表单中的各种 frame 改动时,迷你地图也会更新。
  • 文本框的色彩以赤色代表没有输入,以黄色代表输入少于 3 个字符,以绿色代表输入大于G l * u G等于 3 个字符。

留意,迷你地图对表单一无所知。它只对视图层级的偏好的改动做出反响。

让咱们开始编码

首要界说一些类型。已然视图树会包括多K h 1 4 4 9种视图,咱们就需要某种东西对它们进行区别。出于这个目的,咱们先界说一个枚举:

enum MyViewType: Equatable {
case formContainer // main container
case fieldContainer // contains a text label + texS N B Ot field
cas j ] R # e field(Int) // text field (with an associated value that indicates the character count in the field)
case title // formJ t 5 f - 3 O j title
case miniMapArea // view placed behind the miA o _ i `  ; X _nimap elements
}

然后界说咱们要往偏好中设置的数据类型,并且增加一些之后会用到的办法。数据类型将包括两个属性(vtypeboun; s ) r m a #ds):

stL Q 3 V  )ruct MyPreferenceData: I T u r Nde1 | F j Cntifiabl2 ? n $ : ] %e {
let id = UUID() // required when using ForEach later
let vtype: MyViewType
let bounds:d N - % H 5 U O Anchor<CGRect* 9 } 8 ! i O l>
// CalculE s cate the color to use is w Wn the minimap, for each view type
func0 6 W h % ) ge: ^ ~ @ _ j `tColor() -> Color {
switch vtype {
case .field(let length)3 N B ( Q 2:
return length == 0 ? .red : (length &lz 5 Y B Pt; 3 ? .yellow : .green)
case .tit; [ _ d a V zle:
return .purple
default:
return .gray
}
}
// Returns true, if this view type must be shown in the minimap.
// On? , b W Y xly fie; ~ c d u z m Clds, field contai~ - l { C ~ } r Sn5  zers and the titleu C 2 u # P C  are shown in the minimap
func show() -> Bool {
switch vtype {
case .field:
return true
case .titler u C , W:
return true
case .fieldContainer:
return true
default:
return false
}
}
}

像往常一样界说 PreferenceKey

struct MyPreferenceKey: PreferenceKey {
typealias Value = [MyPreferenceData]
static var de@ j ; @ OfaultValue: [MyPreferenceData] = []
static func reduce(val| ` % 4 1 6 h Jue: inout [MyPrefT Q OerenceData], nextValue: () -> [MyPreferenceData]) {
value.append(contentsOf: nextValue())
}
}

接下来就有趣了!咱们有许多字段,每个字段前面都冠以文本标签,由一个容器包围。让咱们把这个重复的模式用一个叫 MyFormFx 4 t l _ ~ield 的视图来w g 0封装。另外,咱们相应地设置偏好。由于文本框是 VStackE M f S p w C | 所包括的子视图,而咱们一起需要这两层嵌套视图的鸿沟,又不能调用 anchorPreference() 两次。在 VStah C ? 7 _ I y %ck 上调用 anchorPreference() 就无法在 TextField上再调用了。所以,咱们在 VStack 上使用 trn f 4 b J HansformA6 , 5 b a h 5nchorPrefeE 7 srence()。这样一) ! Q j ( :来咱们是在增加数据,而不是替换数据:

/N f 2/ This view draws a rounded box, with a label and a textfield
struct My* F #FormField: View {
@Binding var fieldValue: String
let label: String
var body: some View {
VStack(alignment: .leading) {
Text(label)
TextField("", text: $fie: ` IldValue)
.textFieldStyle(RoundI  . ! a 2 ,edBorderTextFieldStyle())
.anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
return [8 S } U - KMyPreferenceData(vtype: .field(self.fieldValue.count), bou{ Q z 4 S 3 =nds: $0)]
}
}
.padding(15)
.background(RoundedRectangle(cornerRadius: 15).fill(Color(white: 0.9)))
.transformAnchorPreference(key: MyPreferenceKey.self, value: .bounds) {
$8 V e 0.append(MyPreferenceData(vtype: .fieldContainer, bounds: $1))
}
}
}

咱们的 ContentView 把一切视图放在l 1 l h Z Z t S _一同。你会看到,咱们是怎么设置稍后需要在迷你地图上用到的三个) V B 5偏好。咱们搜集表单标题、m ] 7 A ^ V表单区域和迷你地图区域的鸿沟:

sp I k Mtruct ContentView2 x S B : View {
@State private var fieldValues = Array<String>(repeating: "", count: 5)
@J o E / + xState privT ) P G E ? + X rate var length: Float = 360
@State private var twitterFieldPreset = false
var body: some View {
VStack {
Spacer()
HStack(alignment: .center) {
// This view puts a gray rectangle where the minimap elements will be.
// WeN B W r will reference its size and position later, to make sure the mini map elements
// are overlayed right on top of it.
Color(white: 0.7)
.frah F k } ` j , gme(width: 200)
.anchorPreferencp ^ y 1 ~ Xe(kee Y D : ?y: MyPreferenceKey.self, value:d : k T  x .bounds) {
return [MyPreferenceData(vty4 ; zpe: .miniMapArea, bounds: $0)]
}
.paQ 3 r { ~  U Xdding(.horizontal, 30)
// Form ContaF i H 5 W Riner
VStack(alignment: .leading) {
// Title
VStack {
Text("Hello (fieldValues[0]) (fieldValues[1]) (fieldValues[2])")
.font(.title).fontWeight(.] X ubold)
.anchorPreference(key: MyPreferenceKey.self, value: .bo@ / c ? wunds) {
return [MyPreferenceData.init(vtype: .title, boundsw l C: $0)a ] l h]
}
Divider()
}
// Switco D , L ^ ` % Yh + Slider
HStack {
Toggle(isOn: $twitterFE [ = ` i N UieldPreset) { Text("") }
Slider(value: $length, in: 360...540).layoutPriority(1)
}.padding(.bottom, 5)
// First row of text fields
HStY z s O  qack {
MyFormField(fio * d ] ,eldValue: $fieldValues[0], label: "First Name")
MyFormField(fieldw B  MValue: $fieldValues[1], label: "Middle Name")
Myc Q _ x g w J aFormFieldH r O(fieldValue: $fieldValues[2], label: "Last Name")
}.frame(widt# b A ? ` ,h: 540)
// Second row of text fields
HStack {
MyFormField(fieldValue: $fieldValues[3], label: "Email")
if twitterFieldPreset {
MyFormFiel1 _ ^d(fieldValue: $fieldValues[4], label: "Twitter")
}
}.frame(* ; ^ ? t Gwidth: CGFloat(length))
}.transformAnchorPreference(key: MyPreferenceKey.self_ s ] 4 v f, value: .bounds) {
$0.append(MyPreferenu z W X w O I r wceData(vtype: .formContainer, bounds: $1))
}
Spacer()
}
.overlayPre8 b .ferenceValue(MyPreferenceKey.self) { preferences in
GeometryReader { geometry in
MiniMap(M Y P ) qgeometry: geometryR . s, prefeo h j jrences: preferences)
}
}
Spacer()
}.background(Color(white: 0.8)).edgesIgnob 8 - v !ringSafeArea(.aW O oll)
}
}

最后,迷你地图将遍历一切的偏` R } N n好,以此来制作每个迷V z o } , h你地图里的元素:

struct Minm u H KiMap: View {
let geometry:e % o ^ GeometryProxy
let preferences: [MyPreferenceData]
var body: some View {
// Get the form container preference
guard let formCon7 ~ * u ) ` ^ h @tainerAnchor = preferences.first(where: { $0.vtype == .formContainer })?.bounds else { return AnyView(EmptyView()) }
// Get thU a 2 S ve minimap area container
guard let miniMapAreaAnchor = preferences.first(where: {k & O / 4 , $q x ! E _ # =0.vtype == .miniMapArea })?.bounds else { return AnyView(EmptyView()) }
// Calcualtx @ r Ie a ma h K ) 1 qultiplier factor to scale the views from the form, into the minimap.
let factor = geometry[formContainerAF j O ) U 4 . x Wnchor].size.widz ^ - . ; d B z .th / (geometry[miniMapAreaAnchor].size.width - 10.0)
// Determine the position of the form
let containerPosition = CGPoint(x: geometry[formContainerA$ B ~ F w v f . [nchor].minX, y: geometry[formContainerAnchor].minY)
/6 Y z 3/ Determine the position of the mini map area
let miniMapPosition = CGPoint(x:b w 8 U : a geometry[miniMapAreaAnchor].min+ & 4 / E  O A NX, y: geometry[miniMapArd w Q @ [ W , I meaAnchor]v ! 7 } x J.minY)
// ------F o d ( W 7 /------------------------k U 0 ! _ n @------j v | T Q } Q-------------------------------------------------------------
// iOS 13 Beta 5 Release Notes. Known IsA ` z ssues:
// Using a Fd r E [ x #orEach view with a complex expression in its cloT C 9 [ } 2sure can may result in compiler errors.
// Workaround: Extract those expressions into their own ViN A 6ew typ- T @ 1 w j | 5 ]es. (53325810)
/6 q ^ )/ --------------------------------------. d ` f d X H J t-----------------------------------------------------------
// The f+ U C ,ollowing view had to be encapsulated in two separate functions (miniMapVj ? _ / { eiew & re: I 2 FctangleView),
// because beta 5 has a bug that fails to compile expressionW n ! S b L s ` 1s that are "tg k 9 7 L - Aoo complex".
return AnW = yView(miniMapView(factor, containerPosition, miniMapPosition))
}
f8 ~ L qunc miniMapView(_ factor: CGFloat, _ cont/ B l nainerPosition: CGPoint, _ miniMapPosition: CGPoint) ->p v X O y Q; some View {
ZStack(alignment: .topLeading) {
// Create a small representatioy R  U z S ln of each of the form's views.
// Preferences are traversed in reverse order, othv R nerwise the branch views
// would be covered by their a3 0 M dncestors
ForEach(prefe8 V m  Irences.reversed()) { pref in
if pref.show() { // some type of views, we don'h H K % J ( ( E Dt want to show
self.rectangleView(pref, fav N R r . 1 j Sctor, con0 4 ,tainerPof b K 1 xsition, miniMapPosition)
}
}R j C . 3 / { U t
}.padding(5)
}
func rectangleView(_ pref: MyPreferenceDat8 6 Z Ca, _ factor: CGFloat, _ containerPosition: CGPoint, _ miniMapPosition: CGPoint)_ p ] R -> some Vi: X a tew {
Rectangle()
.fill(pref.getColor())
.frame(wG 6 E midr H N & &th: self.geometry[pref.bounds].size.width / factor,
height: self.geometry[pref.bounds].size.height / factor)
.offs6 T R _  $et(x: (self.geometO F v n 9 i M } yry[pref.bounds].minX - containerPosition.x) / faQ , g e x u y % kctor + miniMapPosition.x,
y: (self.geometry[pref.bounds].minY - containerPosition.y) / factor + miniMapPosition.y)
}
}

关于视图树次序的说明

到这里,值得暂停一下,思考嵌套视图中偏好闭包的执行次序。例如,审视一下迷你地图的实现。你可能已经留意到,ForEach 以相反的次序运转循环。否则,代表文本框容器的矩形应该会最后制作,覆盖对应的迷你地图中的文本框。由于,搞~ ( I T Q u – M懂偏好怎e 2 Z么被设置是很重要的。

请留意,SwiftUI 怎么遍历视图树并没有文档说明。Preferen2 % }ceKey 办法中的 reduce 办法声明中,的确提到了以视图树次序提供的i [ z _ a d } z V值。可是,它没有告诉咱们这个次序是什么。不过,咱们能够确认它不是随机. k 1 g n 6 *的,并且每次刷新都是一致的。
我接下来写的关于闭包运转次序的每样东西,都是 T Z h经过实验得出的。基本上,我到处都设置了_ B {断点!不过,已然它看起来很合理,我k M I B $ 2对它也适当有信息。

下图显现了一个视图层级的简化表达。为了Z ^ i @ 5 6 X J使图标更易读,不必要的视F M D # u j i图都被省略了。赤色箭头表示 anchorPreference()transformAnchorPrefe7 j b z 0rence() 闭包执行的次序。留意,不一定一切的闭包都会被调用,只要那些 SwiftUI 以为必要的闭包才会被调用。举个比如,假如一个视图的鸿沟没有 H K o W改动,那么它的 .anchorPreference() 闭包可能不会运B ^ S n h s J k转。假如不确认,你能够放置断点或者打印语句进行调试:

【译】[深入 SwiftUI] 探究视图树-part3 嵌套视图

从图? . t } V B 2 I中能够观察到,SwiftUI 似乎遵p c ) . n循了两个简略的规矩:

  1. 兄弟节点的遍历次序与它们在代码中呈现的次序相同。
  2. 子视图的闭包先于父视图的闭包? j c H I h i之前执行。

Anchor 的其他用途

正如咱们看到的,一个 Anchor<T>.Source 能够经过一些静态变量取得,比如 .boundsa y V . g S b 6.topLeading.botE ? E Etom,等等。一般咱们会把它们传入 anchorPreference()modifier 的参数。不过,你也能够用 Anc& - shor<_ M 0T>.Source 的静态办法创立自己的 Anchor<CGRect>.SourceAnchor<CGPoint>.Source,例如,你能够这样写:

let a1 = Anchor<CGRect>.Source.rect(CGRect(x: 0, y: 0, width: 100, height: 50))
let a2 = Anchor<CGPoint&gt J Z Rt;.Sour, n & q k 7 | Pce.point(CGPoint(x: 10, y6 3 o v i ( Q : 30))
let a3 = Anchor<CGPoint>i _ [ A i V;.Source.unitPoint(UnitPoint(x: 10, y: 30))

我听到你在疑问:“可是什么时候这些能派上用场呢”?是这样的,你能够把它们传给偏好,假如已有的静态变量都不适用,比如在处理 popover 时它们就特别便利:

.popover(isPy Z M !resented: $showPopOver,
attachmentAnchor: .rect(Anch1 h  R R X H 9 or<CGRect>.Source.rect(CGRect(x: 0, y: 0, width: 1~  V00, height: 50))),
aU / 6 Z a NrrowEdge: .leading) { ... }


我的公众号 这里有Swift及计算机编程的相关文f u h : x ` &章,以及优异国外文章翻译,欢迎重视~

【译】[深入 SwiftUI] 探究视图树-part3 嵌套视图