SwiftUI 奇怪的动画行为与 systemImage

Posted

技术标签:

【中文标题】SwiftUI 奇怪的动画行为与 systemImage【英文标题】:SwiftUI odd animation behavior with systemImage 【发布时间】:2022-01-06 16:15:58 【问题描述】:

我在 SwiftUI 中处理有趣的动画时遇到了一个奇怪的问题,涉及对 SwiftUI 的 SF 符号进行动画更改。基本上,我想为一组扩大的圆圈制作动画,这些圆圈随着它们越远而失去不透明度。当我使用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())
       
        
    

要复制错误,请将ExpandingCircle 中的ContentView 替换为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())
    

【问题讨论】:

【参考方案1】:

您的ExpandingCircleImage 令人窒息,因为您不能拥有大小为 0 的系统字体,并且您一直试图将 0 输入到您的 ExpandingCircleImage 视图中。但是,除此之外,您不需要使用计时器来驱动动画。事实上,它使动画看起来很奇怪,因为计时器不准确。接下来,您的ExpandingCircleExpandingCircleImage 应该为自己设置动画并成为完整的效果。

修复 font size = 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 Symbol 制作的。

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 奇怪的动画行为与 systemImage的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI - 奇怪的动画

滚动视图中的 SwiftUI 动画/过渡表现奇怪

SwiftUI 静态列表奇怪的重用行为

SwiftUI:onAppear 的奇怪行为

SwiftUI:NavigationLink 奇怪的行为

SwiftUI TextField (MacOS) 的奇怪行为