SwiftUI基础篇Animation(下)

Animation

概述

文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,Animation未做调试结果,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接

1、通过transition添加和删除视图

可以在设计中包含和排除一个视图,只需要使用一个常规的判断条件即可。

Swift 复制代码
struct FFTransitionView: View {
    @State private var showDetails = false
    @State private var showDetails1 = false
    
    var body: some View {
        //例如,点击按钮添加或删除文本
        VStack {
            Button("Press to show details") {
                withAnimation {
                    showDetails.toggle()
                }
            }
            if showDetails {
                Text("Details go here")
            }
        }
        //默认情况下,SwiftUI使用淡出动画来插入或闪促视图,但是想要自定义效果,
        //可以使用transition()修饰符来自定义
        VStack {
            Button("Press to show details") {
                withAnimation {
                    showDetails1.toggle()
                }
            }
            
            if showDetails1 {
                Text("Details go here")
                    .transition(.move(edge: .bottom))
                
                Text("Details go here")
                    .transition(.slide)
                
                Text("Details go here")
                    .transition(.scale)
            }
        }
    }
}

2、组合transition

当添加或删除视图时,SwiftUI可以使用combined(with:)方法组合过度来制作新的动画。

AnyTransition扩展

为了使组合转换更容易使用和重用,可以在AnyTransition上创建他们作为扩展

Swift 复制代码
extension AnyTransition {
    static var moveAndScale: AnyTransition {
        AnyTransition.move(edge: .bottom).combined(with: .scale)
    }
}

combined多组transition

Swift 复制代码
struct FFTransitionsCombine: View {
    @State private var showDetails = false
    @State private var showDetails2 = false
        
    var body: some View {
        //例如,同时让视图移动和淡出
        VStack {
            Button("Press to show Details") {
                withAnimation {
                    showDetails.toggle()
                }
            }
            
            if showDetails {
                Text("Details go here")
                    .transition(AnyTransition.opacity.combined(with: .slide))
            }
        }
        
        VStack {
            Button("Press to show Details") {
                withAnimation {
                    showDetails2.toggle()
                }
            }
            //已经使用扩展声明了函数moveAndscale
            if showDetails2 {
                Text("Details go here")
                    .transition(.moveAndScale)
            }
        }
    }
}

3、创建不对称transition

SwiftUI可以在添加一个视图时指定transition,在删除时指定另一个transition。所有这些都是使用asymmetric()完成的。

Swift 复制代码
struct FFTransitionAsymmetric: View {
    @State private var showDetails = false
    
    var body: some View {
        //创建一个使用不对称过渡的文本视图:添加时从左面出现,删除时向下移动
        VStack {
            Button("Press to show details") {
                withAnimation {
                    showDetails.toggle()
                }
            }
            
            if showDetails {
                Text("Details go here")
                    .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))
            }
        }
    }
}

4、自定义transition

虽然SwiftUI自带了一系列的转场动画,也可以自定义transition,这个过程分为三个步骤:

  1. 创建一个ViewModifier来表示转换的任何状态
  2. 创建一个AnyTransition扩展,该扩展使用活动状态和标识状态的视图修饰符
  3. 使用transition()修饰符将动画应用到视图上

具体实操步骤

编写一个形状和视图修饰符组合,模仿Keynote中的虹膜动画:

  1. 定义一个ScaledCircle形状,它在一个矩形中创建一个圆,该矩形根据一些可动画化的数据进行缩放。
  2. 创建一个自定义ViewModifier结构体,将任何形状(缩放后的圆)应用于另一个视图的剪辑形状。
  3. 在其包装在AnyTransition扩展中,以便将该修饰符包装在transition中,以便于调用。

ScaledCircle

Swift 复制代码
struct ScaledCircle: Shape {
    //控制绘制矩形内圆的大小。但为0时,圆不可见,当为1时,圆填充矩形
    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)
    }
}

ClipShapeModifier

通用修饰符,可以剪辑任何形状的视图。

Swift 复制代码
struct ClipShapeModifier<T: Shape>: ViewModifier {
    let shape: T
    
    func body(content: Content) -> some View {
        content.clipShape(shape)
    }
}

AnyTransition

结合ScaledCircle和ClipShapeModifier的自定义transition

Swift 复制代码
extension AnyTransition {
    static var iris: AnyTransition {
        .modifier(
            active: ClipShapeModifier(shape: ScaledCircle(animatableData: 0)),
            identity: ClipShapeModifier(shape: ScaledCircle(animatableData: 1)))
    }
}

使用自定义transition

Swift 复制代码
struct FFTransitionCustom: 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()
            }
        }
    }
}

5、Text的Animation

从iOS16以后,SwiftUI可以将Text动画化。因此像这样的代码可以在两种不同的大小之间苹果的显示动画,自动的重新渲染文本。

Swift 复制代码
struct FFAnimateTextSize: View {
    @State private var fontSize = 32.0
    
    var body: some View {
        Text("Hi, metaBBLv")
            .font(.custom("Georgia", size: fontSize))
            .onTapGesture {
                withAnimation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 1).repeatForever()) {
                    fontSize = 72
                }
            }
    }
}

