https://github.com/hyun083/CS193p-Spring-2021/tree/main/Assignments/Assignment%202

 

Lecture3와 Lecture4를 시청하고 작성한 두 번째 programming Assignment. More Memorize라는 주제에 맞게 첫 번째 programming Assignment에 몇 가지 task를 추가한 결과물이다.


Required Tasks

  • 테마추가 (제목, 색상, 이모지, 카드쌍의 수)
  • 테마에서 사용가능한 이모지보다 적은 수의 쌍을 생성
  • 쓰이지 않는 이모지가 발생하면 안 된다.
  • 같은 이모지가 두 쌍 이상 생성되면 안 된다.
  • 테마는 최소 6가지 이상 사용해라.
  • 테마의 추가는 한 줄로 가능하다.
  • new game 버튼을 생성해라.
  • new game 생성마다 무작위 테마가 나와야 한다.
  • 게임에 사용되는 카드는 무작위 순서여야 한다.
  • UI에 테마의 이름이 나오게 할 것.
  • 점수를 표시하여 나타내라.

Extra Credit

  • 카드의 색상에 그라데이션 적용
  • 점수에 시간요소를 추가
  • 고정된 수의 카드쌍이 아닌, 매 순간의 랜덤 개수의 카드쌍을 생성해라.
  • 테마 생성 시 카드쌍의 수와 관계없이 초기형태의 모든 이모지 셋이 고정된 상태로 생성되어야 한다.

Lecture 4에서 실습한 코드를 기반으로 EmojiMemoryGame에서 대부분의 요구사항을 만족시킬 수 있었다.

6개의 고정된 이모지 배열과 테마를 표현하기 위한 [(title:String, color:Color, emoji:[String])] 배열을 생성하였다.

 

이후 CreateMemoryGame 함수를 통해 무작위 테마와 순서가 섞인 이모지를 통해 카드쌍을 생성을 통해 요구사항을 만족하였다.


고민이 많았던 요소인 점수, 테마 제목, 테마 색상은 Model을 담당하는 MemoryGame에 작성하였다.

페널티 요소를 다루기 위해 card 구조체에 Bool 타입의 alreadyBeenSeen 속성을 추가하였다.

 

Extra Credit을 위한 시간요소가 기억에 남는다. Date 타입의 timeSinceLastFlip 변수를 생성하여 마지막으로 카드를 뒤집은 시간을 저장한 뒤 max(10 - (카드 쌍을 확인하는 시간 - 마지막으로 카드를 뒤집은 시간), 1)을 보너스 점수로 계산한 뒤 틀렸다면 점수 차감, 쌍이 맞았다면 점수 획득을 통해 게임을 구성하였다.


view를 담당하는 ContentView에는 지난번 assignment 1에서 작성했던 widthThatBestFits() 함수를 재사용하였다. 카드의 수에 맞게 너비를 자동으로 조절해 준다. 

 

추가된 요소인 점수, 제목, 색상은 ViewModel 역할을 하는 EmojiMemoryGame 객체에서 가져오게 되며, New Game 버튼 또한 ViewModel의 CreateMemoryGame() 함수를 작동시켜 새로운 게임을 생성하게 된다.


result

 

[Lecture 4] Memorize Game Logic


mutating

구조체는 클래스와 마찬자기로 메서드를 가질 수 있는데, 자기 자신(self)의 변경이 일어나는 메서드 작성 시 mutating 키워드를 명시해주어야 한다.

 

Lectrue 3에서 구조체의 대표적인 특징인 copy on write이 있다고 했다. 구조체는 값타입(value type)으로 객체의 할당 혹은 인자로 넘겨줄 경우 값 복사가 일어나지만, 정확히는 사본과 원본의 차이점이 발생할 때 복사가 이루어진다고 했다. 여기서 사본생성의 시점 혹은 이후에 사본 생성 여부를 가늠하게 해주는 장치가 바로 mutating 키워드이다.

 

ObservableObject

 

ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com

