한결과 레지아이스

TIL in SwiftUI about Computed Property & Extension, 22/02/19 본문

Today I Learned/SwiftUI

TIL in SwiftUI about Computed Property & Extension, 22/02/19

miniwho 2022. 2. 19. 17:59

Stanford 유튜브에서 제공하는 SwiftUI 2021 Lecture 5를 보고 클론코딩 하며 정리해본 부분입니다.

https://www.youtube.com/watch?v=ayQl_F_uMS4

SwiftUI 강의이긴 한데, 오늘 배운건 Swift 문법 부분이라 TIL in Swift로 올립니당.

1. Given Code

강의에서 만들던 MemoryGame의 Model 부분 코드의 일부입니다.

1)	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
        {
2)          if let potentialMatchedIndex = indexOfTheOneAndOnlyFaceUpCard {
                if cards[chosenIndex].content == cards[potentialMatchedIndex].content {
                    cards[chosenIndex].isMatched = true
                    cards[potentialMatchedIndex].isMatched = true
                }
3)              indexOfTheOneAndOnlyFaceUpCard = nil
            } else {
                for index in  cards.indices {
                    cards[index].isFaceUp = false
                }
4)              indexOfTheOneAndOnlyFaceUpCard = chosenIndex
            }
            cards[chosenIndex].isFaceUp.toggle()
        }
    }

변수명이 점 많이 길긴 하지만... 1)에서 indexOfTheOneAndOnlyFaceUpCard를 (이하 indexOf라고 지칭할게용) 처음에 옵셔널로 선언해주고, 이 값은 nil로 초기화된 상태로 밑의 choose라는 mutating func 안에서 이용됩니다.

2)에서 옵셔널 바인딩으로 값을 확인해 nil이 아닌 경우에는 3)에서 nil로 바꿔주고, nil인 경우에는 4)에서 chosenIndex를 넣어줍니다. 4)에선 그와중에 cards배열을 순회하며 모든 카드의 isFaceUp 프로퍼티를 false로 바꿔주는데, 이거를 게터/세터와 클로져로 점 간단하게 바꿔볼 것입니다.

2. 중간 과정

1)	private var indexOfTheOneAndOnlyFaceUpCard: Int? {
        get {
            var faceUpCardIndices = [Int]()
            for index in cards.indices {
                if cards[index].isFaceUp {
                    faceUpCardIndices.append(index)
                }
            }
            if faceUpCardIndices.count == 1 {
                return faceUpCardIndices.first
            } else {
                return nil
            }
        }
        set {
            for index in  cards.indices {
                if index != newValue {
                    cards[index].isFaceUp = false
                } else {
                    cards[index].isFaceUp = true
                }
            }
        }
    }

    mutating func choose(_ card: Card) {
        if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}),  !cards[chosenIndex].isFaceUp,
           !cards[chosenIndex].isMatched
        {
2)          if let potentialMatchedIndex = indexOfTheOneAndOnlyFaceUpCard {
                if cards[chosenIndex].content == cards[potentialMatchedIndex].content {
                    cards[chosenIndex].isMatched = true
                    cards[potentialMatchedIndex].isMatched = true
                }
//							indexOfTheOneAndOnlyFaceUpCard = nil
                cards[chosenIndex].isFaceUp = true
            } else {
//              for index in  cards.indices {
//                  cards[index].isFaceUp = false
//          }
3)              indexOfTheOneAndOnlyFaceUpCard = chosenIndex
            }
//          cards[chosenIndex].isFaceUp.toggle()
        }
    }

4줄을 줄이려다 어째 더 한참 길어져버렸다..! 하지만 중간 과정이니 이해합시다.. get과 set을 이렇게 써라~~ 라고 알려주는 부분입니다.

1)은 원래 Stored Property였습니다. 프로퍼티이긴한데값 을 저장하는, 지정된 값이 있는 프로퍼티를 의미합니다. 보통은 대입연산자(=)를 통해 값을 지정해주고, 여기선 = nil이 생략되었었습니다. (위의 given code 얘기)

그런데 이번에는? Type을 알려주는 : Int? 뒤에 대입연산자가 아니고 {}가 나타납니다..! 이런 종류의 프로퍼티를 Computed Property 라고 부르고, 대입이 아니고 {} 안을 계산해서 지정해줍니다.

그런데 이 Computed Property는 get과 set을 가질 수 있습니다.

