0

I'm implementing custom menu view with transition appearance from top trailing position. I'm using matchedGeometryEffect on views background. The problem is with child views of second view. They appears before animation ends. You can see below:

enter image description here

How synchronize these button views with animation of parent view?

Below my code:

struct ContentView: View {
    @State var show = false
    @Namespace var namespace
    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                Spacer()
                Button(action: {
                    withAnimation(.linear(duration: 1.2)) { // Longer duration for demo of issue
                        show.toggle()
                    }
                }) {
                    Image(systemName: "ellipsis.circle")
                        .font(.title3)
                        .background( !show ?
                            Color.clear
                                .matchedGeometryEffect(id: "topMenuToggle", in: namespace, properties: .position)
                            : nil
                        )
                }
            }
            .padding()
            Color.clear
                .overlay(
                    show ? MenuView(namespace: namespace) : nil
                    , alignment: .topTrailing
                )
        }
        .foregroundColor(.white)
        .background(Color.black.ignoresSafeArea())
    }
}
struct MenuView: View {
    let namespace: Namespace.ID
    var body: some View {
        VStack(spacing: 0) {
            Button(action: {
                
            }) {
                HStack(spacing: 0) {
                    Text("Save video")
                        .font(.body)
                    Spacer(minLength: 0)
                    Image(systemName: "arrow.down.circle")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(height: 20)
                }
            }
            .padding(.top, 18)
            .padding(.bottom, 18)
            .padding(.horizontal, 14)
            Rectangle()
                .fill(Color.white.opacity(0.1))
                .frame(maxWidth: .infinity, maxHeight: 1)
            Button(action: {
                
            }) {
                HStack(spacing: 0) {
                    Text("Report")
                        .font(.body)
                    Spacer(minLength: 0)
                    Image(systemName: "exclamationmark.bubble")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(height: 20)
                }
            }
            .padding(.top, 16)
            .padding(.bottom, 18)
            .padding(.horizontal, 14)
        }
        .foregroundColor(.white)
        .frame(width: 250)
        .background(
            Color.white.opacity(0.2)
                .matchedGeometryEffect(id: "topMenuToggle", in: namespace, anchor: .bottomLeading)
        )
        .cornerRadius(12)
    }
}
nurmat
  • 361
  • 3
  • 11
  • You need to put content *into* matched view for common animation. – Asperi Oct 23 '21 at 15:15
  • [SwiftUI Lab](https://swiftui-lab.com) has a great series of posts about `MatchedGeometryEffect` and `AdvancedAnimations` that you will find helpful. – Yrb Oct 23 '21 at 16:36

1 Answers1

1

I was able to fix animation by adding MatchedGeometryEffect for each row item and source view. Maybe it's not correct way but it works somehow. Modified code:

Button(action: {
    withAnimation(.easeInOut(duration: 1.2)) {
        show.toggle()
    }
}) {
    Image(systemName: "ellipsis.circle")
        .background( !show ?
            Color.clear
                .matchedGeometryEffect(id: "topMenuBg", in: namespace, properties: .position)
                .matchedGeometryEffect(id: "topMenuRow0", in: namespace, properties: .position)
                .matchedGeometryEffect(id: "topMenuRow1", in: namespace, properties: .position)
                .matchedGeometryEffect(id: "topMenuRow2", in: namespace, properties: .position)
            : nil
        )
}

MenuView.swift

VStack(spacing: 0) {
    Button(action: {}) {
        HStack(spacing: 0) {}
    }
    .matchedGeometryEffect(id: "topMenuRow0", in: namespace)
    Rectangle()
        .fill(Color.white.opacity(0.1))//
        .frame(maxWidth: .infinity, maxHeight: 1)
        // Before frame() the height is broken weirdly
        .matchedGeometryEffect(id: "topMenuRow1", in: namespace)
    Button(action: {}) {
        HStack(spacing: 0) {}
    }
    .matchedGeometryEffect(id: "topMenuRow2", in: namespace)
}
.frame(width: 250)
.background(
    Color.white.opacity(0.2)
        .matchedGeometryEffect(id: "topMenuBg", in: namespace, anchor: .bottomLeading)
)

Demo:

enter image description here

nurmat
  • 361
  • 3
  • 11