THIS IS ELLIE

스탠포드 SwiftUI강의 복습하기 Lecture3 본문

개발/SwiftUI

스탠포드 SwiftUI강의 복습하기 Lecture3

Ellie Kim 2021. 1. 29. 06:40

모델

모델 MemoryGame은 <CardContent>에 들어오는 타입이 무엇인지 상관하지 않는다. (제네릭 사용)
MemoryGame을 init 하면서 CardContent가 무엇인지 신경 쓰는 쪽이 모델을 위해 만들어준다.

아래 mutating func choose(card: Card)함수를 살펴보자.

mutating func choose(card: Card) {
    print("card가 선택되었다 \(card)")
    
    let chosenIndex: Int = index(of: card)
    cards[chosenIndex].isFaceUp = !cards[chosenIndex].isFaceUp
}

여기서 파라미터로 넘어오는 card는 let이기 때문에 상수이다.
우리는 in-place하게 array에 있는 카드의 속성을 변경하기를 원하니 mutating키워드를 함수에 붙여준다.
구조체에서 이니셜 라이져도 self를 바꾸기 때문에 암묵적으로 이것도 당연하게 mutating이다.
스위프트는 구조체가 바뀌는 시점을 파악하기 위해서 mutating 키워드를 붙여준다고 생각하자.

아래 func index(of card: Caard) -> Int 함수를 살펴보자.

func index(of card: Card) -> Int {
    for index in 0..<cards.count {
        if cards[index].id == card.id {
            return index
        }
    }
    return -1
}

Card는 Identifiable을 채택했기 때문에 id 프로퍼티를 통해 구분할 수 있다.
만약에 for을 했는데 원하는 카드를 찾을 수 없으면 어떻게 할 건가.
다른 언어는 -1 같은 걸 리턴 하지만 스위프트는 좋은 것이 있다.
(옵셔널 기능 설명 전이라 -1로 리턴하게 해 놓음)

 

뷰모델

뷰모델은 모델 변수를 가지고 이는 포털과 같은 역할을한다.
뷰를 위한 변수들은 가지지 않으며 뷰모델은 뷰에게 직접 말하지 않는다.
(뷰가 뷰모델에게 말하기 때문에 뷰모델에서 뷰로 연결하는 점은 없다.)

뷰모델이 ObservableObject 프로토콜을 준수한다.
프로토콜에는 var objectWillChange: ObservableObjectPublisher이 있는데 퍼블리셔고 send를 보낸다.
(var objectWillChange는 선언하지 않아도 사용할 수 있음)
퍼블리셔가 send를 보내야 하는 시점을 생각해보면 유저가 카드를 선택했을 때다.

func choose(card:MemoryGame<String>.Card) {
    objectWillChange.send()
    model.choose(card: card)
}

인텐트 함수에 objectWillChange.send()을 호출한다.
그럼 뷰는 뷰모델을 옵저빙하고 있다가 변화가 감지되는 것을 확인할 수 있다.

하지만 더 많은 인텐트가 있다고 가정해보자.
그럴 때마다 objectWillChange.send()를 보낸다고 생각하니 복잡하다.

그럴 때는 모델 변수에 @Pubished 프로퍼티 래퍼를 설정해준다. 
일일이 objectwillChange.send()를 호출하지 않아도 model이 변화할 때마다 objectwillChange.send()가 호출된다.

 

@ObservedObject를 viewModel 변수에 붙여준다.
이 프로퍼티 래퍼는 objectWillChange.send()가 호출되면 뷰를 다시 그리게 된다.
(SwiftUI는 똑똑하기 때문에 전체를 그리지 않고 필요한 일부를 그린다)
하나의 카드가 플립 되었을 때는 나머지는 그대로 두고 변경이 일어난 카드만 다시 그린다.

아래 함수를 빼낸 이유는 매번 self를 사용하는 게 힘드니 함수화하여 self의 사용을 줄였다.

func body(for size: CGSize) -> some View {
    ZStack {
        if card.isFaceUp {
            RoundedRectangle(cornerRadius: cornerRadius).fill(Color.white)
            RoundedRectangle(cornerRadius: cornerRadius).stroke(lineWidth: edgeLindWidth)
            Text(card.content)
        } else {
            RoundedRectangle(cornerRadius: cornerRadius).fill()
        }
    }
    .font(Font.system(size: min(size.width, size.height) * fontScaleFactor))
}

전체 루프를 살펴보자.


카드를 탭 하면 opTapGesture을 통해 viewmodel.choose(card: card)를 호출. (인텐트)
뷰모델에서 model.choose(card: card)를 호출해 모델한테 변경을 요청을 함.
모델은 mutating func choose(card: Card)으로 배열에 있는 카드에 대한 isFaceUp프로퍼티를 변경함.
뷰모델에서 @Published 프로퍼티 래퍼인 model은 모델이 변경이 있었음을 알고 objectPublished.send()를 호출.
뷰에서 @ObservedObject 프로퍼티 랩퍼인 viewmodel은 objectPublished.send()가 호출됨을 알고 뷰를 다시 그림.


명령형 Imperative programming VS 선언형 declarative programming 
HOW 명령형은 어떻게 할 것인지를 설명한다. 
WHAT 선언형은 무엇을 할 것인지를 정의한다. 


뷰빌더에 사용할 변수 선언

struct ContentView: View {
// (2)
	var body: some View {
    // (1) 
    	HStack { 
        	
        }
    }
}

HStack, ZStack, ForEach를 뷰빌더라하며 내부에 변수 선언이 불가능하다.
변수를 선언하려면 (1)뷰빌더 밖에서 선언하여 뷰빌더에서 사용하거나, (2)연산 프로퍼티를 생성해 사용하는 방법이 있다.


GeometryReader
부모의 크기와 위치에 액세스 할 수 있는 뷰.
developer.apple.com/documentation/swiftui/geometryreader

resource
www.youtube.com/watch?v=SIYdYpPXil4&list=PLpGHT1n4-mAtTj9oywMWoBx0dCGd51_yG&index=3

반응형