[Lecture 3] MVVM and the Swift type system


Generic

'don't care' Type이다.

자료구조를 생성할 때, 정수형이나 문자열 등 타입을 신경 쓰지 않고(don't care) 범용적으로 다루고 싶다면 이때 사용하는 타입이 Generic이다. Generic이 적용된 구조체로는 배열(Array)을 예시로 들 수 있다.

배열의 선언부를 보게 되면 다음과 같은 형태로 구성되어있다고 한다. 저기서 사용되는 ElementGeneric으로서 사용되는 데이터이다.

따라서 배열의 항목을 추가하는 append 메서드에서도 element를 사용하여 타입별로 메서드를 따로 구현하지 않아도 된다.

 

Functions as Type

swift를 접하면서 가장 생소했던 부분이다. swift는 함수를 일급객체로서 사용하는 함수형 프로그래밍 언어이다.

 

일급 객체라는 단어 또한 생소한데, 일급 객체(first-class object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다. 일급 객체가 되기 위한 조건이 세 가지가 있다.

  • 객체를 변수와 상수에 할당할 수 있다.
  • 객체를 인자(argument)로 넘길 수 있다.
  • 객체를 반환값(return)으로 사용할 수 있다.

swift는 함수를 일급 객체로서 다루는 언어이다. 따라서 swift에서는 함수를 변수 및 상수에 할당, 함수를 다른 함수의 인자로 넘기거나, 함수의 반환값으로 함수를 리턴할 수 있다는 얘기이다. 



Lectrue 3에서는 Functions as Types라는 주제로 함수를 타입으로서 활용하며 변수 및 상수로 사용하는 것에 대해 이야기하였다.

(Int, Int) -> Bool 

두 개의 Int형 매개변수를 받아 Bool 타입의 결괏값을 리턴하는 함수타입

 

(Double) -> Void

Double형 매개변수 하나를 입력받고 리턴값이 없는 함수타입

 

() -> Array <String>

매개변수를 받지 않고 문자열 배열을 리턴하는 함수타입

 

이런 식으로 swift는 함수를 변수 및 상수에 타입으로 선언 및 할당이 가능하며, 함수 그 자체로서 동작하게 되어 다른 변수 및 상수에 결괏값을 전달할 수 있게 된다.

 

closure

"inlined function"

다음으로 함수의 인자로서 함수를 받는다는 것을 언급했다.

LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))], content: {
	return ForEach(emojis[0..<emojiCount], id: \.self){ emoji in
		return CardView(content: emoji).aspectRatio(2/3,contentMode: .fit)
	}
})

Lecture 2에서 보았던 뷰 생성파트를 보면 content라는 인자에 중괄호를 열고 View를 반환하는 코드를 작성하는데, 해당 부분이 바로 함수의 인자로 함수를 받는다고 보면 된다. 단지 우리는 inlined fucntion 형태로 작성하였고, swift에서는 inlined functionclosure(폐쇄)라고 부른다. 어째서 closure라는 이름이 붙은 건지는 다음 Lecture에서 설명할 예정이라고 하니 잘 기억해 두자.

 

Type property

개발하는 데에 있어 전역변수는 사용하기에 편하지만, 의도치 않은 실수를 유발하기에 사용 시 리스크가 크다는 단점이 있다.

피치 못할 사정으로 전역변수를 사용해야 하는 경우가 생기는데, 이럴 때 전역변수로서 동작하지만 타입자체에 귀속시키는 방법이 Type property이다. 

 

class EmojiMemoryGame{
    let emojis = ["🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑","🚒","🚐","🛻","🚚",
                  "🚛","🚜","🛵","🏍️","🛺","🚔","🚍","🚘","🚖","✈️","🚤","🛥️"]
    
    func createMemoryGame() -> MemoryGame<String>{
        MemoryGame<String>(number0fPairsOfCards: 4){ pairIndex in
            emojis[pairIndex]	//error!
        }
    }
    
