[SwiftUI] 9. State and Data flow: State 및 Binding 을 사용한 UI 변경

Sean·2023년 5월 4일
0

SwiftUI_튜토리얼

목록 보기
9/11

참고 자료: SwiftUI 튜토리얼 공식문서

이제 막 SwiftUI를 배워보는 한 사람으로서, 코드 작성하고 내가 이해 한 대로 작성할것이다.

이 글의 내용은 위에 있는 참고자료의 공식문서를 번역하여 조금 더 내가 느끼기에 자연스럽게 만들어서 작성하는것이다.

누군가에게 알려주기보다 자신이 정리를 하면서 다시 문서를 보고 어버버거리지 않게 다시 참고하기 위함이기에 해당 문서를 읽는 사람은 swift에 대해서 어느 정도 안다는 가정하에 글을 작성할것이다

시작

Struct: State
Struct: Binding

State를 사용해 view에서 데이터 종속성을 표시하고 바인딩을 사용해 다른 view와 종속성을 공유한다.
SwiftUI 앱의 사용자 인터페이스는 view로 구성된 view의 계층 구조이다.
각 view에는 일부 데이터에 대한 종속성이 있다.
이 데이터가 외부 이벤트 또는 사용자의 작업으로 변경되면 SwiftUI는 자동으로 변경 사항을 업데이트하여 반영한다.

샘플코드에서는 State를 사용해 데이터 종속성을 나타내는 예시와 Binding을 사용해 다른 view 와 데이터를 공유하는 예시를 보여준다.

분석

1. view에서 프로퍼티와 명령형 코드 분리

RecipeEditorConfig.swift

import Foundation

struct RecipeEditorConfig {
    var recipe = Recipe.emptyRecipe()
    var shouldSaveChanges = false
    var isPresented = false
    
    mutating func presentAddRecipe(sidebarItem: SidebarItem) {
        recipe = Recipe.emptyRecipe()

        switch sidebarItem {
        case .favorites:
            // Associate the recipe to the favorites collection.
            recipe.isFavorite = true
        case .collection(let name):
            // Associate the recipe to a custom collection.
            recipe.collections = [name]
        default:
            // Nothing else to do.
            break
        }
            
        shouldSaveChanges = false
        isPresented = true
    }
    
    mutating func presentEditRecipe(_ recipeToEdit: Recipe) {
        recipe = recipeToEdit
        shouldSaveChanges = false
        isPresented = true
    }
    
    mutating func done() {
        shouldSaveChanges = true
        isPresented = false
    }
    
    mutating func cancel() {
        shouldSaveChanges = false
        isPresented = false
    }
}

하나의 view가 하나 이상의 상태 데이터를 관리해야 할 때, 별도의 view 구조체에서 관리하는것이 도움이 될 수 있다.
선언적 인터페이스 코드를 더 읽기 쉽게 만들어주고 프로퍼티와 명령형 코드를 view 밖으로 이동시켜 코드 구조를 단순화한다.
또한 상태 변경에 대한 unit test를 구현하기 쉽게 만든다.

1의 샘플코드 구조체는 view에 필요한 상태 데이터를 저장한다.

해당 메서드는 view에서 발생하는 상태 변경을 트리거하기 위해 mutating func을 사용한다. 이 함수들은 데이터를 업데이트 하여 새로운 상태를 반영한다.

add 함수는 view의 상태를 변경하여 새로운 레시피를 편집하고 있음을 나타낸다.

edit 함수는 기존 레시피를 편집하기 위한 것이다.

해당 구조체는 view에서 발생하는 상태 변경을 트리거하면서도 선언적 인터페이스 코드에서 명령형 코드를 분리하기 위해 done()과 cancel()의 두가지 메서드를 사용한다.

done()은 편집기가 레시피에 대한 변경을 저장하고 view를 닫아야 함을 나타낸다.
cancel()은 done과 비슷하지만 레시피에 대한 변경 사항을 무시하도록 한다.

2. 다른 view에서 상태 데이터 바인딩

RecipeEditor.swift

import SwiftUI

struct RecipeEditor: View {
    @Binding var config: RecipeEditorConfig
    
    var body: some View {
        NavigationStack {
            RecipeEditorForm(config: $config)
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        Text(editorTitle)
                    }
                    
                    ToolbarItem(placement: cancelButtonPlacement) {
                        Button {
                            config.cancel()
                        } label: {
                            Text("Cancel")
                        }
                    }
                    
                    ToolbarItem(placement: saveButtonPlacement) {
                        Button {
                            config.done()
                        } label: {
                            Text("Save")
                        }
                    }
                }
            #if os(macOS)
                .padding()
            #endif
        }
    }
    
    private var editorTitle: String {
        config.recipe.isNew ? "Add Recipe" : "Edit Recipe"
    }
    
    private var cancelButtonPlacement: ToolbarItemPlacement {
        #if os(macOS)
        .cancellationAction
        #else
        .navigationBarLeading
        #endif
    }
    
    private var saveButtonPlacement: ToolbarItemPlacement {
        #if os(macOS)
        .confirmationAction
        #else
        .navigationBarTrailing
        #endif
    }
}

