1

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()
    }
}
Noah
  • 21
  • 2
  • what do you mean by "disable the tabBar" ... I don't see a TabView in your code? – ChrisR Jan 01 '23 at 16:22
  • The TabView is given by the view that includes this view. This view self doesnt know that there is a tabbar, but the Output View should disable the tabbar if there is one. :) – Noah Jan 02 '23 at 17:18

1 Answers1

0

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()
    }
}

ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Thank you for your help. Sadly this results in another problem. The whole view flickers white for a few frames. – Noah Jan 03 '23 at 09:55