Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Basic)

[Stanford] 카드매칭 게임(@ObservableObejct, @ObservedObject, @Published)

by Toughie 2023. 3. 26.

MVC

MVVM

MVVM은 그래픽 사용자 인터페이스(뷰)의 개발을 비즈니스 로직 또는 백-엔드 로직(모델)로부터 분리시켜서 뷰가 어느 특정한 모델 플랫폼에 종속되지 않도록 해주는 패턴이다.

 

 

https://velog.io/@ictechgy/MVVM-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

MVVM 디자인 패턴 in iOS

MVVM은 그래픽 사용자 인터페이스(뷰)의 개발을 비즈니스 로직 또는 백-엔드 로직(모델)로부터 분리시켜서 뷰가 어느 특정한 모델 플랫폼에 종속되지 않도록 해주는 패턴이다. → 패턴을 쓰고자

velog.io

 

개인적으로 지금은 아래 설명이 그나마 잘 와닿는 것 같다.

  • 'UIKit - MVC' 구조는 event driven 방식인데, 'SwiftUI - MVVM' 구조는 data driven 방식이다!
    • UIKit에서는 이벤트에 따라 특정 로직이 실행되고 이에 따라 View가 바뀌는 방식이었다.
    • SwiftUI에서는 데이터의 변경에 따라 로직이 실행되고, 이에 따라 View가 바뀌는 방식이다.
  • 중요한 것은 ViewModel은 View를 설정해줘야 하는 책임으로부터 자유롭다는 것 (MVC, MVP 패턴과 다르게)

 

 

https://www.youtube.com/watch?v=oWZOFSYS5GE&list=PLpGHT1n4-mAsxuRxVPv7kj4-dQYoC3VVu&index=4 

 

Stanford SWiftUI강의를 이어 나갔다.

그리고 아래 카드게임 코드를 정리해본다.

View

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var viewModel: EmojiMemoryGame
    
    // 뷰는 뷰모델을 구독한다고 했다. 모델에서 변화가 생기면 뷰모델은 이를 알아차려
	// Publish(세상에 알리는 신문을 발간한다고 비유..)하면, 이를 구독하고 있는 뷰는
    // 변경된 데이터에 맞춰 UI를 Rebuild 한다.
    // 이를 위한 키워드 @ObservedObject(말 그대로 뷰모델이 (뷰에 의해) 관찰 되는 대상이다.
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))]) {
                ForEach(viewModel.cards) { card in
                    CardView(card: card)
                        .aspectRatio(2/3, contentMode: .fit)
                        .onTapGesture {
                            viewModel.choose(card)
                            
                            //카드뷰에 탭이 들어왔을 때 viewModel 안의 메서드 실행
                            //뷰모델이 뷰를 직접 그리는게 아니라, 제스처가 들어오면
                            //이를 바탕으로 모델의 데이터를 변경한다.
                            //뷰는 뷰모델을 구독하고 있기 때문에 데이터 변경에 따른 publish에 맞춰
                            //알아서 UI를 rebuild 할 것이다.
                            //뷰모델의 choose메서드를 보면 view(UI)와 관련된 것이 없다 ㅋㅋ
                        }
                }
            }
        }
        .foregroundColor(.blue)
        .padding(.horizontal)
    }
}

