SwiftUI中的一切都是视图。
文章来源:Building Custom Views with SwiftUI
更多SwiftUI文章:
SwiftUI布局基础
struct ContentView: View {
var body: some View {
Text("Hello, world!")
// .edgesIgnoringSafeArea(.all)
}
}
复制代码
这段代码包含三个视图:
-
视图等级底部的文本(图中Hello World)
-
内容视图(和文本的布局一致,即图中Hello World四周白线内)
-
根视图(屏幕Size - 安全区域)
如果想将根视图扩展到安全区,可以使用
edgesIgnoringSafeArea(.all)
修饰器
当然,文本和文本的内容视图,我们通常当做同一个来操作
在SwiftUI中,不能给子视图强制规定一个尺寸,而是应该有父视图决定
布局步骤
- 父视图提供给子视图一个Size
- 子视图决定自身的Size(子视图也许不能完全使用父视图的Size)
- 父视图将子视图放在其坐标系中
- SwiftUI会让视图坐标像最接近的像素值取整
例一:查看一段代码的布局:
var body: some View {
Text("Avocado Toast")
.padding(10)
.background(Color.green)
}
复制代码
在设置background
、padding
修饰器时,会在Text视图和根视图中间插入对应的背景视图和边距视图
例二:图片的原尺寸为20x20,我们希望1.5倍尺寸展示图片
struct ContentView: View {
var body: some View {
Image("20x20_avoado")
}
}
复制代码
做法:
.frame(width: 30, height: 30)
复制代码
效果:图片尺寸不会发生变化,但是在图片周围会插入一个30x30尺寸的Frame视图
在SwiftUI中frame并不是一个重要的布局元素,它其实只是一个View。
例三:
// 子视图必须平等竞争一个空间
HStack {
Text("Delicious")
Image("20x20_avocado")
Text("Avocado Toast")
}
.lineLimit(1)
复制代码
-
设置文字底基线对齐
-
设置图片的底基线
例四:让不同容器中的视图对齐
-
自定义对齐方式
extension 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
总体步骤主要包括:
- 创建单个楔形的数据模型
class 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 } } 复制代码
- 绘制单个子图形
struct 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组装所有的楔形
let 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按钮渐变消失,我们可以这样来实现:
@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不知道要如何展示动画呢?比如:我们通过下面代码绘制出了多角形。
Shape(sides: 3) 复制代码
该方法支持传入不同的值,生成不同的多边形。如果我们希望从三边形变成四边型,那么可以写如下代码:
Shape(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需要监听哪些属性的变化。// 代表需要监听的值为Float类型 var animatableData: Float { get { //··· } set { //··· } } 复制代码
-
然而并不是所有类型的属性都能够被SwiftUI所监听,只有遵循
VectorArithmetic
协议的对象AnimatablePair
,CGFloat
,Double
,EmptyAnimatableData
andFloat
才能被SwiftUI监听。 -
要实现Demo中楔形图片变换的效果,需要监听的值有
start
,end
,depth
和hue
这四个值。animatableData
属性的返回值也应该包含这四个值。extension 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
防止视图的新值和旧值相同时,更新子视图struct Wedge: Equatable { // ··· } 复制代码
-
使用
PassthroughSubject
通知SwiftUI值发生变化-
PassthroughSubject
- 使用
PassthroughSubject
通知绑定的属性的视图,属性发生变化,需要重新绘制。
let objectWillChange = PassthroughSubject<Void, Never>() private(set) var wedgeIDs = [Int]() { willSet { objectWillChange.send() } } 复制代码
- 在
contentView
中监听了Ring
模型
@EnvironmentObject var ring: Ring 复制代码
因此在Ring模型的
wedgeIDs
发生变化时,会发出通知告知contentView
使用其绘制的地方,需要重新绘制 - 使用
-
CurrentValueSubject
我们常用的
@Published
属性包装器,实际上就是一种CurrentValueSubject
简单来说:
PassthroughSubject
用于表示事件。CurrentValueSubject
用于表示状态。用现实世界的案例进行类比。PassthroughSubject = 门铃按钮,当有人按门时,只有在你在家时才会收到通知。CurrentValueSubject = 电灯开关,当你在外面时,有人打开了您家中的灯。你回到家,你知道有人打开了它们。
-
参考: