Search code examples
swiftuiswiftui-animation

How to animate 2 views from a single point to their final places on appear


I'm trying to animate the two squares square below from a common center point to their final position when they appear, and back - when they are removed.

Here's what I tried, but the result it that they don't start from an overlapping position - rather, they start close to to each in the center:

struct ContentView: View {
   @State var matched = true
   @State var show = false
   @Namespace var ns
    
   var body: some View {
      VStack {
         HStack {
            Spacer()
            if show {
               square
                  .matchedGeometryEffect(id: matched ? "match" : "", 
                                         in: ns, anchor: .center, isSource: false)
                  .animation(.easeIn)
                  .transition(.move(edge: .trailing ))
                  .onAppear { withAnimation { matched = false } }                        
                  .onDisappear { withAnimation { matched = true } }
            }
            Spacer()
               .matchedGeometryEffect(id: "match", in: ns, anchor: .center, isSource: true)   
            if show {
               square
                  .matchedGeometryEffect(id: matched ? "match" : ""
                                         in: ns, anchor: .center, isSource: false)
                  .animation(.easeIn)
                  .transition(.move(edge: .leading))
            }
            Spacer()
         }
         Button("show") { withAnimation { show.toggle() } }
      }
   }
}

The square square is simply defined as:

var square: some View {
   Rectangle().foregroundColor(.blue)
      .frame(width: 40, height: 40, alignment: .center)
}

What sort of worked was to attach matchedGeometryEffect to an overlay within Spacer, and also to explicitly specify properties: .position in all of the them.

Still, it only works when they appear, but not when disappearing; there's still a gap there.

Spacer().overlay(
   Color.clear
     .matchedGeometryEffect(id: "match", in: ns, properties: .position, anchor: .center, isSource: true)
   )

Is this the right general approach to achieve this effect, and if so, how can I make it work? Or have I overcomplicated it?


Solution

  • Try with geometry matching another clear square with the same dimension and simplified a bit. There's an intrinsic opacity transition that you might want to remove and replace with some color blend mode.

    struct ContentView: View {
      //@State var matched = true
      @State private var show = false
      @Namespace private var ns
      
      var body: some View {
        VStack {
          
          HStack {
            Spacer()
            if show {
              Square(color: .blue)
                .matchedGeometryEffect(id: 1, in: ns, isSource: false)
            }
            Spacer()
              .background(Square(color: .clear)
                            .matchedGeometryEffect(id: show ? 0 : 1, in: ns, isSource: true))
            if show {
              Square(color: .red)
                .matchedGeometryEffect(id: 1, in: ns, isSource: false)
            }
            Spacer()
          }
           
          Button("show") { withAnimation { show.toggle() } }
        }
      }
      
      struct Square: View {
        let color: Color
        var width: CGFloat = 40
        var body: some View {
          Rectangle().foregroundColor(color).frame(width: width, height: width)
        }
      }
      
    }