MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SwiftUI 与SwiftUI视图动画过渡

2022-02-196.2k 阅读

SwiftUI 基础

SwiftUI 视图

SwiftUI 是一种描述式的构建用户界面的方式。在 SwiftUI 中,一切皆为视图(View)。视图是用于呈现内容的基本构建块。例如,创建一个简单的文本视图可以这样写:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
    }
}

这里的 Text 就是一个视图,它显示给定的文本字符串。ContentView 本身也是一个视图,它符合 View 协议,并且有一个 body 属性,返回值类型是 some View,代表一个具体的视图。

视图修饰符

视图可以通过修饰符(modifier)来改变其外观和行为。比如,我们可以给 Text 视图添加字体、颜色等修饰符:

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
           .font(.largeTitle)
           .foregroundColor(.blue)
    }
}

在这个例子中,.font(.largeTitle) 使文本以大标题字体显示,.foregroundColor(.blue) 将文本颜色设置为蓝色。修饰符可以链式调用,方便地对视图进行多种修改。

容器视图

除了像 Text 这样的基本视图,SwiftUI 还有许多容器视图,用于组织和排列其他视图。例如,VStack 是垂直堆叠视图,HStack 是水平堆叠视图。以下是一个使用 VStack 来堆叠两个文本视图的例子:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("First Text")
            Text("Second Text")
        }
    }
}

VStack 中,视图会按照添加的顺序垂直排列。类似地,HStack 会使视图水平排列。另外,ZStack 是将所有子视图堆叠在同一位置,常用于创建带有叠加效果的界面。

SwiftUI 视图动画过渡基础

什么是视图动画过渡

视图动画过渡是指在视图状态发生变化时,以动画的形式呈现这种变化。例如,当一个视图的可见性改变、大小改变或者位置移动时,可以通过动画过渡让这些变化更加平滑和自然,提升用户体验。在 SwiftUI 中,实现视图动画过渡相对简洁,通过特定的修饰符就能为视图添加动画效果。

隐式动画

SwiftUI 支持隐式动画,即当视图的某个可动画属性发生变化时,会自动以动画形式过渡。例如,改变一个矩形的填充颜色:

struct ContentView: View {
    @State private var isRed = false

    var body: some View {
        Button("Toggle Color") {
            isRed.toggle()
        }
       .padding()
        Rectangle()
           .fill(isRed? Color.red : Color.blue)
           .frame(width: 200, height: 200)
    }
}

在这个例子中,当点击按钮时,isRed 的值发生改变,矩形的填充颜色也随之改变。由于 fill 方法中的颜色属性是可动画的,所以颜色的变化会以动画形式呈现,这就是隐式动画。

显式动画

与隐式动画相对的是显式动画,需要开发者明确指定动画的类型、时长等参数。在 SwiftUI 中,可以使用 .animation 修饰符来实现显式动画。例如,我们让一个视图在点击按钮时逐渐消失:

struct ContentView: View {
    @State private var isHidden = false

    var body: some View {
        VStack {
            Button("Hide View") {
                isHidden.toggle()
            }
           .padding()
            Text("Hello, Animation!")
               .opacity(isHidden? 0 : 1)
               .animation(.easeInOut(duration: 1), value: isHidden)
        }
    }
}

在上述代码中,.animation(.easeInOut(duration: 1), value: isHidden) 表示当 isHidden 的值发生变化时,应用一个时长为 1 秒、缓动效果为 easeInOut 的动画到 opacity 属性上,使得文本视图的消失和出现具有动画效果。这里的 value 参数很重要,它告诉 SwiftUI 当 isHidden 变化时触发动画。

常见的视图动画过渡类型

淡入淡出动画

淡入淡出动画是通过改变视图的 opacity 属性来实现的。前面已经有相关示例,如:

struct ContentView: View {
    @State private var isHidden = false

    var body: some View {
        VStack {
            Button("Hide View") {
                isHidden.toggle()
            }
           .padding()
            Text("Hello, Animation!")
               .opacity(isHidden? 0 : 1)
               .animation(.easeInOut(duration: 1), value: isHidden)
        }
    }
}

通过设置 opacity 从 1(完全不透明)到 0(完全透明),并添加动画修饰符,就实现了淡入淡出的效果。这种动画常用于显示或隐藏视图,给用户一种柔和的过渡感觉。

