Advanced swiftui animation part 3:animatablemodifier

Time:2022-5-31
Advanced swiftui animation part 3:animatablemodifier

We have seenAnimatableHow the agreement helps us achievepathandTransformation matrixAnimated. In the last part of this series, we’ll take this one step further.AnimatableModifierIs the most powerful of the three. With it, we can complete the task without restriction.

The name says it all:AnimatableModifier. It is aViewModifier, compliantAnimatable. If you don’t knowAnimatableandanimatableDataHow does it work? Please come to this series firstPart IGo and have a look.

Well, let’s pause and think about what it means to have an animatable modifier You may think it’s too good to be true. Can I really modify my view multiple times through an animation? The answer is simple: Yes, you can.

The complete sample code for this article can be found here:

https://gist.github.com/swiftui-lab/e5901123101ffad6d39020cc7a810798

Example 8 required picture resources. Download here:

https://swiftui-lab.com/?smd_process_download=1&download_id=916

Animatablemodifier cannot realize animation! Why?

If you plan to use it in production codeAnimatableModifierBe sure to read the last section of this article: dancing with the version.

If you try to use this protocol yourself, you may run into a wall at the beginning. Of course I am. In my first attempt, I wrote a very simple animatable modifier, but the view was not animated. I tried again several times, but it didn’t work. Since we were in the early testing stage, I thought this function did not exist, so I gave it up completely. Fortunately, I stuck to it later. Let me emphasize the word “lucky”. It turns out that my first modifier is perfect, but animatable modifiers don’t work in containers. Just the second time I tried, my view was not in the container. If I hadn’t been so lucky, you wouldn’t have seen this third article.

For example, the following modifiers can successfully implement Animation:

MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))

But with the same code, there is no animation in vstack:

VStack {
    MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))
}

Before the problem is officially solved, the following code can be changed in vstack after trial to realize Animation:

VStack {
    Color.clear.overlay(MyView().modifier(MyAnimatableModifier(value: flag ? 1 : 0))).frame(width: 100, height: 100)
}

Basically, we use a transparent view to occupy the space of our actual view. This view will be placed on it and used.overlay(). The only inconvenience is that we need to know how big the actual view is, so that we can set the size of the transparent view behind it. This can be tricky at times, but we have to have some skills. We will see in the following example.

##Animated text

First, we need to make some text animation. For this example, we will create a progress loading indicator, which will be a label with a label:

Advanced swiftui animation part 3:animatablemodifier

Many people think that animation path should be used. However, in that case, the internal label cannot be animated. However, theAnimatableModifierCan be achieved.

Complete code asExample 10In the link at the beginning of the article. The key codes are as follows:

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)
        }
    }
}

As you can see in the example, we did not make arcshape animatable. This is not necessary because the modifier is already creating the shape multiple times with different PCT values.

##Gradient animation

If you have ever tried to animate gradients, you may find that there are some limitations. For example, you can animate the start and end points, but you cannot animate the color of the gradient. Here, we can alsoAnimatableModifierBenefit from.

Advanced swiftui animation part 3:animatablemodifier

This function can be easily realized, and more complex animation can be realized on this basis. If we need to insert an intermediate color, we only need to calculate the average of RGB values. In addition, it should be noted that,modifierAssume that the input color array contains the same number of colors.

Complete code asExample 11In the link at the beginning of the article. The key codes are as follows:

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))
    }
}

More text animations

In our next example, we will animate the text again. However, in this case, we will do it step by step: one character at a time.

Advanced swiftui animation part 3:animatablemodifier

Smooth progressive scaling requires some math, but the results are worth it. The complete code can be found in example 12 in the gist file linked at the top of this page.

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 }
}

##Use your creativity

Before weAnimatableModifierUntil you know it, the following example may seem impossible to implement. Our next challenge is to create a counter.

Advanced swiftui animation part 3:animatablemodifier

The trick of this exercise is to use five text views for each number, using.spring()The animation moves them up and down. We also need to use a.clipShape()Modifier to hide the part painted outside the boundary. To better understand how it works, you can.clipShape()Annotate and greatly reduce the speed of animation.

The complete code can be found in the gist file linked at the top of this pageExample13Obtained in the form of.

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
            }
        }
    }

##Text color animation

If you ever tried to.foregroundColor()For animation, you may have noticed that it works well, except when the view is of text type. I don’t know if this is a mistake or a lack of function. However, if you need to animate the color of the text, you can use the followingAnimatableModifierTo achieve. The complete code can be found in the gist file linked at the top of this pageExample14Found in.

Advanced swiftui animation part 3:animatablemodifier

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))
        }
    }
}

##Dance with version

We have seenAnimatableModifierVery powerful However, there is also a small problem. The biggest problem is that under some combinations of Xcode and ios/macos versions, the application will crash directly at startup. Worse, this usually happens when an application is deployed, but it doesn’t happen when it is compiled and run with Xcode during normal development. You may spend a lot of time developing and debugging and think everything is fine, but when you deploy, you will get this result.

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

For example, if you deploy an application with Xcode 11.3 and execute it on MacOS 10.15.0, it will fail to start and a “symbol not found” error will appear. However, running the same executable on 10.15.1 works.

On the contrary, if we deploy with Xcode 11.1, it works well in all MacOS versions (at least the version I tried).

A similar situation also occurs on IOS. One useAnimatableModifierAfter the application is deployed in Xcode 11.2, it cannot be started in IOS 13.2.2, but it can work normally in IOS 13.2.3.

For now, I will continue to use Xcode 11.1 for my needsAnimatableModifierMacOS entry for. In the future, I may use a newer version of Xcode, but I will raise the application requirements to MacOS 10.15.1 (unless this problem is solved, I am very skeptical).

Summary

We’ve seen itAnimatableHow simple the protocol is and how useful it is. Put your creativity to work and the results will be amazing.

Translated fromThe SwiftUI LabofAdvanced SwiftUI Animations – Part 3: AnimatableModifier

Recommended Today

JS generate guid method

JS generate guid method https://blog.csdn.net/Alive_tree/article/details/87942348 Globally unique identification(GUID) is an algorithm generatedBinaryCount Reg128 bitsNumber ofidentifier , GUID is mainly used in networks or systems with multiple nodes and computers. Ideally, any computational geometry computer cluster will not generate two identical guids, and the total number of guids is2^128In theory, it is difficult to make two […]