SwiftUI - 获取子视图信息(大小、标题等)

场景

使用 SwiftUI 布局界面,经常会遇到需要子视图信息来确定最终形态的情况。

以 SwiftUI 的 NavigationView 为例。下面的代码创建了一个 NavigationView,它具有单个 root view 和一个导航标题

1
2
3
4
5
6
7
8
9
10
struct Test: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!")
                .font(.title)
                .navigationBarTitle("Root View")
                .background(Color.gray)
        }.debug()
    }
}

可以看出,.font 这类修饰器改变的是各自 view 子树的环境。不过,.navigationBarTitle 却恰恰相反。Text 本身并不关心导航标题,不过它的父视图 NavigationView 对此关心。

分析

借助 debug extension,分析得到视图树实际类型如下。

1
2
3
4
5
6
7
8
NavigationView<
    ModifiedContent<
        ModifiedContent<
            Text,TransactionalPreferenceTransformModifier<NavigationTitleKey>
        >, 
        _BackgroundModifier<Color>
    >
>

可以看出,苹果在设计 NavigationView 的时候使用 TransactionalPreferenceTransformModifier 来使得 NavigationView 获取子视图定义的 navigationBarTitle

将实际类型绘制成图(有简化)后,可以看出,backgroundNavigationView 的子视图,而 preference 又是 background 的修饰器的子视图。preference 的“父”视图(图中黑色节点)都可以通过 NavigationTitleKey 读取到子视图设置的 navigationBarTitle

Tips

下面以获取子视图大小为例,讲解如何利用 preference 获取子视图信息。

在使用 preference 时,需要先定义一个 PreferenceKey,这点类比 NavigationTitleKey 即可。下面的代码定义了一个 SizeKey

1
2
3
4
5
6
7
struct SizeKey: PreferenceKey {
    static let defaultValue: CGSize? = nil
    
    static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
        value = value ?? nextValue()
    }
}

利用 GeometryReaderbackground 接受 Text 尺寸作为它的建议尺寸。这里我们不想绘制任何东西,所以使用 Color.clear 视图。

1
2
3
4
Text("Hello, World!")
    .background(GeometryReader { proxy in
        Color.clear.preference(key: SizeKey.self, value: proxy.size)
    })

现在视图树上任意的父视图节点就可以通过 onPreferenceChange 获取到 Text 的大小了。

下面是完整的代码,通过获取 Text 视图的大小,同步 Rectangle 的大小,当 Text 大小改变的时候,Rectangle 也会跟着一起改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import SwiftUI

struct Test: View {
    @State private var size: CGSize? = nil
    
    var body: some View {
        
        VStack {
            Text("Hello, World!")
                .font(.title)
                .background(GeometryReader { proxy in
                    Color.clear.preference(key: SizeKey.self, value: proxy.size)
                })
                .onPreferenceChange(SizeKey.self) { value in
                    self.size = value
                }
            
            Rectangle()
                .foregroundColor(.blue)
                .frame(width: size?.width, height: size?.height)
        }
    }
}

struct SizeKey: PreferenceKey {
    static let defaultValue: CGSize? = nil
    
    static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
        value = value ?? nextValue()
    }
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

小结

利用 PreferenceKeyonPreferenceChange 可以方便地获取到子视图的信息,如果不希望绘制任何东西,可以利用 Color.clear 视图“占位”。

CatchZeng
Written by CatchZeng Follow
AI (Machine Learning) and DevOps enthusiast.