이번 시간에는 Swift Programming Language 에서 제공하는 Type System 에 대해 탐구해 볼 것이다. Swift 의 토대를 이루고 있는 6가지 타입들을 이해하지 못하면 결국 Swift 또한 이해하지 못하기 때문이다.
struct
ContentView, CardView와 같은 UI, Memorize 게임에서의 Model이 struct의 한 예시이다.
class
Object-Oriented Programming 을 구현할 수 있으며 MVVM에서의 View Model이 한 예시이다.
struct와 class의 공통점
문법이 비슷하다. 예를들어 구조체 내부에 함수나 변수를 선언할 때에는 다음과 같은 동일한 구문을 통해 구현된다.
var isFaceUp: Bool // 일반적인 변수 선언의 예
var body: some View {
return Text("Hello World") // 계산의 결과를 할당하는 변수 선언의 예
}
func multiply(operand: Int, by: Int) -> Int {
return operand * by // 함수 선언의 예, 특이하게도 인자에 라벨이 붙는다
}
multiply(operand: 5, by: 6)
func multiply(_ operand: Int, by: Int) -> Int {
return operand * by // 함수 선언의 예, 외부와 내부의 라벨을 구분지어 저장할 수 있다
}
multiply(5, by: 6)
구조체의 내부값을 초기화 시켜주는 특별한 함수인 initializer 를 가질 수 있다. init 은 새로운 struct나 class가 정의될 떄 호출되는 함수이다.
struct RoundedRectangle {
init(cornerRadius: CGFloat) {
// initialize this rectangle with that cornerRadius
}
init(cornerSize: CGSize) {
// initialize this rectangle with that cornerSize
}
}
struct와 class의 차이점 (중요하다!)
struct | class |
Value type Copied when passed or assigned Copy on write Functional programming No inheritance "Free" init initializes ALL vars Mutability must be explicitly stated Your "go to" data structure Everything you've seen so far is a struct (except View which is a protocol) |
Reference type Passed around via pointers (Heap) Automatically reference counted Object-oriented programming Inheritance (single) "Free" init initializes NO vars Always mutable Used in specific circumstances The ViewModel in MVVM is always a class (also UIKit (old style iOS) is class-based) |
protocol
View 가 some View에 해당되거나 View 처럼 동작하게 하는 그 View가 바로 Protocol의 한 예시이다.
generics ("Dont' Care" Type)
타입에 대하여 신경쓰지 않도록 도와준다. Array 가 대표적인 generics의 한 예시이다. Array 는 Swift가 제공하는 기본 자료형 뿐만 아니라 사용자 지정 자료형도 자유롭게 담을 수 있다.
Array 의 실제 구현 코드를 살펴보면 Type 을 상징하는 <Element>변수를 포함하고 있는데 이 변수에 담기는 Type에 따라 Array가 선언될 수 있도록 해준다. Element에는 어떠한 변수가 들어가도 Array는 상관하지 않는다. Element 와 같은 역할을 하는 인자들을 Type Parameter 라고 부른다. generic과 protocol 이 함께 구현되면 엄청난 시너지 효과를 발휘한다고 한다.
struct Array<Element> {
...
func append(_ element: Element) { ... }
}
// Example
var a = Array<int>()
a.append(5)
a.append(22)
enum
Enumeration 그 자체로 Swift에서 매우 강력하다.
function
함수는 Swift에서 First Class Type이다. 함수형 프로그래밍을 지원하는 기본 사항에 해당된다.
// 함수는 모두 타입에 해당된다.
(Int, Int) -> Bool // takes two Ints and returns a Bool
(Double) -> Void // taked a Double and returns nothing
() -> Array<String> // takes no arguments and returns an Array of Strings
() -> Void // takes no arguments and returns nothing (Common case)
var foo: (Double) -> Void // 변수의 타입으로 함수 사용하여 정의할 수 있다.
func doSomething(what: () -> Bool) // 함수를 인자로 넘길 수 있다. 함수형 프로그래밍 필수 방식이다.
함수를 정의하고 사용하는 몇가지 방법에 대하여 살펴보도록 한다.
// Double 을 받아서 Double 을 return 하는 함수 타입 정의
var operation: (Double) -> Double
// 일반적으로 아는 함수의 정의
func square(operand: Double) -> Double {
return operand * operand
}
// 함수 타입의 변수에 함수를 담고 사용할 수 있다.
operation = square
let result1 = operation(4)
// 함수 타입의 면수에 다른 함수도 담고 사용할 수 있다.
operation = sqrt
let result2 = operation(4)
Swift UI 를 통해 함수형 프로그래밍 기법을 사용하여 UI 를 구현했을 때 위와 같은 방식으로 함수를 정의 하는 것 뿐만 아니라 중괄호 '{}' 를 사용하여 inline 형태로 함수를 구현하는 경우가 많이 있었다. 이를 Closure 라고 부른다.
Upgrade Memorize Prototype Code
이전에 작성한 Memorize 게임의 UI Prototype 을 실제 MVVM 패턴에 알맞게 동작하도록 개선해본다. 우선 새로운 Swift File 을 생성하여 Model 코드를 작성해보자.
struct MemoryGame<CardContent> {
var cards: Array<Card>
func choose(_ card: Card) {
}
struct Card {
var isFaceUp: Bool
var isMatched: Bool
var content: CardContent
}
}
처음부터 기이한 구조가 나왔다. 구조체 안쪽에 구조체를 정의함으로써 얻는 이점이 뭐가 있을까? 바로 Namespace 를 지정할 수 있게 되기 떄문이다. 예를들어 하나의 앱에 MemoryGame 뿐만 아니라 포커와 같은 다른 카드게임이 존재한다면 Card 구조체가 어떤 게임에서 사용되는 것인지 구분짓기가 힘들것이다. 그러나 위와 같이 코딩하면 "MemoryGame.Card" 식으로 접근할 수 있게되어서 누구나 쉽게 구분할 수 있게 된다.
UI 프로토타입핑을 진행하였을 때 카드 UI 의 콘텐츠로 이모티콘을 담으면서 콘텐츠 타입을 String 으로 사용했었다. 사실 생각해보면 이미지와 같은 다른 데이터 타입이 들어가서 카드를 표현할 수도 있으며 우리가 Model 코드를 구성할 때에는 "어떻게" 사용될 지 알 수가 없다.
따라서 generics (dont care type) 을 활용하여 카드 구조체의 content 를 정의하게 되었으며 위와 같은 방식으로 사용하면 된다.
이번엔 ViewModel 코드를 잠시 작성해보자
VIewModel은 Model과 View 사이의 중개자 였으며 반드시 Model과 연결되어있어야 한다. 또한 디스크나 네트워크와 같은 것들에 항목을 유지하도록 돕는 일도 수행한다.
ViewModel의 역할 중에는 Model에서 View로 이어지는 GateKeeper 역할을 한다는 점이다. 예를들면 악의적인 행위를 하는 View 나 다른 프로그래머로 부터 Model을 보호하는 것이 해당될 수 있을 것이다. 해결책은 class 의 접근제어자(Private) 를 활용해보는 방법이 있다. private 은 너무나도 제한적이어서 읽고 쓰는 것 둘다 제한시켜버리기 때문에 Swift 에서는 private(set) 을 제공하여 읽을 수는 있지만 쓰지는 못하도록 제한하는 방법을 제공하고 있다.
class EmojiMemoryGame {
private var model: MemoryGame<String>
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
위와 같은 방법도 있을 수는 있다. cards 변수는 함수에 의해 계산된 결과를 담고 있기 때문에 오직 읽어오는 기능만 가능하기 때문이다. 이와 같이 구현하면 추후에 VIew가 카드 배열을 가져와야할 때 Model 이 아닌 ViewModel에서 Card를 가져오게 될 것이다. 하지만 약간의 문제점이 존재한다. Model의 카드와 카드를 담고 있는 배열은 모두 struct type으로 정의된 것이어서 다른 어딘가로 구조체를 전달 할 때 "복사" 가 발생된다. 다시 말해 View에서 카드를 불러올 때마다 매번 복사가 발생되기 때문에 메모리나 처리 성능에 있어서도 비효율적이다. => Copy on write 라는 최적화 개념이 있어서 수정되기 전까진 같은 주소값을 참조하고 있다!
이제것 구조체와 클래스에 각종 변수를 선언했지만 값을 할당하진 안았다. 이전에 SwiftUI를 배웠을 때 모든 변수에는 반드시 값이 할당되어있어야 한다고 했기 때문에 생성자를 정의하여 값을 초기화해줄 수 있도록 만들어줄 필요가 있다. Swift 에서는 initializer 를 선언할 수 있도록 생성자 함수 키워드 "init" 을 제공해주고 있다.
struct MemoryGame<CardContent> {
private(set) var cards: Array<Card>
func choose(_ card: Card) {
}
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))
cards.append(Card(content: content))
}
}
struct Card {
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
}
ViewModel에서 MemoryGame을 활용하여 model 을 초기화 할 때 init을 활용하게 된다. 이 때 모델 입장에서는 상대가 몇개의 카드를 필요로하며 카드에 어떤 내용이 들어가야 할지 먼저 알고 코드로 구현하기 어렵기 때문에 생성자의 인자로 생성할 카드의 개수와 카드 내용물을 생성해주는 함수를 받도록 구현되어있다. 특히, 함수의 인자로 함수를 받는 코드는 함수형 프로그래밍의 시작과도 같을 수 있기에 주의깊게 살펴볼 필요가 있다.
이번에 다시 ViewModel로 돌아가서 Model 객체를 초기에 생성하는 부분에 집중해보자!
class EmojiMemoryGame {
static let emojis = ["🥇", "🥈", "🥉", "🎖", "✈️", "🚀", "🧸", "🪄", "⏰", "📱", "⌚️", "💻", "🖥", "⚽️", "🏀", "🏈", "🥎", "⚾️", "🏓", "🤿", "🥊", "🎗", "🚁", "🚦"]
private var model: MemoryGame<String> =
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
ViewModel에서 Model 의 생성자를 불러와서 초기화하고 있다. 이때 카드의 컨텐츠를 채워줄 함수는 Swift UI 에서 사용했던 방식인 인라인 함수 형태로 표현하였다. 상단에 emoji 배열이 담긴 attribute 를 보면 "static" 이라는 키워드가 사용된 것을 확인할 수 있다. static 은 대표적으로 두가지 역할을 하는데 우선 emoji 배열을 전역 변수처럼 사용할 수 있게하면서 EmojiMemoryGame 이라는 namespace에 가둠으로써 전역 변수의 문제점을 상쇄시키는 역할을 한다. 실제 사용 예시는 다음과 같다.
EmojiMemoryGame.emojis[pairIndex]
static은 Attribute 뿐만 아니라 함수에도 사용될 수 있다.
class EmojiMemoryGame {
// type property
static let emojis = ["🥇", "🥈", "🥉", "🎖", "✈️", "🚀", "🧸", "🪄", "⏰", "📱", "⌚️", "💻", "🖥", "⚽️", "🏀", "🏈", "🥎", "⚾️", "🏓", "🤿", "🥊", "🎗", "🚁", "🚦"]
// type function
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
위 예시에서 createMemoryGame() 을 전역변수로 선언해두었다면 다른 프로그래머가 다음과 같은 의문을 품을 수 있다.
도대체 이 함수는 누가 무슨 목적으로 쓸라고 만들어둔거야? 알수가 없네!!!
마찬가지로 EmojiMemoryGame 의 namespace 안에 이 함수를 종속시킴으로써 위와 같은 의문을 해결할 수 있게 되는 것이다. 지금까지 확인해온 static 변수나 함수를 type property, type function 이라고 부르게 되는데 이유는 간단하다.
EmojiMemoryGame에 종속된 함수 혹은 변수임을 강조하기 위해서라고 한다. 인스턴스가 아니라 타입 자체가 갖고 있는 놈들이다!
다음시간에는 UI 와 ViewModel, Model 을 연동해보도록 하죠! 오늘은 여기까지~
' Apple > Stanford iOS Programming (SwiftUI)' 카테고리의 다른 글
Lecture 4 Review Part2: Memorize Game Logic (0) | 2021.08.20 |
---|---|
Lecture 4 Review Part1: Memorize Game Logic (0) | 2021.08.19 |
Lecture 3 Review Part 1: MVVM and the Swift type system (0) | 2021.08.13 |
Lecture 2 Review Part 2: Learning more about SwiftUI (0) | 2021.08.09 |
Lecture 2 Review Part 1: Learning more about SwiftUI (0) | 2021.08.07 |