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的运用,个人能力有限,没找啥太好的办法完美呈现。最后想说,我也是练习过程,意在熟能生巧,希望同学见谅。

相关推荐
struggle20252 天前
Ollmao (OH-luh-毛程序包及源码) 是一款原生 SwiftUI 应用程序,它与 Ollama 集成,可在 Mac 上本地运行强大的 AI 模型
ios·swiftui·swift
iOS阿玮1 个月前
“小红书”海外版正式更名“ rednote”,突然爆红的背后带给开发者哪些思考?
ios·app·apple
货拉拉技术1 个月前
货拉拉用户端SwiftUI踩坑之旅
ios·swiftui·swift
ZacJi1 个月前
巧用 allowsHitTesting 自定义 SignInWithAppleButton
ios·swiftui·swift
刘争Stanley1 个月前
SwiftUI 是如何改变 iOS 开发游戏规则的?
ios·swiftui·swift
1024小神1 个月前
在swiftui中使用Alamofire发送请求获取github仓库里的txt文件内容并解析
ios·github·swiftui
大熊猫侯佩1 个月前
SwiftUI 撸码常见错误 2 例漫谈
swiftui·xcode·tag·tabview·preview·coredata·fetchrequest
东坡肘子2 个月前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
恋猫de小郭2 个月前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
靴子学长2 个月前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui