iosswiftiphoneanimationswiftui

How to ensure smooth animation of the same View in different stacks when using the focus event of textfield to trigger animation


I used matchedGeometryEffect to "link" the two view's size and position together.

But I tried to use 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? (position1 moves smoothly to position2)

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") // position1
                            .background(Color.blue)
                            .matchedGeometryEffect(id: "xxxxxx", in: ns)
                    }
                }
                if isFocused {
                    HStack {
                        Spacer()
                        Text("xxxxxx") // position2
                            .background(Color.blue)
                            .matchedGeometryEffect(id: "xxxxxx", in: ns)
                    }
                }
            }
            .background(Color.yellow)
            .animation(.easeInOut(duration: 1), value: isFocused)
        }
        .padding()
    }
}

enter image description here


Solution

  • The way you are using .matchedGeometryEffect is to switch between two separate versions of the blue view. This means there are also transitions involved.

    It works more smoothly if you have a single view for the blue view, which is shown as an overlay. Then use hidden placeholders as the source for the .matchedGeometryEffect.

    Here is the updated example with the changes applied:

    VStack {
        HStack {
            TextField("placeholder", text: $text, axis: .vertical)
                .focused($isFocused)
                .frame(minHeight: 28)
                .background(Color.red)
            if !isFocused {
                Text("xxxxxx") // position1
                    .hidden() // 👈 added
                    // .background(Color.blue) // 👈 not needed
                    .matchedGeometryEffect(id: "xxxxxx", in: ns)
            }
        }
        if isFocused {
            HStack {
                Spacer()
                Text("xxxxxx") // position2
                    .hidden() // 👈 added
                    // .background(Color.blue) // 👈 not needed
                    .matchedGeometryEffect(id: "xxxxxx", in: ns)
            }
        }
    }
    .overlay { // 👈 Show the visible version of the blue view as an overlay
        Text("xxxxxx")
            .background(Color.blue)
            .matchedGeometryEffect(id: "xxxxxx", in: ns, isSource: false)
            .animation(.easeInOut(duration: 1), value: isFocused)
    }
    .background(Color.yellow)
    .animation(.easeInOut(duration: 1), value: isFocused)
    

    Animation


    EDIT You were saying in a comment that the animation of the blue text sometimes lags the yellow rectangle when tapping very quickly between Textfield and background.

    I tried adding alignment: .top to the ZStack to see if this would help to reduce the vertical movement. It actually made the problem worse, the blue text then jumps between positions.

    Applying the animation to the overlay via .transaction modifier seems to work better in this case:

    ZStack(alignment: .top) { // 👈 alignment added
        Color.clear
            // ... modifiers as before
        VStack {
            // ... content as before
        }
        .overlay {
            Text("xxxxxx")
                .background(Color.blue)
                .matchedGeometryEffect(id: "xxxxxx", in: ns, isSource: false)
                .transaction { trans in // 👈 .animation replaced with .transaction
                    trans.animation = .easeInOut
                }
        }
        .background(Color.yellow)
        .animation(.easeInOut, value: isFocused) // 👈 duration removed
    }
    .padding()