swiftUI의 핵심기능 중 하나이다. 해당 프로토콜이 적용된 객체에 변경이 감지되면 구독하고 있는 Subscriber에게 변경을 알린다.

가장 많이 사용되는 부분이 MVVM구조의 ViewModel에 해당된다. View에서 ObservableObject를 구독(Subscribe) 하고 ViewModel에서 알리는 변경점을 통해 View의 body의 필요한 부분의 재빌드를 통해 데이터 바인딩이 일어난다.

ObjectWillChange

 

objectWillChange | Apple Developer Documentation

A publisher that emits before the object has changed.

developer.apple.com

ObservableObject를 채택하게 되면 우리는 ObjectWillChange라고 불리는 Publisher를 얻게 된다. 해당 속성을 통해 변경점을 알릴 수 있다.

send()

ObservableObject가 가지고 있는 속성 중 변경이 일어나면, ObjectWillChange.send()를 호출하여 변경사항을 subscriber에게 알리게 된다.

@Published

ObservableObject의 Publisher를 통해 변경점을 알리는 일을 자동으로 해주는 키워드이다. ObservableObject 내에 속성 중에서 변경점을 알리고 싶은 속성에 해당 키워드를 적용하게 되면 변경점이 일어날 경우 자동으로 ObjectWillChange.send()가 호출된다. 주로 ViewModel에 생성된 Model 객체에 사용하게 된다. 따라서 Model의 변경을 자동으로 감지할 수 있고, 이를 통해 Model과 View의 바인딩 작업을 손쉽게 할 수 있다.

@ObservedObject

 

ObservedObject | Apple Developer Documentation

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.

developer.apple.com

ObservableObject를 구독(subscribe) 받기 위해 사용하는 키워드이다. 생성된 객체에 해당 키워드를 적용하면 된다. 주로 View에서 ObservableObject를 채택한 ViewModel을 구독하기 위해 사용하게 된다. 이를 통해 ViewModel은 Model과 View의 바인딩이 가능해진다.

 

참고영상

https://www.youtube.com/watch?v=oWZOFSYS5GE&list=PLpGHT1n4-mAsxuRxVPv7kj4-dQYoC3VVu&index=5&t=4804s 

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

[Assignment2] More Memorize  (1) 2023.10.16
[Lecture 3] MVVM and the Swift type system - 2  (0) 2023.07.12
[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 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 3] MVVM and the Swift type system


MVVM

Model, View, ViewModel로 구성된 디자인 아키텍처이다. 

지금 배우고 있는 SwiftUIMVVM 디자인 아키텍처를 따르고 있다. 

 

디자인 아키텍처?

상호작용 방식을 명확하게 하는 디자인 패러다임. 간단하게 개발 방법 및 규칙을 정해놓은 것이라고 보면 된다.

 

Model

모델에는 데이터와 로직이 담겨있다. 즉 Memorize 앱에서는 카드 자체와 점수를 계산하는 로직들이 담긴다고 보면 된다.

 

View

사용자에게 모델을 보여주는 부분이다. 

 

ViewModel

모델과 뷰를 바인딩해주는 역할을 한다. 여기서 중요한 점은 뷰모델은 뷰의 존재를 몰라야 한다. 즉 모델의 자료변경이 일어나면 해당 부분에 대해서 공표하기(Publish)만 할 뿐이다. 뷰는 뷰모델의 알림을 감지한 뒤 해당 변경이 자신이 나타내는 정보에 해당하는 사항이라면 새로운 뷰를 그려내어 변경된 부분을 사용자에게 보여준다.

 

반대로 사용자의 의도에 의해 모델의 변경이 일어난다면, 뷰는 곧바로 모델에게 알리는 것이 아니다. 뷰모델이 알아챌 수 있는 함수를 호출하여 뷰모델에게 의도를 전달한다. 뒤이어 뷰모델은 모델에게 변경을 지시하여 모델의 변경이 완료된다. 즉 뷰는 직접적으로 모델에 접근하여 변경을 일으킬 수 없다.

 