    private var model: MemoryGame<String> = createMemoryGame()
    
    var cards : Array<MemoryGame<String>.Card> {
        return model.cards
    }
}

Lecture 1, 2에 걸쳐 작성된 memorize 데모에 MVVM 디자인 패턴을 적용하기 위한 작업 중 ViewModel 작성 부분이다.

이때 Model 생성 시에 상수로 선언된 emojis 배열을 사용하려 했으나, 오류가 발생한다. 이유는 바로 ViewModel의 인스턴스 생성 시 model이 생성되기 이전에 emojis 배열이 먼저 생성된다는 보장이 없기 때문이다.

 

emojis 배열을 ViewModel 상위로 생성하여 전역변수로서 선언을 해버리면 되겠지만, 개발론적인 시선에서 코드의 품질유지에 적합한 방법이 아니다.

 

이때 사용한 것이 타입 속성(Type Property)이다. 전역변수처럼 사용하고 싶은 속성에 static 키워드를 붙이면 해당 속성을 전역변수처럼 사용할 수 있다. 단, 소속된 타입을 명시해 주어야 하므로 어느 자료와 연관된 속성인지 단번에 유추가 가능하므로 코드의 유지보수에 용이하게 된다.

 

class EmojiMemoryGame{
    static let emojis = ["🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑","🚒","🚐","🛻","🚚",
                  "🚛","🚜","🛵","🏍️","🛺","🚔","🚍","🚘","🚖","✈️","🚤","🛥️"]
    
    static func createMemoryGame() -> MemoryGame<String>{
        MemoryGame<String>(number0fPairsOfCards: 4){ pairIndex in
            EmojiMemoryGame.emojis[pairIndex]
        }
    }
    
    private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
    
    var cards : Array<MemoryGame<String>.Card> {
        return model.cards
    }
}

 

즉 속성자체를 인스턴스가 아닌 타입 그 자체에 저장하는 것과 같다 해서 타입 속성(Type property)라고 부른다. 해당 개념을 알기 전부터 사실 나도 모르게 자주 쓰고 있는 기능이다. 대표적인 예시로 Int.max 와 같이 인스턴스 생성 없이 바로 접근하여 사용되는 속성들이다.

 

참고 영상

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

'iOS > Stanford' 카테고리의 다른 글

[Assignment2] More Memorize  (1) 2023.10.16
[Lecture 4] Memorize Game Logic - 1  (0) 2023.08.18
[Lecture 3] MVVM and the Swift type system - 1  (0) 2023.07.08
[Assignment 1] Memorize  (0) 2023.07.06
[Lecture 2] Learning more about SwiftUI  (0) 2023.07.05

[Lecture 1] Getting Started with SwiftUI


View

UIKit과 마찬가지로 View라는 구조체(struct)를 통해 사용자의 입력(탭, 스와이프, 핀치.. 등)과 출력을 담당한다.

 

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello!")
                .padding(.all)
        }
        .padding()
    }
}

UI구성의 내용이 담긴 ContentView는 결국 View 구조체이며, 이러한 구조체 안에는 여러 가지 변수, 메서드를 담아낼 수 있다.

눈여겨볼 것은 body 변수인데, 해당 변수는 View도 아닌, some View라고 정의되어 있다.

강의에서는 "something that behave like a View"라고 설명했다. 즉 "View처럼 행동하는 무언가" 정도로 해석이 가능하다.

 

그렇다면 왜 View가 아니라 View처럼 행동하는 무언가일까?

교수님은 해당 부분을 레고에 비유하였다. 

우리가 레고로 집을 만든다고 하였을 때, 작은 단위의 레고를 모아 의자, 소파, 테이블과 같은 작은 단위의 레고를 만들고, 그것들이 모여 거실, 방, 지붕과 같은 큰 단위로 결합하게 된다. SwiftUI 또한 작은 단위의 View가 결합되어 하나의 View를 구성하게끔 동작한다는 것이다. 즉 수많은 종류의 View 중에 어떤 형태의 View가 반환될지 모르기에 some View라는 키워드로 치환된 것이다.

