SwiftUI 学习笔记(五):Animating Views and Transitions

1,427 阅读7分钟

我正在参加「掘金·启航计划」

 SwiftUI 官方教程:SwiftUI Tutorials 仅是几个体现 SwiftUI 简单使用的小 demo 而已,简单易学,循序渐进,先看完可以对 SwiftUI 有一个大概的认知。

五:Animating Views and Transitions

Drawing and Animation - Animating Views and Transitions 为视图和过渡添加动画效果

 使用 SwiftUI 时,你可以单独对视图或视图状态的更改进行动画处理,无论效果位于何处。SwiftUI 为你处理这些组合、重叠和可中断动画的所有复杂性。

 在本节中,将对一个视图进行动画处理,该视图包含一个图表,用于跟踪用户在使用 Landmark 应用程序时进行的徒步旅行。使用 animation(_:) 修饰符,你将看到为视图添加动画效果是多么容易。

Add Hiking Data to the App

 在添加动画之前,你需要对某些内容进行动画处理。在本节中,你将导入徒步旅行数据并对其进行建模,然后添加一些预构建的视图,以便在图表中静态显示该数据。

 将 hikeData.json 文件从下载文件的 "Resources" 文件夹拖到项目的 "Resources" 文件夹中。请务必选择 "Copy items if needed",然后再点按 "Finish" 按钮。

 使用菜单项 "File > Nes > File",在项目的 "Model" 文件夹中创建一个名为 Hike.swift 的新 Swift 文件。

 与 "Landmark" 结构体一样,"Hike" 结构体也遵循 Codable 协议,并且具有与相应数据文件中的键匹配的属性。

import Foundation

struct Hike: Codable, Hashable, Identifiable {
    var id: Int
    var name: String
    var distance: Double
    var difficulty: Int
    var observations: [Observation]
    
    static var formatter = LengthFormatter()
    
    var distanceText: String {
        Hike.formatter.string(fromValue: distance, unit: .kilometer)
    }
    
    struct Observation: Codable, Hashable {
        var distanceFromStart: Double
        
        var elevation: Range<Double>
        var pace: Range<Double>
        var heartRate: Range<Double>
    }
}

 将 hikes 数组加载到模型对象中。

 由于你在最初加载 hikes 数据后永远不会修改它,因此你无需使用 @Published 属性对其进行标记。

final class ModelData: ObservableObject {
    ...
    var hikes: [Hike] = load("hikeData.json")
}

 将 "Hikes" 文件夹从下载文件的 "Resources" 文件夹拖到项目的 "Views" 文件夹中。请务必选择 "Copy items if needed",然后再点按 "Finish" 按钮。

 熟悉新 Views。它们协同工作以显示加载到模型中的 hike 数据。

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

 请务必在本教程中使用实时预览,以便可以试验每个步骤的结果。

Add Animations to Individual Views

 使用 animation(_:)equatable 视图上使用修饰符,SwiftUI 会对视图的可动画属性所做的任何更改进行动画处理。视图的颜色、不透明度、旋转、大小和其他属性都是可设置动画的。当视图非 equatable 时,可以使用 animation(_:value:) 用于在指定值更改时启动动画的修饰符。

 在 HikeView.swift 中,通过添加一个动画修饰符来打开按钮旋转的动画,该修改器从 showDetail 值的更改开始。

...
Button {
    showDetail.toggle()
} label: {
    Label("Graph", systemImage: "chevron.right.circle")
        .labelStyle(.iconOnly)
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .padding()
        .animation(.easeInOut, value: showDetail) // 动画
}
...

 通过在图形可见时放大按钮来添加另一个可动画更改。动画修饰符应用于其包装视图中的所有可动画更改。

Button {
    showDetail.toggle()
} label: {
    Label("Graph", systemImage: "chevron.right.circle")
        .labelStyle(.iconOnly)
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .scaleEffect(showDetail ? 1.5 : 1) // 动画
        .padding()
        .animation(.easeInOut, value: showDetail) // 动画
}

 将动画类型从 easeInOut 更改为 spring()。SwiftUI 包括带有预定义或自定义缓动的基本动画,以及弹簧和流体动画。你可以调整动画的速度、设置动画开始前的延迟或指定动画重复。

Button {
    showDetail.toggle()
} label: {
    Label("Graph", systemImage: "chevron.right.circle")
        .labelStyle(.iconOnly)
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring(), value: showDetail) // .easeInOut 修改为 .spring()
}

 尝试通过在 scaleEffect 修改器上方添加另一个动画修改器来关闭旋转动画。实验性质:试一试 SwiftUI。尝试组合不同的动画效果,看看有什么可能性。

