Search code examples
swiftswiftuiswiftui-animation

Conditionally setting two different transitions on the same View


Good day!

I have two buttons, one with X and one that says Cancel. Depending on which button I press, I want the X button to have a different transition.

When pressing X: I want the removal of the X button to be .opacity.animation(.default)

When pressing Cancel: I want the removal of the X button to be .move(edge: .trailing).combined(with: .opacity).animation(.default)

For context, it's a textField, when I type something the X button appears.

I've tried with a State variable, setting the transition in withAnimation in the Button action. That seems to set the transition for the next cycle/refresh and not the current one. I've also looked into transactions, but I can only manipulate animations from there?

So when I press the X button, the next time I press either X or Cancel it will be what I've set the transition to be. And when I press cancel, the next time it will set transition to be their other one.

gif

As you can see in the GIF, it cycles through the transitions, but I want them to be set for each button.

Expected behaviour:

When pressing X, I want it to fade out using .opacity.

When pressing Cancel, I want the X button to "slide" out, using .move as removal transition.

import Combine
import Foundation
import SwiftUI

struct SearchBar: View {
    @Binding var searchField: String
    let clearSearchField: () -> Void
    let isFocus: Bool
    let setFocus: (Bool) -> Void
    @State var transition: AnyTransition = .opacity.animation(.default)

    var body: some View {
        HStack(spacing: 16) {
            ZStack {
                Color.gray
                VStack(alignment: .leading) {
                    HStack {

                        TextField(
                            "Placeholder",
                            text: $searchField,
                            onEditingChanged: { _ in setFocus(true) }
                        )
                        .foregroundColor(.white)

                        Spacer()

                        if searchField.isNonEmpty {
                            Button {
                                transition = .opacity.animation(.default)
                                withAnimation {
                                    clearSearchField()
                                }

                            } label: {
                                Text("X")
                            }
                            .transition(
                                .asymmetric(
                                    insertion: .opacity.animation(.default),
                                    removal: transition
                                )
                            )
                        }
                    }
                }
                .padding(16)
            }
            .cornerRadius(8)

            if isFocus {
                Button(
                    action: {
                        transition = .move(edge: .trailing).combined(with: .opacity)
                        hideKeyboard()
                        clearSearchField()
                        setFocus(false)

                    },
                    label: {
                        Text("Cancel")
                            .foregroundColor(.white)
                    }
                )
                .transition(.move(edge: .trailing).combined(with: .opacity).animation(.default))
            }
        }
        .frame(height: 48)
        .foregroundColor(searchField.isEmpty ? .grey: .white)
        .padding(16)
        .animation(.default, value: [isFocus])
    }
}

Is this possible?

Appreciate any help!


Solution

  • From what I have observed, you cannot change the transition of a view without also changing its identity. Once the view has appeared with a transition, you can't tell it to disappear with another transition.

    So one way to work around this is to have two X buttons, one with .opacity transition, the other with .move. Use a @State to decide which button to show:

    Example:

    @State var showButtons = false
    @State var shouldMove = false
    
    var body: some View {
        VStack {
            // imagine this is your text field
            Rectangle().frame(width: 100, height: 100).onTapGesture {
                withAnimation {
                    showButtons = true
                }
            }
            Spacer()
            if showButtons {
                // make buttons with different transitions
                // if shouldMove, show the button with the move transition
                // otherwise show the button with the opacity transition
                if shouldMove {
                    makeButton(transition: .asymmetric(insertion: .opacity, removal: .move(edge: .trailing)))
                } else {
                    makeButton(transition: .opacity)
                }
                
                Button("Cancel") {
                    shouldMove = true
                    withAnimation {
                        showButtons.toggle()
                    }
                }
            }
        }.frame(height: 300)
    }
    
    @ViewBuilder func makeButton(transition: AnyTransition) -> some View {
        Button("X") {
            shouldMove = false
            withAnimation {
                showButtons.toggle()
            }
        }.transition(transition)
    }
    

    Another approach is to give the X button a new id each time you want to change transitions, hence making a "new" view.

    @State var showButtons = false
    @State var transition: AnyTransition = .opacity
    @State var buttonId = UUID()
    
    var body: some View {
        VStack {
            Rectangle().frame(width: 100, height: 100).onTapGesture {
                withAnimation {
                    showButtons = true
                }
            }
            Spacer()
            if showButtons {
                Button("X") {
                    transition = .opacity
                    buttonId = UUID() // new id!
                    withAnimation {
                        showButtons.toggle()
                    }
                }
                .transition(transition)
                .id(buttonId)
                
                Button("Cancel") {
                    transition = .asymmetric(insertion: .opacity, removal: .move(edge: .trailing))
                    buttonId = UUID() // new id!
                    withAnimation {
                        showButtons.toggle()
                    }
                }
            }
        }.frame(height: 300)
    }