조금 더 해당 표현에 대해 자세한 설명을 하자면, 화면상에 단 하나의 View만 존재하게 될 경우, 위와 같은 코드로 작성하게 된다. 하지만 해당 코드는 swiftUI에 의해 몇 가지 키워드가 치환된 형태이다. 조금 더 명시적인 형태로 작성하게 되면 아래의 코드로 적을 수 있다.

 

import SwiftUI

struct ContentView: View {
    var body: Text {
        return Text("Hello!")
    }
}

ContentView의 body는 Text라는 View구조체이며, 이는 곧 "Hello"라 적힌 Text View를 반환한다는 것. 하지만 예시보다 복잡한 여러 형태의 View가 조합되면 개발자는 이에 대한 명시를 하기 어려워지므로 some View라는 형태로 적게 된 것이며, 해당 부분에 대한 치환은 컴파일러가 알아서 처리하게 하는 것이다.

 

이처럼 body와 같이 여러 가지 View를 결합하는 View를 Combiner View라고 소개하였으며, 해당 부분의 some View는 대부분 Combiner View를 의미한다고 한다.

 

함수형 프로그래밍

ContentView의 body는 변수로 선언이 되어있지만, 메모리에 저장되지 않는다고 한다. body에 접근할 때마다 body 뒤에 이어진 { ... } 블록의 해당 함수 내용을 수행하고 반환되는 결과를 받아 오는 것이다. 이는 swift의 큰 특징 중 하나인 함수형 프로그래밍에 의한 표현이다.

 

결론

some View라는 표현은 개발자가 편하기 UI 작성하기 위한 표현이며, 이는 최종적으로 컴파일러가 body의 closure에 정의된 함수 내용을 수행하여 반환되는 특정한 View로 치환하게 된다는 이야기이다.


Zstack

View에는 여러 가지 종류의 View가 있으며 이들 중 다른 View를 결합하는 combiner View가 있다 했다. Zstack 또한 강력한 기능을 가진 Combiner View라고 소개되었는데, content { ... } 인자 내에 단순히 View를 나열하기만 하면 스크린 - 사용자 방향 순서로 view를 결합하는 기능을 수행한다. 강의에서는 이렇게 나열을 통해 View를 결합하는 것을 View Builder Machanism이라는 표현으로 소개하였다.

 

Zstack을 통해 생성한 View

Zstack에는 나열한 것을 View로 생성하는 것 이외에도 또 다른 강력한 기능이 있는데, 바로 ZStack에 적용한 속성들이 Zstack 하위의 View에 상속된다는 점이다.

 

View의 공통 속성인 padding()을 Zstack에 적용한 모습

예시의 padding 이외에도 foregroundColor 등 View가 가진 공통 속성들의 적용이 가능하며, 당연하게도 하위 View에 직접 속성을 적용하게 된다면 오버라이딩과 같은 효과를 줄 수 있다.

 

참고 영상

https://www.youtube.com/watch?v=bqu6BquVi2M&ab_channel=Stanford 

'iOS > Stanford' 카테고리의 다른 글

[Assignment 1] Memorize  (0) 2023.07.06
[Lecture 2] Learning more about SwiftUI  (0) 2023.07.05
[Lecture 1] Getting Started with SwiftUI - 1  (0) 2023.06.16
day06_multiTouch  (0) 2022.03.26
day05_view  (0) 2021.12.30

mutating

구조체의 객체를 변경하는 함수에는 함수의 선언시 앞에 mutating을 붙여야한다. 

왜 구조체를 써야하나요?

클래스와 구조체의 큰 차이점은 바로 클래스는 참조타입, 구조체는 값타입이라는 것이다. 클래스 객체는 힙 메모리 내부에 존재하며, 클래스 객체를 전달할 경우(함수나 변수에 전달 시), 객체를 가리키는 포인터가 전달되는 것이다. 이런 식으로 전달이 여러번 이루어 지다 보면 무수히 많은 포인터가 존재하게 되고 만다. 

 

