'Suggestions for refactoring using closures (SwiftUI)

I'm working through the Stanford University cs193p online SwiftUI course, specifically the Set game (assignment #3). I'm not a student at Stanford, but am doing this independently in an effort to learn Swift programming. I have a solution for this assignment which plays the game correctly, so I'm good there. I have quite a bit of programming experience, but not in C++ or C, so my experience with code blocks is very limited.

One of the requirements for this assignment is to "use a closure as a meaningful part of your solution", and this is where I'm a little stuck. My code works correctly, and plays the game correctly (with different shapes, colors, counts, and shading), but does so without what I would consider a definite use of a closure. With that in mind, I'm wondering if any others would be willing to share (in general terms) where they might have incorporated one or more closures in their solution.

I've included an overview of my code below (with function/struct/class names only) to give a broad idea of how my code works. I'd appreciate any suggestions on where a closure (or closures) might be best applied, so that I can meet the stated requirement.

Thanks in advance for any suggestions on this!

Model Code:

struct SetGameModel {
    struct Card : Identifiable {
        let cardColor: SetCardColor
        let cardSymbol: SetCardSymbol
        let cardShade: SetCardShade
        let cardSymbolCount: Int
        var isSelected: Bool
        var isPartOfSet: Bool
        var isPartOfInvalidSet: Bool
        var isInPlay: Bool
        var isVisible: Bool
        let id: Int
    }
    
    private(set) var cards: Array<Card>
    private(set) var numberOfCardsPlayed = 0
    private var numberOfCardsInPlay: Int {
        get { cards.indices.filter( { cards[$0].isInPlay }).count }
    }
    
    var moreCardsInDeck: Bool {
        numberOfCardsInPlay < cards.count
    }
    
    private var cardsSelected: Array<Card> {
        get { cards.filter({card in card.isSelected}) }
    }
    
    private func validSet() -> Bool {
        //       1. All have the same shading, but have unique shape count, unique colors, and unique shapes
        //       2. All have the same shape count, but have unique colors, unique shapes, and unique shading
        //       3. All have the same colors, but have unique shapes, unique shading, and unique shape count
        //       4. All have the same shapes, but have unique shading, unique shape count, and unique colors
        //       5. All have different shapes, different shading, different colors, and different shape count
        func sameShades() -> Bool {
            return cardsSelected[0].cardShade == cardsSelected[1].cardShade &&
            cardsSelected[1].cardShade == cardsSelected[2].cardShade
        }
        func sameSymbols() -> Bool {
            return cardsSelected[0].cardSymbol == cardsSelected[1].cardSymbol &&
            cardsSelected[1].cardSymbol == cardsSelected[2].cardSymbol
        }
        func sameColors() -> Bool {
            return cardsSelected[0].cardColor == cardsSelected[1].cardColor &&
            cardsSelected[1].cardColor == cardsSelected[2].cardColor
        }
        func sameSymbolCount() -> Bool {
            return cardsSelected[0].cardSymbolCount == cardsSelected[1].cardSymbolCount &&
            cardsSelected[1].cardSymbolCount == cardsSelected[2].cardSymbolCount
        }
        func uniqueShades() -> Bool {
            return cardsSelected[0].cardShade != cardsSelected[1].cardShade &&
            cardsSelected[1].cardShade != cardsSelected[2].cardShade &&
            cardsSelected[0].cardShade != cardsSelected[2].cardShade
        }
        func uniqueSymbols() -> Bool {
            return cardsSelected[0].cardSymbol != cardsSelected[1].cardSymbol &&
            cardsSelected[1].cardSymbol != cardsSelected[2].cardSymbol &&
            cardsSelected[0].cardSymbol != cardsSelected[2].cardSymbol
        }
        func uniqueColors() -> Bool {
            return cardsSelected[0].cardColor != cardsSelected[1].cardColor &&
            cardsSelected[1].cardColor != cardsSelected[2].cardColor &&
            cardsSelected[0].cardColor != cardsSelected[2].cardColor
        }
        func uniqueSymbolCount() -> Bool {
            return cardsSelected[0].cardSymbolCount != cardsSelected[1].cardSymbolCount &&
            cardsSelected[1].cardSymbolCount != cardsSelected[2].cardSymbolCount &&
            cardsSelected[0].cardSymbolCount != cardsSelected[2].cardSymbolCount
        }
        
        return (sameShades() && uniqueSymbolCount() && uniqueColors() && uniqueSymbols()) ||
        (sameSymbolCount() && uniqueColors() && uniqueSymbols() && uniqueShades()) ||
        (sameColors() && uniqueSymbols() && uniqueShades() && uniqueSymbolCount()) ||
        (sameSymbols() && uniqueShades() && uniqueSymbolCount() && uniqueColors()) ||
        (uniqueSymbols() && uniqueShades() && uniqueColors() && uniqueSymbolCount())
    }
    