6、使用transactions重写动画

SwiftUI提供了一个withTransaction()函数,可以在运行时重写动画,例如删除隐式动画并用自定义内容替换他。

Swift 复制代码
struct FFAnimationsOverride: View {
    @State private var isZoomed = false
    @State private var isZoomed1 = false
    @State private var isZoomed2 = false
    
    var body: some View {
        VStack {
            Button("Toggle zoom") {
                isZoomed.toggle()
            }
            Spacer()
                .frame(height: 50)
            Text("Zoom text")
                .font(.title)
                .scaleEffect(isZoomed ? 3 : 1)
                .animation(.easeInOut(duration: 2), value: isZoomed)
        }
        //transactions可以覆盖现有的动画。例如,你可能决定在一个特定情况下,文本的动画以一种款素、线性的方式发生,而不是先有的动画。
        //要做到这一点,首先使用想要的动画创建一个新的Transaction实例,然后将他的disablesAnimations值设置为true,这样就可以覆盖任何的现有动画。当一切准备好时,使用withTranscation()。然后继续调整你想要要变的状态。它将使用你的Transcation被动画化。
        Spacer()
        VStack {
            Button("Toggle zoom") {
                var transaction = Transaction(animation: .linear)
                transaction.disablesAnimations = true
                
                withTransaction(transaction) {
                    isZoomed1.toggle()
                }
            }
            Spacer()
                .frame(height: 50)
            Text("Zoom text")
                .font(.title)
                .scaleEffect(isZoomed1 ? 3 : 1)
                .animation(.easeInOut(duration: 2), value: isZoomed1)
        }
        Spacer()
        //对于更多的控制,可以将transaction()修饰符附加到任何视图上。从而可以覆盖该应用在该视图的任何事物。
        VStack {
            Button("Toggle Zoom") {
                var transaction = Transaction(animation: .linear)
                transaction.disablesAnimations = true
                
                withTransaction(transaction) {
                    isZoomed2.toggle()
                }
            }
            Spacer()
                .frame(height: 50)
            Text("Zoom Text 1")
                .font(.title)
                .scaleEffect(isZoomed2 ? 3 : 1)
            Spacer()
                .frame(height: 50)
            Text("Zoom Text 2")
                .font(.title)
                .scaleEffect(isZoomed2 ? 3 : 1)
                .transaction { t in
                    t.animation = .none
                }
        }
    }
}

7、当动画完成时运行一个回调函数

可以选择为SwiftUI的withAnimation()函数提供完成后的回调,并在动画完成时运行代码。这可能是调整某些程序状态的地方,但可以将其用作将动画连接在一起的简单方法。对一个事物进行动画处理,然后对其他事物进行动画处理。

Swift 复制代码
struct FFAnimationFinishCallback: View {
    @State private var scaleUp = false
    @State private var fadeOut = false
    
    @State private var scaleUp1 = false
    @State private var fadeOut1 = false
        
    var body: some View {
        //点击按钮然后放大并淡出
        Button("Tap Me!") {
            withAnimation {
                scaleUp = true
            } completion: {
                withAnimation {
                    fadeOut = true
                }
            }

        }
        .scaleEffect(scaleUp ? 3 : 1)
        .opacity(fadeOut ? 0 : 1)
        //这里有一个小细节可能会让你感觉到震惊,如果你使用弹簧动画,则最后可能会出现很长的运动尾部,其中你的动画正在移动用户无法察觉的。
        //默认行为withAnimation()是认为动画完成的,及时荏苒发生微笑运动的肠胃,但如果希望100%完成,可以覆盖默认值。
        Button("Tap Me!") {
            withAnimation(.bouncy, completionCriteria: .removed) {
                scaleUp1 = true
            } completion: {
                withAnimation {
                    fadeOut1 = true
                }
            }
        }
        .scaleEffect(scaleUp1 ? 3 : 1)
        .opacity(fadeOut1 ? 0 : 1)
        //对于更复杂的效果,请考虑使用相位动画器而不是动画完成的闭包
    }
}

8、使用相位动画器创建多步动画

SwiftUI的phaseAnimator视图和phaseAnimator修改器可以通过持续或触发时循环选择动画段来执行多步动画,创建这些多阶动画需要三个步骤:

  1. 定义阶段,可以是任何类型的序列,但使用枚举最简单CaseIterable。
  2. 读取相位动画中的一个相位,并调整视图以匹配想要该相位的外观。
  3. 添加一个触发器,使相位动画从头开始重复其序列

enum

Swift 复制代码
//通过枚举设置
enum AnimationPhase: Double, CaseIterable {
    case fadingIn = 0
    case middle = 1
    case zoomingOut = 3
}

//可以根据你的命令触发动画序列,而不是无休止的重复。为此,可以添加一个触发器值并且通过SwiftUI监视,例如随机数UUID或递增数,当值发生变化时,开始动画并完整播放。
enum AnimationPhase1: CaseIterable {
    case start, middle, end
}