전체적인 구조와 이때 사용되는 키워드는 다음과 같다. 각 키워드가 어떻게 동작하는지는 뒤에 이어질 수업에서 나올 예정인가 보다.

 

Structure and Class

구조체와 클래스는 하나의 주제로 묶인 자료의 모음이다.

기본적으로 변수와 상수, 그리고 함수를 정의할 수 있다는 점이 둘의 공통점이지만 둘은 어떤 방식으로 이용하느냐에 따라 큰 차이점이 존재한다.

 

Value type and Reference Type

가장 큰 차이점이 바로 구조체는 값 타입, 클래스는 참조 타입이라는 점이다. 

 

구조체는 할당 혹은 인자로서 넘겨주게 되면 그 자체의 값이 복사되어 사본이 넘어간다. 따라서 해당 부분을 값 타입이라 부른다. 물론 복사가 일어날 때마다 사본이 생성되는 것은 아니고, 사본의 변경이 일어나면 그때서야 복사가 일어난다고 한다. 해당 부분은 copy on wirte이라는 정책으로 불린다.

 

클래스는 할당 혹은 인자로서 넘겨주게 되면 해당 객체를 가리키는 포인터가 생성된다. 여기서 메모리의 힙 영역 관리를 위해 swiftARC 정책에 의해 포인터의 개수를 모니터링하다가 포인터의 증감에 따라 개수가 0이 되면 메모리 해제가 일어나게 된다.

 

Initializer

구조체와 클래스 모두 자동적으로 "무료" 생성자가 주어지게 되는데, 여기서 구조체는 각 인자를 초기화할 수 있는 반면 클래스는 그렇지 못하다고 한다.

 

mutable

구조체의 경우 속성 변경이 일어나는 함수를 작성 시 mutating이라는 키워드를 적는다. copy on wirte 정책으로 인해 해당 부분의 변경이 일어나는지 확인해야 하기 때문이다. 중요한 점은 객체 또한 var로 선언해야 변경이 가능하다.

struct A{
    var value = "a"
    mutating func changeValueTo(newVlaue:String){
        self.value = newVlaue
    }
}

var a = A()

print(a.value)
a.changeValueTo(newVlaue: "A")
print(a.value)

//출력 결과
//a
//A

 

반면 클래스의 경우 속성 자체가 변수로 선언되어 있다면, 객체가 let으로 선언이 되어도 변경이 되는 것을 볼 수 있다.

class B{
    var value = "b"
    func changeValueTo(newVlaue:String){
        self.value = newVlaue
    }
}

let b = B()

print(b.value)
b.changeValueTo(newVlaue: "B")
print(b.value)

//출력 결과
//b
//B

 

Usage

swift의 공식문서를 보면 클래스보단 구조체의 사용을 지양한다고 적혀있다. 여러 가지 이유가 있겠지만, 클래스의 무분별한 사용으로 인해 포인터끼리 참조되는 순환 참조로 인한 메모리 누수를 피하기 위함이 아닐까 생각한다. 

 

Lecture 3에서는 이에 대한 이야기는 언급하지 않았고, swift에서 마주하는 대다수가 구조체이며, 클래스는 ViewModel의 구현에 사용된다고 하였다. Model에 저장된 Data의 바인딩이 주목적이므로 자료의 공유가 빈번하게 일어나기 때문에 포인터를 넘기는 것이 더 효율적이기 때문이라고 생각한다.

 

참고 영상

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

 

 

GitHub - hyun083/CS193p-Spring-2021

Contribute to hyun083/CS193p-Spring-2021 development by creating an account on GitHub.

github.com


Lecture 1Lecture 2를 시청하고 주어진 조건에 맞게 Memorize 앱을 작성하면 된다. 열 가지 Required Tasks와  두 가지 Extra Credit이 주어진다.

 

정확한 조건은 CS193p 홈페이지에 올라와있다.


Required Tasks

주 내용은 세 개의 테마 버튼을 생성하여 테마 버튼을 누를 때마다 해당 테마의 이모지로 카드를 바꾸는 것이다.

여기서 포인트는 이모지의 순서를 랜덤 하게 배치하는 것.

 

따라서 3개의 이모지 배열을 생성. 버튼을 누를 때마다 @State 선언된 emojis 배열을 해당 배열로 교체하는 것으로 구성했다.

코드의 재사용을 위해 함수를 작성하였다.

@State var emojis = vehicles
@State var emojiCount = Int.random(in: 4..<vehicles.count)
    
func replaceCard(to arr:[String]){
    emojis = arr.shuffled()
    emojiCount = Int.random(in: 4..<arr.count)
}

함수가 수행될 때마다 emojis 배열이 변경이 되고, 변경이 될 때마다 swift는 교체된 배열을 가지고 새로운 View를 그리게 된다.

여기서 Tasks에 주어진 랜덤 한 순서의 이모지는 Array에 정의되어 있는 shuffled() 함수를 통해 만족하였다.

emojiCount에서 Int.random(in: ) 메서드를 사용했는데, 이 부분은 Extra Credit 부분에서 설명하겠다.

 

HStack{
    Button(action: { replaceCard(to: vehicles) }) {
        Theme(title: "Vehicles", symbol: "car")
    }
	
    Button(action: { replaceCard(to: flowers) }) {
        Theme(title: "Flowers", symbol: "camera.macro")
    }
	
    Button(action: { replaceCard(to: animals) }) {
        Theme(title: "Animals", symbol: "pawprint")
    }
}

struct Theme: View{
    let title: String
    let symbol: String
    
    var body: some View{
        VStack{
            Image(systemName: symbol)
                .font(.largeTitle)
            Text(title)
        }
        .font(.title3)
    }
}

다음으로 작성한 테마 View 구조체. 처음엔 3가지 버튼을 따로 작성하였다가 코드가 지저분해져서 Theme 구조체를 따로 만들었다.

title에는 화면에 보일 테마 이름, symbol은 화면에 보일 테마 이미지를 SF Symbol에 해당하는 이름을 입력받았다.

 


Extra Credit

첫 번째 조건은 테마선택 시 카드의 개수를 랜덤으로 배치하는 것이다.

이때 Int 타입에 정의된 Random(in: ) 타입 메서드를 사용하라고 명시되어 있다. 

random(in:)

Returns a random value within the specified range.

범위를 입력받는 인자를 통해서 명시된 범위 내의 랜덤한 Int를 반환하게 된다.

명시된 조건은 최소 4개의 카드를 보이는 것이기에 4.. <배열크기를 넘겨주었다.

 

두 번째 조건은 바로 화면 크기에 맞추어 카드의 크기를 결정하는 것.

중요한 점은 모든 카드가 배치되었을 때 화면 범위를 벗어나지 않게 끔 적절한 카드 크기를 찾아내는 것이다. (화면이 portrait 상태)

 

여기서 이번 Memorize 앱을 작성하는 데에 가장 긴 시간을 고민했다.

Lecture 2에서 작성된 코드는 LazyVgrid를 통해 고정된 크기의 카드를 나열하였다. 여기서 카드의 수량이 추가되면 카드목록이 화면에 한 번에 잘릴 수 있으니 스크롤 뷰를 통해 카드목록을 탐색할 수 있게 구현하였다.

이번에는 카드의 수량에 맞춰 카드의 크기를 조절해 스크롤 없이 카드목록을 볼 수 있게끔 카드의 크기를 계산하는  widthThatBestFits(cardCount: Int) -> CGFloat 함수를 작성하는 것이 두 번째 조건이었다. 

 

기기가 portrait(세로 방향) 상태일 경우에만 맞추면 된다고 명시되어 있길래 여기서 큰 힌트를 얻었다.

 

카드의 비율은 2:3, 여태 출시된 모든 아이폰의 화면 비율은 카드의 비율보다 세로비율이 더 길게 되어있다. 즉 상단의 제목과 하단의 테마버튼 영역을 제외하고서라도 카드를 정방 배열 형태(2*2, 3*3, 4*4)로 그리드 뷰를 구성한다면 카드와 동일한 2:3의 비율의 그리드뷰를 얻을 수 있고, 해당 뷰를 화면에 넘치지 않게끔 배치가 가능할 것이라 생각하였다.

 