struct CardView: View {
    let card: MemoryGame<String>.Card
    var body: some View {
        ZStack {
            let shape = RoundedRectangle(cornerRadius: 20)
            if card.isFaceUP {
                //타입 프로퍼티
                shape.fill().foregroundColor(Color.white)
                shape.strokeBorder(lineWidth: 3)
                //타입 프로퍼티_ 다만 타입 추론 가능해서 안써도 되는 것
                Text(card.content).font(Font.largeTitle)
                
                
            } else if card.isMatched {
                shape.opacity(0)
                //카드가 매칭되었을 때 사라지는 것이 자연스럽기 때문에
                //opacity를 조절해 사라진 것처럼 보이게.(실제로는 영역 그대로 차지하고 있음)
            }
            else {
                shape.fill()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let game = EmojiMemoryGame()
        ContentView(viewModel: game)
            .preferredColorScheme(.light)
        //ViewModifier - prefer color scheme dark
        
        
        ContentView(viewModel: game)
            .preferredColorScheme(.dark)
        //ViewModifier - prefer color scheme dark
            .previewDevice("iPhone 14")
    }
}

 

Foundation 프레임워크에 대해 다시 살펴보자.

In Swift, a foundation is a framework that provides a set of fundamental classes and protocols for working with data and performing common tasks.

 

The Foundation framework is part of the Swift standard library and includes classes for strings, arrays, dictionaries, dates, file handling, network communication, and more.

말 그대로 작업에 필요한 기본적인 것들이 포함되어 있어서 이름도 Foundation이다.

UIKit을 임포트 하면 Foundation을 따로 임포트 할 필요가 없다. (UIKit이 Superset이기 때문)

 

The Foundation framework provides a core set of functionality for many iOS, macOS, watchOS, and tvOS apps. Some of the tasks that can be performed using the Foundation framework include:

  • Creating and manipulating strings, dates, and times
  • Handling files and directories
  • Working with network connections and URLs
  • Parsing XML and JSON data
  • Using cryptography and encryption
  • Performing basic arithmetic and mathematical operations
  • Managing collections of objects, such as arrays and dictionaries

The Foundation framework is an essential part of developing apps in Swift, and understanding its classes and functionality is crucial for any iOS or macOS developer.

 

ViewModel

@Published

@Published is a property wrapper in SwiftUI that allows a property to be observed for changes and notifies any subscribers of those changes.

When you mark a property with @Published, it creates a publisher that emits a new value whenever the property is set. You can then subscribe to this publisher to receive updates whenever the property changes.

 

아래 코드에서는 모델 앞에 @Published가 붙어있다.

이를 통해 뷰모델이 모델의 변경사항을 관찰하고 subscriber(구독자인 View!)에게 변경 사항을 알릴 수 있다.

프로퍼티 앞에 @Published를 붙이면 프로퍼티가 변경(set)될 때마다 새로운 값을 전달하는 publisher가 생성된다.
(신문이라고 이해하자)

 

import Foundation

//ViewModel은 UI그리는 것을 담당하지 않기 때문에, SwiftUI를 임포트 할 필요가 없다.
//Foundation 프레임워크에 대한 설명은 위 참고

//ViewModel은 뷰에서도 쳐다보고 모델과도 커뮤니케이션 하기에 공유가 핵심인 클래스로 설계.
class EmojiMemoryGame: ObservableObject {
    //can publish to the world " something changed!"
    //MVVM패턴에서 뷰는 뷰모델을 구독한다고 했다.
    //그래서 관찰가능한 객체라는 의미의 ObservableObject 프로토콜을 채택해야 한다.
    //어떤 변화(데이터)가 발생하면 뷰모델은 변화가 발생했다고 publish 할 것이다.
    
    static let emojis: [String] = [
        "🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑","🚒","🚐",
        "🛻","🚚","🚛","🚜","🚂","✈️","🚀","🚁","🛶","⛵️",
        "🛳️","⛴️","🚢","🚤"
    ]
    
    static func createMemoryGame() -> MemoryGame<String> {
        MemoryGame<String>(numberOfPairsOfCards: 12) { pairIndex in
            emojis[pairIndex]
        }
    }
        
    @Published private var model: MemoryGame<String> = createMemoryGame()
    
    //anytime anyone does anything that changes the Model
    //automatically call objectWillChange.send()
    //모델에 변화가 생기면 자동적으로 objectWIllChange.send()메서드가 실행됨.
    // ObservableObject프로토콜에서 제공하는 메서드.
    // 객체의 상태가 변경될 것임을 알려줌. (이에 맞춰 뷰에서 UI를 리빌드 할 것)

    var cards: Array<MemoryGame<String>.Card> {
        return 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 indexOfTheOneAndOnlyFaceUPCard: 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 = indexOfTheOneAndOnlyFaceUPCard {
                if cards[chosenIndex].content == cards[potentialMatchIndex].content {
                    cards[chosenIndex].isMatched = true
                    cards[potentialMatchIndex].isMatched = true
                }
                indexOfTheOneAndOnlyFaceUPCard = nil
            }else {
                for index in cards.indices{
                //배열을 활용한 범위.
                    cards[index].isFaceUP = false
                }
                indexOfTheOneAndOnlyFaceUPCard = chosenIndex
            }
            cards[chosenIndex].isFaceUP.toggle()
        }
    }
    
    init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
        cards = Array<Card>()
        // add numberOfPairsOfCards * 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))
        }
    }
    
    //Nested Type
    struct Card: Identifiable {
        var isFaceUP: Bool = false
        var isMatched: Bool = false
        //제네릭
        var content: CardContent
        var id: Int
    }
}

 

시연

카드를 랜덤하게 섞고, 여러 테마를 만들면 더욱 완성도 있을 것이다.

 

다만 스탠포드 강의를 듣다 보니..

생각보다 강의 밀도가 매우 높고 SwiftUI뿐만 아니라 기존 문법 개념이나 여러 개념들을 함께 가져가는 느낌이다.

 

물론 이게 나쁘다는 뜻은 아닌데, SwiftUI에 대해 기초부터 차근차근 배워나가고 싶은 마음도 크기에

(기본적으로 어느 정도(꽤 많이) 베이스가 있다는 전제하에 강의하시는 느낌.)

 

우선 해당 강의를 통해서는 SwiftUI와 MVVM에 대한 맛보기를 했다 치고,

수강예정 리스트에 있는 다른 강의를 시작해 볼 볼 예정이다.