Volodymyr Bobyr
Volodymyr Bobyr

Reputation: 424

SwiftUI Transition + LinearGradient + Opacity weird behavior

I'm trying to animate opacity on appear/disappear of Text view with a simple linear gradient.

Here's the "minimum" code I have right now:

import SwiftUI

struct ContentView: View {
    @State var shown: Bool = true

    var body: some View {
        VStack {
            ZStack {
                if shown {
                    TextView()
                }
            }
            .frame(height: 100)

            Button("Show Toggle") {
                shown.toggle()
            }
        }
    }
}

struct TextView: View {
    var body: some View {
        text
            .overlay(gradient)
            .mask(text)
            .transition(transition)
    }
    
    var gradient: LinearGradient {
        LinearGradient(
            colors: [Color.white, Color.blue],
            startPoint: .leading, endPoint: .trailing
        )
    }
    
    var transition: AnyTransition {
        .asymmetric(
            insertion: .opacity.animation(.linear(duration: 0.500)),
            removal: .opacity.animation(.linear(duration: 0.500))
        )
    }

    var text: some View {
        Text("Hello World")
            .fontWeight(.bold)
            .font(.largeTitle)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Notice: I'm using the view.overlay(gradient).mask(view) pattern in order for the gradient view to not be greedy.

As a result, the animation looks like this:

Resulting animation

Notice how the view doesn't "just" fade out -- it first turns black and then fades out.

I can fix opacity issues by just doing gradient.mask(text) instead of the other pattern, but then I run into other issues with the gradient view being greedy

Upvotes: 1

Views: 398

Answers (2)

ZoydWheeler
ZoydWheeler

Reputation: 73

Thought it might be helpful to note that .drawingGroup() uses Metal, which may not be necessary for your purposes in this example.

You can achieve the same effect by using .compositingGroup(), without using Metal.

        text
            .overlay(gradient)
            .mask(text)
            .compositingGroup() // here
            .transition(transition)

Per Apple's documentation:

A compositing group makes compositing effects in this view’s ancestor views, such as opacity and the blend mode, take effect before this view is rendered. Use compositingGroup() to apply effects to a parent view before applying effects to this view.

Upvotes: 0

ChrisR
ChrisR

Reputation: 12125

You are drawing a black text, then overlay the gradient, then clip it with the text. Just omit the first black text, e.g.by setting its opacity to 0.

        text.opacity(0)
            .overlay(gradient)
            .mask(text)
            .transition(transition)

or use .drawingGroup which renders the whole view offscreen before displaying:

        text
            .overlay(gradient)
            .mask(text)
            .drawingGroup() // here
            .transition(transition)

Upvotes: 3

Related Questions