SwiftUI 编程 Tips
SwiftUI 编程 Tips
本文记录了使用 SwiftUI 编程时需要注意的一些细节.
- View.onAppear
- 使用 GeometryReader 获取基于父视图的布局空间
- View 协议当作类型使用
- @StateObject 和 @EnvironmentObject
- @ViewBuilder
- View.mask 和 View.background / View.overlay
- Image 实现 AspectFill 内容填充模式( UIView.contentMode 第二个枚举值)
- Shape
前言
SwiftUI 是一个声明式 UI 框架, 它基于 Combine 内置数据双向绑定机制, 在 UI 构建中具有非常大的优势. 但它也存在一些功能短板, 以至于一些自定义程度较高的 UI 组件还是使用 UIKit 更为合适.
我的看法: App 主框架使用 UIKit (众所周知: 国内 App 启动时有很多操作, 各种生命周期也有很多操作) App 内部的业务 UI 使用 Swift UI 搭建
View.onAppear
View.onAppear 类似 UIKit 中 viewDidLoad / viewWillAppear 合体, 需要手动处理仅执行一次的操作.
View 的生命周期函数 onAppear 类似于 UIKit 中 viewWillAppear. 通常我们会处理一些 view 被展示到屏幕前的事件. 但不同于 UIKit 的是 SwiftUI 中没有提供类似 viewDidLoad 的函数. 对于仅需要处理一次的操作(如读取内存数据)每次都会执行, 并覆盖用户的编辑值
具体原因如下: SwiftUI 采用 Combine 的数据双向绑定, 其内部会记录很多 @State. 当某个 @State 发生改变, 会触发 UI 重绘制, 会重新执行 View.onAppear, 如果其中包含读取默认值的代码, 则会重新读取默认值, 覆盖当前修改过的 @State.
使用 GeometryReader 获取基于父视图的布局空间
GeometryReader 是一个特殊 View. 它像函数一样以其父视图的布局空间来包装指定子视图.
其主要目的是对子视图布局提供运行时的正确布局空间数据, 如获取父视图的 width/height.
通常, 我们只直接声明式布局 UI 即可, 比如 ZStack / HStack / VStack 等来布局, SwiftUI 会自动进行布局,并合理利用空间. 但是, SwiftUI 默认是根据子 View 来填充,撑开父容器. 这就有一个问题: 当子视图尺寸明显大于我们指定父视图尺寸的时候, 如果父视图被设置了固定尺寸, 此刻子视图会因为尺寸过大而超出父视图可是范围, 其超出部分并非以左上角原点为锚点对齐,从而造成布局异常.
GeometryReader 就是为了解决这问题, 当父容器固定尺寸, 子视图尺寸过大, 它会读取运行时父容器布局空间, 并以左上角为锚点对齐(真实锚点应该是平台有关, 本文以 iOS 为例.)
View 协议当作类型使用
View 是个协议, 无法直接当作类型使用, 可以使用 any View
代替, 或者返回具体的 View 类型
1
2
3
4
5
6
7
func getView() -> any View {
return Text("")
}
func getView() -> some View {
return Text("")
}
@StateObject 和 @EnvironmentObject
@StateObject 和 @EnvironmentObject 都是用于数据双向绑定对象的注解, 用于修饰 ObservableObject 的对象
@StateObject 用于声明 View 的成员变量, 需要初始化赋值.
@EnvironmentObject 用于声明环境变量, 通过 View.environmentObject() 赋给视图树 RootView 赋值, 之后整个视图树内的 View 可直接声明使用(由系统统一赋值).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DataModel: ObservableObject {
@Published var name: String = "点我试试"
}
struct BannerView: View {
// @StateObject var dataModel: DataModel
@EnvironmentObject var dataModel: DataModel
var body: some View {
Text(dataModel.name)
.onTapGesture {
dataModel.name = "试试就试试"
}
}
}
// BannerView(dataModel: DataModel())
BannerView().environmentObject(DataModel())
@EnvironmentObject 需要当前 View 添加到视图树中才会被系统赋值
同样只有 View 被加入到视图树中, View.onAppear 函数才会被调用
若 View 展示时需要加载网络数据, 更新 dataModel<=> View, 则必须先将 View 添加到视图树这就意味着: 当子视图使用环境变量的时候, 若未被安装到视图树, 则环境变量因未赋值而 Crash
@ViewBuilder
@ViewBuilder 是一个注解, 如下代码会将函数体中所有 View 封装成一个整体 View 返回, 如果不用 @ViewBuilder, 则需要显示写明 return 某个具体 View
1
2
3
4
5
@ViewBuilder
func getView() -> some View {
Text("")
Image("")
}
View.mask 和 View.background / View.overlay
mask 就是整个图层最终绘制到屏幕上的部分, 比如一个矩形按钮, 上面有个 ‘➕’ 的 mask, 那么绘制到屏幕上最终视觉效果就是一个 ➕,
background 就是 View 的背景, 系统会给 View 分层, 将 backgound 独立一层绘制到 View 底下一层
overlay 就是 View 上盖一层, 系统会在 View 上新建一个图层来展示 overlay内容.
Image 实现 AspectFill 的 contentMode
SwiftUI 中 ContentMode 枚举仅有 fill / fit 两个状态, 且 View 关于内容函数 scaledToFill() / scaledToFit() 给人直观感受是只有两种内容模式, 实际上可以通过组合的方式来实现其他mode, 比如 aspectFill.
1
2
3
4
5
6
7
8
9
10
11
12
// 实现 aspectFill 实现
Image(uiImage: UIImage.init(named: "1")!)
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width)
// 更直观的查看效果 - 可以用下面代码来看直接效果
Ellipse()
.fill(Color.purple)
.aspectRatio(CGSize(width: 3, height: 4), contentMode: .fit)
.frame(width: 200, height: 200)
.border(Color(white: 0.75))
Shape
..
THE END.