Article source address :swiftui-lab.com/swiftui-ani…

Author: Javier

Translation: Liaoworking

We’ve already seen how the Animatable protocol helps us animate path and transform matrices, and in the final part of this series, we’ll take it a step further. AnimatableModifier is the most powerful of the three tools. You can do whatever you want with it.

The namesake AnimatableModifier is a view modifier that follows the Animatable protocol (described in Section 1). If you don’t know how Animatable and animatableData work, go back to section 1.

Now think about the use of the Animatable Modifier, which allows you to modify your view multiple times to animate it.

The complete sample code for this article can be found at: gist.github.com/swiftui-lab…

Example8 requires images from an Asset catalog. Download it from here: swiftui-lab.com/?smd_proces…

Why is AnimatableModifier unable to animate?

If you plan to use AnimatableModifier in a production environment, be sure to read the last section and fight the versionCopy the code

If you want to give the protocol a try, chances are you’re going to hit a brick wall. I’ve tried this before, I wrote a very simple Animatable modifier, but the view was not animated, I tried a few other things, but it still didn’t work, luckily I stuck with it for a while and it worked. I’m going to make this one lucky bold. My first modifier was fine, but it didn’t work when it was inside the container… It works the second time because my view is not inside the container, and if I had been lucky in the first place, I would not have written the third article.

For example, the following modifier can be a good animation

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

But in VStack, the same code doesn’t work

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

So how do you make the Animatable Modifiers work in VStack? We can use the following trick:

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

Overlay () adds the actual graphics using a transparent view. We need to know the size of the actual graph to determine the size of the transparent graph, which is sometimes a little bit troublesome.

I have reported this issue to Apple, click here for FB code. You can try it, too.

Text animation:

The first example is to make a load indicator.

My first instinct was to use the Animatable Path, but that doesn’t animate the label, so use the AnimatableModifier.

The complete code can be found at Example10 in the gist at the top.

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: 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) } } }Copy the code

As you can see in the example, we didn’t make the arc move, which is not necessary because the Modifier has been used several times to create the figure with different PCT percentages.

The gradient of animation

If you want to animate a gradient layer. For example, you can move from start to finish, but you can’t change the gradient, but in AnimatableModifier you can:

The implementation is relatively simple, we just need to calculate the RGB average. Note, however, that the modifier assumes that the count of each color array we enter is the same throughout.

The full code can be found in Example11 for gist at the top of this article.

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)) } }Copy the code

More text animations

In our following example we will only animate one letter at a time.

Smooth scaling requires some math. If you write it, you’ll enjoy it. I put the code for Example12 in the gist at the top of the article

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 }
}
Copy the code

How about some ideas

Until we know more about AnimatableModifier, the following counters may be a bit more challenging.

The trick of this exercise is to take five numbers in each column vertically and animate them with.spring(), and we also need.clipShape() to hide the view outside the border. You can comment out.clipShape() and slow down the animation to better understand how it works. The complete code is in Example13 in the gist at the top of the article.

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 } }}Copy the code

Animated text color

If you try to animate.foregroundcolor (), you’ll see that the developer experience is great, and the complete code is in Example14.

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

    }
}
Copy the code

Dancing With Versions

We’ve found the AnimatableModifier to be quite powerful, though slightly buggy. The biggest problem is that the app crashes when it reboots for specific Xcode and iOS and macOS versions, and more frequently during deployment. But compilation and dev environment is fine. I thought it would be fine, but when I compiled it at deployment time, IT would have the following content:

dyld: Symbol not found: _$s7SwiftUI18AnimatableModifierPAAE13_makeViewList8modifier6inputs4bodyAA01_fG7OutputsVAA11_GraphValueVyxG_AA01_fG6Input sVAiA01_L0V_ANtctFZ Referenced from: /Applications/MyApp.app/Contents/MacOS/MyApp Expected in: /System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUICopy the code

For example, Xcode11.3 running on macOS 10.15.0 starts with a “symbol table not found” error, but on 10.15.1 the same files are stored in a stable batch.

Conversely, if deployed on Xcode11.1, it works on all macOS versions (at least the ones I tried)

IOS also has a similar problem. Apps packaged with Xcode 11.2 using AnimatableModifier will not start on iOS 13.2.2, but will work fine on iOS 13.2.3.

So FOR the time being, I’m using Xcode11.1 for stability. It is possible that the later version will be used, but will be upgraded to 10.15.1 for Mac OS (unless this bug is fixed, which I doubt).

Summary and what’s next

We have seen a simple use of the Animatable protocol. Use your creativity, there will be a lot of cool animation.

This concludes the “SwiftUI Advanced Animation “series, and I’ll talk about some custom transitions. This is a summary of these articles.

Follow me on Twitter to make sure you get more content. Comments are welcome. If you want to be alerted when a new article comes out, there’s a link below. swiftui-lab.com/