appthumb
appthumb

Reputation: 521

SwiftUI: Change a view's transition dynamically after the view is created

I want to change a view's transition dynamically after the view is created. I toggle a State variable isTransition1 by clicking a button to switch between transition1 and transition2 as the below. However, it doesn't work as intended if one of these transitions is opacity. The view to be removed immediately after changing transition always keeps its original transition. Surprisingly, if I change transition2 to slide, it will work without problem. The view to be removed will use the new transition. Is there any way to make opacity work here?

let transition1 = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
                                           removal: .move(edge: .leading))

let transition2 = AnyTransition.opacity

struct Wrapper1<Content: View>: View {
  let content: Content

  var body: some View {
    content
  }
}

struct Wrapper2<Content: View>: View {
  let content: Content

  var body: some View {
    content
  }
}

struct TextView: View {
  let count: Int
  let color: Color

  var body: some View {
    ZStack {
      color
        .edgesIgnoringSafeArea(.all)
        .frame(maxWidth: UIScreen.main.bounds.width,
               maxHeight: UIScreen.main.bounds.height)

      Text("Count: \(count)")
        .font(.title)
        .offset(y: -200)
    }
  }
}

struct ContentView: View {
  @State private var count = 0
  @State private var isTransition1 = false

  var body: some View {
    ZStack {
      if count % 2 == 0 {
        Wrapper1(content: TextView(count: count, color: Color.green)
          .transition(isTransition1 ? transition1 : transition2))
        } else {
          Wrapper2(content: TextView(count: count, color: Color.red)
            .transition(isTransition1 ? transition1 : transition2))
        }

      HStack(spacing: 100) {
        Button(action: {
          self.isTransition1.toggle()
        }) {
          Text("Toggle Transition").font(.title)
        }

        Button(action: {
          withAnimation(.linear(duration: 2)) {
            self.count += 1
          }
        }) {
          Text("Increase").font(.title)
        }
      }
    }
  }
}

Upvotes: 6

Views: 1194

Answers (2)

L_Sonic
L_Sonic

Reputation: 585

I had the same issue recently where I wanted to change the transition inbetween states. Nothing seemed to work until I decided to create an intermediate internal state that updates the UI only after it has updated the transition. I am using a view model that is an observable object.

To solve the issue I created an internal state that is a currentValueSubject. I also made my transition a published Variable so as to update the UI after the transition changes.

I update the internal state, which in turn updates the transition, which then updates the UI before changing the state.

    private var internalState: CurrentValueSubject<BookingWizardState, Never> = CurrentValueSubject(.date)
    @Published var state: BookingWizardState
    private let moveForwardTransition = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
                                                                 removal: .move(edge: .leading))
    private let moveBackwardTransition = AnyTransition.asymmetric(insertion: .move(edge: .leading),
                                                                  removal: .move(edge: .trailing))

    @Published var transition: AnyTransition


    func setupSubscriptions() {
    //Set the transition based on the state change
        internalState.map { [weak self] newState in
            guard let self else { return .slide }
            let isForward = self.state.rawValue <= newState.rawValue
            return isForward ? self.moveForwardTransition : self.moveBackwardTransition
        }
        .assign(to: &$transition)
        //Update the external state after a fraction of a second and after the transition has been updated. 
        internalState
            .delay(for: .seconds(0.1), scheduler: RunLoop.main)
            .assign(to: &$state)
    }

Upvotes: 1

Asperi
Asperi

Reputation: 257563

Not sure if I correctly understood what effect you tried to achieve, but try to reset view hierarchy (at least this definitely resets transitions, so they don't affect each other):

  var body: some View {
    ZStack {
      if count % 2 == 0 {
        Wrapper1(content: TextView(count: count, color: Color.green)
          .transition(isTransition1 ? transition1 : transition2))
        } else {
          Wrapper2(content: TextView(count: count, color: Color.red)
            .transition(isTransition1 ? transition1 : transition2))
        }

      HStack(spacing: 100) {
        Button(action: {
          self.isTransition1.toggle()
        }) {
          Text("Toggle Transition").font(.title)
        }

        Button(action: {
          withAnimation(.linear(duration: 2)) {
            self.count += 1
          }
        }) {
          Text("Increase").font(.title)
        }
      }
    }.id(isTransition1)     // << here !!
  }

Upvotes: 1

Related Questions