nurmat
nurmat

Reputation: 481

SwiftUI - Matched Geometry Effect synchronize child views size

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

Upvotes: 0

Views: 1105

Answers (1)

nurmat
nurmat

Reputation: 481

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

Upvotes: 1

Related Questions