前言
之前的兩篇文章animating paths 和 transform matrices 對 Animatable 協議使用做了介紹,今天這篇文章將為大家介紹 AnimatableModifier,使用它可以完成更多的動畫工作。
AnimatableModifier 是一個 ViewModifier,符合 Animatable 協議,如果對這個協議不瞭解可以閲讀之前發佈的兩篇文章。
AnimatableModifier 無法實現動畫
如果是第一次使用 AnimatableModifier,可能會遇到問題。寫一個簡單的動畫,但是沒有動畫效果。 我又試了幾次,也沒有成功。因此我認為該功能不存並且放棄使用。幸運的是,後來我堅持了下來。事實證明,我的第一個 modifier 非常好,但是 animatable modifiers 在容器中不起作用。 我在第二次嘗試時,動畫視圖不在容器內。
例如,以下 modifier 可以成功實現動畫:
MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))
但是相同的代碼,在 VStack 中就沒有動畫了:
VStack {
MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))
}
這個問題在官方解決之前,經過嘗試,可以在 VStack 中改成下面的代碼,就可以實現動畫:
VStack {
Color.clear.overlay(MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))).frame(width: 100, height: 100)
}
這樣寫是使用一個透明視圖佔據實際視圖空間,動畫被放在透明視圖上,使用 .overlay()。有點不方便的是,我們需要知道實際視圖有多大,所以我們可以在它後面設置透明視圖的框架。在下面的示例中可以開到實現代碼。
動畫文本
首先需要製作一些文字動畫。對於這個例子,我們將創建一個進度加載指示器。
可能很多人都認為應該使用動畫路徑實現。但是,內部標籤就無法設置動畫,使用 AnimatableModifier 可以實現。
完整的代碼作為 示例10 在文末鏈接中。關鍵代碼如下:
struct PercentageIndicator: AnimatableModifier {
var pct: CGFloat = 0
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
content
.overlay(ArcShape(pct: pct).foregroundColor(.red))
.overlay(LabelView(pct: pct))
}
struct ArcShape: Shape {
let pct: CGFloat
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: .degrees(0),
endAngle: .degrees(360.0 * Double(pct)), clockwise: false)
return p.strokedPath(.init(lineWidth: 10, dash: [6, 3], dashPhase: 10))
}
}
struct LabelView: View {
let pct: CGFloat
var body: some View {
Text("\(Int(pct * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
}
在示例代碼中可以看到,沒有使 ArcShape animatable 。 因為 modifier 已經多次創建形狀,具有不同的 pct 值。
動畫漸變
在實現漸變動畫時,可能會遇到一些限制。比如,可以為起點和終點設置動畫,但是不能為漸變顏色設置動畫。使用 AnimatableModifier 可以避免出現這種情況。
很容易就可以實現這個功能,在這個基礎上可以實現更多複雜的動畫。如果需要插入中間顏色,我們只需要計算 RGB 值的平均值。另外需要注意,modifier 假設輸入顏色數組都包含相同數量的顏色。
完整的代碼作為 示例11 在文末鏈接中。關鍵代碼如下:
struct AnimatableGradient: AnimatableModifier {
let from: [UIColor]
let to: [UIColor]
var pct: CGFloat = 0
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
var gColors = [Color]()
for i in 0..<from.count {
gColors.append(colorMixer(c1: from[i], c2: to[i], pct: pct))
}
return RoundedRectangle(cornerRadius: 15)
.fill(LinearGradient(gradient: Gradient(colors: gColors),
startPoint: UnitPoint(x: 0, y: 0),
endPoint: UnitPoint(x: 1, y: 1)))
.frame(width: 200, height: 200)
}
// This is a very basic implementation of a color interpolation
// between two values.
func colorMixer(c1: UIColor, c2: UIColor, pct: CGFloat) -> Color {
guard let cc1 = c1.cgColor.components else { return Color(c1) }
guard let cc2 = c2.cgColor.components else { return Color(c1) }
let r = (cc1[0] + (cc2[0] - cc1[0]) * pct)
let g = (cc1[1] + (cc2[1] - cc1[1]) * pct)
let b = (cc1[2] + (cc2[2] - cc1[2]) * pct)
return Color(red: Double(r), green: Double(g), blue: Double(b))
}
}
更多文本動畫
這個示例中,將再次實現一個文本動畫。但是是逐步進行,一次放大一個字符
完整的代碼作為 示例12 在文末鏈接中。關鍵代碼如下:
struct WaveTextModifier: AnimatableModifier {
let text: String
let waveWidth: Int
var pct: Double
var size: CGFloat
var animatableData: Double {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.0) { (n, ch) in
Text(String(ch))
.font(Font.custom("Menlo", size: self.size).bold())
.scaleEffect(self.effect(self.pct, n, self.text.count, Double(self.waveWidth)))
}
}
}
func effect(_ pct: Double, _ n: Int, _ total: Int, _ waveWidth: Double) -> CGFloat {
let n = Double(n)
let total = Double(total)
return CGFloat(1 + valueInCurve(pct: pct, total: total, x: n/total, waveWidth: waveWidth))
}
func valueInCurve(pct: Double, total: Double, x: Double, waveWidth: Double) -> Double {
let chunk = waveWidth / total
let m = 1 / chunk
let offset = (chunk - (1 / total)) * pct
let lowerLimit = (pct - chunk) + offset
let upperLimit = (pct) + offset
guard x >= lowerLimit && x < upperLimit else { return 0 }
let angle = ((x - pct - offset) * m)*360-90
return (sin(angle.rad) + 1) / 2
}
}
extension Double {
var rad: Double { return self * .pi / 180 }
var deg: Double { return self * 180 / .pi }
}
計數器動畫
如果你沒有用過或者對 AnimatableModifier 不瞭解,下面這個示例基本上是無法實現的。下面我們來介紹一下如何創建一個計數器動畫:
這個練習的訣竅是為每個數字使用 5 個文本視圖,並使用 .spring() 動畫上下移動它們。 我們還需要使用 .clipShape() 修飾符來隱藏在邊框之外繪製的部分。 為了更好地理解它是如何工作的,您可以評論 .clipShape() 並大大減慢動畫的速度。 完整代碼在本頁頂部鏈接的 gist 文件中以 Example13 的形式提供。
這個動畫實現的主要內容是每個數字使用 5 個文本視圖,並使用 .spring() 動畫上下移動它們。然後使用 .clipShape() 修飾符來隱藏邊框之外區域。如果想跟清晰的理解他們是如何實現的,可以通過 .clipShape() 讓動畫速度變慢。
完整的代碼作為 示例13 在文末鏈接中。關鍵代碼如下:
struct MovingCounterModifier: AnimatableModifier {
@State private var height: CGFloat = 0
var number: Double
var animatableData: Double {
get { number }
set { number = newValue }
}
func body(content: Content) -> some View {
let n = self.number + 1
let tOffset: CGFloat = getOffsetForTensDigit(n)
let uOffset: CGFloat = getOffsetForUnitDigit(n)
let u = [n - 2, n - 1, n + 0, n + 1, n + 2].map { getUnitDigit($0) }
let x = getTensDigit(n)
var t = [abs(x - 2), abs(x - 1), abs(x + 0), abs(x + 1), abs(x + 2)]
t = t.map { getUnitDigit(Double($0)) }
let font = Font.custom("Menlo", size: 34).bold()
return HStack(alignment: .top, spacing: 0) {
VStack {
Text("\(t[0])").font(font)
Text("\(t[1])").font(font)
Text("\(t[2])").font(font)
Text("\(t[3])").font(font)
Text("\(t[4])").font(font)
}.foregroundColor(.green).modifier(ShiftEffect(pct: tOffset))
VStack {
Text("\(u[0])").font(font)
Text("\(u[1])").font(font)
Text("\(u[2])").font(font)
Text("\(u[3])").font(font)
Text("\(u[4])").font(font)
}.foregroundColor(.green).modifier(ShiftEffect(pct: uOffset))
}
.clipShape(ClipShape())
.overlay(CounterBorder(height: $height))
.background(CounterBackground(height: $height))
}
func getUnitDigit(_ number: Double) -> Int {
return abs(Int(number) - ((Int(number) / 10) * 10))
}
func getTensDigit(_ number: Double) -> Int {
return abs(Int(number) / 10)
}
func getOffsetForUnitDigit(_ number: Double) -> CGFloat {
return 1 - CGFloat(number - Double(Int(number)))
}
func getOffsetForTensDigit(_ number: Double) -> CGFloat {
if getUnitDigit(number) == 0 {
return 1 - CGFloat(number - Double(Int(number)))
} else {
return 0
}
}
}
動畫文本顏色
通常情況下是通過 .foregroundColor() 為動畫添加顏色,但是在文本類動畫中使用沒有效果,不知道是缺少什麼配置還是什麼原因。我通過下面的方法實現給文本動畫添加顏色。
完整的代碼作為 示例14 在文末鏈接中。關鍵代碼如下:
struct AnimatableColorText: View {
let from: UIColor
let to: UIColor
let pct: CGFloat
let text: () -> Text
var body: some View {
let textView = text()
return textView.foregroundColor(Color.clear)
.overlay(Color.clear.modifier(AnimatableColorTextModifier(from: from, to: to, pct: pct, text: textView)))
}
struct AnimatableColorTextModifier: AnimatableModifier {
let from: UIColor
let to: UIColor
var pct: CGFloat
let text: Text
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
return text.foregroundColor(colorMixer(c1: from, c2: to, pct: pct))
}
// This is a very basic implementation of a color interpolation
// between two values.
func colorMixer(c1: UIColor, c2: UIColor, pct: CGFloat) -> Color {
guard let cc1 = c1.cgColor.components else { return Color(c1) }
guard let cc2 = c2.cgColor.components else { return Color(c1) }
let r = (cc1[0] + (cc2[0] - cc1[0]) * pct)
let g = (cc1[1] + (cc2[1] - cc1[1]) * pct)
let b = (cc1[2] + (cc2[2] - cc1[2]) * pct)
return Color(red: Double(r), green: Double(g), blue: Double(b))
}
}
}
版本相關問題
通過上面介紹可以看出 AnimatableModifier 非常強大,但是還存在一些問題。另外在 Xcode 和 iOS/macOS 某些版本中,App 在啓動時會崩潰。而且是在部署時,正常開發編譯中是不會發生這種情況。
dyld: Symbol not found: _$s7SwiftUI18AnimatableModifierPAAE13_makeViewList8modifier6inputs4bodyAA01_fG7OutputsVAA11_GraphValueVyxG_AA01_fG6InputsVAiA01_L0V_ANtctFZ
Referenced from: /Applications/MyApp.app/Contents/MacOS/MyApp
Expected in: /System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI
例如,如果 App 在 Xcode 11.3 上部署並在 macOS 10.15.0 上執行,就會出現 “Symbol not found” 錯誤。然而,在 macOS 10.15.1 上運行相同的可執行文件可以正常工作。
關於我們
Swift社區是由 Swift 愛好者共同維護的公益組織,我們在國內以微信公眾號的運營為主,我們會分享以 Swift實戰、SwiftUl、Swift基礎為核心的技術內容,也整理收集優秀的學習資料。
特別感謝 Swift社區 編輯部的每一位編輯,感謝大家的辛苦付出,為 Swift社區 提供優質內容,為 Swift 語言的發展貢獻自己的力量。