하지만 구조체 객체의 경우 힙 메모리에 존재하지 않고, 구조체를 전달하게 될 때, 구조체 객체가 계속해서 복사된다. 스위프트의 경우, 객체의 내용이 변경되었을 때에만 복사가 이루어지므로 경우에 따라 클래스보다 구조체를 이용하는 것이 메모리 관리에 더욱 효율적일수가 있다. 이렇게 객체의 내용에 변경이 생길 시, 스위프트에게 알려야하는데 그러한 알림이 바로 mutating 키워드다.

 

따라서 예제의 Concentration 클래스를 구조체로 변경하기 위해서 func chooseCard(at index: Int)에 mutating키워드를 적어주었다. 카드배열을 순회후 카드의 앞면, 뒷면 여부가 변경되기 때문이다.


protocol

별도의 구현이 없는 함수와 변수의 리스트이다. 별도의 저장공간이 없으며 변수와 함수의 선언만 작성되어있다. 따라서 프로토콜을 상속받은 뒤 상속받은 곳에서 구현을 해야하는데, 유의할 점은 프로토콜에 선언된 모든 변수와 함수를 구현해야 한다는 점이다.

*오브젝티브-C에서의 프로토콜은 선택적으로 구현을 할 수 있으므로 차이점을 잘 알아두어야 한다. 

 

선언은 아래와 같이 구현은 하지 않은채로 작성하면 된다.

protocol someProtocol : inheritancedProtocol1, inheritencedProtocol2 {
	var someProperty : Int{get, set}	//읽기 쓰기가 가능한 변수
	func aMethod (...) -> someType		//함수 
	mutating func changeIt()		//객체변경 함수
	init(arg:Type)				//초기화
 }

프로토콜에는 저장공간이 할당되어있지 않으므로 선언만 가능하며, 여러개의 프로토콜을 동시에 상속받을수도 있고, 프로토콜이 다른 프로토콜을 상속받는 것 또한 가능하다. 당연히 마지막에는 상속받은 모든 프로토콜의 사항들을 구현해주어야 한다. 

 

사용방법

struct someStruct : someProtocol, otherProtocol {
	//someStruct 구조체의 내용을 작성.
	//물론 someProtocol, otherProtocol의 모든 변수와 함수의 내용을 구현해야한다.
}
extension something : someProtocol {
	//something에 추가할 someProtocol의 모든 변수와 함수를 구현.
}

상속 및 확장이 가능하며 프로토콜의 모든 함수와 변수의 구현을 해주면 된다.

 

objective - C

@objc protocol UIScrollViewDelegate {
	@available(iOS 2.0, *)
    optional func scrollViewDidScroll(_ scrollView: UIScrollView)

    @available(iOS 3.2, *)
    optional func scrollViewDidZoom(_ scrollView: UIScrollView)
    ...
}

objective-C(objc)로 작성된 프로토콜을 들여다 보면 optional이라는 키워드를 볼 수 있다. 앞서 말한 objc로 작성된 프로토콜은 선택적 구현이 가능하다 했는데, 바로 저 optional이 선택적 구현이 가능한 함수이다.


String

문자열(String)은 문자(Character)의 배열이다. 문자열을 다룰때 주의해야 할 점은 스위프트의 문자열은 유니코드방식으로 구성되어있다보니 정수형태의 index를 사용할 수 없다. 스위프트에서 문자열을 다루려면 String.index를 다루어야 한다. 다행히 String은 collection이라는 프로토콜을 상속받았고, String에는 index를 다루기 위한 편리한 collection의 메소드들이 구현되어있다.

//문자열 생성
let str:String = "apple computer"
//첫번째 String.index반환
let firstIndex = str.startIndex
//첫번째 인덱스로부터 4번째 즉 5번째 String.index반환
let fifthIndex = str.index(firstIndex, offsetBy: 4)
//다섯번째 문자 반환
let fifthCharacter = str[fifthIndex]

