Search code examples
iosswiftswiftui

How to animate view movement without using offset?


How would I animate a view moving from the left to the right without using something like the .offset modifier? I thought that if SwiftUI saw a view with the same ID disappear in one place and appear in another, it would animate the transition between the two but that didn't work like I expected.

Here, clicking "Swap" removes the sign "Test" from the left stack and adds it to the right stack. How would I animate this transition so that the "Test" view appears to move over instead of jumping?

enter image description here

import SwiftUI

struct Sign: Identifiable {
    let id = UUID()
    let text: String
}

struct SignView: View {
    
    @State var sign: Sign
    
    var body: some View {
        Text(sign.text)
            .frame(width: 75, height: 25, alignment: .center)
            .foregroundStyle(Color.white)
            .background(Color.blue)
    }
}

class SignManager: ObservableObject {
    @Published var leftSlot: Sign?
    @Published var rightSlot: Sign?
}

struct MotionTestView: View {
    
    @ObservedObject var manager = SignManager()
    
    var body: some View {
        VStack {
            HStack {
                ZStack {
                    Text("Empty")
                        .frame(width: 75, height: 25, alignment: .center)
                        .foregroundStyle(Color.white)
                        .background(Color.blue.opacity(0.3))
                    if let sign = manager.leftSlot {
                        SignView(sign: sign)
                            .id(sign.id)
                    }
                }
                
                ZStack {
                    Text("Empty")
                        .frame(width: 75, height: 25, alignment: .center)
                        .foregroundStyle(Color.white)
                        .background(Color.blue.opacity(0.3))
                    if let sign = manager.rightSlot {
                        SignView(sign: sign)
                            .id(sign.id)
                    }
                }
            }
            
            Button("Swap", action: {
                withAnimation {
                    (manager.leftSlot, manager.rightSlot) = (manager.rightSlot, manager.leftSlot)
                }
            })
        }
        .onAppear {
            withAnimation {
                manager.leftSlot = Sign(text: "Test")
                manager.rightSlot = nil
            }
        }

    }
}


Solution

  • It sounds like you are looking for matchedGeometryEffect.

    @StateObject var manager = SignManager()
    @Namespace var ns
    
    var body: some View {
        VStack {
            HStack {
                ZStack {
                    Text("Empty")
                        .frame(width: 75, height: 25, alignment: .center)
                        .foregroundStyle(Color.white)
                        .background(Color.blue.opacity(0.3))
                    if let sign = manager.leftSlot {
                        SignView(sign: sign)
                            .matchedGeometryEffect(id: "sign", in: ns)
                    }
                }
                
                ZStack {
                    Text("Empty")
                        .frame(width: 75, height: 25, alignment: .center)
                        .foregroundStyle(Color.white)
                        .background(Color.blue.opacity(0.3))
                    if let sign = manager.rightSlot {
                        SignView(sign: sign)
                            .matchedGeometryEffect(id: "sign", in: ns)
                    }
                }
            }
            ...
    

    The SignViews do not have the same id. One has manager.leftSlot.text, and the other has manager.rightSlot.text, which are never the same in this case. And even if they did have the same id, their structural identities are different as they are in different if statements.