5

I'd like to see how I can make it so changes to a Text view's source change with a fade in and out.

So if I've got a simple view like the following:

struct TestView: View {
    
    @State var text = "I am a shrubber."
    
    func changeText() {
        text = (text == "I am a shrubber.") ? "My name is Roger the Shrubber." : "I am a shrubber."
    }
    
    var body: some View {
        VStack {
            Text(self.text)
            TestButton(label: "Change", action: changeText)
        }
    }
    
}

If I tap the button the text changes instantly.

Instant transition

Now, if I wrap the changing code in a withAnimation block, or add an .animation view modifier to the Text view, it just animates the change in the frame size. Which isn't what I'm looking for:

struct TestView: View {
    
    @State var text = "I am a shrubber."
    
    func changeText() {
        withAnimation {
            text = (text == "I am a shrubber.") ? "My name is Roger the Shrubber." : "I am a shrubber."
        }
    }
    
    var body: some View {
        VStack {
            Text(self.text)
            TestButton(label: "Change", action: changeText)
        }
    }
    
}

Frame resizing

Of course, I could get close to the effect I'm looking for by using .transition view modifiers and separate text fields (this answer: https://stackoverflow.com/a/60984127/5919644), like this:

struct TestView: View {
    
    @State var changed = false
    
    var body: some View {
        VStack {
            if !changed {
                Text("I am a shrubber.")
                    .transition(AnyTransition.opacity.animation(.easeInOut(duration:0.3)))
            }
            if changed {
                Text("My name is Roger the Shrubber.")
                    .transition(AnyTransition.opacity.animation(.easeInOut(duration:0.3)))
            }
            
            TestButton(label: "Change", action: { self.changed.toggle() })
        }
    }
    
}

Fading

But that is terrible pattern and doesn't work with arbitrary text changes. And even then the fade in and out happen at the same time. I'd like to have the fade out/in happen sequentially.

1
  • have you tried using animation duration?, For example, '.animation(Animation.easeInOut(duration: 3)) ' Commented Aug 22, 2020 at 5:47

3 Answers 3

17

You can force SwiftUI to create different Text View for each currentText by setting id and use animated transition:

struct TestView: View {    
    @State private var text: String = "Alpha"
    
    private var values: [String] = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel"]
    
    private func updateText() {
        self.text = values.randomElement()!
    }
    
    var body: some View {
        VStack {
            Text(text)
                .id(text)
                .transition(.opacity.animation(.linear))

            Button("Change", action: updateText).padding()
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

5

Okay, so I think I've found the best way to handle this myself: watching for changes via a binding's publisher, then manually fading out, changing text, and fading back.

The view looks like this:

struct FadingTextView: View {
    
    @Binding var source: String
    var transitionTime: Double
    
    @State private var currentText: String? = nil
    @State private var visible: Bool = false
    private var publisher: AnyPublisher<[String.Element], Never> {
        source
            .publisher
            .collect()
            .eraseToAnyPublisher()
    }
    
    init(text: Binding<String>, totalTransitionTime: Double) {
        self._source = text
        self.transitionTime = totalTransitionTime / 3
    }
    
    private func update(_: Any) {
        guard currentText != nil else {
            currentText = source
            DispatchQueue.main.asyncAfter(deadline: .now() + (transitionTime)) {
                self.visible = true
            }
            return
        }
        guard source != currentText else { return }
        self.visible = false
        DispatchQueue.main.asyncAfter(deadline: .now() + (transitionTime)) {
            self.currentText = source
            DispatchQueue.main.asyncAfter(deadline: .now() + (transitionTime)) {
                self.visible = true
            }
        }
    }
    
    var body: some View {
        Text(currentText ?? "")
            .opacity(visible ? 1 : 0)
            .animation(.linear(duration: transitionTime))
            .onReceive(publisher, perform: update(_:))
    }
    
}

Then it can be used for arbitrary text changes like this:

struct TestView: View {
    
    @State private var text: String = "Alpha"
    
    private var values: [String] = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel"]
    
    private func updateText() {
        self.text = values.randomElement()!
    }
    
    var body: some View {
        VStack {
            FadingTextView(text: $text, totalTransitionTime: 1.0)
            Button("Change", action: updateText).padding()
        }
    }
}

And looks like this:

Result screen recording

Comments

1

You can also apply asymmetric transition with conditional insertion delay like this:

struct FadeInOutView<Value: Hashable, Content: View>: View {

    let value: Value
    let content: (Value) -> Content
    
    @State private var didAppear = false

    var body: some View {
        content(value)
            .transition(.asymmetric(
                insertion: .opacity.animation(
                    .easeInOut(duration: 0.35).delay(didAppear ? 0.2 : 0)
                ),
                removal: .opacity.animation(.easeInOut(duration: 0.2))
            ))
            .id(value)
            .onAppear { didAppear = true }
    }
}

With didAppear condition content will appear without any delay.

But using .id modifier makes SwiftUI recreate internal view state every time identifier's value changes and re-render the whole view. So it becomes more performance heavy, and it's better not to use this solution on complex views.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.