'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 |
|---|
