SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效

概述

作为 Apple 开发中的全栈秃头老码农们,我们不但需要精通代码编写更需要有过硬的界面设计艺术功底。为了解决撸码与撸图严重脱节这一窘境,苹果从 iOS 13(macOS 11)开始引入了 SF Symbols 图形字符。

有了 SF Symbols,我们现在可以从系统内置"千姿百态"的图形字符库中毫不费力的恣意选取心爱的图像来装扮我们的 App 了。我们还可以更进一步为它们添加优美流畅的动画效果。

在本篇博文中,您将学到如下内容:

  1. 符号动画,小菜一碟!
  2. 自动触发动画
  3. 更顺畅的符号过渡特效
  4. 所见即所得:SF Symbols App
  5. 完整源代码

在 WWDC 24 中,苹果携手全新的 SF Symbols 6.0 昂首阔步而来,让小伙伴们的生猛撸码愈发如虎添翼。

那还等什么呢?让我们马上开始玩转符号动画之旅吧!

Let's go!!!


1. 符号动画,小菜一碟!

SF Symbols 是兼容 Apple 多个平台的一套系统、完整、优美的图形字符库。从伴随着 SwiftUI 1.0(iOS 13)横空出世那年算起,到现在已经进化到 SF Symbols 6.0 版本了。

它的 Apple 官方网站在此: SF Symbols,大家可以前去观赏其中的细枝末节。

目前,最新的 SF Symbols 6.0 内置了超过 6000 枚风格各异的图形字符,等待着小伙伴们的顽皮"采摘"。

SF Symbols 字符库不仅仅包含静态字符图像,我们同样可以在 SwiftUI 和 UIKit 中轻松将其升华为鲜活的动画(Animations)和过渡(Transitions)效果。

下面,我们在 SwiftUI 中仅用一个 symbolEffect() 视图修改器即让字符栩栩如生了:

swift 复制代码
Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
    .symbolEffect(.wiggle, options: .repeat(.continuous))

我们还可以恣意改变动画的种类,比如从 wiggle 改为 variableColor 效果:

swift 复制代码
Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
    .symbolEffect(.variableColor, options: .repeat(.continuous))

我们甚至可以更进一步,细粒度定制 variableColor 动画效果的微妙细节:

swift 复制代码
Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
    .symbolEffect(.variableColor.cumulative, options: .repeat(.continuous))

2. 自动触发动画

除了一劳永逸的让动画重复播放以外,我们还可以自动地根据 SwiftUI 视图中的状态来触发对应的动画。

如下代码所示,只要 animTrigger 状态发生改变,我们就播放 wiggle 动画 2 次(每次间隔 2 秒):

swift 复制代码
VStack {
    Image(systemName: "bell.circle")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .symbolRenderingMode(.hierarchical)
        .frame(width: 150)
        .symbolEffect(.wiggle, options: .repeat(.periodic(2, delay: 2)), value: animTrigger)

    Button("触发动画") {
        animTrigger.toggle()
    }
}

我们还可以用 symbolEffect() 修改器的另一个重载版本,来手动控制动画的开始和停止:

swift 复制代码
VStack {
    Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 150)
        .symbolEffect(.wiggle, options: .repeat(.continuous), isActive: animPlaying)
    
    Button(animPlaying ? "停止动画" : "开始动画") {
        animPlaying.toggle()
    }
}

如上代码所示,当 animPlaying 状态为真时我们播放动画,当它为假时则停止动画。

3. 更顺畅的符号过渡特效

SF Symbols 字符图形库除了提供变幻莫测的海量动画以外,还弥补了强迫症码农们对于不同字符切换过渡时僵硬、不自然的"心结"。

比如,在下面的代码中我们根据 notificationsEnabled 是否开启,切换显示了不同的图形字符:

swift 复制代码
@State var notificationsEnabled = false

Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(width: 66)

但是,这样做却释放出一些"行尸走肉"的气息,让用户非常呲楞:

所幸的是,利用 contentTransition() 视图修改器我们可以将其变得行云流水、一气呵成:

swift 复制代码
Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .contentTransition(.symbolEffect(.replace))
    .frame(width: 66)

我们还可以用 symbolVariant() 修改器来重构上面的代码,效果保持不变:

swift 复制代码
Image(systemName: "bell")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .symbolVariant(!notificationsEnabled ? .slash : .none )
    .contentTransition(.symbolEffect(.replace))
    .frame(width: 66)

通过 symbolRenderingMode() 修改器,我们还能在过渡特效基础之上再应用字符的其它特殊渲染效果,比如分层:

swift 复制代码
Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
	.resizable()
	.aspectRatio(contentMode: .fit)
	.symbolRenderingMode(.hierarchical)
	.contentTransition(.symbolEffect(.replace))
	.frame(width: 66)

当然,如果我们愿意的话同样可以更加细粒度地定制过渡的类型(downUp):

