SwiftUI keep getting default .opacity transition but .identity needed

Hello folks,

I am attempting stanfords 2020 193p assignment 3 and am having trouble regarding a part with explicit animation.

When I am starting a new game, the cards start flying from off screen to on screen. When I click the card to choose it, it shows a yellow rectangle but instead of showing it on the card during the offset traansition, it's showing that on the card's final location. I am assuming the reason it's doing that is because as far as SwiftUI knows, the cards location is already there and all animation is doing it is showing the transition to the location.

Here's is how the animation first happens. What I want to happen when choosing a card during the offset transition, I want card itself to turn yellow during the transition.

Here's my code for the main view SetGameView.swift (I have removed irrelevant code):

import SwiftUI

struct SetGameView: View {
    
    @ObservedObject var viewModel: CardSetGame
    
    @State private var cardsFaceUp: Array<SetGameEngine.Card> = Array<SetGameEngine.Card>()
    
    let duration : Double = 10.0
    
    private func updateFaceUpCards() {
        withAnimation(.linear(duration: duration)) {
            cardsFaceUp = self.viewModel.cardsFaceUp
        }
    }
    
    private func randomLocationOffScreen(for size: CGSize) -> CGSize {
        let width = CGFloat.random(in:0..<size.width)
        let height = CGFloat.random(in:0..<size.height)
        return CGSize(width: width, height: height)
        
    }
    
    var body: some View {
        VStack {

            GeometryReader { geometry in
                Grid(cardsFaceUp) { card in
                    CardView(card: card).onTapGesture {
                        
                        self.viewModel.choose(card:card)
                        
                        
                        self.updateFaceUpCards()
                            
                    }.transition(.offset(randomLocationOffScreen(for: geometry.size)))
                        .padding(5)
                        .aspectRatio(2/3, contentMode: .fit)
                    
                    }.onAppear {
                        self.updateFaceUpCards()
                    }
            }
        }
    }
}



struct CardView: View {
    var card: SetGameEngine.Card
    
    var body: some View {
        GeometryReader { geometry in
            self.body(for: geometry.size)
        }
    }
    
    func body(for size: CGSize) -> some View {
        ZStack {
             VStack {
                 ForEach(0..<card.numberOfSymbols) { _ in
                    self.createContent()
                 }.frame( maxHeight: size.height/6)

                }
             .frame(maxWidth: size.width * maxCardContentWidthFactor, maxHeight: size.height * maxCardContentHeightFactor, alignment: .center)
             
        } .cardify(isChosen: card.isChosen)   
    }
    
    @ViewBuilder
    func createContent() -> some View {
       //Returns a View
    }
        
    func colour() -> Color {
        //returns a Color
    }
          
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        SetGameView(viewModel: CardSetGame())
    }
}

Here's my code for Cardify.swift

struct Cardify: ViewModifier {
    var isChosen: Bool
    func body(content: Content) -> some View {
        ZStack {
            if isChosen {
                RoundedRectangle(cornerRadius: cornerRadius).fill(Color.yellow)
                    .transition(.identity)
            } else {
                RoundedRectangle(cornerRadius: cornerRadius).fill(Color.white)
            }

            RoundedRectangle(cornerRadius: cornerRadius).stroke(lineWidth: edgeLineWidth)
            content
            
        }
    }
    
    let cornerRadius: CGFloat = 10.0
    let edgeLineWidth: CGFloat = 3.0
}

extension View {
    func cardify(isChosen: Bool) -> some View {
        self.modifier(Cardify(isChosen: isChosen))
    }
}

Since animation shows only changes, I thought I should modify the code as this stanford lecture and use opacity to determine to show card w/ yellow highlight or not.

Here's the new modified Cardify.swift file:

struct Cardify: ViewModifier {
    var isChosen: Bool
    func body(content: Content) -> some View {
        ZStack {
            
            RoundedRectangle(cornerRadius: cornerRadius).fill(Color.yellow)
                .opacity(isChosen ? 1 : 0)
                .transition(.identity)

            RoundedRectangle(cornerRadius: cornerRadius).fill(Color.white)
                .opacity(isChosen ? 0 : 1)
                .transition(.identity)

            RoundedRectangle(cornerRadius: cornerRadius).stroke(lineWidth: edgeLineWidth)
            content
            
        }
    }
    
    let cornerRadius: CGFloat = 10.0
    let edgeLineWidth: CGFloat = 3.0
}

At this point I almost have what I want. When I click the cards during its transition, it's highlighting them yellow as desired but it's still using the default opacity transition even though I specify it to use the .identity transition and I don't know why.

Here's the video after making the opacity change.

Questions about private Apple frameworks, including SwiftUI, are actually off-topic for this forum. I'd suggest that you ask over Apple Developer Forums instead.


I do appreciate that you try to minimize the question, but it's still hard to see what's going on for code if this size, especially if it doesn't even compile.

So should you put back the code you omit? No, quite the opposite, you remove even more codes. Your question is about the opacity animation. All that we care about is that, which is in Cardify. You might as well cardify a simple Circle on a simple view. viewModel used to determine the card side can be replaced (mocked) with a @State boolean. Even RoundRectangle can be a big distraction, let's just use normal Rectangle for the sake of debugging.

So the code would look much closer to

struct Cardify: ViewModifier {
  var isChosen: Bool

  func body(content: Content) -> some View {
    ZStack {
      Rectangle()
        .fill(Color.yellow)
        .opacity(isChosen ? 1 : 0)
        .transition(.identity)
      Rectangle()
        .fill(Color.white)
        .opacity(isChosen ? 0 : 1)
        .transition(.identity)
      Rectangle()
        .stroke(lineWidth: edgeLineWidth)
      content
    }
  }
}

extension View {
  func cardify(isChosen: Bool) -> some View {
    modifier(Cardify(isChosen: isChosen))
  }
}

And we can mock the view with

struct ContentView: View {
  @State var isChosen = false

  var body: some View {
    VStack {
      Circle().cardify(isChosen: isChosen)
      Button("Flip") {
        withAnimation {
          isChosen.toggle()
        }
      }
    }
  }
}

Now we'd have a lot less code to deal with, so that you can experiment on with less distraction.

* Note:
I have't tested the above code (not near computer rn). I'll test it once I got the chance.


As a hint, transition, in which the view is inserted/removed, is different from animation, in which you blend the current into the target view, are different things.

Ahhh thanks for the shortened code. I wasn't sure how exactly to reduce the code more while trying to maintain my app issue.

So I compiled your code and seems to have recreated the issue. If I click flip, the background changes from while to yellow via a fade in, instead of doing it instantly even with the .transition(.identity) text.

I did mention it in a hint, but be careful about the distinction between transition and animation.