SwiftUI中的一切都是视图。
SwiftUI布局基础
1 | struct ContentView: View { |
这段代码包含三个视图:
视图等级底部的文本(图中Hello World)
内容视图(和文本的布局一致,即图中Hello World四周白线内)
根视图(屏幕Size - 安全区域)
如果想将根视图扩展到安全区,可以使用
edgesIgnoringSafeArea(.all)
修饰器
当然,文本和文本的内容视图,我们通常当做同一个来操作
在SwiftUI中,不能给子视图强制规定一个尺寸,而是应该有父视图决定
布局步骤
- 父视图提供给子视图一个Size
- 子视图决定自身的Size(子视图也许不能完全使用父视图的Size)
- 父视图将子视图放在其坐标系中
- SwiftUI会让视图坐标像最接近的像素值取整
例一:查看一段代码的布局:
1 | var body: some View { |
在设置background
、padding
修饰器时,会在Text视图和根视图中间插入对应的背景视图和边距视图
例二:图片的原尺寸为20x20,我们希望1.5倍尺寸展示图片
1 | struct ContentView: View { |
做法:
1 | .frame(width: 30, height: 30) |
效果:图片尺寸不会发生变化,但是在图片周围会插入一个30x30尺寸的Frame视图
在SwiftUI中frame并不是一个重要的布局元素,它其实只是一个View。
例三:
1 | // 子视图必须平等竞争一个空间 |
设置文字底基线对齐
设置图片的底基线
例四:让不同容器中的视图对齐
自定义对齐方式
1
2
3
4
5
6
7
8
9extension VerticalAlignment {
private enum MidStarAndTitle : AlignmentID {
// 告诉SwiftUI如何计算默认值
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.bottom]
}
}
static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
}设置文字的基线
SwiftUI绘图
SwiftUI默认提供了多种样式的图形,比如圆形,胶囊和椭圆
实现渐变色
角渐变色
使用角渐变色填充圆
使用渐变色填充圆环
实现复杂图形绘制
完整代码可参见:官方Demo
总体步骤主要包括:
- 创建单个楔形的数据模型
1
2
3
4
5
6
7
8
9
10
11class Ring: ObservableObject {
/// A single wedge within a chart ring.
struct Wedge: Equatable {
/// 弧度值(所有楔形的弧度值之合最大为2π,即360°)
var width: Double
/// 横轴深度比例 [0,1]. (用来计算楔形的长度)
var depth: Double
/// 颜色值
var hue: Double
}
}- 绘制单个子图形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18struct WedgeShape: Shape {
func path(in rect: CGRect) -> Path {
// WedgeGeometry是用来计算绘制信息的类,详细代码见Demo。
let points = WedgeGeometry(wedge, in: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius,
startAngle: .radians(wedge.start), endAngle: .radians(wedge.end),
clockwise: false)
path.addLine(to: points[.bottomTrailing])
path.addArc(center: points.center, radius: points.outerRadius,
startAngle: .radians(wedge.end), endAngle: .radians(wedge.start),
clockwise: true)
path.closeSubpath()
return path
}
// ···
}- 用ZStack组装所有的楔形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let wedges = ZStack {
ForEach(ring.wedgeIDs, id: \.self) { wedgeID in
WedgeView(wedge: self.ring.wedges[wedgeID]!)
// use a custom transition for insertions and deletions.
.transition(.scaleAndFade)
// remove wedges when they're tapped.
.onTapGesture {
withAnimation(.spring()) {
self.ring.removeWedge(id: wedgeID)
}
}
}
// 如果不加这个Spacer(),会使Mac程序,在没添加任何楔形时,APP尺寸为0。
Spacer()
}为了更好的理解这个工程,你还需要一些知识:
如何使用
Animatable
自定义复杂的动画假如我们想实现一个简单的SwiftUI动画,比如点击Button按钮渐变消失,我们可以这样来实现:
1
2
3
4
5
6
7
8
9@State private var hidden = false
var body: some View {
Button("Tap Me") {
self.hidden = true
}
.opacity(hidden ? 0 : 1)
.animation(.easeInOut(duration: 2))
}在这个例子中,我们使用
@State
修饰变量hidden
,当hidden
的值发生变化时,SwiftUI会自动为我们处理渐变动画。而SwiftUI能够为我们自动执行动画的前提是:SwiftUI已经知道要如果展示该动画效果,那什么情况下,SwiftUI不知道要如何展示动画呢?比如:我们通过下面代码绘制出了多角形。
1
Shape(sides: 3)
该方法支持传入不同的值,生成不同的多边形。如果我们希望从三边形变成四边型,那么可以写如下代码:
1
2
3Shape(sides: isSquare ? 4 : 3)
.stroke(Color.blue, lineWidth: 3)
.animation(.easeInOut(duration: duration))但是运行代码后发现,这段代码并无动画过渡效果。这是因为在执行动画的过程中,SwiftUI会从起始状态到终止状态分成不同的阶段来绘制,像
opacity
从0-1,可能会分成0,0.1,0.2,0.3,···,0.9,1.0,SwiftUI依次进行绘制,从而展示过渡状态。同理,从三角形变到四边形,SwiftUI也需要绘制中间状态,但SwiftUI并不知道该如何绘制3.5边形。这时就需要我们自己来告诉SwiftUI该如何绘制了。
animatableData
是Animatable
协议中,唯一需要实现的方法,通过这个方法来告诉SwiftUI需要监听哪些属性的变化。1
2
3
4
5// 代表需要监听的值为Float类型
var animatableData: Float {
get { //··· }
set { //··· }
}然而并不是所有类型的属性都能够被SwiftUI所监听,只有遵循
VectorArithmetic
协议的对象AnimatablePair
,CGFloat
,Double
,EmptyAnimatableData
andFloat
才能被SwiftUI监听。要实现Demo中楔形图片变换的效果,需要监听的值有
start
,end
,depth
和hue
这四个值。animatableData
属性的返回值也应该包含这四个值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15extension Ring.Wedge: Animatable {
typealias AnimatableData = AnimatablePair<AnimatablePair<Double, Double>, AnimatablePair<Double, Double>>
var animatableData: AnimatableData {
get {
.init(.init(start, end), .init(depth, hue))
}
set {
start = newValue.first.first
end = newValue.first.second
depth = newValue.second.first
hue = newValue.second.second
}
}
}接下来在这四个属性发生变化时,SwiftUI会通过函数
func path(in rect: CGRect) -> Path {}
重新绘制,这样就可以展示动画的过渡效果了
一些琐碎的知识点
使用
drawingGroup()
提高复杂UI的渲染效率以往每创建一个楔形,都是一个单独的View,当楔形数量非常多时,再加上每个View都在执行动画,非常耗费性能。在SwiftUI中,可以通过
drawingGroup()
将相同类型的View通过Metal绘制在一张画布上,从而减少渲染耗费的性能,避免卡顿。使用
Equatable
防止视图的新值和旧值相同时,更新子视图1
2
3struct Wedge: Equatable {
// ···
}使用
PassthroughSubject
通知SwiftUI值发生变化PassthroughSubject
- 使用
PassthroughSubject
通知绑定的属性的视图,属性发生变化,需要重新绘制。
1
2
3
4
5
6
7let objectWillChange = PassthroughSubject<Void, Never>()
private(set) var wedgeIDs = [Int]() {
willSet {
objectWillChange.send()
}
}- 在
contentView
中监听了Ring
模型
1
@EnvironmentObject var ring: Ring
因此在Ring模型的
wedgeIDs
发生变化时,会发出通知告知contentView
使用其绘制的地方,需要重新绘制- 使用
CurrentValueSubject
我们常用的
@Published
属性包装器,实际上就是一种CurrentValueSubject
简单来说:
PassthroughSubject
用于表示事件。CurrentValueSubject
用于表示状态。用现实世界的案例进行类比。PassthroughSubject = 门铃按钮,当有人按门时,只有在你在家时才会收到通知。CurrentValueSubject = 电灯开关,当你在外面时,有人打开了您家中的灯。你回到家,你知道有人打开了它们。
参考:
https://stackoverflow.com/questions/60482737/what-is-passthroughsubject-currentvaluesubject