[SwiftUI] 11. State and Data flow: 커스텀 바인딩을 사용해 정보 소스 정의

Sean·2023년 5월 4일
0

SwiftUI_튜토리얼

목록 보기
11/11

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

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

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

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

시작

상태 변수 대신 사용할 수 있는 커스텀 바인딩을 대안으로 제공한다.
앱의 다른 뷰에 바인딩되는 소스를 정의하는 가장 일반적인 방법은 State 속성을 사용하여 상태 변수를 선언하는 것이다.
그러나 @State 를 사용하여 정의할 수 없는 동적인 소스가 필요한 특수한 경우가 있을 수 있다.
예를 들어 이 튜토리얼에서는 레거시 id를 사용해 소스의 레시피를 검색해야 한다.
앱은 이를 위해 커스텀 바인딩을 반환하는 계산된 속성을 만들어 수행한다.

분석

1. 단일 데이터 원본 식별, 정의

Struct: StateObject
Property: projectedValue
Property: wrappedValue

DetailView.swift

import SwiftUI

struct DetailView: View {
    @Binding var recipeId: Recipe.ID?
    @EnvironmentObject private var recipeBox: RecipeBox
    @State private var showDeleteConfirmation = false
    
    private var recipe: Binding<Recipe> {
        Binding {
            if let id = recipeId {
                return recipeBox.recipe(with: id) ?? Recipe.emptyRecipe()
            } else {
                return Recipe.emptyRecipe()
            }
        } set: { updatedRecipe in
            recipeBox.update(updatedRecipe)
        }
    }

    var body: some View {
        ZStack {
            if recipeBox.contains(recipeId) {
                RecipeDetailView(recipe: recipe)
                    .navigationTitle(recipe.wrappedValue.title)
                    #if os(iOS)
                    .navigationBarTitleDisplayMode(.inline)
                    #endif
                    .toolbar {
                        RecipeDetailToolbar(
                            recipe: recipe,
                            showDeleteConfirmation: $showDeleteConfirmation,
                            deleteRecipe: deleteRecipe)
                    }
            } else {
                RecipeNotSelectedView()
            }
        }
    }
    
    private func deleteRecipe() {
        recipeBox.delete(recipe.id)
    }
}

이 튜토리얼 앱은 DetailView 에서 레시피의 세부 정보를 표시한다.
이 view는 레시피 id는 알지만 레시피 자체는 모르기에 모든 레시피를 담고 있는 데이터 저장소인 recipeBox에서 레시피를 검색한다.
view는 레시피를 검색해야 하기에 레시피의 상태 변수를 선언하는 대신 레시피의 단일 데이터로 커스텀 바인딩을 사용한다.

커스텀 바인딩을 사용하는 것은 SwiftUI의 유용한 기능 중 하나이지만, 항상 최선이 아닐 수 있다.
상태 변수 또는 객체를 사용할 수 없는 경우에만 사용하도록 하며 대부분의 경우에는 view 내부에 로컬인 상태는 State 변수로, 공유 데이터 모델인 경우에는 StateObject로 단일 데이터를 정의하여 SwiftUI가 값이나 객체를 관리하도록 하는것이 좋다.

레시피의 값을 가져오기 위해 이 튜토리얼에서는 State 변수를 선언하는 대신에 속성을 구현한다.
해당 속성은 레시피를 반환하지 않는다. 대신 바인딩 된 레시피를 반환한다.
이를 통해 view는 레시피를 다른 view와 단일 데이터로 공유할 수 있다.

바인딩은 값에 대한 읽기 및 쓰기 엑세스를 제공한다. 이 레시피 값에 대한 엑세스를 제공하기 위해 레시피 속성은 초기화 메서드를 사용해 바인딩을 만든다.

바인딩의 get은 데이터 저장소에서 레시피를 검색하는데 사용한다.
set은 레시피를 새 레시피 값으로 업데이트하는데 사용한다.

DetailView는 레시피를 바인딩 값으로 RecipeDetailView에 전달하며 세부 정보 view에서 레시피 값을 읽고 쓸 수 있게 한다.

계산된 속성인 레시피가 바인딩을 반환하므로 state 변수를 바인딩으로 전달시에 필요한 접두사인 $ 를 포함할 필요가 없다.
State로 정의된 변수 의 경우 $ 접두사는 SwiftUI가 projectValue를 전달하도록 지시한다.

navigationTitle()에서 문자열 값이 아닌 문자열에 대해서는 바인딩을 받지 않는다.
따라서 view는 레시피 바인딩의 wrappedValue를 전달한다.

wrappedValue는 바인딩이 참조하는 기본값이다.
레시피 속성이 바인딩을 반환하므로 해당 속성의 wrappedValue는 실제 레시피의 값이다.
따라서 해당 코드에서 레시피 바인딩의 wrappedValue를 가져와 레시피의 title을 전달한다.

참고자료

Struct: StateObject

Property: projectedValue
Property: wrappedValue

기타

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

  • 도대체 source of truth 가 한국말로 뭔데?
    위에서는 소스나 코드라고 쓰고 밑에서는 단일 데이터라고 번역했다.

  • 일단 공식문서 튜토리얼은 이거로 종료가 되었다.
    후에 해당 지식을 가지고 정리본을 만들 생각이다.

profile
"잘 할 수 있을까?"를 고민하기보단 재밌어 보이는건 일단 하고, 잘하기 위해 그냥 계속합니다.

0개의 댓글