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 视图。然而,除此之外,您不需要使用计时器来驱动动画。事实上,它使动画看起来很奇怪,因为计时器不准确。接下来,您的 ExpandingCircleExpandingCircleImage 应自行设置动画并成为完整效果。

修复字体大小 = 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