SwiftUI stocking geometry effects and tabBar animation

213 views Asked by At

Hey guys I have some issues with my code. I just experimented a bit with the matchedGeometryEffect in SwiftUI and it works great. But now I ran into some issues:

  1. I cannot just deactivate the tabBar when the DetailView is dismissed because the view jumps up a bit.

  2. The View transition is sometimes buggy and the console gives me (constantly) the output

Multiple inserted views in matched geometry group Pair<String, ID>(first: "bg", second: SwiftUI.Namespace.ID(id: 415)) have `isSource: true`, results are undefined.

Is there a better way to animate this smoothly and disable the tabBar?

Here is my code:

struct FullscreenView: View {
    @Namespace var animationNamespace
    
    @State var shouldShowFullsceen = false
    @State var shouldShowDetails = false
    
    var body: some View {
        Input()
            .padding()
            .onTapGesture {
                withAnimation(.interactiveSpring(
                    response: 0.6,
                    dampingFraction: 0.7,
                    blendDuration: 0.7
                )) {
                    shouldShowFullsceen = true
                }
            }
            .overlay {
                if shouldShowFullsceen {
                    Output()
                        .onTapGesture {
                            withAnimation(.interactiveSpring(
                                response: 0.6,
                                dampingFraction: 0.7,
                                blendDuration: 0.7
                            )) {
                                shouldShowFullsceen = false
                                shouldShowDetails = false
                            }
                        }
                }
            }
    }
}

extension FullscreenView {
    @ViewBuilder
    func Input() -> some View {
        Content()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(BackgroundView())
    }
    
    @ViewBuilder
    func Output() -> some View {
        DetailedContent()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(FullscreenBackground())
    }
}

extension FullscreenView {
    @ViewBuilder
    func Content() -> some View {
        Image("dog")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(maxHeight: 300)
            .matchedGeometryEffect(id: "content", in: animationNamespace)
    }
}

extension FullscreenView {
    @ViewBuilder
    func DetailedContent() -> some View {
        VStack {
            Content()
            
            ScrollView(.vertical) {
                Text(dummyText)
                    .padding()
                    .opacity(shouldShowDetails ? 1 : 0)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .padding()
        }
        .transition(.identity)
        .onAppear {
            withAnimation(.interactiveSpring(
                response: 0.6,
                dampingFraction: 0.7,
                blendDuration: 0.7
            ).delay(0.1)) {
                shouldShowDetails = true
            }
        }
    }
}

extension FullscreenView {
    @ViewBuilder
    func BackgroundView() -> some View {
        Color.orange
            .clipShape(RoundedRectangle(cornerRadius: 15))
            .matchedGeometryEffect(id: "bg", in: animationNamespace)
    }
}

extension FullscreenView {
    @ViewBuilder
    func FullscreenBackground() -> some View {
        BackgroundView()
            .ignoresSafeArea()
    }
}

struct FullscreenView_Previews: PreviewProvider {
    static var previews: some View {
        FullscreenView()
    }
}
1

There are 1 answers

1
ChrisR On

Regarding the animation and console warning:

  1. Don't overlay Output view. Show either the Input or the Output View with if ... else, then .matchedGeometryEffect can do the transition.

  2. You should use .matchedGeometryEffect with isSource: specified to true, for both image and background.

  3. get rid of .transition(.identity).

Here is the full code with comments:

struct FullscreenView: View {
    @Namespace var animationNamespace
    
    @State var shouldShowFullsceen = false
    @State var shouldShowDetails = false
    
    var body: some View {
        
        if shouldShowFullsceen == false { // show only one matched view at a time
            Input()
                .padding()
                .onTapGesture {
                    withAnimation(.interactiveSpring(
                        response: 0.6,
                        dampingFraction: 0.7,
                        blendDuration: 0.7
                    )) {
                        shouldShowFullsceen = true
                    }
                }
        } else { // show only one matched view at a time
            Output()
                .onTapGesture {
                    withAnimation(.interactiveSpring(
                        response: 0.6,
                        dampingFraction: 0.7,
                        blendDuration: 0.7
                    )) {
                        shouldShowFullsceen = false
                        shouldShowDetails = false
                    }
                }
            }
    }
    

    func Input() -> some View {
        Content()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(BackgroundView())
    }
    

    func Output() -> some View {
        DetailedContent()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(FullscreenBackground())
    }
    
    
    func Content() -> some View {
        Image(systemName: "tortoise")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(maxHeight: 300)
            .padding()
            .matchedGeometryEffect(id: "content", in: animationNamespace, isSource: true) // add isSource
    }
    
    
    func DetailedContent() -> some View {
        VStack {
            Content()
            
            ScrollView(.vertical) {
                Text("dummyText")
                    .padding()
                    .opacity(shouldShowDetails ? 1 : 0)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .padding()
        }
//        .transition(.identity) // take this out
        .onAppear {
            withAnimation(.interactiveSpring(
                response: 0.6,
                dampingFraction: 0.7,
                blendDuration: 0.7
            ).delay(0.1)) {
                shouldShowDetails = true
            }
        }
    }
    
    func BackgroundView() -> some View {
        Color.orange
            .clipShape(RoundedRectangle(cornerRadius: 15))
            .matchedGeometryEffect(id: "bg", in: animationNamespace, isSource: true) // add isSource
    }
    
    func FullscreenBackground() -> some View {
        BackgroundView()
            .ignoresSafeArea()
    }
}