skip to Main Content

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

2

Answers


  1. 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.

    • The (single) blue view then moves between the two locations, so no transitions are involved.

    • It seems that the overlay needs its own .animation modifier too.

    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()
    
    Login or Signup to reply.
  2. (This is for iOS 17+)

    You can restructure your views to animate between HStackLayout and VStackLayout, instead of between the geometries of two different Texts. So you only need one Text("xxxxxx"). Then, add a geometryGroup.

    let layout = isFocused ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
    layout {
        TextField("placeholder", text: $text)
            .focused($isFocused)
            .frame(minHeight: 28)
            .background(Color.red)
        Text("xxxxxx")
            .background(Color.blue)
            // instead of a Spacer() and an HStack, we can position the text on the trailing side by using "maxWidth: .infinity, alignment: .trailing" too
            .frame(maxWidth: isFocused ? .infinity : nil, alignment: .trailing)
    }
    .geometryGroup()
    .background(Color.yellow)
    .animation(.easeInOut, value: isFocused)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search