//출력결과 Index(_rawBits: 262401)
print(fifthIndex)
//출력결과 e
print(fifthCharacter)


//nil을 반환할 수 있기에 if let을 사용한다.
if let firstSpaceIndex = str.firstIndex(of: " "){
    //공백 다음 첫번째 인덱스 저장
    let firstIndexAfterSpace = str.index(firstSpaceIndex, offsetBy: 1)
    let firstCharacterAfterSpace = str[firstIndexAfterSpace]
    //출력결과 c
    print(firstCharacterAfterSpace)
    
    //공백이후 두번째 문자열 저장
    let secondWord = str[firstIndexAfterSpace..<str.endIndex]
    //출력결과 computer
    print(secondWord)
}

String.index를 출력해보면 정수형태가 아닌 것을 알 수 있다. 예시와 같이 메소드를 활용해 문자열을 배열로서 활용해보도록 하자.

 

카드뒤집기 예제에서도 문자열을 활용해보았다.

    //이모티콘 후보들이다. 배열로 담겨있었으나 문자열로 변경하였다.
    var emojiChoices = "🦇😱🙀👿🎃👻🍭🍬🍎"
    
    //emojiChoices에서 이모지를 뽑아 [카드의 id: 이모지] 쌍으로 딕셔너리를 생성한다.
    var emoji = [Int:String]()
    
    //카드에 이모지를 그려넣는 함수. 카드를 매개변수로 받는다. 이모지를 반환한다.
    func emoji(for card: Card) -> String{
        if emoji[card.identifier] == nil, emojiChoices.count > 0{
            //만약 매개변수 카드의 id가 emoji딕셔너리의 키로 존재하지 않는다면, 
            //이모지 후보에서 랜덤하게 뽑아온뒤 [카드id:이모지] 딕셔너리에 추가한다.
            let randomStringIndex = emojiChoices.index(emojiChoices.startIndex,
            offsetBy: emojiChoices.count.arc4random)
            emoji[card.identifier] = String(emojiChoices.remove(at: randomStringIndex))
        }
        
        //카드 id를 키값으로 이모지 반환
        return emoji[card.identifier] ?? "?"
    }

NSAttributedString

문자열의 모든 문자가 각각 딕셔너리를 가지는 문자열이다. 딕셔너리에는 폰트, 색상 등 여러가지가 될수 있다. 즉 각각의 문자가 속성을 가진 문자열인 셈이다. 

//속성 생성 및 정의
let attributes:[NSAttributedString.Key:Any] = [
	.strokeColor:UIColor.orange,
	.strokeWidth:5.0
]
//속성문자열 생성
let attributedString = NSAttributedString(string: "flipCount: \(flipCount)", attributes: attributes)
//문자열 활용
flipCountLabel.attributedText = attributedString

*NS가 붙은 API들은 대부분 오래된 objc클래스이다.

주의할 점은 NSAttributedString은 수정할 수 없다. 따라서 문자열을 수정하고 싶다면 setAttributedString()메소드가 담겨있는 NSMutableAttributedString()으로 생성해야 한다는 점을 알아두자.

 

속성문자열은 카드뒤집기 예제의 flipCount에 적용해보았다.

    //flipCount 글자를 속성문자열로 수정하는 함수.
    private func updateFlipCount(){
        let attributes:[NSAttributedString.Key:Any] = [
            .strokeColor:UIColor.orange,
            .strokeWidth:5.0
        ]
        let attributedString = NSAttributedString(string: "flipCount: \(flipCount)",
        attributes: attributes)
        flipCountLabel.attributedText = attributedString
    }
    
    //카드를 뒤집은 수를 나타내는 레이블.
    @IBOutlet weak var flipCountLabel: UILabel!{
    	//didset을 통해 값의 변경이 일어날 때 마다 updateFlipcount()가 실행된다.
        didSet{
            updateFlipCount()
        }
    }
    
    //flipCountLabel에 표시할 변수. 당연하게도 0부터 시작한다.
    var flipCount = 0 {
        //변수의 값 변동이 일어날 때마다 수행되는 메소드. flipCountLabel에 변경된 값을 전달하고 표시한다.
        didSet{
            updateFlipCount()
        }
    }

