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

预计阅读时间 3 分钟

场景

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

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

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,分析得到视图树实际类型如下。

NavigationView<
    ModifiedContent<
        ModifiedContent<
            Text,TransactionalPreferenceTransformModifier<NavigationTitleKey>
        >, 
        _BackgroundModifier<Color>
    >
>

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

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

Tips

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

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

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

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

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

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

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

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 视图“占位”。

MakeOptim

MakeOptim

MakeOptim 是个人所学总结完再总结而形成的高质量文集。旨在通过这些文章,沉淀自己,帮助他人。