따라서 LazyVgird의 너비를 랜덤 하게 주어지는 (카드목록의 수의 제곱근)으로 나눈 값을 LazyVgird에 들어갈 GridItem의 최소 크기로 지정한다면 SwiftUI는 정방배열의 LazyVgird를 그려내게 된다.

 

func widthThatBestFits(for width: CGFloat, cardCount: Int) -> CGFloat{
    return width/ceil(sqrt(CGFloat(cardCount)))-10
}

따라서 특정 너비와 카드의 수를 입력받아 (카드 수의 제곱근)의 반올림 값으로 특정 너비를 나누는 widthThatBestFits 함수를 구현하였다. 저기서 10을 빼준 것은 카드 간의 간격의 여유를 위해 뺀 것이다.

 

이어서 LazyVgrid의 너비값을 어떻게 가져와야 할지 검색을 해본 결과 GeometryReader라는 Container View를 찾게 되었다.

GeometryReader

A container view that defines its content as a function of its own size and coordinate space.
GeometryReader{ geometry in
	ScrollView{
		let width = geometry.size.width
	}
}

하위뷰의 좌표 정보와 크기 정보를 가져오는 Container View이며 예시와 같은 형태로 size에 정의된 widthheight 속성을 통해 하위 뷰의 너비와 높이에 접근할 수 있다.

 

GeometryReader{ geometry in
	ScrollView{
		let width = widthThatBestFits(for: geometry.size.width, cardCount: emojiCount)
		LazyVGrid(columns: [GridItem(.adaptive(minimum: width))], spacing: 7){
			ForEach(emojis[0..<emojiCount], id: \.self) { emoji in
				CardView(content:emoji)
					.aspectRatio(2/3, contentMode: .fit)
			}
		}
	}
	.foregroundColor(.red)
}

따라서 다음과 같이 GridItem의 최소 크기를 지정하였고, 예상대로 정방 형태의 그리드 뷰를 생성함으로써 portrait 상태에서는 어떠한 기기이든 스크롤 없이 주어진 카드의 전체를 확인할 수 있게 되었다.

 


이렇게 작성된 ContentView.swift 파일의 전체 코드는 다음과 같다.

 

 

[Lecture 2] Learning more about SwiftUI


property initialrize

swift의 변수 및 상수는 초기화 없이 선언만 이루어진 경우, 사용할 수 없다. 따라서 객체 생성 시, 초기값이 주어지지 않은 속성의 경우 반드시 생성자의 인자를 통해 초기값을 할당해주어야 한다.

 

초기값 vs 생성자

그렇다면 초기값이 있는 속성의 경우 생성자를 통해 값을 넘겨주면 어떻게 될까?

당연하게도 생성자로 넘겨준 값으로 할당이 된다. 

 

onTapGesture()

func onTapGesture(
    count: Int = 1,
    perform action: @escaping () -> Void
) -> some View

View의 탭 제스처 감지 함수이다. action에 동작할 클로저를 작성하고, count의 경우 action이 수행될 탭 횟수를 적으면 된다. 기본값은 1이다.

 

@State

A property wrapper type that can read and write a value managed by SwiftUI.

한번 생성된 View는 수정이 불가하다. View가 나타내는 정보를 수정하고 싶을 때는 수정된 정보를 가지고 있는 View를 새로 생성하여 대체하는 것이다. 이때 사용하는 키워드가 @State이다. 변수를 가리키는 포인터로 동작하며, 해당 포인터가 가리키는 변수 값에 변동이 감지된다면, 해당 변수를 참조 중인 View는 새로 그려지게 된다. rxSwift에서 Observable의 역할을 수행하는 키워드이다. 

 

Button

Creates a button that displays a custom label.
init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)

버튼으로 동작하는 View를 생성. actionlabel 두 개의 클로저를 입력받으며,