Button {
    showDetail.toggle()
} label: {
    Label("Graph", systemImage: "chevron.right.circle")
        .labelStyle(.iconOnly)
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .animation(nil, value: showDetail) // 修改
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring(), value: showDetail)
}

 请先删除这两个动画修饰符,然后再转到下一节。

Animate the Effects of State Changes

 现在你已经了解了如何将动画应用到各个视图,是时候在你更改状态值的地方添加动画了。

 在这里,你将动画应用于当用户点击按钮并切换 showDetail 状态属性时发生的所有更改。

 将调用 showDetail.toggle() 包裹到 withAnimation 函数调用中。受 showDetail 属性影响的两个视图 —— 显示按钮和 HikeDetail 视图 —— 现在都有动画过渡。

Button {
    // showDetail.toggle() 调用包裹到 withAnimation 调用中
    withAnimation {
        showDetail.toggle()
    }
} label: {
    Label("Graph", systemImage: "chevron.right.circle")
        .labelStyle(.iconOnly)
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
}

 放慢动画速度,看看 SwiftUI 动画是如何被中断的。

 将四秒长的基本动画传递给 withAnimation 函数。

 你可以将相同类型的动画传递给传递给动画的 animation(_:value:) 修饰语。

withAnimation(.easeInOut(duration: 4)) {
    showDetail.toggle()
}

 尝试在动画中间打开和关闭图形视图,在 Live Preview 中进行预览。

 在继续下一节之前,请通过删除调用的输入参数来还原 withAnimation 函数以使用默认动画。

Customize View Transitions

 默认情况下,视图通过淡入和淡出在屏幕上和屏幕外进行过渡。你可以使用 transition(_:) 修饰符自定义此过渡。

 向有条件可见的 HikeView 添加一个 transition(_:) 修饰符。现在,图形通过滑入和滑出视线而出现和消失。

...
if showDetail {
    HikeDetail(hike: hike)
        .transition(.slide)
}
...

 提取刚刚作为 AnyTransition 的静态属性添加的过渡,并在视图的过渡修饰符中访问新属性。当你扩展自定义过渡时,这可以使你的代码保持干净。

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.slide
    }
}

...
if showDetail {
    HikeDetail(hike: hike)
        .transition(.moveAndFade)
}
...

 切换到使用 move(edge:) 过渡,以便图形从同一侧滑入和滑出。

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.move(edge: .trailing)
    }
}

 使用 asymmetric(insertion:removal:) 修饰符为视图出现和消失提供不同的过渡。

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        //        AnyTransition.slide
        //        AnyTransition.move(edge: .trailing)
        .asymmetric(
            insertion: .move(edge: .trailing).combined(with: .opacity),
            removal: .scale.combined(with: .opacity)
        )
    }
}

Compose Animations for Complex Effects

 当你单击条形下方的按钮时,图形会在三组不同的数据集之间切换。在本节中,你将使用组合动画为构成图形的胶囊提供动态的波纹过渡。

 在 HikeView 中,将 showDetail 的默认值更改为 true,并将预览固定到画布上。这使你可以在处理另一个文件中的动画时在上下文中查看图形。

 在 HikeGraph.swift 中,定义一个新的波纹动画并将其应用于每个生成的图形胶囊。

extension Animation {
    static func ripple() -> Animation {
        Animation.default
    }
}

...
GraphCapsule(
    index: index,
    color: color,
    height: proxy.size.height,
    range: observation[keyPath: path],
    overallRange: overallRange
)
.animation(.ripple())
...

 将动画切换为弹簧动画,减少阻尼分数以使条形跳跃。你可以在实时预览中通过在海拔、心率和配速之间切换来查看动画效果。

extension Animation {
    static func ripple() -> Animation {
        Animation.spring(dampingFraction: 0.5)
    }
}

 稍微加快动画速度,以缩短每个条形移动到新位置所需的时间。

extension Animation {
    static func ripple() -> Animation {
        Animation.spring(dampingFraction: 0.5)
            .speed(2)
    }
}

 为每个动画添加基于胶囊在图形上的位置的延迟。

extension Animation {
    static func ripple(index: Int) -> Animation {
        Animation.spring(dampingFraction: 0.5)
            .speed(2)
            .delay(0.03 * Double(index))
    }
}

...
GraphCapsule(
    index: index,
    color: color,
    height: proxy.size.height,
    range: observation[keyPath: path],
    overallRange: overallRange
)
.animation(.ripple(index: index))
...

 观察自定义动画在图形之间过渡时如何提供波纹效果。请务必先取消固定预览,然后再继续学习下一教程。

 本节专注于动画和过渡的学习,暂时就到这里。

参考链接

参考链接:🔗