大小变化动画

改变视图的大小也是常见的动画过渡类型。可以通过修改 framescaleEffect 等属性来实现。以下是一个通过 scaleEffect 实现视图大小变化的例子:

struct ContentView: View {
    @State private var isEnlarged = false

    var body: some View {
        VStack {
            Button("Enlarge") {
                isEnlarged.toggle()
            }
           .padding()
            Image(systemName: "star.fill")
               .font(.system(size: 50))
               .scaleEffect(isEnlarged? 2 : 1)
               .animation(.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0), value: isEnlarged)
        }
    }
}

在这个代码中,scaleEffect 根据 isEnlarged 的值来改变视图的缩放比例。.animation 中的 spring 动画类型使得缩放效果具有弹簧般的弹性,更加生动。

位置移动动画

视图的位置移动可以通过修改 offset 属性来实现。例如,让一个圆形在点击按钮时从左到右移动:

struct ContentView: View {
    @State private var isMoved = false

    var body: some View {
        VStack {
            Button("Move Circle") {
                isMoved.toggle()
            }
           .padding()
            Circle()
               .fill(Color.green)
               .frame(width: 50, height: 50)
               .offset(x: isMoved? 200 : 0, y: 0)
               .animation(.linear(duration: 1), value: isMoved)
        }
    }
}

这里通过 offsetx 坐标根据 isMoved 的值来改变圆形的水平位置,并且添加了线性动画,使移动过程平滑。

旋转动画

旋转动画可以通过 rotationEffect 来实现。以下是一个让文本视图在点击按钮时旋转 360 度的例子:

struct ContentView: View {
    @State private var isRotated = false

    var body: some View {
        VStack {
            Button("Rotate Text") {
                isRotated.toggle()
            }
           .padding()
            Text("Rotate Me")
               .rotationEffect(.degrees(isRotated? 360 : 0))
               .animation(.easeInOut(duration: 1), value: isRotated)
        }
    }
}

rotationEffect 中的 .degrees 表示旋转的角度,根据 isRotated 的值来决定旋转的度数,并添加动画效果。

复杂视图动画过渡

组合动画

在实际应用中,常常需要同时应用多种动画过渡效果。例如,让一个视图在消失时不仅透明度变为 0,还同时缩小并旋转:

struct ContentView: View {
    @State private var isHidden = false

    var body: some View {
        VStack {
            Button("Hide View") {
                isHidden.toggle()
            }
           .padding()
            Rectangle()
               .fill(Color.orange)
               .frame(width: 150, height: 150)
               .opacity(isHidden? 0 : 1)
               .scaleEffect(isHidden? 0.1 : 1)
               .rotationEffect(.degrees(isHidden? 360 : 0))
               .animation(.easeInOut(duration: 1), value: isHidden)
        }
    }
}

在这个例子中,Rectangle 视图同时应用了透明度、缩放和旋转的动画,通过 .animation 修饰符统一控制动画效果,使得视图在隐藏时呈现出复杂且连贯的动画过渡。

条件动画

有时候,我们希望根据不同的条件应用不同的动画。例如,根据视图的状态,选择不同的动画类型。以下是一个根据视图是否被选中来应用不同动画的例子:

struct ContentView: View {
    @State private var isSelected = false

    var body: some View {
        VStack {
            Button("Toggle Selection") {
                isSelected.toggle()
            }
           .padding()
            Rectangle()
               .fill(isSelected? Color.blue : Color.green)
               .frame(width: 200, height: 200)
               .scaleEffect(isSelected? 1.5 : 1)
               .animation(isSelected?
                            .spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0) :
                            .easeInOut(duration: 0.5), value: isSelected)
        }
    }
}

在这个代码中,当 isSelectedtrue 时,应用弹簧动画;当 isSelectedfalse 时,应用缓入缓出动画。通过这种方式,可以根据不同的条件为视图提供更加个性化的动画过渡效果。

顺序动画

顺序动画是指让多个动画按照一定的顺序依次执行。在 SwiftUI 中,可以使用 withAnimation 结合 DispatchQueue.main.asyncAfter 来实现。例如,先让一个视图淡入,然后再放大:

struct ContentView: View {
    @State private var isVisible = false

