Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Essentials in SwiftUI #6

Closed
feldblume5263 opened this issue Jun 8, 2022 · 0 comments
Closed

Data Essentials in SwiftUI #6

feldblume5263 opened this issue Jun 8, 2022 · 0 comments

Comments

@feldblume5263
Copy link
Collaborator

feldblume5263 commented Jun 8, 2022

SwiftUI에서 데이터를 위해 고려해야 할 점 3가지

  1. View가 일을 하기 위해 어떤 데이터가 필요할까?
  2. View가 어떻게 데이터를 조작할까?
  3. 데이터는 어디에서 오게 될까? <- Source of Truth

가장 중요한 것은 여기서 이 3번 Source Of Truth 입니다.

State & Binding

예시를 하나 들어볼게요!

스크린샷 2022-06-09 오후 5 15 18

먼저 View 이런 창을 만들기 위해, 필요한 데이터는 title, coverName, author입니다.
또, 독서 진행정도를 나타내는 Progress도 필요한 것입니다.

struct BookCard : View {
    let book: Book
    let progress: Double

    var body: some View {
        HStack {
            Cover(book.coverName)
            VStack(alignment: .leading) {
                TitleText(book.title)
                AuthorText(book.author)
            }
            Spacer()
            RingProgressView(value: progress)              
        }
    }
}

먼저 Book 과 Progress는 데이터를 표시할 뿐 변하지 않기 때문에 let으로 선언해줍니다..

그리고 이 데이터들은 여기서 초기화 되지 않고, SuperView에서 전달되어집니다.
즉, Source Of Truth는 더 높은 계층에 있는 것이죠!

위의 코드 BookCard는 SuperView의 body가 실행 될 때 마다 초기화 됩니다.

이를 다이어그램으로 표기하면 다음과 같습니다.

스크린샷 2022-06-09 오후 5 14 42

그리고 이와 같이 progress를 누르면 독서 진행에 대한 세부 정보를 확인할 수 있는 뷰와, 업데이트할 수 있는 SheetView를 추가한다고 가정해봅시다.

스크린샷 2022-06-09 오후 5 16 53

이를 위해 다음과 같이 코드를 작성할 수 있습니다.

struct EditorConfig {
    var isEditorPresented = false
    var note = ""
    var progress: Double = 0

    mutating func present(initialProgress: Double) {
        progress = initialProgress
        note = ""
        isEditorPresented = true
    }
}
struct BookView: View {
    @State private var editorConfig = EditorConfig()

    func presentEditor() { editorConfig.present(…) }

    var body: some View {
        …
        Button(action: presentEditor) { … }
        …
    }
}

다음과 같이 EditorConfig를 별개의 Struct로 만들어서 사용하고 있는데,
이렇게 캡슐화해서 사용하게 되면 몇가지 이점이 있습니다.

첫번째, EditorConfig는 속성에 대한 불변성을 유지하고 독립적으로 테스트할 수 있습니다.
두번째, EditorConfig는 값 유형이기 때문에 진행 상황과 같이 EditorConfig의 프로퍼티 등을 변경하면, EditorConfig 자체에 대한 변경으로 볼 수 있습니다.

그리고 다음과 같이 mutating func으로 데이터를 조작합니다.

자 그럼, 필요한 데이터와 어떻게 조작할 지 정해졌죠?

그럼 가장 중요한, 이 데이터의 Source of Truth는 무엇일까요??

먼저, Editor Config는 지역적인 객체입니다.
SuperView에서 오는 것도 아니고, 현재 코드에서 생성되어서 사용되는 객체입니다.

SwiftUI에서 가장 간단한 Source Of Truth는 @State 입니다.

조금 더 자세히 보면, EditorConfig가 사용된 View, 즉, BookView는 일시적으로만 존재하는 View입니다.

스크린샷 2022-06-09 오후 5 35 37

하지만 우리가 이 객체를 State로 관리하기 때문에, View가 사라진 후에도 SwiftUI가 해당 객체를 계속 유지시켜줍니다.