FunctionTypes

함수를 하나의 타입으로 선언한다. 

//함수 타입 선언 Double을 입력받고 Double을 반환한다.
var operation: (Double) -> Double
//sqrt메소드 입력(제곱근 반환)
operation = sqrt

//출력결과 2.0
print(operation(4.0))

sqrt메소드와 달리 직접 함수를 작성해서 대입하는 것도 가능하다.

//함수 타입선언
var operation: (Double) -> Double

//함수생성
func changeSign(operand: Double) -> Double{
    return -operand
}

//생성한 함수 대입
operation = changeSign(operand:)
//출력결과 -4.0
print(operation(4.0))

하지만 함수를 새로 생성하는 점은 코드의 라인이 길어지는 단점이 생긴다.


closure

이전 예시에서 더 짧게 코드를 줄이는 방법이 클로저를 활용하는 방법이다. 클로저는 인라인 함수로 인자에 함수를 적어서 넣을수 있다.

var operation: (Double) -> Double

//{}중괄호 안에 함수의 내용을 적으면 되며 반환값은 "in return" 키워드를 사용한다.
operation = {(operand:Double) -> Double in return -operand}

operation = changeSign(operand:)
print(operation(4.0))

여기서 더 강력한 기능은 스위프트의 타입추론이다. 위에 정의한 operation에서 이미 매개인자와 출력인자의 타입이 정의되어있으므로 클로저에서는 타입생략이 가능한 것이다.

//입력인자와 출력인자에 대한 타입과 갯수가 정의되어있다.
var operation: (Double) -> Double

//매개인자들의 타입 생략 가능.
operation = {(operand) in return -operand}

//출력결과 -4.0
print(operation(4.0))

이어서 스위프트는 operation에는 리턴값이 존재한다는 것 또한 인지하고 있다. 즉 return이라는 키워드를 생략할 수 있게 되는데, 

//입력인자와 출력인자에 대한 타입과 갯수가 정의되어있다.
var operation: (Double) -> Double

//매개인자들의 타입 생략 가능.
operation = {(operand) in -operand}

//출력결과 -4.0
print(operation(4.0))

이런식으로 in이라는 키워드만 적으면 된다. 클로저에는 더 강력한 기능이 있는데, 바로 매개인자의 치환이 가능하다는 것이다. 첫번째 매개인자는 $0, 두번째 매개인자가 있다면 $1, 이런식으로 $0, $1, $2 ... 을 통해 매개인자를 대치할 수 있게 되는데, 그렇게 되어 최종적으로 다음 예시처럼 완성이 된다.

//입력인자와 출력인자에 대한 타입과 갯수가 정의되어있다.
var operation: (Double) -> Double

//매개인자의 부호변경
operation = {-$0}

//출력결과 -4.0
print(operation(4.0))

map()

배열에는 map()이라는 메소드가 존재한다. 하나의 인수를 받고 새로운 배열로 반환을 하는데, 이때 인자는 함수를 받는다. 즉 배열의 요소를 하나씩 뽑아서 인자로 받은 함수에 입력한뒤 결과값을 배열로 반환하는 메소드이다. 앞서 설명한 클로저와 항상 쓰이는 메소드이다.

let primes = [2.0,3.0,4.0,5.0]
let negativePrimes = primes.map({-$0})

//출력결과 [-2.0, -3.0, -4.0, -5.0]
print(negativePrimes)

여기서 TrailingClosure라는 문법이 있는데, 어떠한 함수라도 마지막 인자가 클로저라면 클로저를 괄호밖으로 내놓고 작성하는 문법이다. 