1의 샘플코드의 구조체에 필요한 데이터를 포함하고, 편집기의 상태를 변경하는 메서드가 있으므로 해당 구조체로 2의 샘플코드의 view에서 동작되는것을 확인한다.

2의 구조체는 view가 모양을 결정하는 데 사용할 상태 데이터를 포함하는 1의 구조체를 바인딩 변수 (@Binding var)로 선언한다.

Binding은 view에 필요한 데이터에 대해서 양방향 Read-Write를 제공한다.
그러나 데이터를 소유하지는 않으며 대신 다른 view가 바인딩하고 사용하는 인스턴스를 생성하고 소유한다.

2의 view 구조체는 바인딩 변수 config를 1의 구조체에 전달한다.
이 변수를 바인딩으로 전달하기 위해서 앞에 $ 기호를 붙인다.
1의 구조체는 바인딩으로 받기에 데이터를 읽고 쓸 수 있다.

버튼의 액션에서 done() 이나 cancel() 메서드를 사용하는 것은 내부의 동작을 한 줄로만 사용할 수 있게 도와준다.
내부의 ispresented나 shouldSaveChanges의 설정을 명시적으로 설정을 할 수 있지만 명령형 코드를 최소한으로 유지하면 선언적 인터페이스 코드를 더 읽기 쉽고 유지보수에 용이하게 해준다.

3. 다른 view에서 상태 변수 만들기

Document: Model data

ContentListView.swift

import SwiftUI

struct ContentListView: View {
    @Binding var selection: Recipe.ID?
    let selectedSidebarItem: SidebarItem
    @EnvironmentObject private var recipeBox: RecipeBox
    @State private var recipeEditorConfig = RecipeEditorConfig()

    var body: some View {
        RecipeListView(selection: $selection, selectedSidebarItem: selectedSidebarItem)
            .navigationTitle(selectedSidebarItem.title)
            .toolbar {
                ToolbarItem {
                    Button {
                        recipeEditorConfig.presentAddRecipe(sidebarItem: selectedSidebarItem)
                    } label: {
                        Image(systemName: "plus")
                    }
                    .sheet(isPresented: $recipeEditorConfig.isPresented,
                           onDismiss: didDismissEditor) {
                        RecipeEditor(config: $recipeEditorConfig)
                    }
                }
            }
    }
    
    private func didDismissEditor() {
        if recipeEditorConfig.shouldSaveChanges {
            if recipeEditorConfig.recipe.isNew {
                selection = recipeBox.add(recipeEditorConfig.recipe)
            } else {
                recipeBox.update(recipeEditorConfig.recipe)
            }
        }
    }
}

2의 view는 1의 인스턴스에 대한 바인딩을 가지고 있다.
에디터는 데이터를 읽고 쓸 수 있지만 데이터를 소유하고 있지 않다.
대신 3의 view 가 데이터를 생성하고 소유하며 SwiftUI가 이 데이터를 3의 view의 수명 동안 관리 한다.

3의 view 구조체는 사용자가 레시피 추가를 위해 레시피 편집기를 표시하는 역할도 하기에 1의 구조체 인스턴스를 만드는 이상적인 위치가 된다.

1의 구조체에 대한 변수 선언에는 State 프로퍼티 속성이 포함되어있으며(@State private var xxx) 이는 SwiftUI가 1의 구조체 인스턴스를 생성하고 관리하도록 지시한다.
view의 상태가 변경될 때마다 SwiftUI는 view를 다시 초기화하고 데이터 인스턴스를 view에 다시 연결하며 현재 데이터 상태를 반영하는 coputed 속성에 정의된 view를 다시 빌드한다.
Model data 문서를 참조하면 자세한 내용을 확인 할 수 있다.

1의 add함수가 데이터 인스턴스 내의 데이터를 변경할 때마다 SwiftUI는 3의 view 인스턴스를 재 초기화하고 1의 구조체를 관리하는 인스턴스에 다시 연결한다.
그 후에 SwiftUI는 현재 데이터 상태 반영을 위해 computed 속성에서 view를 재구성한다.

didMiss~ 함수는 해당 조건에 맞으면 사용자가 레시피를 수정 할 때 변경 사항을 저장한다.

참고자료

Struct: State
Struct: Binding

Document: Model data

기타

당연 틀린 부분 지적은 감사하나 비난은 정중하게 사양하겠다.

  • 글을 쓰면서 느끼는거지만 이거 개판임.
  • 그냥 단순히 혼자 코드 보면서 중요한거만 적고 있는거라 내용을 참 이해하기 어렵게 써놨다.
  • 나중에 문서 코드 말고 직접 만들어서 구현해보면서 해당 내용을 압축 정리해야겠다.
profile
"잘 할 수 있을까?"를 고민하기보단 재밌어 보이는건 일단 하고, 잘하기 위해 그냥 계속합니다.

0개의 댓글