transition主要是用来设置组件的入场还有离场动画的,需要注意的是在transition里设置的过度方式后需要加上.animation方法,不然会出现动画失效的情况。
.transition(
.asymmetric(
insertion: .scale(scale: 0, anchor: UnitPoint(x: offset.x / blockSize + 0.5, y: offset.y / blockSize + 0.5))
.animation(.linear(duration: 0.4).delay(0.3)),
removal: .opacity.animation(nil)
)
)
例如这里的.scale和.opacity都有加.animation来指定动画的变化方式。
组件的入场动画也可以使用另一种方式,离场动画就比较复杂,
struct A: View {
@State private var appeared: Bool = false
var body: some View {
Text("Hello World")
.opacity(appeared ? 1 : 0)
.animation(.easeOut, value: appeared)
.onAppear(perform: {
appeared.toggle()
})
}
}
withAnimation{}包裹的操作所引起的组件更新都会产生动画效果,官方文档说是最终引起动画效果的是那些叶子组件,这里我猜想应该是类似于Text,Rectangle等SwiftUI提供的基础组件都实现了一些协议或方法,使得指定了动画效果的那次更新会将产生更新的那些叶子组件执行动画过渡。
与withAnimation类似,但是.animation()作用于某个组件,使得这个组件的子叶子组件更新会附带动画效果,.animation()可以附加第二个参数value,使得这个value值变化才会引起动画效果。
.animation指定可以更加具体,实际项目中的数据变化过程中在加入withAnimation在model层会很难看,不利于维护,在一些接近叶子组件的组件中并且可以预见所引起的更新可以使用withAnimation ,还有就是如果出现并不是某个值改变就一定要动画,只有变成指定数字时才会出现动画时,这时候用withAnimation更合适。
直接上代码:
struct PolygonShape: Shape {
var sides: Double
var scale: Double
var animatableData: AnimatablePair<Double, Double> {
get { AnimatablePair(sides, scale) }
set {
sides = newValue.first
scale = newValue.second
}
}
init(sides: Int, scale: Double) {
self.sides = Double(sides)
self.scale = scale
}
func path(in rect: CGRect) -> Path {
// hypotenuse
let h = Double(min(rect.size.width, rect.size.height)) / 2.0
// center
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var path = Path()
let extra: Int = Double(sides) != Double(Int(sides)) ? 1 : 0
for i in 0..<Int(sides) + extra {
let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180
// Calculate vertex position
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))
if i == 0 {
path.move(to: pt) // move to first vertex
} else {
path.addLine(to: pt) // draw line to next vertex
}
}
path.closeSubpath()
return path
}
}
struct Wrapper: View {
@State var sides: Int = 1
@State var scale: Double = 1
var body: some View {
VStack {
PolygonShape(sides: sides, scale: scale)
.stroke(Color.blue, lineWidth: 3)
.frame(width: 300, height: 300, alignment: .center)
.animation(.easeInOut(duration: 2))
HStack {
Button(action: {
sides = 1
scale = 1
}, label: {
Text("1")
.foregroundColor(.white)
.font(.system(size: 18))
.frame(width: 50, height: 35, alignment: .center)
.background(Color.green)
.cornerRadius(6)
})
Button(action: {
sides = 3
scale = 0.7
}, label: {
Text("3")
.foregroundColor(.white)
.font(.system(size: 18))
.frame(width: 50, height: 35, alignment: .center)
.background(Color.green)
.cornerRadius(6)
})
Button(action: {
sides = 7
scale = 0.4
}, label: {
Text("7")
.foregroundColor(.white)
.font(.system(size: 18))
.frame(width: 50, height: 35, alignment: .center)
.background(Color.green)
.cornerRadius(6)
})
Button(action: {
sides = 30
scale = 1
}, label: {
Text("30")
.foregroundColor(.white)
.font(.system(size: 18))
.frame(width: 50, height: 35, alignment: .center)
.background(Color.green)
.cornerRadius(6)
})
}
}
}
}
swiftUI会自动控制animatableData属性,这个是Animatable协议里面的,个人猜想是:当PolygonShape里面的sides和scale改变后,swiftUI get animatable后发现数据变化,便会基于每一帧来set animatable值,从而更新sides和scale,当然动画也需要withAnimation或者.animation指定什么时候触发。Path里面的animatableData所引起的组件是每一帧都会更新的。
