概述
作为 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() 视图修改器即让字符栩栩如生了:
Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
.symbolEffect(.wiggle, options: .repeat(.continuous))
我们还可以恣意改变动画的种类,比如从 wiggle 改为 variableColor 效果:
Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
.symbolEffect(.variableColor, options: .repeat(.continuous))
我们甚至可以更进一步,细粒度定制 variableColor 动画效果的微妙细节:
Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
.symbolEffect(.variableColor.cumulative, options: .repeat(.continuous))
2. 自动触发动画
除了一劳永逸的让动画重复播放以外,我们还可以自动地根据 SwiftUI 视图中的状态来触发对应的动画。
如下代码所示,只要 animTrigger 状态发生改变,我们就播放 wiggle 动画 2 次(每次间隔 2 秒):
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() 修改器的另一个重载版本,来手动控制动画的开始和停止:
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 是否开启,切换显示了不同的图形字符:
@State var notificationsEnabled = false
Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 66)
但是,这样做却释放出一些“行尸走肉”的气息,让用户非常呲楞:
所幸的是,利用 contentTransition() 视图修改器我们可以将其变得行云流水、一气呵成:
Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
.resizable()
.aspectRatio(contentMode: .fit)
.contentTransition(.symbolEffect(.replace))
.frame(width: 66)
我们还可以用 symbolVariant() 修改器来重构上面的代码,效果保持不变:
Image(systemName: "bell")
.resizable()
.aspectRatio(contentMode: .fit)
.symbolVariant(!notificationsEnabled ? .slash : .none )
.contentTransition(.symbolEffect(.replace))
.frame(width: 66)
通过 symbolRenderingMode() 修改器,我们还能在过渡特效基础之上再应用字符的其它特殊渲染效果,比如分层:
Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
.resizable()
.aspectRatio(contentMode: .fit)
.symbolRenderingMode(.hierarchical)
.contentTransition(.symbolEffect(.replace))
.frame(width: 66)
当然,如果我们愿意的话同样可以更加细粒度地定制过渡的类型(downUp):
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 了:
我们可以在 https://developer.apple.com/sf-symbols 下载 SF Symbols App。
还拿上面第一个例子中的字符来举例,我们可以在 SF Symbols App 中随意为它应用各种动画效果,直到满意为止:
我们再如法炮制换一个 AirPods “把玩”一番:
至此,我们完全掌握了 SwiftUI 中 SF Symbols 符号的动画和过渡特效,小伙伴们一起享受这干脆利落、丝般顺滑的灵动风味吧!’
更多 Swift 深入系统的学习,请小伙伴们到我的《Swift语言开发精讲》专栏来逛一逛吧:
- Swift 语言开发精讲
5. 完整源代码
本文对应的全部源代码在此,欢迎品尝:
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 的“拔刀相助”让撸码更加轻松!
感谢观赏,再会了!😎