1

Very early into SwiftUI programming, I am basically trying to show a list of cards inside a container view, with the top most one being shown... Each card has a text view and when tapped the card view is removed from the stack/container view and the next immediate one will be shown. I wanted to add a transition animation while removing the current one and presenting the next one, but so far i had very little luck with it.. Surprisingly, transition works as expected, if i use a Bool variable and use it to show/hide only the top card view, but not if i want to perform removal and insertion at the same time..

import SwiftUI
import Combine

public struct CardsStackView: View {
   @ObservedObject
   public var viewModel: CardsStackViewModel

   public init(cards: [Card]) {
      self.viewModel = CardsStackViewModel(cards: cards)
   }

   public var body: some View {
      ZStack {
        Rectangle()
            .foregroundColor(.white)
            .zIndex(1)
        
        // idea is to remove the top card with slide transition and show next top one with scale transition alongside changing opacity.. wanting to do these both in sync
        cardView(for: viewModel.cards[viewModel.top])
            .zIndex(2)
            .transition(.asymmetric(insertion: .scale.combined(with: .opacity), removal: .slide.combined(with: .opacity)))
       }
    }

    private func cardView(for card: Card) -> some View {
      let view = CardView(card: card)

      viewModel.subscription = view.publisher
        .receive(on: DispatchQueue.main)
        .sink { event in
            if case .dismissed = event {
                // Animating, while changing the top published value in view model inside navigateToNext method
                withAnimation {
                    viewModel.navigateToNext()
                }
            }
      }

      return view
    }
}

Here's the View Model that has the Binding variable:

import SwiftUI
import Combine

public class CardsStackViewModel: ObservableObject {
   public let cards: [Card]
   var subscription: AnyCancellable?

   @Published
   var top: Int = 0

   public init(cards: [Card]) {
      self.cards = cards
   }
    
   func navigateToNext() {
       guard top < cards.count - 1 else {
          return
       }
    
       top += 1
   }
 }

Here's Card View

import SwiftUI
import Combine

public enum CardViewEvent {
   case dismissed
}

public struct CardView: View {
   public let card: Card
   public let publisher = PassthroughSubject<CardViewEvent, Never>()

   public init(card: Card) {
      self.card = card
   }

   public var body: some View {
       ZStack(alignment: .leading) {
           Text(card.title)
            .onTapGesture {
               self.publisher.send(.dismissed)
            }
       }
   }
}

Here's Card Model:

import SwiftUI

public struct Card: Identifiable {
   public let id = UUID().uuidString
   public let title: String

   public init(title: String) {
      self.title = title
   }
}

Appreciate if anyone can point out what's wrong with it. Or is this not the ideal approach?

Prasad
  • 166
  • 7
  • Check out. :- https://stackoverflow.com/questions/57730074/transition-animation-not-working-in-swiftui?rq=1 – Meet Dec 18 '21 at 03:38
  • I've looked at it before.. The Bool State property approach works like they mentioned to hide/show a child view. But it doesn't suit the UI design i have where one child/card view always shows at any time... but transitions to another one... – Prasad Dec 18 '21 at 03:59

1 Answers1

1

Turns out changing the body of ZStack by adding the card view inside a ForEach loop fixed the issue.

import SwiftUI
import Combine

public struct CardsStackView: View {
  @ObservedObject
  public var viewModel: CardsStackViewModel

  public init(cards: [Card]) {
     self.viewModel = CardsStackViewModel(cards: cards)
  }

  public var body: some View {
     ZStack {
       Rectangle()
           .foregroundColor(.white)
    
       // Putting the view in a ForEach loop did the trick for me
       ForEach(0..<viewModel.cards.count, id: \.self) { i in
          if i == viewModel.top {
             cardView(for: viewModel.cards[i])
              .transition(.asymmetric(insertion: .scale.combined(with: 
               .opacity), removal: .slide.combined(with: .opacity)))
          }
       }
     }
   }

   private func cardView(for card: Card) -> some View {
     let view = CardView(card: card)

     viewModel.subscription = view.publisher
       .receive(on: DispatchQueue.main)
       .sink { event in
           if case .dismissed = event {
               // Animating, while changing the top published value in 
               // view model inside navigateToNext method
               withAnimation {
                   viewModel.navigateToNext()
               }
           }
     }

     return view
   }
 }
Prasad
  • 166
  • 7