    var body: some View {
        VStack {
            Button("Show Animation") {
                withAnimation(.easeInOut(duration: 1)) {
                    isVisible.toggle()
                }
                DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
                    withAnimation(.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0)) {
                        isVisible.toggle()
                    }
                }
            }
           .padding()
            Rectangle()
               .fill(Color.purple)
               .frame(width: 150, height: 150)
               .opacity(isVisible? 1 : 0)
               .scaleEffect(isVisible? 1.5 : 1)
        }
    }
}

在这个例子中,点击按钮后,首先应用淡入动画,1 秒后,再应用放大的弹簧动画,实现了动画的顺序执行。

过渡动画

过渡动画基础

过渡动画是一种特殊类型的动画,用于在视图之间进行切换时提供平滑的过渡效果。在 SwiftUI 中,使用 .transition 修饰符来实现过渡动画。例如,在两个文本视图之间切换时应用淡入淡出过渡:

struct ContentView: View {
    @State private var showFirstText = true

    var body: some View {
        VStack {
            Button("Toggle Text") {
                showFirstText.toggle()
            }
           .padding()
            if showFirstText {
                Text("First Text")
                   .transition(.opacity)
            } else {
                Text("Second Text")
                   .transition(.opacity)
            }
        }
    }
}

这里的 .transition(.opacity) 表示在视图切换时应用淡入淡出的过渡效果。

常见的过渡动画类型

  1. 淡入淡出过渡:如上述例子,.transition(.opacity) 使视图在切换时以淡入淡出的方式过渡,是最基本的过渡类型之一。
  2. 滑动过渡:可以使用 .transition(.slide) 来实现滑动过渡。例如,从左侧滑入新的视图:
struct ContentView: View {
    @State private var showNewView = false

    var body: some View {
        VStack {
            Button("Show New View") {
                showNewView.toggle()
            }
           .padding()
            if showNewView {
                Text("New View")
                   .transition(.slide)
            } else {
                Text("Original View")
            }
        }
    }
}

在这个例子中,新视图会从屏幕边缘滑入,默认是从底部滑入,也可以通过指定 SlideTransition 的参数来改变方向。 3. 缩放过渡.transition(.scale) 用于实现缩放过渡。例如,新视图以放大的方式出现:

struct ContentView: View {
    @State private var showZoomView = false

    var body: some View {
        VStack {
            Button("Show Zoom View") {
                showZoomView.toggle()
            }
           .padding()
            if showZoomView {
                Text("Zoom View")
                   .transition(.scale)
            } else {
                Text("Normal View")
            }
        }
    }
}

showZoomViewtrue 时,新视图会从较小的尺寸逐渐放大到正常大小。

  1. 模态过渡.transition(.modal) 常用于创建模态视图的过渡效果。例如,创建一个模态对话框:
struct ContentView: View {
    @State private var showModal = false

    var body: some View {
        VStack {
            Button("Show Modal") {
                showModal.toggle()
            }
           .padding()
            if showModal {
                Text("Modal Content")
                   .frame(width: 200, height: 200)
                   .background(Color.white)
                   .cornerRadius(10)
                   .shadow(radius: 5)
                   .transition(.modal)
            }
        }
    }
}

这里的模态视图以一种类似于从底部弹出并带有一定透明度变化的效果显示。

自定义过渡动画

除了使用 SwiftUI 提供的常见过渡动画类型,还可以自定义过渡动画。自定义过渡动画需要创建一个符合 AnyTransition 协议的实例。例如,我们创建一个从右下角飞入的自定义过渡动画:

extension AnyTransition {
    static var flyInFromBottomRight: AnyTransition {
        let insertion = AnyTransition.move(edge:.bottomTrailing)
           .combined(with:.opacity)
        let removal = AnyTransition.scale
           .combined(with:.opacity)
        return AnyTransition(insertion: insertion, removal: removal)
    }
}

struct ContentView: View {
    @State private var showCustomView = false

    var body: some View {
        VStack {
            Button("Show Custom View") {
                showCustomView.toggle()
            }
           .padding()
            if showCustomView {
                Text("Custom View")
                   .transition(.flyInFromBottomRight)
            } else {
                Text("Default View")
            }
        }
    }
}

在这个例子中,我们扩展了 AnyTransition,定义了一个 flyInFromBottomRight 的自定义过渡动画。新视图会从右下角飞入并淡入,移除时会缩放并淡出。

