Swift UI 是数据驱动,不是事件驱动
数据驱动与事件驱动的对比,
例子: 添加一个 popUp 视图
数据驱动, 最上方本来就有一个 popUp,用数据控制他的显示与隐藏
事件驱动,是创建一个 popUp,添加在最上面
事件驱动,可能重复创建与添加,
数据驱动,安全些
效果 1,展开与收起
@State 修饰的属性, 作为驱动数据源, source of truth,
@State 修饰的属性改变,该 View 刷新
@State var showMoon: String? = nil
func toggleMoons(_ name: String) -> Bool {
return name == showMoon
}
下面的展示有三种情况,
- 初始化,格子们都是收起,点击一个展开
- 有一个格子展开,点击该格子,该格子收起
- 有一个格子 A 展开,点击格子 B,格子 A 收起, 格子 B 展开
上面的数据结构,清爽地解决,
初始化,showMoon = nil
, 不等于任一个格子带的属性 name,默认全部收起
其他两种情况,逻辑类似
Button(action: {
withAnimation(.easeInOut) {
self.showMoon = self.toggleMoons(planet.name) ? nil : planet.name
}
}) {
Image(systemName: "moon.circle.fill")
}
if self.toggleMoons(planet.name) {
MoonList(planet: planet)
}
效果 2,自定义转场 Transition 动画
asymmetric, 不对称动画,
出场的动画 inserted,
与移除的动画 removed,
可以不一样
extension AnyTransition {
static var customTransition: AnyTransition {
let insertion = AnyTransition.move(edge: .top)
.combined(with: .scale(scale: 0.2, anchor: .topTrailing))
.combined(with: .opacity)
let removal = AnyTransition.move(edge: .top).combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
Transition 动画,还可以比较自由的组合效果
combined
效果 3,封面的星星轨道
轨道动画,需要用到 GeometryEffect ,几何图形动画
3.1 GeometryEffect 几何效果
GeometryEffect 协议,自带方法
func effectValue(size: CGSize) -> ProjectionTransform
这个方法提供动画点的某一时刻的计算坐标
3.1.1, GeometryEffect 协议,继承自 Animatable 协议
Animatable 自带属性,
var animatableData: Self.AnimatableData
动画时间内,动画属性 animatableData ,
在开始值和最终值,之间变化
3.1.2, 圆环轨道效应 OrbitEffect
就是给一个区域,在这个区域里面画圆
animatableData 干涉角度 angle,起作用
struct OrbitEffect: GeometryEffect {
// 初始值,任意
let initialAngle = CGFloat.random(in: 0 ..< 2 * .pi)
var angle: CGFloat = 0
let radius: CGFloat
var animatableData: CGFloat {
get { return angle }
set { angle = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
// 计算圆公式,很简单
let pt = CGPoint(x: cos(angle + initialAngle) * radius,
y: sin(angle + initialAngle) * radius)
let translation = CGAffineTransform(translationX: pt.x, y: pt.y)
return ProjectionTransform(translation)
}
}
3.1.3 , self.animationFlag ? 2 * .pi : 0
决定了动画的 from & to
func makeOrbitEffect(diameter: CGFloat) -> some GeometryEffect {
return OrbitEffect(angle: self.animationFlag ? 2 * .pi : 0,
radius: diameter / 2.0)
}
3.2, 错误调用示范
出现就刷新,调用动画
onAppear
的时机中,刷新 @State
修饰的值
本场景下,进入界面,就出现了 onAppear
, 调用了轨道动画
展开后,该轨道动画会出现错位,bug 明显
@State private var animationFlag = false
func makeSystem(_ geometry: GeometryProxy) -> some View {
let planetSize = geometry.size.height * 0.25
let moonSize = geometry.size.height * 0.1
let radiusIncrement = (geometry.size.height - planetSize - moonSize) / CGFloat(moons.count)
let range = 1 ... moons.count
return
ZStack {
// ...
ForEach(range, id: \.self) { index in
// individual "moon" circles
self.moon(planetSize: planetSize, moonSize: moonSize, radiusIncrement: radiusIncrement, index: CGFloat(index))
.modifier(self.makeOrbitEffect(
diameter: planetSize + radiusIncrement * CGFloat(index)
))
.animation(Animation
.linear(duration: Double.random(in: 10 ... 100))
.repeatForever(autoreverses: false)
)
}
}
.onAppear {
self.animationFlag.toggle()
}
}
func makeOrbitEffect(diameter: CGFloat) -> some GeometryEffect {
return OrbitEffect(angle: self.animationFlag ? 2 * .pi : 0,
radius: diameter / 2.0)
}
3.3, 正确调用示范
上面的代码很合理,但是运行出问题,
因为他在方法中修改 @State
修饰的属性,
该方法为
@inlinable public func onAppear(perform action: (() -> Void)? = nil) -> some View
不具有逃逸效果,超出该函数的作用域,依然可以执行,
改为如下,则 OK
@State private var animationFlag = false
var body: some View {
GeometryReader { geometry in
self.makeSystem(geometry) {
animationFlag.toggle()
}
}
}
func makeSystem(_ geometry: GeometryProxy, action completionHandler: @escaping () -> Void) -> some View {
let planetSize = geometry.size.height * 0.25
let moonSize = geometry.size.height * 0.1
let radiusIncrement = (geometry.size.height - planetSize - moonSize) / CGFloat(moons.count)
let range = 1 ... moons.count
return
ZStack {
ForEach(range, id: \.self) { index in
// individual "moon" circles
self.moon(moonSize: moonSize)
.modifier(self.makeOrbitEffect(
diameter: planetSize + radiusIncrement * CGFloat(index)
))
.onAppear {
withAnimation(Animation
.linear(duration: Double.random(in: 10 ... 100))
.repeatForever(autoreverses: false)) {
completionHandler()
}
}
}
}
}
3.3.1 SwiftUI 的 debug print
View 里面的代码,只能声明式,
添加 View 、或者条件判断语句 ( if else )
extension View {
func Print(_ vars: Any...) -> some View {
for v in vars { print(v) }
return EmptyView()
}
}
3.3.2 SwiftUI 使用 func 取代 custom View
当 Model 属性计算与 View 创建,混合在一起的时候,
使用 func 创建 View,
使用 custom View, 直接声明式写 UI
声明式 UI, 代码结构与 UI 高度一致