개발을 시작하는 이야기

[SwiftUI] Lecture 4: Memorize Game Logic 본문

개발 이야기/Swift

[SwiftUI] Lecture 4: Memorize Game Logic

Teiresias 2022. 4. 14. 02:13

강의 보기 : Youtube :: Stanford 

이번 강의에서 주된 내용은 Enum과 Optional에 대한 이야기

Enum

값타입으로 관련 데이터를 가지고 있을수 있다.

enum의 상태 체크는 switch를 이용한다.

break는 아무 일도 하고 싶지 않을 때 사용한다.

default는 기본값에 해당하는, 케이스가 없는 경우 사용한다. if문의 else 와 같은 느낌

 

switch에서 열거형을 case로 구분할때, 튜플에서 label을 추가해서 해당 값에 접근할 수 있다.

저장프로퍼티는 가질수 없고, 함수 사용은 가능하다.

 

CaseIterable를 이용해서 모든 타입에 접근할 수 있다.

enum A: CaseIterable {
    case a
    case b
    case c
}

for e in A.allCases {
    print(e)
}

Optional

열거형으로 none, some으로 이루어진다. some은 제네릭 타입

var hello: String?
var hello: Optional<String> = .none

var hello: String? = "hello"
var hello: Optional<String> = .some("hello")

var hello: String? = nil
var hello: Optional<String> = .none

옵셔널 값을 해제하는 방법

  • 값의 뒤에 '!'를 붙여 강제 해제
  • 다른 변수에 할당, if let , guard let
  • '??'를 사용하여 nil일때의 기본 값을 지정

이번 강의에서는 지난번의 MVVM에 맞춰서 View를 정리해주고, 로직을 정리했다.

View

import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel: EmojiMemoryGame
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]) {
                ForEach(viewModel.cards) { card in
                    CardView(card: card)
                        .aspectRatio(2/3, contentMode: .fit)
                        .onTapGesture {
                            viewModel.choose(card)
                        }
                }
            }
        }
        .foregroundColor(/*@START_MENU_TOKEN@*/.red/*@END_MENU_TOKEN@*/)
        .padding(.horizontal)
    }
}

struct CardView: View {
    let card: MemoryGame<String>.Card
        
    var body: some View {
        ZStack {
            let shape = RoundedRectangle(cornerRadius: 20.0)
            if card.isFaceUp {
                shape.fill().foregroundColor(.white)
                shape.strokeBorder(lineWidth: 3)
                Text(card.content).font(.largeTitle)
            } else if card.isMatched {
                shape.opacity(0)
            } else {
                shape.fill()
            }
        }
    }
}

ViewModel

import SwiftUI

class EmojiMemoryGame: ObservableObject {
    static let emojis = ["💡", "📋", "🖥", "😺", "🗺", "😱", "🙈", "🤔", "📪", "👨‍🏫", "📱", "🎉", "📄", "💁", "📞", "👨‍💻", "⚒", "🙋", "🤵‍♂️", "😀", "😃", "😄", "😁"]
    
    static func createMemoryGame() -> MemoryGame<String> {
        MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
            emojis[pairIndex]
        }
    }
        
    @Published private var model: MemoryGame<String> = createMemoryGame()
    
    var cards: Array<MemoryGame<String>.Card> {
        model.cards
    }
    
    //MARK: - Intent(s)
    
    func choose(_ card: MemoryGame<String>.Card) {
        model.choose(card)
    }
}

Model

import Foundation

struct MemoryGame<CardContent> where CardContent: Equatable {
    private(set) var cards: Array<Card>
    
    private var indexOfTheOneAndIOnlyFaceUpCatd: Int?
    
    mutating func choose(_ card: Card) {
        if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
           !cards[chosenIndex].isFaceUp,
           !cards[chosenIndex].isMatched
        {
           if let potentialMatchIndex = indexOfTheOneAndIOnlyFaceUpCatd {
               if cards[chosenIndex].content == cards[potentialMatchIndex].content {
                   cards[chosenIndex].isMatched = true
                   cards[potentialMatchIndex].isMatched = true
               }
           indexOfTheOneAndIOnlyFaceUpCatd = nil
           } else {
               for index in cards.indices {
                    cards[index].isFaceUp = false
                }
                indexOfTheOneAndIOnlyFaceUpCatd = chosenIndex
            }
            cards[chosenIndex].isFaceUp.toggle()
        }
        print("\(cards)")
    }
    
