SwiftUI opacity transition fades each views and subviews separately

151 Views Asked by At

TLDR: how to render a whole subview before fading it in?

Consider this code snippet: a simple white view and a button that toggles an overlaying FrontView. The FrontView is a superposition of 2 black views, a fullscreen one and a smaller one.

struct ContentView: View {
    @State private var frontViewIsOpen = false
    
    var body: some View {
        ZStack {
            Color.white
                .zIndex(0)
            if frontViewIsOpen {
                FrontView()
                    .transition(.opacity)
                    .zIndex(1)
            }
            VStack {
                Spacer()
                Button("Toggle") {
                    frontViewIsOpen.toggle()
                }
            }
            .padding(.bottom, 50)
            .zIndex(2)
        }
        .animation(.linear(duration: 1), value: frontViewIsOpen)
        .ignoresSafeArea()
    }
}

struct FrontView: View {
    var body: some View {
        Color.black
            .overlay {
                Color.black.frame(width: 100, height: 100)
            }
    }
}

On toggle, the FrontView fades in during 1 second. Here are the screenshots at t=0, t=0.5 and t=1:

swiftui fade animation steps

During the whole animation, the inner 100pt black view is clearly visible. I guess SwiftUI not only fades-in the FrontView, but also all subviews in the tree. At t=0.5, the FrontView alpha is 0.5, the inner view is also 0.5, so with transparency, the intersection of both should be 0.75.

I recreated this test on UIKit. While fading-in/out a view, UIKit does not fade subviews automatically, resulting with a transition rendering like:

uikit fade animation steps

I'm trying to reproduce the UIKit transition behavior in SwiftUI. I tried to remove the inner views transaction animation with .transaction { $0.animation = nil }, or to set to the inner view transition to .identity, without success.

I there a way to render the whole subview before transitionning ?

2

There are 2 best solutions below

0
On BEST ANSWER

Just add .compositingGroup to FrontView:

var body: some View {
    Color.black
        .overlay {
            Color.black.frame(width: 100, height: 100)
        }
        .compositingGroup() // <- HERE
}
0
On

You can make FrontView a compositingGroup.

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.

This means that the opacity will only be applied to the big black view before the small black view is rendered, and will not affect the small black view. The code example in the documentation also demonstrates such a situation where an opacity effect would otherwise be applied twice.

Color.black
    .overlay {
        Color.black.frame(width: 100, height: 100)
    }
    .compositingGroup()

Alternatively, drawingGroup also achieves the same result, by flattening the view hierarchy and drawing both black views as "one thing", but it won't work with non-native SwiftUI views.