转场:指的是定义视图如何从视图层中被【插入】或【移除】的。一个视图元素的出现和消失,就叫转场
transition
修饰器transition
转场必须要与 animation
配对使用,才会产生动画。例如:
VStack { ... }
.offset(y: geometry.size.height/2)
.transition(.offset(x: -600, y: 0))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0))
但 'animation
' 方法在 iOS 15.0 往后已经被弃用了,需要使用 withAnimation
或 animation(_:value:)
代替。但我们往往希望转场动画是自动开始的,这里的监视值 value
填什么呢?我们可能需要人为设置一个开关。例如:
struct AnimationTest: View {
@State private var animationAmount = 1.0
var body: some View {
Button("Tap Me") {
// important button function here
}
.overlay(
Circle()
.stroke(.indigo)
.scaleEffect(animationAmount)
.opacity(1.5 - animationAmount)
.transition(.offset(x: -600, y: 0))
.animation(.easeInOut(duration: 1).repeatForever(autoreverses: false),
value: animationAmount
)
)
// 在视图出现时改变前面设定的值,即可激活
.onAppear {
animationAmount = 1.5
}
}
}
<aside> 💡
注意:如果该视图是用 if 等判断控制是否显示的,注意 animation 修饰符要加到父元素上才有效。参见: 对处于判断条件下的视图,其视图的展示和隐藏是通过条件判断实现的,那它的动画修饰符最好加到父元素上,否则可能动画修饰符不会起效果。比如给元素A设置了 animation(.default),但是实际上A如果消失了,是看不到动画效果的,因为动画想发生时,A已经不在了。所以要把 animation(.default) 加到A的上一级父元素上。
</aside>
// 例如:设置以下代码,发现 CustomTabbarView 的转场效果没有生效
if showTabbar {
CustomTabbarView(selectedTab: $selectedTab, showAddProjectView: $showAddProjectView)
.transition(.move(edge: .bottom))
.animation(.easeInOut(duration: 3), value: showTabBar)
}
// 改用【把视图当作属性】的写法就可以了
// 1.把条件判断部分一起包裹成属性
@ViewBuilder
private var tabBarView: some View {
if showTabBar {
CustomTabbarView(selectedTab: $selectedTab, showAddProjectView: $showAddProjectView)
.transition(.move(edge: .bottom))
}
}
// 2.在视图中使用该属性,给它添加动画修饰符
tabBarView
.animation(.easeInOut(duration: 0.2), value: showTabBar)
在 SwiftUI 中,@ViewBuilder
和条件语句对视图的创建和转场效果有非常重要的影响。通过这个例子可以看到:
原本写法:
if showTabbar
是在普通的 View
代码块中执行的。SwiftUI 会根据 showTabbar
的状态判断是否渲染 CustomTabbarView
;这种情况下,SwiftUI 会把 transition
看作是 CustomTabbarView
的修饰符。然而,如果视图被移除(例如 showTabbar
变为 false
),SwiftUI 就没有机会处理这个转场效果,因为整个 CustomTabbarView
被从视图层级中删除了,导致 transition
无法执行使用 @ViewBuilder
的写法:
@ViewBuilder
是专门为创建复杂视图层次结构设计的,它允许你使用条件语句动态构建视图。通过将 @ViewBuilder
应用于一个 var
变量;在这种情况下会告诉 SwiftUI,即使状态改变时视图消失,SwiftUI 也需要处理视图层次的变化,从而保证动画和转场能正常生效。@ViewBuilder
创建视图,SwiftUI 能正确处理 showTabBar
状态的变化,不仅是移除或添加视图,它会识别这种状态变化,并且会根据 transition
指定的转场动画来处理视图的进入离开。因此即使 CustomTabbarView
被移除,SwiftUI 仍然会先应用转场动画,再删除视图。@ViewBuilder
可以告知 SwiftUI 需要动态创建和管理视图层次,这使 SwiftUI 可以正确地应用视图的转场效果关于 @ViewBuilder
的其他特性:
@ViewBuilder
允许在一个方法或计算属性中根据条件返回不同的视图@ViewBuilder
允许视图的创建被延迟到实际需要时。这对于条件渲染特别重要,因为它可以避免创建不需要显示的视图.transition
.animation
修饰符需要应用于整个条件块,而不仅仅是视图本身。@ViewBuilder
使得这成为可能@ViewBuilder
可以帮助 SwiftUI 正确地推断出条件渲染中的视图类型在视图后面添加 .transition
修饰符,即可创建普通转场。常见的内置转场类型包括: scale
、opaque
、offset
、move
、slide
等
// 可以理解成实际上是定义该视图的一个初始位置,swiftUI 自动生成动画
// 例如:从自身底部缩放
.transition(.scale(scale: 0, anchor: .bottom))
// 例如:从左侧-600的位置移入
.transition(.offset(x: -600, y: 0))
我们可以调用 combined(with:)
方法,将多个转场效果结合起来,打造混合式转场效果
//例如:结合移动和缩放
.transition(
AnyTransition.offset(x: -600, y: 0)
.combined(with: .scale)
)
//例如:结合移动和缩放和透明度 3 种
.transition(
AnyTransition.offset(x: -600, y: 0)
.combined(with: .scale)
.combined(with: .opacity)
)
前面提到的转场都是对称的,也就是插入和移除使用的是同样的转场效果。举例就是如果视图出现时是放大,那移除时就是缩小。如果希望插入和移除使用不一样的效果,就要使用 assymetric
方法,来指定 insertion
,和 removal
的转场。
.transition(
.asymmetric(
//视图出现时的效果
insertion: .scale(scale: 0, anchor: .bottom),
//视图移除时的效果
removal: .offset(x: -600, y: 0)
)
)
//【定义时】首先扩展 AnyTransition 类,如下:
extension AnyTransition {
// 然后声明一个自定义转场
static var offsetScaleOpacity: AnyTransition {
// 这个自定义转场混合了3种转场方式
AnyTransition.offset(x: -600, y: 0).combined(with: .scale).combined(with: .opacity)
}
}
//【使用时】给相应的视图添加 transition 修飾器,然后使用刚定义的转场属性:
Rectangle()
.transition(.offsetScaleOpacity)
// AnyTransition 的拓展,可以同时包含多个:
extension AnyTransition {
//第一个:对称的
static var scaleDownAndUp: AnyTransition {
AnyTransition.offset(y: 600).combined(with: .scale(scale: 0, anchor: .bottom)).combined(with: .opacity)
}
//第二个:非对称的
static var slideInAndOut: AnyTransition {
AnyTransition.asymmetric(
//标注颜色的这一行和对称转场的写法是一样的
insertion: AnyTransition.offset(x: 800).combined(with: .opacity).combined(with: .scale(scale: 0, anchor: .trailing)),
removal: AnyTransition.offset(x: -800).combined(with: .opacity).combined(with: .scale(scale: 0, anchor: .leading))
)
}
}
先构建一个自定义的视图修改器。然后对转场类进行拓展:
struct CornerRotateModifier: ViewModifier {
let amount: Double
let anchor: UnitPoint
func body(content: Content) -> some View {
content
.rotationEffect(.degrees(amount), anchor: anchor)
//这个clip可以让视图出到画面外时,不会被渲染出来
.clipped()
}
}
extension AnyTransition {
// 这是一个静态的计算型属性。静态表示它属于类型而不是实例,可以通过类型直接访问。
// **AnyTransition**:这是用于管理视图过渡的类型。它允许你定义视图切换时的过渡效果。
static var pivot: AnyTransition {
// 这是 AnyTransition 的一个构造器,表示我们要使用一个自定义的过渡效果。
.modifier(
// active 和 identity:这两个参数定义了在【激活状态】和【非激活状态】下的过渡效果
// 在这里,它们都使用了前面定义的 CornerRotateModifier 视图修改器进行定义初始和结束状态
active: CornerRotateModifier(amount: -90, anchor: .topLeading),
identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
)
}
}
完整例子:我们编写一个形状和视图修改器组合,模仿 Keynote 中的虹膜动画 —— 它会让新的幻灯片出现在扩大的圆圈中。
ScaledCircle
形状,该形状在矩形内创建一个圆形,该矩形根据一些可设置动画的数据进行缩放ViewModifier
结构以应用任何形状(在示例中为缩放后的圆形)作为另一个视图的剪辑形状AnyTransition
扩展中,以将该修饰符包装在转换中以便于访问struct ScaledCircle: Shape {
// This controls the size of the circle inside the
// drawing rectangle. When it's 0 the circle is
// invisible, and when it’s 1 the circle fills
// the rectangle.
var animatableData: Double
func path(in rect: CGRect) -> Path {
let maximumCircleRadius = sqrt(rect.width * rect.width + rect.height * rect.height)
let circleRadius = maximumCircleRadius * animatableData
let x = rect.midX - circleRadius / 2
let y = rect.midY - circleRadius / 2
let circleRect = CGRect(x: x, y: y, width: circleRadius, height: circleRadius)
return Circle().path(in: circleRect)
}
}
// A general modifier that can clip any view using a any shape.
struct ClipShapeModifier<T: Shape>: ViewModifier {
let shape: T
func body(content: Content) -> some View {
content.clipShape(shape)
}
}
// A custom transition combining ScaledCircle and ClipShapeModifier.
extension AnyTransition {
static var iris: AnyTransition {
.modifier(
active: ClipShapeModifier(shape: ScaledCircle(animatableData: 0)),
identity: ClipShapeModifier(shape: ScaledCircle(animatableData: 1))
)
}
}
// An example view move showing and hiding a red
// rectangle using our transition.
struct ContentView: View {
@State private var isShowingRed = false
var body: some View {
ZStack {
Color.blue
.frame(width: 200, height: 200)
if isShowingRed {
Color.red
.frame(width: 200, height: 200)
.transition(.iris)
.zIndex(1)
}
}
.padding(50)
.onTapGesture {
withAnimation(.easeInOut) {
isShowingRed.toggle()
}
}
}
}