ios – Using geometry effect results in “Multiple inserted views in matched geometry group Pair” warnings

When I use matched geometry view modifier I sometimes get the following warning multiple times with different values ​​for ‘first’:

Multiple inserted views in matched geometry group Pair<Int, ID>(first: 42, second: SwiftUI.Namespace.ID(id: 84)) have `isSource: true`, results are undefined.

The error appears after I click the ‘newGame’ button (for exactly three times in a row) or tap on the ‘cardDeck’ (after I have clicked on ‘newGame’ for exactly two times). While the animation still works (see GIF below), I’d like to understand why I get this warning and how I can fix the problem.

Please find the corresponding code below.

Thank you very much for your help!

ContentView:

import SwiftUI

struct ContentView: View {
   @ObservedObject var viewModel = ShapeSetGame()

   @Namespace private var dealingNamespace

   var body: some View {
       VStack {
           gameBody
           Spacer()
           HStack {
               cardDeck
               Spacer()
               newGame
               Spacer()
               discardPile
           }
           .padding(3)
       }
   }

   var gameBody: some View {
       AspectVGrid(items: viewModel.cardsInGame, aspectRatio: CardConstants.aspectRatio) { card in
           CardView(
               amount: card.content.amount.rawValue,
               type: card.content.type.rawValue,
               color: card.content.color.rawValue,
               shading: card.content.shading.rawValue,
               selected: viewModel.selectedCards.contains(card),
               mismatch: viewModel.mismatchCards.contains(card),
               matched: card.isMatched,
               faceUp: true
           )
           .matchedGeometryEffect(id: card.id, in: dealingNamespace)
           .zIndex(zIndex(of: card))
           .padding(3)
           .onTapGesture {
               withAnimation {
                   viewModel.choose(card)
               }
           }
       }
   }

   var cardDeck: some View {
       ZStack {
           ForEach(viewModel.cardDeck) { card in
               CardView(
                   amount: card.content.amount.rawValue,
                   type: card.content.type.rawValue,
                   color: card.content.color.rawValue,
                   shading: card.content.shading.rawValue,
                   selected: viewModel.selectedCards.contains(card),
                   mismatch: viewModel.mismatchCards.contains(card),
                   matched: card.isMatched,
                   faceUp: false
               )
               .matchedGeometryEffect(id: card.id, in: dealingNamespace)
               .zIndex(zIndex(of: card))
           }
       }
       .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
       .onTapGesture {
           withAnimation {
               viewModel.deal()
           }
       }
       .disabled(viewModel.cardDeckComplete)
   }

   var discardPile: some View {
       ZStack {
           ForEach(viewModel.discardedCards) { card in
               CardView(
                   amount: card.content.amount.rawValue,
                   type: card.content.type.rawValue,
                   color: card.content.color.rawValue,
                   shading: card.content.shading.rawValue,
                   selected: viewModel.selectedCards.contains(card),
                   mismatch: viewModel.mismatchCards.contains(card),
                   matched: card.isMatched,
                   faceUp: true
               )
               .matchedGeometryEffect(id: card.id, in: dealingNamespace)
           }
       }
       .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
   }

   var newGame: some View {
       Button(action: {
           withAnimation {
               viewModel.new()
           }
        
       }, label: {
           Text("New Game")
       })
       .padding()
       .foregroundColor(.white)
       .background(.gray)
   }

   private func zIndex(of card: ShapeSetGame.ShapeSetCard) -> Double {
       -Double(viewModel.cardDeck.firstIndex(where: { $0.id == card.id }) ?? 0)
   }

   private struct CardConstants {
       static let aspectRatio: CGFloat = 2/3
       static let undealtHeight: CGFloat = 90
       static let undealtWidth: CGFloat = undealtHeight * aspectRatio
       static let dealDuration: Double = 0.5
       static let totalDealDuration: Double = 2
   }

}

ViewModel:

import SwiftUI

class ShapeSetGame: ObservableObject {
    @Published var model = SetGame<ShapeAmount, ShapeType, ShapeColor, ShapeShading>()

    typealias ShapeSetCard = Card<ShapeAmount, ShapeType, ShapeColor, ShapeShading>

    var cardsInGame: Array<ShapeSetCard> {
        return model.cardsInGame
    }

    var discardedCards: Array<ShapeSetCard> {
        return model.discardedCards
    }

    var cardDeck: Array<ShapeSetCard> {
        return model.cardDeck
    }

    var selectedCards: Array<ShapeSetCard> {
        return model.selectedCards
    }

    var mismatchCards: Array<ShapeSetCard> {
        return model.mismatchCards
    }

    var cardDeckComplete: Bool {
        return model.cardsInGame.count == 0 && model.discardedCards.count == 0
    }

    var cardDeckIsEmpty: Bool {
        return model.cardDeck.count == 0
    }

    enum ShapeAmount: Int {
        case one = 1, two = 2, three = 3
    }

    enum ShapeType: String {
        case oval, diamond, squiggle
    }

    enum ShapeColor: String {
        case red, blue, green
    }

    enum ShapeShading: String {
        case solid, striped, outlined
    }

    // MARK: - Intent(s)
    func deal() {
        model.dealThreeCards()
    }

    func new() {
        model.newGame()
    }

    func choose(_ card: ShapeSetCard) {
        model.choose(card)
    }
}

extension ShapeSetGame.ShapeAmount: CaseIterable {

}

extension ShapeSetGame.ShapeType: CaseIterable {

}

extension ShapeSetGame.ShapeColor: CaseIterable {

}

extension ShapeSetGame.ShapeShading: CaseIterable {

}

Model:

import Foundation