    init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
        cards = Array<Card>()
        // add numberOfPairsOfCards x 2 cards to cards array
        for pairIndex in 0..<numberOfPairsOfCards {
            let content: CardContent = createCardContent(pairIndex)
            cards.append(Card(content: content, id: pairIndex*2))
            cards.append(Card(content: content, id: pairIndex*2+1))
        }
    }
    
    struct Card: Identifiable {
        var isFaceUp: Bool = false
        var isMatched: Bool = false
        var content: CardContent
        var id: Int
    }
}

mutating

swift에서 클래스는 래퍼런스 타입이고, 구조체와 열거형은 값 타입이다.

값 타입의 속성은 기본적으로 인스턴스 메서드 내에서 수정을 할 수 없는데,

값 타입의 속성을 수정하기 위해서 인스턴스 메서드에서 mutating 키워드를 사용한다.

 

함수 내부에서 설정한 chosenIndex를 변경하기 위해서 mutating을 사용한다.

@Published 와 @ObservedObject

ObservedObject는 필수구현을 필요로 하지 않는 프로토콜로, Combine에 속한 기능이다. 클래스에서만 사용이 가능하고 ObservedObject를 준수한 클래스는 objectWillChange라는 프로퍼티를 사용할 수 있는데, 이는 objectWillChange.send()를 이용하기 위함이다. 이 함수는 변경된 사항이 있다는것을 알려주는 역할을 한다.

 

변수가 적으면 send() 함수를 통해 간단히 해결할 수 있겠지만, 변수의 양도 많고 수정되는 부분이 많아 복잡하다면 관리하기 힘들기 때문에 이를 대신해주는 기능이 @Published 속성 래퍼가 해준다. 해당 변수가 변경되면 자동으로 objectWillChange.send() 호출해준다.

where

where절은 크게 두가지 용도로 사용하게 된다.

 1. 패턴과 결합하여 조건을 추가

 2. 타입에 대한 제약 추가

let tuples: [(Int, Int)] = [(1, 2), (1, -1), (1, 0), (0, 2)]

// 값 바인딩, 와일드카드 패턴
for tuple in tuples {
    switch tuple {
    case let (x, y) where x == y: print("x == y")
    case let (x, y) where x == -y: print("x == -y")
    case let (x, y) where x > y: print("x > y")
    case (1, _): print("x == 1")
    case (_, 2): print("y == 2")
    default: print("\(tuple.0), \(tuple.1)")
    }
}
/*
 x == 1
 x == -y
 x > y
 y == 2
 */
 
 //타입과 결합한 활용
let anyValue: Any = "ABC"

switch anyValue {
case let value where value is Int: print("value is Int")
case let value where value is String: print("value is String")
case let value where value is Double: print("value is Double")
default: print("Unknown type")
}   

// value is String

Equatable

값이 같은지 비교할 수 있는 형식으로, 프로토콜을 준수하는 유형은 등호 연산자 (==) 또는 같지 않음 연산자(!=)를 사용해 동등성을 비교할 수 있다. 

if let  @ ,

if let으로 선언한 변수를 조건문에서 바로 활용하기 위해서는 &&이 아닌, ','로 구분을 해주면 사용이 가능하다.

if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
           !cards[chosenIndex].isFaceUp,
           !cards[chosenIndex].isMatched
        {

살짝의 찍먹으로 맛봤지만 SwiftUI에서 MVVM은 필수라고 하는 데에는 다 이유가 있구나 싶다.

과정이 진행될수록 점점 복잡해지고, 모르는 것들도 중간에 종종 나오면서 공부가 많이 되는것 같다.

 

참고 자료: mutating, Published, ObservedObject, Equatable