动画过渡的性能优化

避免不必要的动画

在应用中,要避免为那些用户几乎察觉不到的视图变化添加动画。例如,一些微小的布局调整或者颜色的细微变化,如果添加动画可能会增加性能开销,而对用户体验提升不大。只有对那些明显的、能提升用户体验的视图变化添加动画,这样可以减少不必要的计算资源消耗。

优化动画参数

合理选择动画的时长、缓动函数等参数也能提升性能。过长的动画时长可能会让用户等待,而过短的时长可能导致动画不流畅。同时,复杂的缓动函数可能需要更多的计算资源。例如,在一些简单的淡入淡出动画中,使用线性缓动函数 .linear 可能比复杂的弹簧动画更节省资源,而且效果也能满足需求。

批量更新视图

尽量批量更新视图而不是逐个更新。例如,当多个视图的属性需要同时改变并添加动画时,可以通过一个 withAnimation 块来包裹所有的视图变化。这样 SwiftUI 可以更高效地处理动画,而不是为每个视图的变化单独启动一个动画过程。

struct ContentView: View {
    @State private var isChanged = false

    var body: some View {
        VStack {
            Button("Change Views") {
                withAnimation {
                    isChanged.toggle()
                }
            }
           .padding()
            Text("View 1")
               .opacity(isChanged? 0 : 1)
               .scaleEffect(isChanged? 0.5 : 1)
            Text("View 2")
               .opacity(isChanged? 0 : 1)
               .scaleEffect(isChanged? 0.5 : 1)
        }
    }
}

在这个例子中,两个文本视图的透明度和缩放变化都在一个 withAnimation 块中处理,提高了动画处理的效率。

避免过度复杂的动画

虽然复杂的动画可能看起来很酷,但过度复杂的动画,如大量的组合动画、高频率的动画切换等,可能会导致性能问题,尤其是在性能较低的设备上。要根据应用的目标设备和用户场景,权衡动画的复杂程度,确保在提供良好用户体验的同时,不牺牲性能。

与 UIKit 的交互中的动画过渡

在 UIKit 视图中嵌入 SwiftUI 视图并添加动画

有时候,我们需要在传统的 UIKit 项目中使用 SwiftUI 视图,并为其添加动画过渡。可以通过 UIHostingController 来嵌入 SwiftUI 视图。例如,在一个 UIViewController 中嵌入一个 SwiftUI 视图,并在按钮点击时为其添加淡入动画:

import UIKit
import SwiftUI

class ViewController: UIViewController {
    private var hostingController: UIHostingController<ContentView>?

    override func viewDidLoad() {
        super.viewDidLoad()

        let contentView = ContentView()
        hostingController = UIHostingController(rootView: contentView)
        hostingController?.view.frame = view.bounds
        view.addSubview(hostingController!.view)

        let button = UIButton(type:.system)
        button.setTitle("Fade In", for:.normal)
        button.addTarget(self, action: #selector(fadeInSwiftUIView), for:.touchUpInside)
        button.frame = CGRect(x: 100, y: 100, width: 200, height: 50)
        view.addSubview(button)
    }

    @objc func fadeInSwiftUIView() {
        UIView.animate(withDuration: 1) {
            self.hostingController?.view.alpha = 1
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("SwiftUI in UIKit")
           .font(.largeTitle)
           .foregroundColor(.blue)
    }
}

在这个例子中,通过 UIHostingControllerContentView 嵌入到 UIViewController 中,然后通过 UIKit 的 UIView.animate 方法为嵌入的 SwiftUI 视图添加淡入动画。

在 SwiftUI 视图中使用 UIKit 动画并结合过渡

反过来,在 SwiftUI 视图中也可以调用 UIKit 的动画,并与 SwiftUI 的过渡结合使用。例如,我们可以在 SwiftUI 视图中使用 UIKit 的弹簧动画来移动一个视图,同时结合 SwiftUI 的过渡效果:

import SwiftUI
import UIKit

struct ContentView: View {
    @State private var isMoved = false

    var body: some View {
        VStack {
            Button("Move with UIKit Animation") {
                let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
                UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: [], animations: {
                    view.frame.origin.x = self.isMoved? 200 : 0
                }, completion: nil)
                self.isMoved.toggle()
            }
           .padding()
            Rectangle()
               .fill(Color.green)
               .frame(width: 100, height: 100)
               .offset(x: isMoved? 200 : 0, y: 0)
               .transition(.slide)
        }
    }
}

在这个代码中,按钮点击时,首先使用 UIKit 的弹簧动画移动一个 UIView,同时 SwiftUI 视图中的 Rectangle 也通过 offsettransition 实现位置移动和过渡效果,两者结合为用户提供丰富的动画体验。

动画过渡在实际应用中的案例

导航栏切换动画

在一个具有导航功能的应用中,导航栏切换时的动画过渡可以提升用户体验。例如,在 SwiftUI 中创建一个简单的导航应用,并为导航栏切换添加淡入淡出动画:

struct ContentView: View {
    @State private var showDetail = false

