iosswiftiphoneanimationswiftui

SwiftUI How to achieve smooth animation of the same view in different stacks


I set a view, when the state is different, it is in HStack or VStack. But when I set the animation, it does not meet my expectations. I want the view on the right to move up and down smoothly.(position1 moves smoothly to position2)

struct ContentView: View {
    @State var show = false
    var body: some View {
        VStack {
            VStack {
                HStack {
                    Text("test spacer")
                        .contentShape(Rectangle())
                        .frame(minWidth: 0, maxWidth: .infinity)
                        .background(Color.red)
                    if !show {
                        Text("xxxxxx")
                            .background(Color.blue) // position1
                    }
                }
                if show {
                    HStack {
                        Spacer()
                        Text("xxxxxx")
                            .background(Color.blue) // position2
                    }
                }
            }
            .background(Color.yellow)
            .animation(.easeInOut(duration: 1), value: show)

            Button("button") {
                show.toggle()
            }
        }
        .padding()
    }
}

enter image description here

I used Sweeper's answer and used matchedGeometryEffect to solve the problem.

But I tried to change Text to Textfield and changed the timing of triggering the animation to focus. When the keyboard pops up, the animation does not execute smoothly.

How to make the elements in the view slide smoothly?

struct ContentView: View {
    @State private var text = ""
    @Namespace var ns
    @FocusState private var isFocused: Bool
    
    var body: some View {
        ZStack {
            Color.clear
                .contentShape(Rectangle())
                .onTapGesture {
                    isFocused = false
                }
            VStack {
                HStack {
                    TextField("placeholder", text: $text, axis: .vertical)
                        .focused($isFocused)
                        .frame(minHeight: 28)
                        .background(Color.red)
                    if !isFocused {
                        Text("xxxxxx")
                            .background(Color.blue)
                            .matchedGeometryEffect(id: "xxxxxx", in: ns)
                    }
                }
                if isFocused {
                    HStack {
                        Spacer()
                        Text("xxxxxx")
                            .background(Color.blue)
                            .matchedGeometryEffect(id: "xxxxxx", in: ns)
                    }
                }
            }
            .background(Color.yellow)
            .animation(.easeInOut(duration: 1), value: isFocused)
        }
        .padding()
    }
}

enter image description here


Solution

  • Use a matchedGeometryEffect to "link" the two view's size and position together.

    struct ContentView: View {
        @State var show = false
        @Namespace var ns
        var body: some View {
            VStack {
                VStack {
                    HStack {
                        Text("test spacer")
                            .contentShape(Rectangle())
                            .frame(minWidth: 0, maxWidth: .infinity)
                            .background(Color.red)
                        if !show {
                            Text("xxxxxx")
                                .background(Color.blue)
                                // here
                                .matchedGeometryEffect(id: "xxxxxx", in: ns)
                        }
                    }
                    if show {
                        HStack {
                            Spacer()
                            Text("xxxxxx")
                                .background(Color.blue)
                                // and here
                                .matchedGeometryEffect(id: "xxxxxx", in: ns)
                        }
                    }
                }
                .background(Color.yellow)
                .animation(.easeInOut(duration: 1), value: show)
    
                Button("button") {
                    show.toggle()
                }
            }
            .padding()
        }
    }