action에는 버튼이 탭 될 경우 수행될 동작을 정의한다.

labelViewBuilder로 동작하여 View를 나열하는 것으로 버튼의 외형을 정의하면 된다.

 

ForEach

A structure that computes views on demand from an underlying collection of identified data.
struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable

View 구성시 for문을 사용할 수 없다. 따라서 Collection 타입에 담겨있는 자료들을 표현하기 위해 ForEach를 사용한다.

ForEach()는 자료가 담겨있는 Data, 식별자 역할을 하는 ID, ViewBuilder로 동작하는 Content로 구성된다.

 

순서대로 가져오고 싶은 Collection 타입의 데이터와 해당 자료를 어떤 기준으로 식별할지 나타내는 ID를 지정, 이후 반환될 View를 구성해 주면 된다. Content의 경우 CombinerView와 마찬가지로 ViewBuilder 형태로 동작하기에 단순히 View를 나열하는 것만으로 View를 구성할 수 있게 된다.

 

LazyVgrid

A container view that arranges its child views in a grid that grows vertically, creating items only as needed.
init(
    columns: [GridItem],
    alignment: HorizontalAlignment = .center,
    spacing: CGFloat? = nil,
    pinnedViews: PinnedScrollableViews = .init(), @ViewBuilder content: () -> Content
)

수직형태의 그리드 뷰를 생성할 때 사용하는 View이다. 마찬가지로 수평형태의 그리드 뷰는 LazyHgrid를 통해 만들 수 있다.

columns는 한 열 안에 배치할 View의 간격 및 크기를 지정. 

alignment는 배치될 View 간의 정렬 지정.

spacing의 경우 각 열간의 수직 간격을 지정. 

pinnedViews는 그리드의 섹션별 헤더와 풋터를 지정하는 부분.

contentViewbuilder로써 각 그리드의 요소를 어떤 방식으로 표현할지 정하는 부분이다. 

 

ForEach를 통해 데이터의 묶음을 표현하기 유리한 View이다.

 

SF Symbol

 

SF Symbols - Apple Developer

SF Symbols는 Apple 플랫폼의 시스템 서체인 San Francisco와 매끄럽게 통합되도록 디자인된 아이콘 라이브러리로 약 4,500개의 기호를 포함하고 있습니다.

developer.apple.com

SwiftUI에 관련된 내용은 아니지만 이번 강의를 통해 알게 된 SF Symbol이 인상 깊었다.

apple 플랫폼의 서체와 잘 어울리는 아이콘 라이브러리로써 원하는 아이콘을 찾고, 별도의 저장과정 없이 이름을 통해서 불러낼 수 있다.

 

참고 영상

https://youtu.be/3lahkdHEhW8

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

[Lecture 3] MVVM and the Swift type system - 1  (0) 2023.07.08
[Assignment 1] Memorize  (0) 2023.07.06
[Lecture 1] Getting Started with SwiftUI - 2  (0) 2023.06.22
[Lecture 1] Getting Started with SwiftUI - 1  (0) 2023.06.16
day06_multiTouch  (0) 2022.03.26