밖에서 이 프로퍼티의 값을 얻어갈 때, get{ }이 실행됩니다.

반대로 밖에서 이 프로퍼티의 값이 변경될 때는, set{ }이 실행됩니다. set에는 특별한 변수가 있는데, newValue입니다. 밖에서 대입된 값을 newValue라는 이름으로 쓸 수 있습니다.

2)와 3)이 각각 get과 set이 실행되는 부분입니다. 2)는 indexOf의 값을 얻어오는 get 부분이고, 3)은 indexOf의 값을 정해주는 set 부분입니다.

3)을 먼저 보면, 원래 given code에선 이부분이 모든 카드를 false로 만들어주고,  indexOf에 chosenIndex를 대입해주고, 그리고 나서 토글을 해서 true로 바꿔 이녀석만 앞면이게 만들어줍니다.

근데 이거를..! indexOf에 chosenIndex가 대입되었을때, set에서 newValue는 chosenIndex가 되고, 해당 인덱스의 카드를 앞면으로, 나머지는 뒷면으로 해줍니다.

2)에서는요, 이 부분에서 바인딩을 통해 하고자 하는 것을 먼저 설명하겠습니다. 내가 현재 카드를 뒤집었을 때, 카드가 이미 한 장 뒤집혀서 내가 뒤집은 게 두장째면 if문이 실행되고 내가 지금 뒤집은게 첫 장 째면 else문이 실행되어 indexOf를 지정해줍니다. 그래서 원래는, if문이 실행되고 나면 indexOf를 nil로 초기화해주어야 했습니다. 근데 이 부분이 없어졌습니다!! 왜 그러냐면, get에서 그냥 지금 뒤집힌 카드가 있는지 보고 indexOf에 nil을 넣어주거나 뒤집힌 index를 넣어주거나 지가 알아서 해주기 때문입니다..!

get을 대충 보면, 모든 카드를 순회해서 뒤집혀있는 카드 숫자를 세고, 그게 한 장이면 해당 인덱스를, 한 장이 아니면 nil을 리턴해줍니다. 이렇게 편할수가~~~~~

근데 이제 코드가 길어졌으니... 정말 놀랍게도 클로져를 통해서 얘네를 줄여보겠습니다. 눈알 튀어나올 준비~~~

3. 다 줄인 코드

		private var indexOfTheOneAndOnlyFaceUpCard: Int? {
        get { cards.indices.filter({ cards[$0].isFaceUp }).oneAndOnly }
        set { cards.indices.forEach { cards[$0].isFaceUp = ($0 == newValue) } }
    }
    
    mutating func choose(_ card: Card) {
        if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}),  !cards[chosenIndex].isFaceUp,
            !cards[chosenIndex].isMatched
        {
            if let potentialMatchedIndex = indexOfTheOneAndOnlyFaceUpCard {
                if cards[chosenIndex].content == cards[potentialMatchedIndex].content {
                    cards[chosenIndex].isMatched = true
                    cards[potentialMatchedIndex].isMatched = true
                }
                cards[chosenIndex].isFaceUp = true
            } else {
                indexOfTheOneAndOnlyFaceUpCard = chosenIndex
            }
            
        }
    }

(대충 눈알 튀어나오는 사진)

열댓줄은 되어보이던 부분이~~~~ 2줄이 되어버렸습니다........

Swift에서 Array는 filter와 forEach라는 메서드를 가지고 있습니다. 이 둘은 함수를 매개변수로 갖는 고차함수라고 합니다.

**func** filter(_ isIncluded: (Bound) **throws** -> Bool) **rethrows** -> [Bound]

**func** forEach(_ body: (Bound) **throws** -> Void) **rethrows**

여기서 Bound는 The type for which the expression describes a range.이라고 합니다.

throws랑 rethrows는 일단 무시할건데, 에러처리에 사용되는 키워드이고 제가 아직 몰라서 입니다...

get의 cards.indices.filter({ cards[$0].isFaceUp }).oneAndOnly 요걸 먼저 보겠습니다.

cards는 Array<String>입니다.

indices는 사진으로 첨부할게용.

일케 Array의 range를 리턴해줍니다. 0..<4 요런식으로.

 

filter는 label없이 인자를 하나 받는데, 이 인자는 (Bound)를 인자로 받고 Bool을 리턴하는 함수입니다. 그리고 [Bound], 배열을 리턴합니다.

