美文网首页SwiftUISwiftUISwiftUI
[译] SwiftUI 官方教程 (六)

[译] SwiftUI 官方教程 (六)

作者: Willie_ | 来源:发表于2019-06-08 22:18 被阅读8次

    完整中文教程及代码请查看 https://github.com/WillieWangWei/SwiftUI-Tutorials

    动画 View 与转场

    使用 SwiftUI 时,无论用作何处,我们都可以单独为 view 添加动画,或者对 view 的状态进行动画处理。 SwiftUI 为我们处理所有动画的组合、重叠和中断的复杂性。

    在本文中,我们会给包含图表的 view 设置动画,跟踪用户在使用 Landmarks app 时行为。我们会看到通过使用 animation(_:) 方法为 view 设置动画是多么简单。

    下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。

    • 预计完成时间:20 分钟
    • 项目文件:下载

    1. 给单个 View 添加动画

    当我们在一个 view 上使用 animation(_:) 方法时, SwiftUI 会动态的修改这个 view 的可动画属性。一个 view 的颜色、透明度、旋转、大小以及其他属性都是可动画的。

    1.1 在 HikeView.swift 中,打开实时预览来测试显示和隐藏图表。

    确保在本文中过程中都打开了实时预览,这样就可以测试到每一步的结果。

    1.2 添加 animation(.basic()) 方法来打开按钮的旋转动画。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        self.showDetail.toggle()
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .padding()
                            .animation(.basic())
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    1.3 添加一个在图表显示时让按钮变大的动画。

    animation(_:) 会作用于 view 所包装的所有可动画的修改。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        self.showDetail.toggle()
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                            .animation(.basic())
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    1.4 把动画类型从 .basic() 改成 .spring()

    SwiftUI 包含带有预设或自定义缓动的基本动画,以及弹性和流体动画。我们可以调整动画的速度、在动画开始之前设置延迟,或指定动画的重复。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        self.showDetail.toggle()
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                            .animation(.spring())
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    尝试在 scaleEffect 方法上方添加另一个动画方法来关闭旋转动画。

    围绕 SwiftUI 尝试结合不同的动画效果,看看都有哪些效果。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        self.showDetail.toggle()
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .animation(nil)
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                            .animation(.spring())
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    1.6 在继续下一节前,删除两个 animation(_:) 方法。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        self.showDetail.toggle()
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
    
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
    
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    2. 将状态的改变动画化

    现在我们已经学会如果给单个 view 添加动画,是时候给状态的值的改变添加动画了。

    这一节,我们会给用户点击按钮并切换 showDetail 状态属性时发生的所有更改添加动画。

    2.1 将 showDetail.toggle() 的调用包装到 withAnimation 函数中。

    showDetail 属性影响的公开按钮和 HikeDetail view 现在就都有了动画过渡。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    减缓动画,看看 SwiftUI 动画是如何可以中断的。

    2.2 给 withAnimation 方法传递一个 4 秒的基础动画。

    我们可以传递相同类型的动画给 animation(_:)withAnimation 函数。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation(.basic(duration: 4)) {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    2.3 尝试在动画期间打开和关闭图表 view 。

    2.4 在进入下一节前,从 withAnimation 函数中移除缓慢动画。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                }
            }
        }
    }
    

    3. 自定义 View 的转场

    默认情况下,view 通过淡入和淡出过渡到屏幕上和屏幕外。我们可以使用 transition(_:) 方法来自定义转场。

    3.1 给满足条件时显示的 HikeView 添加一个 transition(_:) 方法。

    现在图标会滑动显示和消失。

    HikeView.swift

    import SwiftUI
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                        .transition(.slide)
                }
            }
        }
    }
    

    3.2 将转场提取为 AnyTransition 的静态属性。

    这可以在您展开自定义转场时保持代码清晰。对于自定义转场,我们可以使用与 SwiftUI 所用相同的 . 符号。

    HikeView.swift

    import SwiftUI
    
    extension AnyTransition {
        static var moveAndFade: AnyTransition {
            AnyTransition.slide
        }
    }
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                        .transition(.moveAndFade)
                }
            }
        }
    }
    

    3.3 换成使用 move(edge:) 转场,这样图表会从同一边滑入和滑出。

    HikeView.swift

    import SwiftUI
    
    extension AnyTransition {
        static var moveAndFade: AnyTransition {
            AnyTransition.move(edge: .trailing)
        }
    }
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                        .transition(.moveAndFade)
                }
            }
        }
    }
    

    3.4 使用 asymmetric(insertion:removal:) 方法来给 view 显示和消失时提供不同的转场。

    HikeView.swift

    import SwiftUI
    
    extension AnyTransition {
        static var moveAndFade: AnyTransition {
            let insertion = AnyTransition.move(edge: .trailing)
                .combined(with: .opacity)
            let removal = AnyTransition.scale()
                .combined(with: .opacity)
            return .asymmetric(insertion: insertion, removal: removal)
        }
    }
    
    struct HikeView: View {
        var hike: Hike
        @State private var showDetail = false
    
        var body: some View {
            VStack {
                HStack {
                    HikeGraph(data: hike.observations, path: \.elevation)
                        .frame(width: 50, height: 30)
    
                    VStack(alignment: .leading) {
                        Text(hike.name)
                            .font(.headline)
                        Text(hike.distanceText)
                    }
    
                    Spacer()
    
                    Button(action: {
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                    }
                }
    
                if showDetail {
                    HikeDetail(hike: hike)
                        .transition(.moveAndFade)
                }
            }
        }
    }
    

    4. 给复杂的效果组合动画

    单击条形下方的按钮时,图形会在三组不同的数据之间切换。在本节中,我们将使用组合动画为构成图形的 Capsule 提供动态、波动的转场。

    4.1 把 showDetail 的默认值改成 true ,并把 HikeView 的预览固定在 canvas 中,

    这让我们在其他文件中制作动画时依然能在上下文中看到图表。

    4.2 在 GraphCapsule.swift 中,添加一个新的计算动画属性,并将其应用于 Capsuleshape

    GraphCapsule.swift

    import SwiftUI
    
    struct GraphCapsule: View {
        var index: Int
        var height: Length
        var range: Range<Double>
        var overallRange: Range<Double>
    
        var heightRatio: Length {
            max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
        }
    
        var offsetRatio: Length {
            Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
        }
    
        var animation: Animation {
            Animation.default
        }
    
        var body: some View {
            Capsule()
                .fill(Color.gray)
                .frame(height: height * heightRatio, alignment: .bottom)
                .offset(x: 0, y: height * -offsetRatio)
                .animation(animation)
            )
        }
    }
    

    4.3 将动画改为弹性动画,使用初始速度让条形图跳跃。

    GraphCapsule.swift

    import SwiftUI
    
    struct GraphCapsule: View {
        var index: Int
        var height: Length
        var range: Range<Double>
        var overallRange: Range<Double>
    
        var heightRatio: Length {
            max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
        }
    
        var offsetRatio: Length {
            Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
        }
    
        var animation: Animation {
            Animation.spring(initialVelocity: 5)
        }
    
        var body: some View {
            Capsule()
                .fill(Color.gray)
                .frame(height: height * heightRatio, alignment: .bottom)
                .offset(x: 0, y: height * -offsetRatio)
                .animation(animation)
            )
        }
    }
    

    4.4 加快动画速度,缩短每个小节移动到新位置所需的时间。

    GraphCapsule.swift

    import SwiftUI
    
    struct GraphCapsule: View {
        var index: Int
        var height: Length
        var range: Range<Double>
        var overallRange: Range<Double>
    
        var heightRatio: Length {
            max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
        }
    
        var offsetRatio: Length {
            Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
        }
    
        var animation: Animation {
            Animation.spring(initialVelocity: 5)
                .speed(2)
        }
    
        var body: some View {
            Capsule()
                .fill(Color.gray)
                .frame(height: height * heightRatio, alignment: .bottom)
                .offset(x: 0, y: height * -offsetRatio)
                .animation(animation)
            )
        }
    }
    

    4.5 根据 Capsule 在图表上的位置为每个动画添加延迟。

    GraphCapsule.swift

    import SwiftUI
    
    struct GraphCapsule: View {
        var index: Int
        var height: Length
        var range: Range<Double>
        var overallRange: Range<Double>
    
        var heightRatio: Length {
            max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
        }
    
        var offsetRatio: Length {
            Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
        }
    
        var animation: Animation {
            Animation.spring(initialVelocity: 5)
                .speed(2)
                .delay(0.03 * Double(index))
        }
    
        var body: some View {
            Capsule()
                .fill(Color.gray)
                .frame(height: height * heightRatio, alignment: .bottom)
                .offset(x: 0, y: height * -offsetRatio)
                .animation(animation)
            )
        }
    }
    

    4.6 观察自定义动画在图表之间转场时是如何营造波纹效果的。

    相关文章

      网友评论

        本文标题:[译] SwiftUI 官方教程 (六)

        本文链接:https://www.haomeiwen.com/subject/vruwxctx.html