[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

Lecture 1: Getting started with SwiftUI


iOS 관련 유명한 강의인 스탠포드 대학의 cs193p Developing Applicaitons for iOS 강의다.

해당 강의는 누구나 스탠포드 유튜브 채널에서 시청이 가능하다.

기존의 2017버전 강의에서는 실시간 강의영상이었다면 이번에는 코로나 팬데믹으로 인한 온라인 강의 영상이므로 교수님이 작성하는 코드나 제공되는 시청자료들을 더 좋은 환경에서 볼 수 있다.

 

기존 2017버전에서는 UIKit 프레임워크를 이용한 MVC 패턴의 앱개발이 주 내용이었다면, 2021 버전에서는 이제 현업에서 적극적으로 사용되는 MVVM 패턴과 이에 맞게 애플에서 개발한 swiftUI 프레임워크를 이용한 앱개발이 주 내용이 될 것이다.

 

지난번 2017강의를 들으면서 내용들을 전부 옮겨 적으려 해서 학습에도, 기록에도 힘이 들었기에 이번에는 강의 내용을 모두 옮겨적는다기보단 내가 보았을 때 흥미로운 내용 위주로, 하나의 강의 내에 여러 흐름으로 나누어서 작성하는 것을 통해 빠르게 학습하고 빠르게 기록하는 것에 초점을 두고 글을 적으려고 한다.


이번 첫강의에서는 swiftUI로 작성된 프로젝트를 전반적으로 훑어보면서 swiftUI 프레임워크의 구성을 간략하게 알아보았다.

 

swiftUI project File Hierarchy

가장 먼저 swiftUI로 프로젝트를 생성하게 되면 다음과 같은 파일이 자동으로 생성된다.

(프로젝트명)App.swift 파일과 ContetnView.swift 파일이 이번 swiftUI 프레임워크의 주요 파일이다.

 

import SwiftUI

@main
struct MemoriesApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}​

 

App.swift 파일은 다음과 같이 작성이 되어있으며, 주의깊게 봐야할 부분이 @main 어노테이션이다.

해당 어노테이션은 UIKit으로 생성한 프로젝트에서 AppDelegate 파일에 있던 어노테이션으로 앱의 진입지점(entry point)를 나타낸다. 즉 swiftUI의 진입지점(entry point)은 App.swift 파일이며 해당 App 객체의 생성으로 앱의 실행으로 이어진다는 얘기이다. 

 

다음으로 주목해야 할 부분은 App 객체의 body 부분에 할당된 ContentView() 객체이다.

ContentView() 객체를 생성하는 것을 볼 수 있고, 해당 객체는 ContentView.swift 파일에 선언되있는 것을 확인할 수 있다.

 

 

ContentView.swift 파일을 살펴보게면 xcode에서 3개의 영역를 제공한다.

  1. 코드를 작성하는 textEditor 영역
  2. 앱의 UI가 어떻게 보이게 될지 미리 보여주는 preView 영역
  3. 사용자에게 보여질 UI를 구성하는 각 개별의 View를 설정 할 수 있는 inspector 영역

흥미로운 점은 이 3개의 영역이 실시간으로 연동이 된다는 점이다. 강의에서는 sync 되어있다는 표현을 사용했는데 즉, 사용자가 마주하는 Text, Image와 같은 UI뷰의 설정을 3개의 영역 모두에서 접근이 가능하며 해당 수정사항이 실시간으로 일어난다는 것.

 

 

기존의 UIKit 프레임워크의 XML 형태와 달리 xcode가 코드로 짜여진 UI를 실시간으로 처리하는 것처럼 와닿았다. 

 

해당 부분이 반가운 점은 UIKit을 사용할 때에는 기존의 UI를 IBAction, IBOutlet을 통해 연결했는데, 가끔씩 라인을 이동한다던지, 이름을 변경하게 되었을 때 싱크가 끊어지는 경우가 종종 생긴다는 점이었다.

 

애플도 해당 문제점을 정확히 파악하고 있었을 것이고 확실한 것은 아니지만 SwiftUI를 통해 해당 문제점을 해결하기 위래 노력을 많이 한 것으로 느껴졌다.

 

import SwiftUI

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

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

ContentView.swift의 내용을 살펴보면 View 구조체와 PreViewProvider 구조체가 보인다.

PreViewProvider의 경우 이름에서 알 수 있듯이 UI의 실시간 반영을 위한 preView를 위한 코드부분이다. 

 

결국 개발자가 UI를 구성하는 코드를 작성하는 곳은 ContentView라는 이름의 View 구조체인 것이다.

 

참고 영상

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

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

[Lecture 2] Learning more about SwiftUI  (0) 2023.07.05
[Lecture 1] Getting Started with SwiftUI - 2  (0) 2023.06.22
day06_multiTouch  (0) 2022.03.26
day05_view  (0) 2021.12.30
day04_swift_part2  (0) 2021.10.25

+ Recent posts