    var cardsVisible: Array<Card> {
        get { cards.filter({card in card.isVisible}) }
    }
    
    init() {
        cards = []
        newGame()
    }
    
    mutating func dealMoreCards() {
        if cardsSelected.count == 3 {
            resetCurrentSet(valid: validSet())
        }
        if numberOfCardsInPlay < cards.count {
            for index in ((numberOfCardsInPlay - 1)...(numberOfCardsInPlay + 2)) {
                cards[index].isInPlay = true
                cards[index].isVisible = true
            }
        }
    }
    
    mutating func newGame() {
        var cardIndex = 0
        cards = []
        for color in SetCardColor.allCases {
            for symbol in SetCardSymbol.allCases {
                for shade in SetCardShade.allCases {
                    for symbolCount in (1...3) {
                        cards.append(Card(cardColor: color, cardSymbol: symbol, cardShade: shade, cardSymbolCount: symbolCount, isSelected: false, isPartOfSet: false, isPartOfInvalidSet: false, isInPlay: false, isVisible: false, id: cardIndex))
                        cardIndex += 1
                    }
                }
            }
        }
        cards.shuffle()
        for index in (0...11) {
            cards[index].isInPlay = true
            cards[index].isVisible = true
        }
    }
    
    mutating func markCardsAsASet(valid: Bool) {
        for index in (0...2) {
            let selectedCardId = cardsSelected[index].id
            if let selectedCardIndex = cards.firstIndex(where: { $0.id == selectedCardId}) {
                cards[selectedCardIndex].isPartOfSet = true
                cards[selectedCardIndex].isPartOfInvalidSet = !valid
            }
        }
    }
    
    mutating func resetCurrentSet(valid: Bool) {
        var hold: [Int] = []
        for index in (0...2) {
            let selectedCardId = cardsSelected[index].id
            if let selectedCardIndex = cards.firstIndex(where: { $0.id == selectedCardId}) {
                hold.append(selectedCardIndex)
            }
        }
        for index in (0...2) {
            cards[hold[index]].isPartOfSet = false
            cards[hold[index]].isPartOfInvalidSet = false
            cards[hold[index]].isSelected = false
            if valid {
                cards[hold[index]].isVisible = false
            }
        }
    }
    
    mutating func choose( _ card: Card) {
        if cardsSelected.count == 3 {
            resetCurrentSet(valid: validSet())
            dealMoreCards()
        }
        let selectedCardId = card.id
        if let selectedCardIndex = cards.firstIndex(where: { $0.id == selectedCardId }) {
            // A card has been selected
            // If fewer than three cards have been selected,
            //    If the touched card is already selected, deselect it.  Otherwise select it.
            if cardsSelected.count < 3  && cards[selectedCardIndex].isVisible {
                cards[selectedCardIndex].isSelected.toggle()
            }
            if cardsSelected.count == 3 {
                markCardsAsASet(valid: validSet())
            }
        }
    }
}

ViewModel Code:

class SetGameVM: ObservableObject {
    typealias Card = SetGameModel.Card
    @Published private var model: SetGameModel
    
    init() {
        model = SetGameModel()
    }
    
    var cards: Array<Card> {
        model.cardsVisible
    }
    
    var inPlayCount: Int {
        model.numberOfCardsPlayed
    }
    
    var moreCardsAvailable: Bool {
        model.moreCardsInDeck
    }
    
    
    // MARK: - Intent(s)
    
    func choose( _ card: Card) {
        model.choose(card)
    }
    
    func dealMoreCards() {
        model.dealMoreCards()
    }
    
    func startNewGame() {
        model.newGame()
    }
}

View Code:

struct SetGameView: View {
    @ObservedObject var game: SetGameVM
    
    var body: some View {
        VStack {
            Text("Set!")
            AspectVGrid(items: game.cards, aspectRatio: 2/3, content: { card in
                CardView(card: card)
                    .padding(4)
                    .onTapGesture {
                        game.choose(card)
                    }
            })
            HStack {
                if (game.moreCardsAvailable) {
                    Button("Deal 3 More Cards") {
                        game.dealMoreCards()
                    }
                    .buttonStyle(.bordered)
                    .tint(.blue)
                    Spacer()
                }
            }
            Button("New Game") {
                game.startNewGame()
            }
            .buttonStyle(BorderlessButtonStyle())
            .tint(.green)
        }
    }
}

