컴바인을 활용해서 실무에 좀 더 유용한 내용을 알아봅시다. Combine을 활용하는 가장 직접적인 예는 아마도 이 글에서 살펴볼 Swift 5.1부터 제공되는 Property Wrapper와 이를 이용해 만들어진 SwiftUI의 몇 가지 기능이 이에 해당할지도 모르겠습니다.
이 글에서 살펴볼 것은 네 가지 키워드입니다. 바로 State, Binding, Observed Object, 그리고 Environment Object입니다. 이 키워드들은 Combine을 이용해 구현한 Property Wrapper를 응용해서 만들어진 것들이지요.
이 글은 Swift 5.1, Xcode 11.3을 기준으로 작성하였습니다.
State
State는 이름처럼 상태 그 자체를 나타냅니다. 영어로는 Source of Truth라고도 하기는 하던데... 뭐 하여간 상태가 변하면 UI도 이에 맞게 변해야겠지요. 실제로 사용되는 키워드는 @State
입니다. 아래 예제를 봅시다.
struct ContentView: View {
@State private var name = "World"
var body: some View {
VStack {
Text("Hello, \(name)!")
.padding()
Button(
action: { self.switchName() },
label: { Text("Switch") }
)
}
}
func switchName() {
if name == "World" {
name = "Universe"
} else {
name = "World"
}
}
}
위 예제는 버튼을 누를 때 마다 name
프로퍼티의 값을 바꾸는데 이에 따라 뷰도 자동으로 내용이 업데이트되는 것을 확인할 수 있습니다. State로 명시된 프로퍼티인 name
과 UI가 아주 긴밀하게 움직이는 모습을 볼 수 있지요. Combine 식으로 표현하자면, State로 표기된 퍼블리셔(Publisher)의 변화에 따라 서브스크라이버(Subscriber)인 View가 그 변화에 대응하고 있는 것입니다.
@State
는 해당 View 내에서만 액세스 가능합니다. 따라서 @State
로 적용할 프로퍼티는 가급적 private
로 선언하는 것이 좋습니다. 다른 말로 표현하자면, State는 결국 특정 View의 특징 혹은 소유물이라는 의미로도 해석할 수 있습니다. 그래서 해당 프로퍼티를 소유하고 있는 View는 마음대로 액세스 할 수 있다면 외부의 다른 View는 액세스 하지 못 하거나 혹은 읽기만 가능해지는 디자인이 만들어집니다.
Binding
State는 특정 View 소유의 프로퍼티이자 상태를 명시할 때 사용합니다. 그렇다면 이와는 다르게 남의 View의 State를 가져와서 자기 자신이 사용하게 되는 경우도 있을 수 있습니다. Binding은 이렇게 남의 소유의 State를 자신과 묶어줄 때 사용합니다.
Binding은 두 가지 형태로 표현됩니다. 하나는 $
마크로 자신의 State를 남에게 연결시켜주는 경우, 그리고 남의 State를 연결하기 위해 @Binding
을 사용하는 경우입니다.
struct MyToggleButton: View {
@Binding var value: Bool
var body: some View {
Button(action: {
self.value.toggle()
}, label: {
Text(self.value ? "Hello" : "World")
})
}
}
struct ContentView: View {
@State private var value = false
var body: some View {
VStack {
MyToggleButton(value: $value)
}
}
}
위 예제도 버튼을 누를 때마다 값이 토글 되는 버튼을 표시하는 예제입니다. 다만 차이가 있다면 해당 값을 자신이 아닌 다른 View가 바꾼다는 것이지요. 즉 ContentView
소유의 value
프로퍼티를 $
마크를 붙여서 연결하도록 MyToggleButton
에 넘겨주었고, MyToggleButton
이 이 연결을 받아서 값을 마음대로 바꾸고 있습니다.
결과적으로 State의 소유권을 잠시 남에게 양도해 줄 수 있는 기능이라고 볼 수 있겠네요.
ObservableObject and ObservedObject
앞서 본 @State
와 @Binding
은 스위프트의 값(Value) 타입에만 사용할 수 있다는 특징이 있습니다. 그렇다면 클래스의 경우는 어떻게 할 수 있을까요? 이를 위해서 ObservableObject
라는 프로토콜과 @ObservedObject
라는 Property Wrapper가 제공됩니다.
class MyData: ObservableObject {
@Published var name = "World"
@Published var buttonTitle = "Switch to Universe"
func switchName() {
if name == "World" {
name = "Universe"
buttonTitle = "Switch to World"
} else {
name = "World"
buttonTitle = "Switch to Universe"
}
}
}
struct ContentView: View {
@ObservedObject var data = MyData()
var body: some View {
VStack {
Text("Hello, \(data.name)!")
.padding()
Button(
action: { self.data.switchName() },
label: { Text(self.data.buttonTitle) }
)
}
}
}
@ObservedObject
가 되기 위해서는 반드시 ObservableObject
프로토콜을 따르는 클래스여야 합니다. 또한 각 프로퍼티 값이 바뀌었을 때 제대로 업데이트가 된 것을 모니터링하려면 해당 프로퍼티에는 @Published
를 붙여서 퍼블리셔로 쓸 수 있도록 해야 합니다.
이름처럼 반드시 클래스여야 하기 때문에 오브젝트 공유가 가능해진다는 장점이 있다는 것을 기억합니다. 그 외에는 State와 거의 비슷해 보이네요. 대신 외부 객체를 연결(Binding)시킬 때 $
를 붙이지 않아도 됩니다.
참고로
ObservedObject
프로토콜은 과거에는BindableObject
라는 이름이었습니다. 비슷하게,@ObjectObserving
은 과거에는@ObjectBinding
이라는 이름이었습니다. 그리고 요즘은didSet
을 통해 업데이트를 알려줘야 한다거나objectWillChange
를 구현해 줘야 하는 등의 일은@Published
를 통해 자동으로 채워지므로 꼭 필요하지는 않습니다. 혹시나 과거 버전의 튜토리얼을 참고하신다면 이 변화에 주의합시다.
EnvironmentObject
@EnvironmentObject
는 @ObservedObject
와 비슷하게 쓸 수 있지만, 다른 특징으로 공유 환경을 만들어 준다는 점이 있습니다. 물론 이름이 오브젝트(Object)가 들어가 있기 때문에 클래스 타입만 사용할 수 있겠지요?
class SharedData: ObservableObject {
@Published var configName = "default"
...
}
struct ContentView: View {
@EnvironmentObject var sharedData: SharedData
...
}
struct FooView: View {
@EnvironmentObject var sharedData: SharedData
...
}
코드 의미를 보면 sharedData
는 ContentView
와 FooView
에서 공유하려는 오브젝트임을 알 수 있습니다. 여기서 한 가지만 더 추가되면 양쪽의 sharedData
프로퍼티는 싱글턴 오브젝트처럼 공유할 수 있게 됩니다.
Xcode에서 SwiftUI 프로젝트를 생성했다면 SceneDelegate.swift
라는 파일이 생성되어 있을 것입니다. 여기에 보면 UIHostingController
를 생성할 때 ContentView
(최초의 뷰) 인스턴스를 넣어주는 부분이 있습니다. 여기를 약간 고치면 됩니다.
var sharedData = SharedData()
...
window.rootViewController =
UIHostingController(rootView: ContentView()
.environmentObject(sharedData))
이렇게 하면 위에서 생성된 sharedData
는 공유 오브젝트가 되어서 모든 뷰에서 공유할 수 있게 됩니다. 물론 @ObservedObject
의 특징도 같이 사용할 수 있습니다.
마무리
Combine Framework에 대한 글을 쓸 때는 정말 이걸 뭐에 써먹나 했더니만 왜 이걸 만들었는지를 SwiftUI를 공부하다 보면 머리를 탁 치게 만듭니다. 물론 Rx를 베꼈다는 평을 받고는 있지만, 써드파티 라이브러리가 가지는 단점인 '플랫폼 버전 변화에 따른 대응을 기다릴 필요'가 없다는 큰 장점은 버리기 힘들지요. 물론 아직 현업에 쓰기엔 약간 부족하기도 하고 OS 최소 버전 제한 등 여럿 걸리는 점이 없지는 않지만 그래서 더더욱 SwiftUI의 미래가 기대됩니다.
관련 글
'기술적인 이야기 > 애플 플랫폼 개발' 카테고리의 다른 글
Swift와 ObjC에서 Deprecated 처리하기 (0) | 2020.03.03 |
---|---|
2020년 4월부터 소셜 로그인 지원 시 애플 인증 강제 (0) | 2020.02.18 |
약간은 더 현실적인 Combine 예제들 (0) | 2020.01.27 |
Combine 이벤트 체인이 왜 필요할까? (0) | 2020.01.23 |
Combine Framework는 어떤 녀석일까요? (0) | 2020.01.15 |
댓글