swift 复制代码
Image(systemName: "bell")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .symbolVariant(!notificationsEnabled ? .slash : .none )
    .symbolRenderingMode(.hierarchical)
    .contentTransition(.symbolEffect(.replace.downUp))
    .frame(width: 66)

4. 所见即所得:SF Symbols App

上面我们介绍了 SF Symbs 动画和过渡中诸多"妙计和花招"。

不过平心而论,某个或者某几个字符可能更适合某些特定的动画和过渡效果,那我们怎么才能用最快的速度找到它们最佳的动画"伴侣"呢?

除了通过撸码经验和 SF Symbols 官方文档以外,最快的方法恐怕就是使用 macOS 上的 SF Symbols App 了:

我们可以在 developer.apple.com/sf-symbols 下载 SF Symbols App。

还拿上面第一个例子中的字符来举例,我们可以在 SF Symbols App 中随意为它应用各种动画效果,直到满意为止:

我们再如法炮制换一个 AirPods "把玩"一番:

至此,我们完全掌握了 SwiftUI 中 SF Symbols 符号的动画和过渡特效,小伙伴们一起享受这干脆利落、丝般顺滑的灵动风味吧!

5. 完整源代码

本文对应的全部源代码在此,欢迎品尝:

swift 复制代码
import SwiftUI

struct ContentView: View {
    
    @State var notificationsEnabled = false
    @State var animPlaying = false
    @State var animTrigger = false
    
    var body: some View {
        NavigationStack {
            Form {
                LabeledContent(content: {
                    Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 66)
                    
                }, label: {
                    Text("生硬的过渡")
                })
                .frame(height: 100)
                
                LabeledContent(content: {
                    //Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
                    Image(systemName: "bell")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .symbolVariant(!notificationsEnabled ? .slash : .none )
                        .contentTransition(.symbolEffect(.replace))
                        .frame(width: 66)
                    
                }, label: {
                    Text("流畅的过渡")
                })
                .frame(height: 100)
                
                LabeledContent(content: {
                    Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .symbolRenderingMode(.hierarchical)
                        .contentTransition(.symbolEffect(.replace))
                        .frame(width: 66)
                        
                    
                }, label: {
                    Text("按层过渡")
                })
                .frame(height: 100)
                
                LabeledContent(content: {
                    //Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
                    Image(systemName: "bell")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .symbolVariant(!notificationsEnabled ? .slash : .none )
                        .symbolRenderingMode(.hierarchical)
                        .contentTransition(.symbolEffect(.replace.downUp))
                        .frame(width: 66)
                        
                    
                }, label: {
                    Text("downUP 按层过渡")
                })
                .frame(height: 100)
                
                HStack {
                    VStack {
                        Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 150)
                            .symbolEffect(.wiggle, options: .repeat(.continuous), isActive: animPlaying)
                        
                        Button(animPlaying ? "停止动画" : "开始动画") {
                            animPlaying.toggle()
                        }
                    }
                    
                    VStack {
                        Image(systemName: "bell.circle")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .symbolRenderingMode(.hierarchical)
                            .frame(width: 150)
                            .symbolEffect(.wiggle, options: .repeat(.periodic(2, delay: 2)), value: animTrigger)

                        Button("触发动画") {
                            animTrigger.toggle()
                        }
                    }
                }
                .buttonStyle(.borderless)
                .frame(height: 100)
                .padding()
            }
            .font(.title2)
            .navigationTitle("符号动画与过渡演示")
            .toolbar {
                
                ToolbarItem(placement: .topBarLeading) {
                    Text("大熊猫侯佩 @ \(Text("CSDN").foregroundStyle(.red.gradient))")
                        .foregroundStyle(.gray)
                        .font(.headline.weight(.heavy))
                }
                
                ToolbarItem(placement: .primaryAction) {
                    Button("开启或关闭通知") {
                        withAnimation {
                            notificationsEnabled.toggle()
                        }
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

总结

在本篇博文中,我们讨论了如何在 SwiftUI 中花样玩转 SF Symbols 符号动画和过渡特效的各种"姿势",我们最后还介绍了 macOS 中 SF Symbols App 的"拔刀相助"让撸码更加轻松!

感谢观赏,再会了!8-)

相关推荐
season_zhu10 小时前
iOS开发:关于日志框架
ios·架构·swift
iOS阿玮12 小时前
苹果2024透明报告看似更加严格的背后是利好!
uni-app·app·apple
大熊猫侯佩15 小时前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩15 小时前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩15 小时前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple
iOS阿玮1 天前
苹果审核被拒4.1-Copycats过审技巧实操
uni-app·app·apple
大熊猫侯佩2 天前
SwiftUI 如何取得 @Environment 中 @Observable 对象的绑定?
swiftui·swift·apple
大熊猫侯佩2 天前
SwiftUI 6.0(iOS 18)将 Sections 也考虑进自定义容器子视图布局(下)
swiftui·swift·apple