//向动画增加计算属性
enum AnimationPhase2: CaseIterable {
    case fadingIn, middle, zoomingOut
    
    var scale: Double {
        switch self {
        case .fadingIn: 0
        case .middle: 1
        case .zoomingOut: 3
        }
    }
    
    var opacity: Double {
        switch self {
        case .fadingIn: 0
        case .middle: 1
        case .zoomingOut: 0
        }
    }
}

使用上面的enum做的多种例子

Swift 复制代码
struct FFAnimationmulti_step: View {
    @State private var animationStep = 0
    @State private var animationStep1 = 0
    
    var body: some View {
        //例如,创建一个简单动画,使某些文本开始很小且不可见,放大到自然大小并完全不透明,然后放大到非常大且不可见。
        Text("Hi, metaBBLv")
            .font(.largeTitle)
            .phaseAnimator([0,1,3]) { view, phase in
                view
                    .scaleEffect(phase)
                    .opacity(phase == 1 ? 1 : 0)
            }
        //由于没有提供触发器,所以它将永远运行。
        //使用包装视图PhaseAbunator来编写它,它的优点是多个视图可以在阶段之间一起移动
        VStack(spacing: 50) {
            PhaseAnimator([0,1,2]) { value in
                Text("Hi, metaBBLv")
                    .font(.largeTitle)
                    .scaleEffect(value)
                    .opacity(value == 1 ? 1 : 0)
                Text("Goodbye, metaBBLv")
                    .font(.largeTitle)
                    .scaleEffect(3 - value)
                    .opacity(value == 1 ? 1 : 0)
            }
        }
        Text("Hi, metaBBLv")
            .font(.largeTitle)
            .phaseAnimator(AnimationPhase.allCases) { view, phase in
                view
                    .scaleEffect(phase.rawValue)
                    .opacity(phase.rawValue == 1 ? 1 : 0)
            }
        //在下面的例子中,点击按钮会触发使用枚举情况的三步动画。首先,定义所需的各种动画阶段,然后每当属性发生变化时都会遍历。
        Button("Tap Me!") {
            animationStep += 1
        }
        .font(.largeTitle)
        .phaseAnimator(AnimationPhase1.allCases, trigger: animationStep) { content, phase in
            content
                .blur(radius: phase == .start ? 0 : 10)
                .scaleEffect(phase == .middle ? 3 : 1)
        }
        //为了获得更多的控制,可以准确指定每个阶段使用那个动画。.bouncy在快速和慢速动画之间移动.easeInOut获得更多的变化。
        Button("Tap Me 1") {
            animationStep1 += 1
        }
        .font(.largeTitle)
        .phaseAnimator(AnimationPhase1.allCases, trigger: animationStep1) { content, phase in
            content
                .blur(radius: phase == .start ? 0 : 10)
                .scaleEffect(phase == .middle ? 3 : 1)
        } animation: { phase in
            switch phase {
            case .start, .end: .bouncy
            case .middle: .easeInOut(duration: 2)
            }
        }
        //由于通过计算属性添加了动画,所以在调用时更加的简洁
        Text("Hi, metaBBLv")
            .font(.largeTitle)
            .phaseAnimator(AnimationPhase2.allCases) { content, phase in
                content
                    .scaleEffect(phase.scale)
                    .opacity(phase.opacity)
            }

    }
}

关于Animation

先对对于认真对照demo练习的小伙伴,说声sorry。关于Animation这部分代码拆分的不好,尝试过将其分开,像前面的部分一样,但是也考虑到一些小伙伴所见即所得,就贴了整块的代码,CV即见结果。在另外一个角度,Animation相关代码确实相对较多,复杂度一般,但是涉及了一些extension和独立struct的运用,个人能力有限,没找啥太好的办法完美呈现。最后想说,我也是练习过程,意在熟能生巧,希望同学见谅。

相关推荐
humiaor18 小时前
Xcode报错:No exact matches in reference to static method ‘buildExpression‘
swiftui·xcode
humiaor9 天前
Xcode报错:Return from initializer without initializing all stored properties
swiftui·binding
墨痕诉清风9 天前
苹果软件产品使用的 TCP 和 UDP 端口列表(apple、mac)
网络协议·tcp/ip·apple
HyperAI超神经10 天前
Apple Intelligence深夜炸场!苹果发布4颗自研芯片,iPhone/iWatch/AirPods大升级
ios·iphone·apple·apple 发布会·iphone 16·a18芯片
喜好儿aigc15 天前
Apple “Glowtime”活动:iPhone 16、Apple Intelligence亮相
ios·aigc·iphone·apple·苹果
2401_854391081 个月前
SwiftUI 革命:打造未来派用户界面的艺术
ui·ios·swiftui
肥肥呀呀呀1 个月前
苹果应用提审基本规范
apple·提审
Swift社区2 个月前
Swift 中的函数式核心与命令式外壳:单向数据流
ios·swiftui·swift
一牛2 个月前
Swift Actors: 防止数据竞争
swift·apple
东坡肘子2 个月前
肘子的 Swift 周报 #042| 经验是柄双刃剑
swiftui·swift·apple