0

The matched geometry effect works properly when my code looks like this:

    @State var isZoomed: Bool = false
    @State var showText: Bool = false
    @Namespace var namespace
    
    var user: UserModel = exampleUsers[0]
    
    
    var body: some View {
        VStack {
            if !isZoomed {
                VStack {
                    Image(user.localProfileUrl!)
                            .resizable().aspectRatio(contentMode: .fit)
                            .matchedGeometryEffect(id: "rec", in: namespace)
                            .mask(RoundedRectangle(cornerRadius: 10)
                                .matchedGeometryEffect(id: "rec", in: namespace))
                        .frame(height: 100)
                }
                .overlay {
                    GeometryReader { geo in
                        VStack(alignment: .leading) {
                            Text(user.artistName!.uppercased())
                                .font(.title)
                                .bold()
                                .matchedGeometryEffect(id: "artistName", in: namespace)
                            Text(user.occupation)
                                .matchedGeometryEffect(id: "occupation", in: namespace)
                            Text("\(user.followers) Followers")
                                .matchedGeometryEffect(id: "followerCount", in: namespace)                        }
                        .frame(maxWidth: .infinity, maxHeight: 100, alignment: .bottomLeading)
                        .opacity(showText ? 1 : 0)
                        .padding()
                    }
                }
                .onTapGesture {
                    withAnimation() {
                        isZoomed.toggle()
                        showText.toggle()
                    }
                }
            }
            
            if isZoomed {
                VStack {
                    Image(user.localProfileUrl!)
                            .resizable().aspectRatio(contentMode: .fit)
                            .matchedGeometryEffect(id: "rec", in: namespace)
                            .mask(RoundedRectangle(cornerRadius: 20, style: .continuous)
                                .matchedGeometryEffect(id: "rec", in: namespace))
                            .frame(height: 500)
                }
                .overlay {
                    GeometryReader { geo in
                        VStack(alignment: .leading) {
                            Text(user.artistName!.uppercased())
                                .font(.title)
                                .bold()
                                .matchedGeometryEffect(id: "artistName", in: namespace)
                            Text(user.occupation)
                                .matchedGeometryEffect(id: "occupation", in: namespace)
                            Text("\(user.followers) Followers")
                                .matchedGeometryEffect(id: "followerCount", in: namespace)
                        }
                        .frame(maxWidth: .infinity, maxHeight: 500, alignment: .bottomLeading)
                        .opacity(showText ? 1 : 0)
                        .padding()
                    }
                }
                .onTapGesture {
                    withAnimation() {
                        isZoomed.toggle()
                        showText.toggle()
                    }
                }
                .offset(y: -100)
            }
        }

The effect smoothly transitions from a small frame to a large frame and back to a small frame (as intended). However, if I refactor the code to split the two views into separate vars like so:

    var body: some View {
        VStack {
            if !isZoomed {
                nonZoom
            }
            
            if isZoomed {
                fullZoom
            }
        }
    }

essentially putting the two Vstacks into separate vars, the geometry effect breaks:

enter image description here

So, why is such a simple tweak (which is quite a necessary one considering we need to separate views into files later on as the project gets more and more complicated) break the effect? And how do we fix it?

  • If you mean computable vars then they are evaluated every time, whereas views are cached/tracked/compared. So instead of refactoring to vars put then into separated views. Like in https://stackoverflow.com/a/63131527/12299030. – Asperi Jul 08 '22 at 16:24
  • Actually needed minimal reproducible example to debug, maybe you just missed something... – Asperi Jul 08 '22 at 17:29
  • Ater looking through the solution below, I'm getting it to work, but I'm still not sure why the issue happens. Which does scare me a bit because I'm not sure how many other small issues I'll run into like this because of my lack of understanding of the geometry effect. – Apekshik Panigrahi Jul 09 '22 at 03:42
  • @Asperi Yup! I did think of splitting it into views and passing the namespace in, and it's what I should be doing once I put them in separate files, but I was really confused as to why it doesn't work with vars. – Apekshik Panigrahi Jul 09 '22 at 03:45

1 Answers1

1

Actually it runs fine with me, also in vars.

struct ContentView: View {
    
    @State var isZoomed: Bool = false
    @Namespace var namespace
    
    var body: some View {
        VStack {
            if !isZoomed {
                nonZoom
            } else {
                fullZoom
            }
        }
    }
    
    var nonZoom: some View {
        VStack {
            Image("yoga")
                .resizable().aspectRatio(contentMode: .fit)
                .matchedGeometryEffect(id: "rec", in: namespace)
                .mask(RoundedRectangle(cornerRadius: 10)
                    .matchedGeometryEffect(id: "rec", in: namespace))
                .frame(height: 100)
        }
        .overlay {
                VStack(alignment: .leading) {
                    Text("ETHAN BECKER")
                        .font(.title)
                        .bold()
                        .matchedGeometryEffect(id: "artistName", in: namespace)
                    Text("Animation Artist")
                        .matchedGeometryEffect(id: "occupation", in: namespace)
                    Text("3.000 Followers")
                    .matchedGeometryEffect(id: "followerCount", in: namespace)                        }
                .frame(maxWidth: .infinity, maxHeight: 100, alignment: .bottomLeading)
                .opacity(isZoomed ? 1 : 0)
                .padding()
        }
        .onTapGesture {
            withAnimation() {
                isZoomed.toggle()
            }
        }
    }
    
    var fullZoom: some View {
        VStack {
            Image("yoga")
                .resizable().aspectRatio(contentMode: .fit)
                .matchedGeometryEffect(id: "rec", in: namespace)
                .mask(RoundedRectangle(cornerRadius: 10)
                    .matchedGeometryEffect(id: "rec", in: namespace))
                .frame(height: 500)
        }
        .overlay {
                VStack(alignment: .leading) {
                    Text("ETHAN BECKER")
                        .font(.title)
                        .bold()
                        .matchedGeometryEffect(id: "artistName", in: namespace)
                    Text("Animation Artist")
                        .matchedGeometryEffect(id: "occupation", in: namespace)
                    Text("3.000 Followers")
                    .matchedGeometryEffect(id: "followerCount", in: namespace)                        }
                .frame(maxWidth: .infinity, maxHeight: 500, alignment: .bottomLeading)
                .opacity(isZoomed ? 1 : 0)
                .padding()
        }
        .onTapGesture {
            withAnimation() {
                isZoomed.toggle()
            }
        }
    }
}

Additionally I think you don't need showText as it toggles together with isZoomed. Also as your two views just differ by frame size, you can even reduce it to one var view and use .frame on that:

enter image description here

struct ContentView: View {
    
    @State var isZoomed: Bool = false
    @Namespace var namespace
    
    var body: some View {
        VStack {
            if !isZoomed {
                userImage
                    .frame(height: 100)
            } else {
                userImage
                    .frame(height: 500)
            }
        }
    }
    
    var userImage: some View {
        VStack {
            Image("yoga")
                .resizable().aspectRatio(contentMode: .fit)
                .matchedGeometryEffect(id: "rec", in: namespace)
                .mask(RoundedRectangle(cornerRadius: 10)
                    .matchedGeometryEffect(id: "rec", in: namespace))
        }
        .overlay {
                VStack(alignment: .leading) {
                    Text("ETHAN BECKER")
                        .font(.title)
                        .bold()
                        .matchedGeometryEffect(id: "artistName", in: namespace)
                    Text("Animation Artist")
                        .matchedGeometryEffect(id: "occupation", in: namespace)
                    Text("3.000 Followers")
                    .matchedGeometryEffect(id: "followerCount", in: namespace)                        }
                .frame(maxWidth: .infinity, maxHeight: 500, alignment: .bottomLeading)
                .opacity(isZoomed ? 1 : 0)
                .padding()
        }
        .onTapGesture {
            withAnimation() {
                isZoomed.toggle()
            }
        }
    }
}
ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Thank you for helping with the bug! But, can you also provide an explanation as to why it didn't work earlier in my version? Why does putting the frame modifier outside of each view fix the geometry effect? Thanks! – Apekshik Panigrahi Jul 09 '22 at 03:39
  • 1
    Actually your original code separated in vars also worked for me. I'll edit my answer. – ChrisR Jul 09 '22 at 09:05