    var body: some View {
        NavigationView {
            VStack {
                Button("Go to Detail") {
                    showDetail.toggle()
                }
               .padding()
            }
           .navigationTitle("Home")
            if showDetail {
                Text("Detail View")
                   .navigationTitle("Detail")
                   .transition(.opacity)
            }
        }
    }
}

在这个例子中,当点击按钮切换到详情视图时,详情视图通过淡入的过渡动画显示,使得导航切换更加平滑自然。

卡片式交互动画

在一些卡片式布局的应用中,卡片的展示和隐藏动画过渡很重要。例如,创建一个卡片列表,点击卡片时,卡片展开并显示详细内容,同时添加缩放和淡入动画:

struct Card: Identifiable {
    let id = UUID()
    let title: String
    let detail: String
}

struct ContentView: View {
    @State private var selectedCard: Card? = nil
    let cards = [
        Card(title: "Card 1", detail: "This is the detail of Card 1"),
        Card(title: "Card 2", detail: "This is the detail of Card 2")
    ]

    var body: some View {
        VStack {
            ForEach(cards) { card in
                Button(card.title) {
                    if selectedCard == card {
                        selectedCard = nil
                    } else {
                        selectedCard = card
                    }
                }
               .padding()
            }
            if let selectedCard = selectedCard {
                VStack {
                    Text(selectedCard.title)
                       .font(.largeTitle)
                    Text(selectedCard.detail)
                }
               .padding()
               .frame(width: 300, height: 300)
               .background(Color.white)
               .cornerRadius(10)
               .shadow(radius: 5)
               .scaleEffect(selectedCard == self.selectedCard? 1 : 0.1)
               .opacity(selectedCard == self.selectedCard? 1 : 0)
               .animation(.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0), value: selectedCard)
            }
        }
    }
}

在这个例子中,当点击卡片时,卡片的详细内容以缩放和淡入的动画形式展示,为用户提供了良好的交互体验。

轮播图动画

轮播图是很多应用中常见的组件,其动画过渡效果对于展示内容很关键。以下是一个简单的 SwiftUI 轮播图示例,并添加滑动过渡动画:

struct Slide: Identifiable {
    let id = UUID()
    let image: String
}

struct ContentView: View {
    @State private var currentIndex = 0
    let slides = [
        Slide(image: "slide1"),
        Slide(image: "slide2"),
        Slide(image: "slide3")
    ]

    var body: some View {
        VStack {
            TabView(selection: $currentIndex) {
                ForEach(slides) { slide in
                    Image(slide.image)
                       .resizable()
                       .scaledToFill()
                       .frame(width: 300, height: 200)
                       .clipped()
                       .tag(slides.firstIndex(of: slide)!)
                       .transition(.slide)
                }
            }
           .tabViewStyle(PageTabViewStyle())
            HStack {
                ForEach(0..<slides.count) { index in
                    Circle()
                       .fill(currentIndex == index? Color.blue : Color.gray)
                       .frame(width: 10, height: 10)
                }
            }
           .padding()
        }
    }
}

在这个轮播图中,当切换图片时,应用了滑动过渡动画,使图片切换更加流畅,提升了用户浏览图片的体验。

通过以上对 SwiftUI 视图动画过渡的深入探讨,我们可以看到 SwiftUI 为开发者提供了丰富且强大的动画过渡功能,无论是简单的视图变化还是复杂的交互场景,都能通过合理运用这些技术来实现出色的用户界面效果。在实际开发中,需要根据应用的需求和性能要求,精心设计和优化动画过渡,为用户带来更加流畅、美观的使用体验。