struct SetGame<Feature1, Feature2, Feature3, Feature4> where Feature1: CaseIterable, Feature1: Equatable, Feature2: CaseIterable, Feature2: Equatable, Feature3: CaseIterable, Feature3: Equatable, Feature4: CaseIterable, Feature4: Equatable {

   typealias SetCard = Card<Feature1, Feature2, Feature3, Feature4>
   private(set) var cardDeck: Array<SetCard> = []
   var cardsInGame: Array<SetCard> = []
   private let startNumberOfCardsInGame = 12
   var selectedCards: Array<SetCard> = []
   var mismatchCards: Array<SetCard> = []
   var discardedCards: Array<SetCard> = []

   mutating func newGame() {
       cardDeck = []
       cardsInGame = []
       selectedCards = []
       mismatchCards = []
       discardedCards = []
       startGame()
       for _ in 0..<startNumberOfCardsInGame {
           cardsInGame.append(cardDeck[0])
           cardDeck.remove(at: 0)
       }
   }

   mutating func dealThreeCards() {
       let matchedCardsInGame = cardsInGame.filter { $0.isMatched }
       if matchedCardsInGame.count == 3 {
           exchangeCards()
       } else {
           if cardDeck.count > 0 {
               for _ in 0..<3 {
                   cardsInGame.append(cardDeck[0])
                   cardDeck.remove(at: 0)
               }
           }
       }
       for card in cardDeck {
           print (card.id)
       }
   }

   mutating func choose(_ card: SetCard) {
       mismatchCards = []
       removeCards()
       let cardsInGame = cardsInGame.filter { $0.isMatched == false }
       if let chosenIndex = cardsInGame.firstIndex(where: { $0.id == card.id }) {
           if selectedCards.contains(card) {
               if let selectedIndex = selectedCards.firstIndex(where: { $0.id == card.id }) {
                   selectedCards.remove(at: selectedIndex)
               }
          } else {
               if selectedCards.count == 2 {
                   checkForMatch(card1: selectedCards[0], card2: selectedCards[1], card3: card)
               } else {
                   selectedCards.append(cardsInGame[chosenIndex])
               }
           }
       }
   }

   init() {
       startGame()
   }

   //MARK: - Helper functions
   private mutating func startGame() {
       var identifier = 0
       for f1 in Feature1.allCases {
           for f2 in Feature2.allCases {
               for f3 in Feature3.allCases {
                   for f4 in Feature4.allCases {
                       let card = SetCard(content: SetCard.Content(amount: f1, type: f2, color: f3, shading: f4), id: identifier)
                       cardDeck.append(card)
                       identifier += 1
                   }
               }
           }
       }
       cardDeck.shuffle()
   }

   private mutating func checkForMatch(card1: SetCard, card2: SetCard, card3: SetCard) {
       if ((card1.content.amount != card2.content.amount) && (card1.content.amount != card3.content.amount) && (card2.content.amount != card3.content.amount)) || ((card1.content.amount == card2.content.amount) && (card1.content.amount == card3.content.amount) && (card2.content.amount == card3.content.amount)) {
               if ((card1.content.type != card2.content.type) && (card1.content.type != card3.content.type) && (card2.content.type != card3.content.type)) || ((card1.content.type == card2.content.type) && (card1.content.type == card3.content.type) && (card2.content.type == card3.content.type)) {
                       if ((card1.content.color != card2.content.color) && (card1.content.color != card3.content.color) && (card2.content.color != card3.content.color)) || ((card1.content.color == card2.content.color) && (card1.content.color == card3.content.color) && (card2.content.color == card3.content.color)) {
                               if (card1.content.shading != card2.content.shading) && (card1.content.shading != card3.content.shading) && (card2.content.shading != card3.content.shading) || ((card1.content.shading == card2.content.shading) && (card1.content.shading == card3.content.shading) && (card2.content.shading == card3.content.shading)) {
                                       if let chosenIndex1 = cardsInGame.firstIndex(where: { $0.id == card1.id }), let chosenIndex2 = cardsInGame.firstIndex(where: { $0.id == card2.id }), let chosenIndex3 = cardsInGame.firstIndex(where: { $0.id == card3.id }) {
                                           cardsInGame[chosenIndex1].isMatched = true
                                           cardsInGame[chosenIndex2].isMatched = true
                                           cardsInGame[chosenIndex3].isMatched = true
                                           selectedCards = []
                                       }
                               } else {
                                   addMismatchCards(card1: card1, card2: card2, card3: card3)
                               }
                        } else {
                          addMismatchCards(card1: card1, card2: card2, card3: card3)
                       }
               } else {
                   addMismatchCards(card1: card1, card2: card2, card3: card3)
               }
       } else {
           addMismatchCards(card1: card1, card2: card2, card3: card3)
       }
   }

   private mutating func addMismatchCards(card1: SetCard, card2: SetCard, card3: SetCard) {
       mismatchCards.append(card1)
       mismatchCards.append(card2)
       mismatchCards.append(card3)
       selectedCards = []
   }

   private mutating func exchangeCards() {
       for card in cardsInGame {
           if card.isMatched == true {
               if let selectedIndex = cardsInGame.firstIndex(where: { $0.id == card.id }) {
                   if cardDeck.count > 0 {
                       discardedCards.append(card)
                       cardsInGame[selectedIndex] = cardDeck[0]
                       cardDeck.remove(at: 0)
                   } else {
                       discardedCards.append(card)
                       cardsInGame.remove(at: selectedIndex)
                   }
               }
           }
       }
   }

   private mutating func removeCards() {
       for card in cardsInGame {
           if card.isMatched == true {
               if let selectedIndex = cardsInGame.firstIndex(where: { $0.id == card.id }) {
                   discardedCards.append(card)
                   cardsInGame.remove(at: selectedIndex)
               }
           }
       }
   }

}

Leave a Comment