본문은 아래 내용을 참고하여 작성하였습니다.
SwiftUI 에서의 데이터 흐름은 아래 2가지 원칙을 기반으로 설계 되었다.
1.데이터 의존성(Data Dependency) 2.단일원천자료(Singe Source of Truth)
✅ Every time you read a piece of data in your view, you are creating a dependency for that view.
Data access = dependency
: view 에서 데이터를 읽는 것은 view 안에 있는 데이터의 의존성을 만든다. 모든 view는 해당 data dependencies의 function 이다.
뷰는 매번 데이터가 변경될때마다 그 값을 반영해야 하므로 데이터에 대한 의존성을 가진다.
UIKit 에서는 데이터의 변경사항을 뷰에 반영하기 위해 데이터에 대한 의존성 정의를 'didSet' 등 수작업으로 진행했다.
→ 하지만, 이러한 방식은 코드가 복잡해지고 실수할 가능성이 많다. 또한 버그 발생의 주요 원인이기도 하다.
// UIKit
// count 값이 변경되면 뷰에 변경 정보를 반영
var count = 0 {
didset { countLabel.text = "\(count)"}
}
// 새로운 데이터가 추가되면 UITableView 갱신
func appendData() {
data.append("New Data")
tableview.reloadData()
}
SwiftUI 에선 위의 문제를 제공된 툴로 뷰가 어떤 데이터에 대해 의존성이 있는지만 알려준다면, 나머지는 프레임워크에서 알아서 처리하도록 설계하였다.
외부의 이벤트 (타이머, 알림) 혹은 사용자의 인터렉션에 의해 앱의 state 를 변형시키는 어떠한 Action 을 발생시키면, 프레임워크에서 이 Action 이 수행되고, 시스템은 변경(Mutation)된 State를 감지해 해당 State 에 의존하고 있는 View 를 update(갱신) 하여 새로운 버전의 UI를 생성한다
→ 단방향 데이터 흐름
→ View 의 변화를 이해하고 예측하기 쉽다
State의 변경이 감지 되었을때는 body 프로퍼티를 다시 호출하게 되지만, 모든 것을 다시 그리는 것이 아니라 뷰계층 구조를 다라 내려가면서 @State
를 소유한 뷰를 비교하고 유효성을 검사하여 변경된 부분만 다시 랜더링한다.
truth → 진짜, 찐..실재의 . 오리지널의 진리의 정도로 해석하면된다.
스윗한 SwiftUI 책에서는 이를 "단일원천자료"라고 해석하는데, 뭔가 좀 더 와닿는 말로 번역하자면 "근본이 되는 하나의 자료 원천" 정도 아닐까..?
✅ Every piece of data that you read in your view hierarchy has a source of truth, and it should always have a single source of truth
View Hierachy 에서 읽는 모든 데이터의 조각은 원천의 자료를 가져야하고, 이것은 항상 단일원천자료여야만 한다.
뷰가 참조하는 데이터는 Single Source of Truth 이여야한다.
Single Source of Truth (단일원천자료)
를 가져야한다 = 동일한 데이터 요소가 여러 곳으로 나뉘어 중복되지 않고 한 곳에서 다루어지고 수정되어야 한다는 것을 의미
Duplicated Source of Truth
는 데이터 불일치를 발생시킬 수 있다.
즐겨찾기 값이 Feed 화면과 Detail 화면에서 같은 모습이여야하는데, 개별적인 값을 소유한다면, 각 화면에서는 불일치가 발생할 수 있고 이것을 해결하는 것은 매우 어려울 것이다.
struct Feed: View { let isStarred: Bool = true}
struct Detail: View { let isStarred: Bool = false} // 불일치
struct Feed: View {
let isStarrd: Bool = true
var body: some View {
Detail(isStarred: self.isStarrd)
}
}
그런데 고정된 값이 아니라, 사용자가 별버튼을 눌었을때 값이 바뀌게 한다면 ..?
그러면 생성자를 통해 값을 넘겨주더라구, 두뷰가 각각 고유한 다른 값을 가지게 되고 그 값이 중간에 변경가능하여 불일치 문제가 발생가능하다!
→ 이 문제를 피하려면 값이 변할때 마다 동기화 작업을 별도로 해줘야한다.
struct Feed: View { @State var isStarred: Bool = true}
struct Detail: View { @State var isStarred: Bool = true}
이번에는 Detail 에 별도의 @State
없이 데이터를 참조하기 위해 @Binding
프로퍼티 래퍼를 사용하여 문제를 해결해보겠다.
@Binding
프로퍼티는 전달받은 데이터를 읽거나 직접 변경할 수 있도록 만들어진 타입으로 상세화면 그자체가 별도의 원천 자료를 가지는 대신 메인 화면의 것을 참조하게 된다.
→ 두 화면의 불일치가 발생하지 않고, 동기화 코드를 따로 작성하지 않아솓 된다.
→ 개념적으로 @State
가 적용된 프로퍼티는 Derived Value (원천자료)
/ @Biding
이 적용된 프로퍼티는 Derived Data (파생자료)
에 해당된다.
struct Feed: View { @State var isStarred: Bool = true}
struct Detail: View { @Biding var isStarred: Bool\}
SwiftUI 의 변화를 internal change 와 external change 두가지로 나울 수 있다.
먼저 internal change 부터 살펴보자
@State
는 view 안에서 locally 하게 사용하기 위해 만들어진 source of truth 이다.
이것이 뷰 내부에서 사용하기때문에, Apple 은 State 가 해당 뷰만 소유하고 관리한다는 것을 명시적으로/확실하게 보여주기 위하여 State에 private 키워드를 붙여 사용하는 것을 권장한다.
@State
변수를 선언하는 것은 프레임워크에 변수를 위한 persistence storage 를 할당하고 이것의 의존성을 트래킹한다는 것을 알려준다는 것이다.
→ @State
는 뷰 자체에서 가져야할 상태 프로퍼티이자 원천자료로, 어떤 데이터에 대한 영속적인 상태를 저장하고 관찰하는 역할
@State
를 정의한다는 것은, 곧 SwiftUI 에 이 데이터는 변경될 수 있고 View 가 이것에 의존성을 가지고 있다고 알리는 것이다. → 데이터가 변경되면 뷰를 재생성한다
→ 파생자료에 사용
Source of truth 를 보유하지 않고 명시적으로 Source of truth에 대한 의존성을 정의하기 위해 @Binding
을 사용한다.
@Binding
을 사용하면, @Binding
변수와 연결된 데이터에 읽고 쓸 수 있다. 그리고 프레임워크는 항상 이것의 sync 를 보장할 것이다.
@State var bar: Bool = false
var body: some View {
Toggle("Toggle", isOn: $bar)
}
그리고 @State
에서 @Binding
을 가져오기 위해 $
사인을 사용한다.
지금까지 본것은 내부의 데이터와 view 안에서 이벤트가 발생했을때에 대한 것을 다뤘다.
@State
는 view 안에서의 local/private 한 변화를 의미한다.@Binding
은 @State
에 의존성을 선언한다.ObervableObject는 프로토콜로 AnyObject를 채택하고 있어 구조체나 열거형 타입에는 사용할 수 없다. 클래스타입에 사용할 수 있다. (값타입이 아닌 참조타입에 사용가능)
@State
는 뷰 자신이 상태값을 가지는 반면, @ObservedObject
는 뷰 외부의 모델에 의해 의존성을 가지고 그 데이터의 변화를 감지하기 위해 사용한다.@State
와 다르게 persistence storage 를 스스로 관리해야한다.protocol ObservableObject: AnyObejct {...}
사용법
ObservableObject
프로토콜을 채택한다.@Published
프로퍼티 래퍼를 붙인다.class Foo: ObservableObject {
@Published var show = false
}
@Published
키워드를 붙이면, 프로퍼티의 변경 시점에 즉시 변화를 알린다.
하지만 그 시점을 자신이 지정하고 싶은 경우도 있을 것이다. 이 경우 ObservableObject
의 프로토콜에 선언된 obejctWillChange
프로퍼티를 이용하면 된다.
@Published
또한 이것으로 내부에는 구현되어 있다.@Published
나 ObjectWillChangePublisher 나 둘 모두 특정 시점에 관련있는 모든 객체에 알림을 전달하는 기능은 동일하며, 그 시점을 결정할 수 있는지만 차이가 있다.// 아래코드는 @Published var score = 0 과 동일하게 작동한다.
let objectWillChange = ObjectWillChangePublisher()
var score = 0 {
willSet { obejctWillChange.send() }
}
@ObservedObject
가 모델에 대한 직접적인 의존성을 만드는데 사용했다면, @EnvironmentObject
는 간접적인 의존성을 만드는데 사용왼다.
@ObservedObject
는 특정 모델에 대한 참조를 뷰에 직접 전달하여 의존성을 만들어 주었다. 그리고 그 자식 뷰에 동일 모델에 대한 참조를 전달하기 위해 매번 @Binding
을 통해 연결해주었다.
하지만 @EnvironmentObject
는 어떠한 ancester view 에 데이터를 주입해줄 수 있다.
@ObservedObject
(@ObservedObject 로 받아서 @Binding 으로 직접적인 의존성을 생성하는 경우)
@EnvironmentObject
먼저 environmentObject 수식어를 이용해 특정뷰에 대한 환경요소로 ObservableObject 모델을 등록한다.
그럼 그 뷰를 포함한 모든 자식 뷰에서 @EnvironmentObject
프로퍼티 래퍼를 이용해 등록해 두었던 모델에 대한 의존성을 만들 수 있다.
contentView.environmentObject(foo)
struct SomeViewDownTheHeirarchy: View {
@EnvironmentObject var foo: Foo
...
}
SwiftUI 는 colorScheme
, locale
, sizeCategory
등과 같은 많은 environment value 들을 제공한다.
그리고 때로는 이러한 value를 기반으로 view 를 적용해야할때도 있다.
그럴때는 @Environment
프로퍼티 래퍼를 사용하면 된다.
아래와 같이 사용할 수 있다
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
...
}
이러면 전체 뷰 계층구조가 영향을 받게 될 것이다.
// 다크모드를 적용하는 예시
contentView.environment(\.colorScheme, .dark)
✅ A state object behaves like an observed object, except that SwiftUI knows to create and manage a single object instance for a given view instance, regardless of how many times it recreates the view.
애플이 추천하는 StateObject와 ObservedObject의 사용법
struct UpperView: View {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
LowerView(viewModel: viewModel)
}
}
struct LowerView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text("Hello")
}
}
위와 같이 상위 View에서 객체로 만들어서 따로 저장해두고, 하위 View도 이 Observable Object의 변화를 감지하여, 같은 정보에 접근할 수 있도록 할 수 있을 것이다.