13

How would it be possible to animate Text or TextField views from Swift UI?

By animation I mean, that when the text changes it will "count up".

For example given some label, how can an animation be created that when I set the labels text to "100" it goes up from 0 to 100. I know this was possible in UIKit using layers and CAAnimations, but using the .animation() function in Swift UI and changing the text of a Text or TextField does not seem to do anything in terms of animation.

I've taken a look at Animatable protocol and its related animatableData property but it doesn't seem like Text nor TextField conform to this. I'm trying to create a label that counts up, so given some value, say a Double the changes to that value would be tracked, either using @State or @Binding and then the Text or TextField would animate its content (the actual string text) from what the value was at to what it was set to.

Edit:

To make it clearer, I'd like to recreate a label that looks like this when animated:

enter image description here

6
  • AFAIK, animatableData is for animating paths only. Also, when you say "go up", what do you mean? move physically up? or increase its number? Commented Jul 10, 2019 at 21:21
  • @kontiki Ah gotcha, and I'm looking to create a label that animates by increasing the number. I've updated my question with an example gif Commented Jul 10, 2019 at 21:24
  • 1
    Thanks for the clarification, I'll think about it ;-) From the top of my head, I think you may need to have a @State variable with a counter, and a timer that increases that number. Simultaneously, you would have a view that receives the percentage number as a parameter. Technically is not a SwiftUI animation, but it could work. Commented Jul 10, 2019 at 21:33
  • What you're describing is not an "animation" in the UIKit sense. You want to create a Timer that fires repeatedly and changes the text in your label. (I don't know what's driving the completion percentage, but if you have a progress block you can update it in there too) Commented Jul 10, 2019 at 23:53
  • 1
    With time I learnt that animatableData can be used to animate Text after all. I posted a new answer to your question. Commented Sep 9, 2019 at 19:44

2 Answers 2

21

There is a pure way of animating text in SwiftUI. Here's an implementation of your progress indicator using the AnimatableModifier protocol in SwiftUI:

enter image description here

I've written an extensive article documenting the use of AnimatableModifier (and its bugs). It includes the progress indicator too. You can read it here: https://swiftui-lab.com/swiftui-animations-part3/

struct ContentView: View { @State private var percent: CGFloat = 0 var body: some View { VStack { Spacer() Color.clear.overlay(Indicator(pct: self.percent)) Spacer() HStack(spacing: 10) { MyButton(label: "0%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0 } } MyButton(label: "27%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0.27 } } MyButton(label: "100%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 1.0 } } } }.navigationBarTitle("Example 10") } } struct Indicator: View { var pct: CGFloat var body: some View { return Circle() .fill(LinearGradient(gradient: Gradient(colors: [.blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing)) .frame(width: 150, height: 150) .modifier(PercentageIndicator(pct: self.pct)) } } 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) } } } 
Sign up to request clarification or add additional context in comments.

2 Comments

Awesome stuff, this is exactly what I was looking to do!
MyButton code is here gist.github.com/swiftui-lab/…
5

You can use a CADisplayLink in a BindableObject to create a timer that updates your text during the animation. Gist

 class CADisplayLinkBinding: NSObject, BindableObject { let didChange = PassthroughSubject<CADisplayLinkBinding, Never>() private(set) var progress: Double = 0.0 private(set) var startTime: CFTimeInterval = 0.0 private(set) var duration: CFTimeInterval = 0.0 private(set) lazy var displayLink: CADisplayLink = { let link = CADisplayLink(target: self, selector: #selector(tick)) link.add(to: .main, forMode: .common) link.isPaused = true return link }() func run(for duration: CFTimeInterval) { let now = CACurrentMediaTime() self.progress = 0.0 self.startTime = now self.duration = duration self.displayLink.isPaused = false } @objc private func tick() { let elapsed = CACurrentMediaTime() - self.startTime self.progress = min(1.0, elapsed / self.duration) self.displayLink.isPaused = self.progress >= 1.0 self.didChange.send(self) } deinit { self.displayLink.invalidate() } } 

And then to use it:

@ObjectBinding var displayLink = CADisplayLinkBinding() var body: some View { Text("\(Int(self.displayLink.progress*100))") .onAppear { self.displayLink.run(for: 10.0) } } 

6 Comments

I was hoping this would be possible without using UIKit / Objective-C since I'm trying to do this purely using Swift UI. Thanks for the answer, I didn't know about CADisplayLink
CADisplayLink isn't actually UIKit, it's Core Animation. What you're looking to do is not possible without using a percentage to drive your animation (not to be confused with UIPercentDrivenInteractiveTransition). In your comment above you mention that it's possible in UIKit. I don't believe that's correct without using a timer like I outlined. Do you have a link?
I suggest checking out SwiftUI-Processing, in particular RenderClock.swift. Under the hood this is still using CADisplayLink.
Sure, here is a link, this is a project I maintain but was written with UIKit. It uses a custom layer and @NSManaged to manipulate the label text, etc (take a look at UICircularRingLayer.swift). It's possible. As to whether or not this is possible in SwiftUI without using CoreAnimation or some equivalent was the purpose of this question, since I've been trying to rewrite the library I linked in Swift UI. I would have hoped that something like animatableData for Path and Shape could be used for Text but I guess not.
I'll however, mark this is as the answer since as you mention it doesn't seem like there is another way. At least not with the current API's/knowledge the community has.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.