에러라기보단, 기술을 이해 없이 사용하여 마주한 문제 해결 정도로 봐야겠다.


문제 정의

비동기 함수를 통한 동작으로 UI반영이 안된다.

 

위치정보를 담당하는 locationManager 객체가 @Published로 정의되어 있다.

따라서 locationManager의 값이 변경이 되면 swiftUI는 자동으로 화면을 새로 그려낸다.

따라서 나는 ViewModel에서 locationManager의 속성 변경을 통해 UI를 갱신하도록 하였다.

 

locationManager 객체에는 사용자의 위치를 갱신하는 함수 locationManager(_, didUpdateLocations) 함수가 있다.

lastLocation 속성을 변경하는 동작이다. 해당 함수는 delegate 패턴으로 작성되며, 개발자는 해당 함수를 직접 호출할 수 없다.

그러면 어떻게 사용하느냐?

 

locationManager객체의 startUpdatingLocation() 함수를 통한 대리 호출만 가능하다.

해당 함수는 ViewModel에 정의하여, 여기서 호출이 일어나면 locationManager의 속성이 바뀌게 되고,

이를 SwiftUI가 자동으로 UI에 반영해 준다.

 

그리고 locationManager에는 사용자의 위치를 기반으로 주소값을 갱신하는 함수가 있다.

lastLocation을 기준으로 주소를 가져오게 된다.

 

이때 든 생각

어차피 따로 쓸 일 없는데, 호출 한 번으로 사용자 위치 갱신할 때 한 번에 주소도 갱신하면 안 됨?

 

 

내가 의도한 순서:

위치 갱신 -> 갱신한 위도, 경도를 기준으로 주소 갱신 -> 변경된 속성을 SwiftUI가 감지하여 UI 갱신

 

이런 단순한 생각으로 ViewModel에 다음과 같이 작성하였으나,

사용자 위치만 갱신될 뿐 정작 중요한 주소 레이블이 UI에 반영이 되지 않았다.

 

실제 동작

 

열심히 함수에 print()를 찍어 동작이 어떻게 이루어지다 파악해 보니 내가 작성한 것은 다음과 같이 동작했다.

delegate 패턴으로 작성된 locationManager(_, didUpdateLocation) 함수로 넘어가기 전에 비동기로 주소갱신이 이루어진다.

 

당연하게도 갱신이 되지 않은 위치로 주소를 가져오니 이전 주소가 나오고,

위도, 경도 값이 경신된 이후 함수가 종료된다.

locationManager의 대리호출과 비동기 호출의 조합으로 동작의 순서가 엉망이 돼버린 것.

 


문제 해결

짧지 않은 기간 동안의 삽질과 아직 해결되지 않은 의문점이 남아있지만 아무튼 지금은 의도된 동작대로 작동한다.

 

 

일단 위치갱신과 주소갱신을 한 번에 하겠다는 생각을 버렸다.

따라서 주소갱신함수는 locationManager의 속성을 바꾸는 것이 아닌, Sting을 반환하는 비동기 함수로 변경하였다.

* 비동기로 굳이 작성하는 이유는 주소를 반환하는 reverseDeocoderLocation() 함수가 비동기로 작성되었기 때문.

 

이어서 locationManager에 있던 주소 레이블 속성을 viewModel로 가져왔다.

이렇게 각 함수를 ViewModel에서 따로 호출할 수 있게 변경하였다.

 

이어서 View에서는 onChange를 통해 위도, 경도의 갱신이 완료되면, 주소를 가져오게 하여 동작의 순서를 정해주었다.


정리

동작을 분리한 이유는 다음 한 줄로 정리할 수 있다.

 

UI의 반영은 메인 스레드에서만 이루어진다.

 

ViewModel을 담당하는 FitcastManager@MainActor로 선언된 이유가 바로 여기서 작동하는 동작들은 모두 메인스레드에서 이루어지게 하기 위함이다.

 

 

따라서 기존의 방식대로 주소 갱신이 비동기로 동작하게 된다면 메인스레드가 아닌 새로 생성된 스레드에서 작동하게 되는 것이고, 여기서 이루어진 갱신은 UI에 반영될 수 없다고 판단하였다.

 

따라서 UI에 반영할 주소값의 갱신은 @MainActor로 선언된 ViewModel의 속성으로 두어 비동기로 작동하더라도 메인스레드에서 동작하게끔 수정해 주었다. 

 

사실 해당 문제를 해결하기까지 아직 의문점이 남은 부분이 많이 있다. 아직 aync/await의 사용법과 MainActor와 관련된 이해도가 떨어져서 그렇다고 생각한다. 해당 개념에 대해 쉽게 설명한 글이 많으므로 나중에 완벽하게 이해된다면 다시 한번 리팩토링을 통해 후기로 돌아와야겠다.

 

 

 

 

[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

[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