스크린샷 2022-06-09 오후 5 38 52

그리고 다시 렌더링할 때, 해당 View와 EditorConfig는 자동으로 다시 연결되는거죠.

스크린샷 2022-06-09 오후 5 35 37

자 이제 다음으로 Sheet 부분의 ProgressEditor을 살펴봅시다.

먼저 해당 변수는 값을 변경시키기 때문에 var로 선언이 됩니다.

struct BookView: View {
    @State private var editorConfig = EditorConfig()
    var body: some View {
        …
        ProgressEditor(editorConfig: ...)
        …
    }
}

struct ProgressEditor: View {
    var editorConfig: EditorConfig
}

저 변수가 값을 변경한다고 BookView에 반영이 될까요?
안됩니다. 값타입으로 선언되어있기 때문입니다.

값타입이 변경된다고 해도, 원본은 변경되지 않을 것입니다.

그렇다면 마찬가지로 State로 명시해서 전달하면??

마찬가지로 안됩니다.
State는 새로운 Source Of Truth를 만들어내기 때문에 그 자체의 View에만 영향을 주고, 공유되고 있는 BookView에는 영향을 주지 못합니다.

스크린샷 2022-06-09 오후 5 45 36

그렇다면 어떻게 Source of Truth에 대한 읽기/쓰기 접근을 View 간에서 서로 공유할 수 있을까요??

SiwftUI에서는 Binding을 사용합니다.

struct BookView: View {
    @State private var editorConfig = EditorConfig()
    var body: some View {
        …
        ProgressEditor(editorConfig: $editorConfig)
        …
    }
}

struct ProgressEditor: View {
    @Binding var editorConfig: EditorConfig
    …
        TextEditor($editorConfig.note)
    …
}

그렇다면 저렇게 값을 넘길 때 $ 를 써서 넘기는 이유는 무엇일까요??

$ 를 써야 State로부터 Binding을 생성할 수 있기 때문입니다.

State property wrapper의 Projected value는 Binding입니다.
wrapped value 뿐만아니라 projected value를 추가하여 기능을 확장할 수 있는데, projected value 를 통해 property wrapper가 저장하기 전에 값을 조정 했는 지 알려주는 역할을 하며, projected value를 생성하면 $을 통해 이에 접근할 수 있습니다.

private(set) var projectedValue: ??? <- 어떤 타입도 가능

