SwiftUI State and Data Flow
Updated:
🔷 데이터 흐름 이해하기
iOS 에서 데이터는 텍스트에 포함된 문자열이나 토글의 상태, 사용자 정보 등 UI를 표현하는데 사용되는 모든 정보를 의미합니다.
SwiftUI 에서 데이터를 변경하고 전달하는 방식은 기존과 크게 달라 졌습니다
👉 프로퍼티 잢 수정하기
-
프로퍼티 값을 수정하려고 할때 body 내에서 값을 변경하려고 하면 self 를 변할 수 없다는 error 메세지가 나옵니다
-
그래서 SwiftUI 에서는 뷰에서 어떤 상태를 저장하고 수정하는 방법으로 @State, @Binding 과 같은 몇 가지 새로운 도구를 제공합니다. 아래는 @State 를 사용해서 UI 상에서 데이터를 변경하는 예시 입니다
struct ContenView: View {
@State private var framework: String = "UIKit"
var body: some View {
Button(framework) {
self.framework = "SwiftUI"
}
}
}
-
위의 코드와 같이 ContentView 에 @Stater 가 선언된 framework 프로퍼티는 항상 초기값만을 그대로 유지하고 있을 뿐 변경이 발생하더라도 직접 값을 바꾸는 대신, SwiftUI 에서 제공하는 저장소에 그 값을 전달하고 참조하는 형태로 동작합니다
-
SwiftUI 에서 이 값들이 언제 읽혀지고 변경되는지 알 수 있어서 적절한 시점에 뷰를 갱신하는 것과 같은 작업들을 대신 수행할 수 있습니다. UIKit 에서 다루던 데이터와 다른 새로운 데이터 흐름을 파악하는게 중요합니다
👉 데이터 흐름의 2가지 원칙
데이터 의존성
뷰는 매법 데이터가 변경될 때마다 그 값을 반영해야 하므로, 데이터에 대한 의존성을 가집니다.
아래의 코드는 UIKit에서 데이터를 변경하는 대표적인 예시 코드 입니다
// count 값이 변경되면 뷰에 변경 정보를 반영
var count = 0 {
didSet { countLabel.text = "\(count)"}
}
// 새로운 데이터가 추가되면 UITableView 갱신
func appendData() {
data.append("New Data")
tableView.reloadData()
}
위와 같이 UIKit 에서는 데이터가 추가되거나 변경되면, 변경 사항을 뷰에 반영하려고 tableView.reloadData()
와 같이 추가 코드를 작성해야 합니다. 이렇게 되면 데이터의 대한 의존성 정의를 수작업으로 하는 것으로 코드가 복잡해지고 code 양이 많아 질수로 bug 가 발생될 가능성이 높습니다
📌 SwiftUI 에서는 이런 문제를 해결하기 위해, 뷰가 어떤 데이터에 대해 의존성이 있는지 알려만 주면 나머지는 프레임 워크에서 알아서 처리하도록 설계 되어 있습니다
프레임워크 내에서 데이터 변화에 동작이 수행되고 시스템은 변경된 상태를 감지해 해당 상태에 의존하고 있는 뷰를 갱신하여 새로운 버전의 UI 를 생성합니다.
예를 들어 Toggle 상태가 true, false 상태로 바뀔때마다 프레임 워크가 자동으로 이를 감지하고 자동으로 뷰를 새로 그려줍니다. body 프로퍼티를 다시 호출하게 되지만, 모든 것을 다시 그리는 것이 아니라 구조를 따라 내려가면서 @State 를 소유한 뷰를 비교하고 유효성을 검사하여 변경된 부분만 다시 랜더링 하기 때문에 매우 효율적으로 작업을 수행할 수 있습니다.
단일 원척 자룔 (Single Source of Truth)
뷰가 참조하는 데이터는 단일 원천 자료여야 합니다.
// SuperView 에서 다른 값을 전달해줍니다 : 원천 자료로서 다른 값을 전달해줌
struct SuperView : View {
let name = "Jacob"
var body: some View { SubView(name: name) }
}
// SubView 의 SuperView 의 name 으로 부터 파생된 자료라고보면 됨
struct SubView: View {
let name: String
let body: some View { Text("\(name)")}
}
단일 원천 자료를 가져야 한다는 것은 데이터 요소가 여러 곳으로 나뉘어 중복되지 않고 한 곳에서 다루어지고 수정되어야 한다는 것임
이때 생성자를 통해 값을 넘겨 주었다 하더라도 두 뷰가 각각 고유한 값을 가지며, 그 값이 중간에 변경될 수 있어 다시 불일치 문제가 발생할 수 있습니다. 이 문제를 피하기 위해선 값이 변할 때마다 별도의 동기화 작업을 해 주어야 합니다.
struct MainView: View {
@state private var isFavorite: Bool = true
}
// MainView 에 있는 isFavorite 참조하기
struct DetailView: View {
@Binding var isFavorite: Bool
}
@Binding 프로퍼티 래퍼는 전달받은 데이터를 읽거나 직접 변결할 수 있도록 만들어 진 타입으로, 상세 화면 그 자체가 별도의 원천 자료를 가지는 대신 메인 화면의 것을 참조하게 됩니다. 즉 개념적으로 @State 가 적용된 프로퍼티는 원천자료, @Binding 이 적용된 프로퍼티는 파생 자료에 해당합니다.
🔷 Data Tools
UIKit 에서는 단순히 어떤 값을 저장해 두기 우한 일반 프로퍼티와 상태 값을 지닌 프로퍼티를 별도로 구분하여 사용하지 않습니다. 하지만 SwiftUI 에서는 뷰의 상태를 저장해 두기 위해 @State
를 사용하며, 이것과 합쳐져 앱에 대한 원천 자료를 구성합니다. 그리고 이러한 것들로 파생된 데이터들까지 합쳐지면 앱에서 사용하는 전체 대이터가 만들어 지게 됩니다
👉 @State, @Binding
-
@State : 뷰 자체에서 가져야 할 상태 프로퍼티이자 원천 자료로, 어떤 데이터에 대한 영속적인 상태를 저장하고 관찰하는 역활을 수행합니다
-
@Binding: 상위 뷰가 가진 상태를 하위 뷰에서 사용하고 수정할 수 있게 해 주는 파생자료에 해당 합니다. 연산 프로퍼티의 형태로 사용되어 그 자신이 직접 값을 보유하지 않고, 값을 Read, Write 하고 개시 되고 다른 뷰에 갱신된 데이터를 전달하는 역활을 합니다
// Toggle과 Stepper 구현 예시.
// 뷰에서 읽고 변경할 값을 저장하기 위해 State가 사용되며
// 자식 뷰에서 상위 뷰의 값을 변경하기 위해 Binding이 사용됩니다.
struct Example01: View {
// @State 는 자신의 UI 상태를 저장하기 위한 데이터를 설계되었으므로, 해당 뷰가 소유하고 관리한다는 개념을 명시적으로 나타내기 위해 항상 private 접근 레벨을 사용하는것이 좋습니다
@State private var isFavorite = true
@State private var count = 0
var body: some View {
VStack(spacing: 30) {
// $isFavorite: 내부적으로 projectedValue 라는 프로퍼티를 이용하게 되는데 이 타입이 Binding 타입이기에 Binding 타입으 ㅣ매개 변수에 상태 프로퍼티의 값을 전달 해 줄수 있습니다
// 토글에 Binding 타입을 사용하는 이유는, 토글은 ContentView 가 가진 상태를 표현하거나 변경하는 역활만 하면 됩니다
Toggle(isOn: $isFavorite) {
Text("isFavorite: \(isFavorite.description)")
}
// 같은 뷰 내에서 값을 읽거나 쓰는 경우는 접두어 없이 일반 변수 처럼 사용할 수 있습니다.
Stepper("Count : \(count)", value: $count)
}
.padding()
}
}
- 위와 같이 상태 프포퍼티 @State 정의하는것은 SwiftUI 에 이 데이터는 변할 수 있고, 뷰가 이것에 의존성을 가지고 있다고 선언하는것이다. 즉, 데이터가 변경되면 변경사항을 감지해서 자동으로 View 에 갱신해 주는 것입니다.
👉 ObservableObject, @ObservableObject
-
@State가 뷰의 상태를 저장하고 다루기 위한 원천 자료로 이용되었다면, 뷰 외부의 모델이 가진 원천 자료를 다루기 위한 도구로써 ObservableObject 가 사용됩니다.
-
ObservableObject 는 프로토콜이며, AnyObject 를 채택하고 있으므로 구조체(struct), 열거형(enum) 타입을 사용할 수 없습니다.
-
@ObservedObject 는 ObservableObject 프로콜을 준수하는 모델에 해당 뷰가 의존성을 가진다는것을 알리기 위해 사용하는 속성입니다.
-
@State는 뷰 자신이 상태값을 가지는 데 반해, @ObservedObject 는 뷰 외부의 모델에 의존성을 가지고 그 데이터의 변화를 감지하기 위해 사용한다는 점이 다릅니다.
// User Model
final class User: ObservableObject {
let objectWillChange = ObjectWillChangePublisher()
let name = "Jacob"
var score = 0
}
// ObservableObject 프로토콜을 채택한 모델을 다룰 때
// ObservedObject를 사용할 수 있습니다.
struct Example02: View {
@ObservedObject var user = Ch05_1_DataFlow.User()
var body: some View {
VStack(spacing: 30) {
Text(user.name).font(.title)
// 버튼을 눌러도 View 에서는 변경되지 않습니다
Button(action: { self.user.score += 1 }) {
Text(user.score.description).font(.title)
}
}
.padding()
.border(Color.black)
}
}
위의 코드를 실행해 보면 값이 변경되는데 View 전달 되지 않아서 화면에 표시에 변경되지 않습니다. 정확히 어떤 데이터가 변경사항을 어느 시점에 뷰에 전달할 것인지 알려 줘야 뷰에 반영이 됩니다
👉 @Published
- 위의 기능을 가능하기 위해선 변수 앞에 @Published 를 선언하면 됩니다
@Published var score = 0
objectWillChange
- @Published cㅓ럼 프로퍼티의 변경 시점에 즉시 알리는 것이 아닐, 그 시점을 자신이 정하여 알리고 싶은 경우도 있는데 이때는, Observable Object 프로토콜에 선언된 objectWillChange 프로퍼티를 이용할 수 있습니다
let objectWillChange = ObjectWillChangePublisher()
var score = 0 {
willSet{ objectWillChange.send()}
}
실질적으로 @Published
는 ObjectWillChangePublisher
가 send 매서드를 호출하는 코드를 좀 간소화 한것입니다. send 매서드를 호출하는 코드를 작성하는 대신에 @Published
로 이용할 수 있습니다.
따라서 @Published
sk ObjectWillChangePublisher
는 둘 모두 특정 시점에 관련있는 모든 객체에 알림을 전달하는 기능은 동일하며, 그 시점을 결정할 수 있는지에 대해서만 차이가 있습니다.
👉 @EnvironmentObject
@ObservedObject
가 모델에 대한 직접적인 의존성을 만드는 데 사용했다면, @EnvironmentObject
는 간접적인 의존성을 만드는데 사용하는 래퍼 타입입니다
위의 그림과 같이 특정 모델에 대한 참조를 뷰에 직접 전달하여 의존성을 만들어 줍니다. 그리고 그 자식 뷰의 동일 모델에 대한 참조를 전달하기 위해서는 매번 @Binding 을 이용해서 연결해 주어야 합니다
📌 @EnvironmentObject
를 사용하면 다음의 그림과 같이 다르게 됩니다
먼저 environmentObject 수식어를 이용해 특정 뷰에 대한 환경 요소로 Observable Object 모델을 등록 합니다. 그 뷰를 포함한 모든 자식 뷰에서 @Environment Object 프로퍼티 래퍼를 이용해서 등록해 두었던 모델에 대한 의존성을 만들 수 있습니다.
@ObservedObject VS @EnvironmentObject
-
@ObservedObject 뷰는 서브트리에서 해당 모델을 사용하지 않는 뷰가 있더라도 또 다른 자식 뷰가 사용한다면 꼭 모델을 넘겨 줘야 합니다 (빨간색 View 부분에만 model 의 데이터를 참조 한다고 할때, 부모뷰, 조상뷰에서 @Binding 을 통해서 전달해 주어야 합니다)
-
@EnvironmentObject 은 Environment 가 가진 속성을 함께 가지고 있기 때문에 부모뷰가 어떤 값을 가진다면 그 자식 뷰에 직접 전달받지 않고 어떤 뷰던지 동일한 데이터에 접근할 수 있게 되는것입니다 ( 빨간 View 가 Environment 에 직접 model 데이터의 연결 )
// User Model
final class User: ObservableObject {
let name = "Jacob"
}
// EnvironmentObject를 이용하면 뷰의 서브 트리 전체가 동일한 데이터를 공유할 수 있습니다.
struct Example03: View {
var body: some View {
Ch05_1_DataFlow.Superview()
// environmentObject 수식어에 자식 뷰에서 공통으로 사용할 수 있는 인스턴스를 만들어 전달합니다. 이 인스턴스는 ObservableObject 프로토콜을 준수 합니다
// 이제 Superview 의 모든 자식 뷰는 @Environment Object 를 이용해 동일한 User 인스턴스에 접근 할 수 있습니다
.environmentObject(Ch05_1_DataFlow.User())
}
}
struct Superview: View {
var body: some View { Ch05_1_DataFlow.Subview() }
}
struct Subview : View {
@EnvironmentObject var user: User
var body: some View {
Text(user.name.description).font(.title)
}
}
👉 @StateObject
-
iOS 14 이후에 업데이트 된 wrapper 로써 정의는 instantiates an observable object 입니다
-
SwiftUI 는 언제든지 뷰를 다시 만들 수 있습니다. 그래서 주어진 inputs 을 가지고 뷰를 초기화 하면 항상 동일한 뷰가 되는데, 뷰안에 데이터 객체인 ObservedObject 를 만드는 것은 안전하지 않습니다
-
그래서 새로히 적용된 StateObject 응 사용함으로써 뷰안에서도 안전하게 ObservedObject 의 instance 를 만들 수 있습니다
// Book Model
final class Book: ObservableObject {
@Published var title = "Greate Expectations"
// A unique identifier that never changes
let identifier = UUID()
}
struct LibraryView: View {
@StateObject var book = Book()
var body: some View {
BookView(book: book)
}
}
struct BookView: View {
@ObservedObject var book : Book
var body: some View {
BookEditView(book: book)
}
}
struct BookEditView: View {
@ObservedObject var book : Book
//....
}
위의 코드에서 StateObject 는 ObservableObject 와 같이 동작하는데, 특이점은 자식뷰가 몇번이고, 다시 만들어도 view instance 에 대해 single object instance 를 만들 고 관리한다는 것입니다.
그래서 위의 예제에서 Library View 에 대해서는 unique Book Instance 를 가지게 되는것입니다.
🔷 M
🔶 🔷 📌 🔑 👉
🗃 Reference
Managing Model Data in Your App- https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
SwiftUI @StateObject - https://eunjin3786.tistory.com/410
스윗한 SwiftUI - https://book.jacobko.info/#/book/1190014815
Leave a comment