This is the 8th day of my participation in the First Challenge 2022

preface

After introducing the Use of the Animatable protocol in two previous articles, Animating Paths and Transform Matrices, this article will introduce you to the AnimatableModifier, which can be used to do more animation work.

AnimatableModifier is a ViewModifier that conforms to the Animatable protocol. Read the two previous posts if you are not familiar with this protocol.

AnimatableModifier cannot implement animation

If you are using AnimatableModifier for the first time, you may encounter problems. Write a simple animation, but no animation effect. I tried a few more times, but it didn’t work. Therefore, I consider this feature dead and abandoned. Fortunately, I persevered. My first modifier turned out to be pretty good, but the Animatable Modifiers didn’t work in containers. On my second try, the animated view is not in the container.

For example, the following modifier can be successfully implemented for animation:

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

But the same code has no animation in the VStack:

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

This problem before the official solution, after trying, can be changed in the VStack to the following code, you can achieve animation:

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

This is done using a transparent view that takes up the real view space. The animation is placed on the transparent view using.overlay(). Somewhat inconveniently, we need to know how big the actual view is, so we can set the transparent view frame behind it. The implementation code can be accessed in the following example.

Animated text

First we need to do some text animation. For this example, we will create a progress load indicator.

Many people might think that the animation path implementation should be used. However, internal tabs cannot be set to animation, which can be done using the AnimatableModifier.

The complete code is shown as example 10 at the end of the article link. 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)
        }
    }
}
Copy the code

As you can see in the sample code, ArcShape Animatable is not enabled. Because modifier has been created multiple times for shapes with different PCT values.

Animation gradient

There are some limitations you may encounter when implementing gradient animations. For example, you can animate a starting and ending point, but not a gradient color. Use AnimatableModifier to avoid this situation.

This feature is easy to implement, and more complex animations can be implemented from there. If we need to insert an intermediate color, we just need to calculate the average of the RGB values. Also note that the modifier assumes that the input color array all contains the same number of colors.

The complete code is shown as example 11 at the end of the article link. 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))
    }
}
Copy the code

More Text animations

In this example, a text animation will be implemented again. But do it gradually, one character at a time

The complete code is shown as example 12 at the end of the article link. The key codes are as follows:

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

Counter animation

If you haven’t used AnimatableModifier or are unfamiliar with AnimatableModifier, the following example is basically impossible to implement. Here’s how to create a counter animation:

The trick to this exercise is to use five text views for each number and move them up and down using the.spring() animation. We also need to use the.clipShape() modifier to hide parts drawn outside the border. To better understand how it works, you can comment.clipShape() and slow down the animation considerably. The complete code is provided as Example13 in the gist file linked at the top of this page.

The main content of this animation implementation is to use five text views per number and move them up and down using the.spring() animation. Then use the.clipShape() modifier to hide the area outside the border. If you want to understand exactly how they do this, you can use.clipShape() to slow down the animation.

The complete code is shown as example 13 at the end of the article link. The key codes are as follows:

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

The usual way to add color to an animation is through.foregroundcolor (), but using it in text-based animation has no effect. I use the following method to add color to the text animation.

The complete code is shown as example 14 at the end of the article link. The key codes are as follows:

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

Version-related Issues

As you can see from the above, AnimatableModifier is very powerful, but there are still some problems. In addition, in Xcode and some versions of iOS/macOS, apps crash upon startup. And at deployment time, this doesn’t happen in normal development compilations.

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, if the App is deployed on Xcode 11.3 and executed on macOS 10.15.0, the “Symbol Not found” error will appear. However, running the same executable on macOS 10.15.1 will work fine.

The SwiftUI Lab Advanced SwiftUI Animations — Part 3: AnimatableModifier

The full sample code for this article can be found at:

Gist.github.com/swiftui-lab…

Image resources required for Example 8. Download it here:

Swiftui-lab.com/?smd_proces…