(이하는 제 생각입니다.. 정확하지 않아요. (Bound)가 정확히 뭔지 찾아봐도 모르겠고 어렵네용..)

클로져 안에서 $0처럼 $뒤에 숫자가 있는건 그 숫자 번째 인자를 의미합니다. 얘는 인자를 하나 받으니 $0이 유일한 인자이고요.

여기서 Bound가 cards.indices가 되는거고,

다시 써보면 filter( { cards[0..<cards.count].isFaceUp } ) 이렇게 됩니다.

그래서 cards를 순회하며 isFaceUp이 true인 애들을 모아서 새로 배열을 만들어서 리턴하는 거라고 이해했습니다. 근데 왜이렇게 되는건지 모르겠어요. 넘 어려워요..

forEach도 마찬가지인데, 얘는 인자로 (Bound)를 받는 건 맞지만 리턴하는 건 없고 받는 함수도 리턴이 Void로 없습니다.

cards.indices.forEach { cards[$0].isFaceUp = ($0 == newValue) }를 다시써보면

forEach( { cards[0..<cards.count].isFaceUp = (0..<cards.count == newValue) } )이고,

이제 cards[0] 부터 cards[cards.count - 1] 까지를 순회하며 isFaceUp에, index의 값이 newValue와 같은지 비교한 Bool값을 대입해줍니다.

이부분이 조금 이해가 안가서 좀 더 적어보면, 저걸 좀 덜 줄인 모습은

cards.indices.forEach { index in cards[index].isFaceUp = ( index == newValue) } 였습니다.

여기서 index는 $0으로 치환해주고 앞의 index in을 생략할 수 있습니다. 그렇게 줄어든 한 줄입니다.

결국 이 한 문장이

for index in  cards.indices {
		if index != newValue {
				cards[index].isFaceUp = false
		} else {
				cards[index].isFaceUp = true
		}
}

아까 이걸 대체해줍니다! 많이 짧아졌죠. 머리는 아프지만. 더 직관적이라고 그러는데 저는 잘 모르겠네요.. 공부를 더 해야 그렇게 되나 봅니다..

위에서 filter뒤에 .oneAndOnly를 안살펴봤는데, 이제 살펴보겠습니다.

extension Array {
    var oneAndOnly: Element? {
        if count == 1 {
            return first
        } else {
            return nil
        }
    }
}

지금 .ondAndOnly는 filter를 통해 만들어진 새로운 배열에 붙어있다고 볼 수 있겠죠?

근데 Array는 이런 프로퍼티를 가지지 않습니다.

하지만 스위프트에선 내맘대로 이런 프로퍼티를 Array에 추가해 줄 수 있습니다..! (대충 눈알 튀어나오는 사진)

extension이라는 아주아주 파워풀한 기능 덕분입니다..

다른 언어였다면 새로운 서브클래스를 만들어서 기존의 클래스를 상속받고 프로퍼티를 추가해서 써야한다고 알고 있는데, 스위프트에선 그냥 기존의 클래스에 아무거나 추가해도 되나봅니다...

그래서 Array에 computed 프로퍼티를 추가해준 부분입니다. Array는 Element라는 generic을 쓰기 때문에 oneAndOnly도 Element라는 타입을 가지고, self.이 생략되었는데 하여간 해당 Array의 .count가 1이면 .first를 리턴해줍니다. 아니면 nil을요.

get {
        var faceUpCardIndices = [Int]()
        for index in cards.indices {
            if cards[index].isFaceUp {
                faceUpCardIndices.append(index)
            }
        }
        if faceUpCardIndices.count == 1 {
            return faceUpCardIndices.first
        } else {
            return nil
        }
    }

get { cards.indices.filter({ cards[$0].isFaceUp }).oneAndOnly }

그래서 이 긴 게터가~~~ 한 줄로~~~ 바뀌었답니다~~!

원래 후행 클로져라고 함수의 마지막 인자가 클로저라면 괄호를 생략할 수 있는데요, 그래서 생략하게 되면

cards.indices.filter { cards[$0].isFaceUp }.oneAndOnly 이렇게 됩니다. 근데 이러면 .oneAndOnly가 어디에 붙는건지 이상해 보인다고 Stanford강의에선 괄호 생략을 안하더라고요. 대신 위의 forEach 예문을 보면 괄호가 잘 생략된 걸 볼 수 있습니다.

열심히 정리하고 나니 정리가 하나도 안 된 정리같네요... 정리력을 길러야겠어요.

Comments