참고 (https://eunjin3786.tistory.com/472)

또한 아래의 TextEditor처럼 Binding의 Binding도 가능합니다.

결론은 다음과 같습니다.

바뀌지 않는 변수에 대해서는 프로퍼티를 사용하고,
View가 소유한 일시적인 변수에 대해서는 @State를 사용하고, 다른 View에서 소유한 일시적인 변수를 변경하기 위해서 @binding을 사용합니다.
image

ObserableObject

@State는 유용하지만, 지역의 일시적인 상태관리에 특화된 도구입니다.

ObservableObject는 데이터의 지속 및 동기화를 포함하여 데이터의 수명 주기를 관리하고, 부작용을 처리하고, 보다 일반적으로 기존 구성 요소와 통합하기 위해 사용합니다.

ObservableObject는 다음과 같이 objectWillChange를 구현해야 합니다.

스크린샷 2022-06-09 오후 6 15 50

ObservableObject를 채택하면, 새로운 Source of Truth를 만들어내게 되는데,
이 ObservableObject의 각각의 View에 대한 의존성을 통해 자동으로 view의 데이터의 일관성을 유지해 줍니다.

스크린샷 2022-06-09 오후 6 19 59

ObservableObject는 다음과 같이 View전체의 데이터를 중앙집중화 하는 역할을 하기도 하고,

스크린샷 2022-06-09 오후 6 22 58

다음과 같이 각각의 view에 필요한 데이터만 노출하는 방식으로 사용할 수도 있습니다.

스크린샷 2022-06-09 오후 6 23 11

UI를 progress에 따라 업데이트하고 싶으면 @published를 붙여주면 됩니다.

class CurrentlyReading: ObservableObject {
    let book: Book
    @Published var progress: ReadingProgress

    // …
}

struct ReadingProgress {
    struct Entry : Identifiable {
        let id: UUID
        let progress: Double
        let time: Date
        let note: String?
    }

    var entries: [Entry]
}

@published는 Publisher를 노출하여 프로퍼티를 관찰 가능하게 만드는 프로퍼티 래퍼이고, publish된 값이 변화하기 직전 마다 값이 변화한다고 알려줍니다.
이를 통해 View와 Data의 싱크를 걱정하지 않을 수 있게 됩니다.

SwiftUI는 ObservableObject 객체와 뷰에 서로 의존성을 만드는방법으로 3가지 property wrapper를 제공합니다.

@ObservedObject
@StateObject
@EnvironmentObject

먼저 @ObservedObject에 대해서 설명하면,
ObservedObject는 ObservableObject를 준수하여 사용할 수 있는 속성 래퍼입니다.
이를 사용하면 프로퍼티의 변경 추적을 뷰가 시작하도록 SwiftUI에 알립니다.

ObservedObject는 인스턴스의 소유권을 가지지 않으며, 수명 주기를 관리하는 것은 사용자의 책임입니다.
클래스 기반이기 때문에 Strong reference cycle과 같은 문제에 대해서 주의가 필요하고 생명주기에 개발자가 신경을 써줘야 한다는 의미로 해석이 됩니다.

/// The current reading progress for a specific book.
class CurrentlyReading: ObservableObject {
    let book: Book
    @Published var progress: ReadingProgress

    // …
}

struct ReadingProgress {
    struct Entry : Identifiable {
        let id: UUID
        let progress: Double
        let time: Date
        let note: String?
    }

    var entries: [Entry]
}

struct BookView: View {
    @ObservedObject var currentlyReading: CurrentlyReading

    var body: some View {
        VStack {
            BookCard(
                currentlyReading: currentlyReading)

            //…

            ProgressDetailsList(
                progress: currentlyReading.progress)
        }
    }
}

@ObservedObject를사용하면, SwiftUI는 자동적으로 objectWillChange를 subscribe합니다. 이를 통해 값이 변화면 이를 전달받아서 새롭게 view를 렌더링하게 됩니다.
즉, Publisher가 보내주는 신호에 따라 변경되면 View를 만료시키고 새로 그리게 됩니다.

스크린샷 2022-06-09 오후 6 36 55

그렇다면 여기서 나온 것 처럼 왜 objectDidChange가 아니라 objectWillChange일까요??

SwiftUI가 무언가가 변경될 때를 알아야 모든 변경 사항을 단일 업데이트로 통합할 수 있기 때문입니다.
각각의 업데이트들을 사후로 개별로 반영하는 것보다 곧 바뀐다는 것을 인지하고 한번에 업데이트를 모아서 할 수 있게 하기 위해서라고 해석이 됩니다.

struct BookView: View {
    @ObservedObject var currentlyReading: CurrentlyReading

    var body: some View {
        VStack {
            BookCard(
                currentlyReading: currentlyReading)

            HStack {
                Button(action: presentEditor) { /* … */ }
                    .disabled(currentlyReading.isFinished)

                Toggle(
                    isOn: $currentlyReading.isFinished
                ) {
                    Label(
                        "I'm Done",
                        systemImage: "checkmark.circle.fill")
                }
            }
            //…
        }
    }
}

ObsesrvedObject데이터도 $로접근하여 Binding할 수 있습니다.

즉, 변경사항을 알리는 것 뿐 만 아니라 이벤트를 통해 스스로 수정되어 양방향으로 바인딩할 수 있는 것이죠.

반면에 StateObject는 인스턴스를 스스로 소유합니다.

@feldblume5263 feldblume5263 self-assigned this Jun 8, 2022
@feldblume5263 feldblume5263 changed the title 영상 제목 영어 원문 그대로 Data Essentials in SwiftUI Jun 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants