[譯] SwiftUI 官方教程 (六)

因爲 API 變更,此文章部份內容已失效,最新完整中文教程及代碼請查看 github.com/WillieWangW…git

微信技術羣

SwiftUI 表明將來構建 App 的方向,歡迎加羣一塊兒交流技術,解決問題。github

動畫 View 與轉場

使用 SwiftUI 時,不管用做何處,咱們均可以單獨爲 view 添加動畫,或者對 view 的狀態進行動畫處理。 SwiftUI 爲咱們處理全部動畫的組合、重疊和中斷的複雜性。spring

在本文中,咱們會給包含圖表的 view 設置動畫,跟蹤用戶在使用 Landmarks app 時行爲。咱們會看到經過使用 animation(_:) 方法爲 view 設置動畫是多麼簡單。canvas

下載項目文件並按照如下步驟操做,也能夠打開已完成的項目自行瀏覽代碼。swift

  • 預計完成時間:20 分鐘
  • 項目文件:下載

1. 給單個 View 添加動畫

當咱們在一個 view 上使用 animation(_:) 方法時, SwiftUI 會動態的修改這個 view 的可動畫屬性。一個 view 的顏色、透明度、旋轉、大小以及其餘屬性都是可動畫的。bash

1.1 在 HikeView.swift 中,打開實時預覽來測試顯示和隱藏圖表。微信

確保在本文中過程當中都打開了實時預覽,這樣就能夠測試到每一步的結果。app

1.2 添加 animation(.basic()) 方法來打開按鈕的旋轉動畫。ide

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 觀察自定義動畫在圖表之間轉場時是如何營造波紋效果的。

相關文章
相關標籤/搜索