struct oneSymbol: View {
    let card: SetGameVM.Card
    let mySize: CGSize
    @ViewBuilder
    var body: some View {
        switch card.cardSymbol {
        case .Squiggle: ZStack {
            Squiggle(rectSize: mySize).fill(cardColor(colorIn: card.cardColor)).opacity(1).zIndex(0)
            if card.cardShade == SetCardShade.None {
                Squiggle(rectSize: mySize).fill(.white).padding(4).zIndex(1)
            }
            if card.cardShade == SetCardShade.HalfShaded {
                Squiggle(rectSize: mySize).fill(cardFgColor(colorIn: card.cardColor)).padding(4).zIndex(1)
            }
        }
        case .Diamond: ZStack {
            Diamond().fill(cardColor(colorIn: card.cardColor)).opacity(1).zIndex(0)
            if card.cardShade == SetCardShade.None {
                Diamond().fill(.white).padding(5).zIndex(1)
            }
            if card.cardShade == SetCardShade.HalfShaded {
                Diamond().fill(cardFgColor(colorIn: card.cardColor)).padding(4).zIndex(1)
            }
        }
        case .Oval: ZStack {
            //todo: Change the circle calls below to Ovals
            Circle().fill(cardColor(colorIn: card.cardColor)).opacity(1).zIndex(0)
            if card.cardShade == SetCardShade.None {
                Circle().fill(.white).padding(5).zIndex(1)
            }
            if card.cardShade == SetCardShade.HalfShaded {
                Circle().fill(cardFgColor(colorIn: card.cardColor)).padding(3).zIndex(1)
            }
        }
        }
#warning ("todo: Resize border width on shaded and transparent shapes, depending on the view size")
    }
    
    private func cardColor(colorIn: SetCardColor) -> Color {
        switch colorIn {
        case .Blue: return .blue
        case .Yellow: return .yellow
        case .Red: return .red
        }
    }
    
    private func cardFgColor(colorIn: SetCardColor) -> Color {
        switch card.cardShade {
        case .Solid: return cardColor(colorIn: colorIn)
        case .None: return .white
        case .HalfShaded:
            switch colorIn {
            case .Blue: return .black
            case .Yellow: return .green
            case .Red: return .gray
            }
        }
    }
}


struct CardView: View {
    let card: SetGameVM.Card
    
    private func spacerHeight(height: CGFloat) -> CGFloat {
        let x = height / CGFloat(9)
        return x
    }
    
    private var spacerMultiplier: CGFloat {
        return CGFloat(4 - card.cardSymbolCount)
    }
    
    var body: some View {
        VStack {
            ZStack {
                let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
                shape.fill().foregroundColor(.white)
                shape.strokeBorder(lineWidth: DrawingConstants.lineWidth)
                GeometryReader { geometry in
                    VStack {
                        Spacer(minLength: spacerHeight(height: geometry.size.height * spacerMultiplier))
                        oneSymbol(card: card, mySize: geometry.size)
                        if card.cardSymbolCount > 1 {
                            Spacer(minLength: spacerHeight(height: geometry.size.height))
                            oneSymbol(card: card, mySize: geometry.size)
                            if card.cardSymbolCount == 3 {
                                Spacer(minLength: spacerHeight(height: geometry.size.height))
                                oneSymbol(card: card, mySize: geometry.size)
                            }
                        }
                        Spacer(minLength: spacerHeight(height: geometry.size.height * spacerMultiplier))
                    }
                }
                CardBorder(card: card)
            }
        }
    }
    
    private struct CardBorder : View {
        // If the card is part of a valid set, highlight it in green
        // If the card is part of an invalid set, highlight it in red
        // if the card has been selected, but is not part of a set, highlight it in mint
        let card: SetGameVM.Card
        @ViewBuilder
        var body: some View {
            if card.isPartOfSet {
                let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
                if card.isPartOfInvalidSet   {
                    shape.strokeBorder(.red, lineWidth: DrawingConstants.lineWidth + 4)
                }
                else {
                    shape.strokeBorder(.green, lineWidth: DrawingConstants.lineWidth + 4)
                }
            } else {
                if card.isSelected {
                    let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
                    shape.strokeBorder(.mint, lineWidth: DrawingConstants.lineWidth + 3)
                }
            }
        }
    }
    
    
    private struct DrawingConstants {
        static let cornerRadius: CGFloat = 10
        static let lineWidth: CGFloat = 3
        static let fontScale: CGFloat = 0.75
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let game = SetGameVM()
        SetGameView(game: game)
            .previewInterfaceOrientation(.portraitUpsideDown)
    }
}




Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source