SwiftUI 奇怪的动画行为与 systemImage
SwiftUI odd animation behavior with systemImage
当我 运行 遇到一个涉及动画更改 SwiftUI 的 SF 符号的奇怪问题时,我在 SwiftUI 中摆弄一个有趣的动画。基本上,我想为一组不断扩大的圆圈制作动画,这些圆圈会随着距离变远而失去不透明度。当我使用 Circle()
形状为圆圈设置动画时效果很好,但当我使用 Image(systemName: "circle")
时会抛出一个奇怪的错误。即,它抛出 No symbol named 'circle' found in system symbol set
,我在 Xcode 中得到了可怕的“紫色”错误。为什么我的动画适用于形状而不适用于 SF 符号?
带形状的动画代码:
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
@State var firstIndex: Int = 0
@State var secondIndex: Int = 10
@State var thirdIndex: Int = 20
@State var fourthIndex: Int = 30
private func changeIndex(index: Int) -> Int {
if index == 40 {
return 0
} else {
return index + 1
}
}
var body: some View {
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(index: firstIndex)
ExpandingCircle(index: secondIndex)
ExpandingCircle(index: thirdIndex)
ExpandingCircle(index: fourthIndex)
}
.onReceive(timer) { time in
withAnimation(.linear(duration: 0.25)) {
self.firstIndex = changeIndex(index: firstIndex)
self.secondIndex = changeIndex(index: secondIndex)
self.thirdIndex = changeIndex(index: thirdIndex)
self.fourthIndex = changeIndex(index: fourthIndex)
}
}
}
}
其中 ExpandingCircle
定义为:
struct ExpandingCircle: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Circle()
.strokeBorder(Color.red, lineWidth: 4)
.frame(width: getSize(), height: getSize())
.opacity(getOpacity())
}
}
要重现错误,请将 ContentView
中的 ExpandingCircle
换成 ExpandingCircleImage
:
struct ExpandingCircleImage: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Image(systemName: "circle")
.foregroundColor(.red)
.font(.system(size: getSize()))
.opacity(getOpacity())
}
}
您的 ExpandingCircleImage
令人窒息,因为您不能使用大小为 0 的系统字体,并且您一直试图将 0 提供给您的 ExpandingCircleImage
视图。然而,除此之外,您不需要使用计时器来驱动动画。事实上,它使动画看起来很奇怪,因为计时器不准确。接下来,您的 ExpandingCircle
或 ExpandingCircleImage
应自行设置动画并成为完整效果。
修复字体大小 = 0 问题时您将遇到的下一个问题是 .font(.system(size:))
不能按原样设置动画。你需要为它写一个AnimatableModifier
。看起来像这样:
struct AnimatableSfSymbolFontModifier: AnimatableModifier {
var size: CGFloat
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
func body(content: Content) -> some View {
content
.font(.system(size: size))
}
}
extension View {
func animateSfSymbol(size: CGFloat) -> some View {
self.modifier(AnimatableSfSymbolFontModifier(size: size))
}
}
animatableData
变量是关键。它教会 SwiftUI 改变什么来渲染动画。在这种情况下,我们正在动画字体的大小。视图扩展只是为了方便,所以我们可以使用 .
表示法。
为这样的视图设置动画的另一个技巧是使用多个动画,这些动画只进行整体的一部分。换句话说,如果你使用四个圆圈,第一个到 25%,下一个从 25% 到 50%,然后 50% 到 75%,最后 75% 到 100%。您似乎还希望环随着展开而褪色,所以我也将其写入。下面的代码将有两个动画视图,一个用形状制作,一个用 SF 符号制作。
struct ContentView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(maxSize: 100)
}
.frame(height: 100)
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircleImage(maxSize: 100)
}
.frame(height: 100)
Spacer()
}
}
}
struct ExpandingCircle: View {
let maxSize: CGFloat
@State private var animate = false
var body: some View {
ZStack {
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.75 : 1)
.scaleEffect(animate ? 0.25 : 0)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.5 : 0.75)
.scaleEffect(animate ? 0.5 : 0.25)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.25 : 0.5)
.scaleEffect(animate ? 0.75 : 0.5)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0 : 0.25)
.scaleEffect(animate ? 1 : 0.75)
}
.frame(width: maxSize, height: maxSize)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
struct ExpandingCircleImage: View {
let maxSize: CGFloat
@State private var animate = false
var body: some View {
ZStack {
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.25) : 1)
.opacity(animate ? 0.75 : 1)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.5) : (maxSize * 0.25))
.opacity(animate ? 0.5 : 0.75)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.75) : (maxSize * 0.5))
.opacity(animate ? 0.25 : 0.5)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize) : (maxSize * 0.75))
.opacity(animate ? 0 : 0.25)
}
.foregroundColor(.red)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
记得在您的代码中包含 AnimatableModifier
。
当我 运行 遇到一个涉及动画更改 SwiftUI 的 SF 符号的奇怪问题时,我在 SwiftUI 中摆弄一个有趣的动画。基本上,我想为一组不断扩大的圆圈制作动画,这些圆圈会随着距离变远而失去不透明度。当我使用 Circle()
形状为圆圈设置动画时效果很好,但当我使用 Image(systemName: "circle")
时会抛出一个奇怪的错误。即,它抛出 No symbol named 'circle' found in system symbol set
,我在 Xcode 中得到了可怕的“紫色”错误。为什么我的动画适用于形状而不适用于 SF 符号?
带形状的动画代码:
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
@State var firstIndex: Int = 0
@State var secondIndex: Int = 10
@State var thirdIndex: Int = 20
@State var fourthIndex: Int = 30
private func changeIndex(index: Int) -> Int {
if index == 40 {
return 0
} else {
return index + 1
}
}
var body: some View {
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(index: firstIndex)
ExpandingCircle(index: secondIndex)
ExpandingCircle(index: thirdIndex)
ExpandingCircle(index: fourthIndex)
}
.onReceive(timer) { time in
withAnimation(.linear(duration: 0.25)) {
self.firstIndex = changeIndex(index: firstIndex)
self.secondIndex = changeIndex(index: secondIndex)
self.thirdIndex = changeIndex(index: thirdIndex)
self.fourthIndex = changeIndex(index: fourthIndex)
}
}
}
}
其中 ExpandingCircle
定义为:
struct ExpandingCircle: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Circle()
.strokeBorder(Color.red, lineWidth: 4)
.frame(width: getSize(), height: getSize())
.opacity(getOpacity())
}
}
要重现错误,请将 ContentView
中的 ExpandingCircle
换成 ExpandingCircleImage
:
struct ExpandingCircleImage: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Image(systemName: "circle")
.foregroundColor(.red)
.font(.system(size: getSize()))
.opacity(getOpacity())
}
}
您的 ExpandingCircleImage
令人窒息,因为您不能使用大小为 0 的系统字体,并且您一直试图将 0 提供给您的 ExpandingCircleImage
视图。然而,除此之外,您不需要使用计时器来驱动动画。事实上,它使动画看起来很奇怪,因为计时器不准确。接下来,您的 ExpandingCircle
或 ExpandingCircleImage
应自行设置动画并成为完整效果。
修复字体大小 = 0 问题时您将遇到的下一个问题是 .font(.system(size:))
不能按原样设置动画。你需要为它写一个AnimatableModifier
。看起来像这样:
struct AnimatableSfSymbolFontModifier: AnimatableModifier {
var size: CGFloat
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
func body(content: Content) -> some View {
content
.font(.system(size: size))
}
}
extension View {
func animateSfSymbol(size: CGFloat) -> some View {
self.modifier(AnimatableSfSymbolFontModifier(size: size))
}
}
animatableData
变量是关键。它教会 SwiftUI 改变什么来渲染动画。在这种情况下,我们正在动画字体的大小。视图扩展只是为了方便,所以我们可以使用 .
表示法。
为这样的视图设置动画的另一个技巧是使用多个动画,这些动画只进行整体的一部分。换句话说,如果你使用四个圆圈,第一个到 25%,下一个从 25% 到 50%,然后 50% 到 75%,最后 75% 到 100%。您似乎还希望环随着展开而褪色,所以我也将其写入。下面的代码将有两个动画视图,一个用形状制作,一个用 SF 符号制作。
struct ContentView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(maxSize: 100)
}
.frame(height: 100)
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircleImage(maxSize: 100)
}
.frame(height: 100)
Spacer()
}
}
}
struct ExpandingCircle: View {
let maxSize: CGFloat
@State private var animate = false
var body: some View {
ZStack {
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.75 : 1)
.scaleEffect(animate ? 0.25 : 0)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.5 : 0.75)
.scaleEffect(animate ? 0.5 : 0.25)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.25 : 0.5)
.scaleEffect(animate ? 0.75 : 0.5)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0 : 0.25)
.scaleEffect(animate ? 1 : 0.75)
}
.frame(width: maxSize, height: maxSize)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
struct ExpandingCircleImage: View {
let maxSize: CGFloat
@State private var animate = false
var body: some View {
ZStack {
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.25) : 1)
.opacity(animate ? 0.75 : 1)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.5) : (maxSize * 0.25))
.opacity(animate ? 0.5 : 0.75)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.75) : (maxSize * 0.5))
.opacity(animate ? 0.25 : 0.5)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize) : (maxSize * 0.75))
.opacity(animate ? 0 : 0.25)
}
.foregroundColor(.red)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
记得在您的代码中包含 AnimatableModifier
。