인자가 유일하다면 메소드의 괄호를 생략도 가능한데, 이러한 문법들은 모두 인자로 받는 함수가 길어지게 되었을 때의 가독성을 위한 문법이다. 편한 방식을 선택해서 작성하면 된다.

let primes = [2.0,3.0,4.0,5.0]

let negativePrimes = primes.map({-$0})
//출력결과 [-2.0, -3.0, -4.0, -5.0]
print(negativePrimes)

//클로저를 괄호 밖에서 작성해도 된다.
let primes2 = primes.map() {Int($0)}
//출력결과 [2, 3, 4, 5]
print(primes2)

//인자가 유일하다면, 괄호를 생략가능하다.
let primes3 = primes.map {1.0/$0}
//출력결과 [0.5, 0.3333333333333333, 0.25, 0.2]
print(primes3)

filter()

배열이 상속받고 있는 collection 프로토콜의 메소드이다. 하나의 함수를 인자로 받고, Bool을 반환한다. true를 반환하는 요소만 배열에 추가하여 사용한다.

let arr = [1,2,3,4,5,6,7,8,9,10]
//짝수만 새로운 배열에 추가
let evenArr = arr.filter(){$0 % 2 == 0}

//출력결과 [2,4,6,8,10]
print(evenArr)

 

카드뒤집기 예제에서는 앞면인 카드를 확인하는 연산변수(computedProperty)에 적용해보았다. 주석처리된부분이 filter를 통해 삭제된 라인들이다. 상당수의 라인이 제거된다.

//유일하게 앞면 상태의 카드를 나타내는 속성 이후에 두번째로 오픈되는 카드와 id를 비교하기위해 사용된다.
    var indexOfOneAndOnlyFaceupCard:Int? {
        //indexOfOneAndOnlyFaceupCard 호출시
        get{
            //카드 배열 속 앞면인 카드들을 모두 반환. 
            //만약 앞면인 카드가 단 한장이라면 해당 카드인덱스 반환. 한장보다 많다면 nil 반환
            return cards.indices.filter{cards[$0].isFaceUp == true}.oneAndOnly

//            //리턴을 위한 임시 변수 생성. optional로 생성한다.
//            var foundIndex:Int?
//
//            //카드배열을 돌면서 앞면상태인 카드를 탐색한다.
//            for index in cards.indices{
//                if cards[index].isFaceUp{
//                    //앞면인 카드발견 + foundIndex가 nil, 즉 앞면인 카드가 오로지 한장이라면,
//                    if foundIndex == nil {
//                        //앞면이 유일한 카드는 해당 카드가 된다.
//                        foundIndex = index
//                    } else{
//                        //foundIndex에 값이 있는경우, 즉 앞면인 카드가 한장보다 많다면, 유일하게 앞면인 상태가 아니므로 nil을 반환한다.
//                        return nil
//                    }
//                }
//            }
//            //foundIndex의 값으로 indexOfOneAndOnlyFaceupCard설정
//            return foundIndex
        }
        //indexOfOneAndOnlyFaceupCard 대입시
        set{
            //카드 배열을 돌면서 카드의 앞면, 뒷면을 설정한다.
            for index in cards.indices{
                //사용자가 선택한 카드번호(nexValue)가 아닌 나머지 카드들은 모두 뒷면으로 설정한다.
                cards[index].isFaceUp = (index == newValue)
            }
        }
    }
    
extension Collection{
    var oneAndOnly:Element?{
    	원소가 한개라면? 첫번째를(하나뿐인 원소를) 반환해라. 두개이상이라면 nil을 반환한다.
        return count == 1 ? first : nil
    }
}

'iOS > Stanford' 카테고리의 다른 글

day06_multiTouch  (0) 2022.03.26
day05_view  (0) 2021.12.30
day03_swift_part1  (0) 2021.10.12
day02_MVC  (0) 2021.10.03
day01_ios  (0) 2021.09.28

+ Recent posts