반응형

Raywenderlich 사이트의 강좌 번역본입니다.

[원문 : https://www.raywenderlich.com/11781349-understanding-data-flow-in-swiftui]

Understanding Data Flow in SwiftUI

SwiftUI view 에서 데이터가 변경하면 UI를 자동으로 업데이트 할 수 있습니다. 그것은 마치 마법처럼 보입니다. 하지만 이것은 마법이 아니며, data flow(데이터 흐름) 라고 합니다. 

데이터 흐름(Data flow)는 SwiftUI의 핵심(key pillars)중 하나입니다. UI를 정의하고, 동작을 위한 데이터를 제공하고, 대부분의 경우에, 잘 동작합니다. 모든것이 동기화 되어 있고, 데이터 변경에 따라 UI가 잘못된 상태가 될 위험이 없습니다.

SwiftUI를 효과적으로 사용하기 위해, 데이터 흐름의 개념을 이해해야 합니다. 이번 튜토리얼에서, Fave Flicks라는 이름의 앱을 만들것입니다. 데이터 흐름을 관리하는 다른 방법과 앱에 안전하게 새로운 기능을 추가하는 방법을 배우게 될것입니다.

좀 더 구체적으로 살펴보면 다음과 같은 것들을 배우게 될 것입니다.

  • 다른 데이터 흐름 프로퍼티 래퍼(Diferrent data flow property wrappers).
  • 원천 자료 관리하기(Managing sources of truth).
  • 뷰에서 생성된 데이터 처리하기(Handling data created by your view)
  • 뷰에 외부 데이터 전달하기(Passing external data to your view).
  • 뷰들 간에 데이터 공유하기(Sharing data between views).

이를 완료하면, SwiftUI에서 뷰에 데이터를 넣는 것이 훨씬 더 편안하게 느껴질 것입니다. 

시작하기(Getting Started)

자료 다운로드

우선, 이 튜토리얼의 위와 아래에 있는  자료 다운로드로 프로젝트 자료를 다운로드 하세요. 그리고나서 Xcode로 시작 프로젝트를 열어서 빌드하고 실행하세요.

Fave Flicks는 좋아하는 영화의 맞춤형 목록입니다. 지금은, 영화의 기본 목록과 새로운 항목을 추가하기 위한 화면으로 구성되어 있습니다.

좋아하는 영화를 저장하고 싶은가요? 곧 새로운 기능을 추가하고 앱을 완성하기 위해 데이터 흐름의 마법을 통합할 것입니다. :]

State의 문제점 (The Problem With State)

Apple이 SwiftUI를 만들기로 결정한 중요한 이유가 몇가지 있으며, 하나는 state의 관리와 관련 있습니다. 이를 설명하기 위해서, 여기에 뷰를 수동으로 업데이트해야 할때 다른 상태(states)를 관리하는 것이 어떻게 문제가 되는지를 보여주는 몇가지 시나리오가 있습니다. 

UIKit을 사용하는 경우, 데이터가 변경될때 UI를 업데이트 해주는것을 여러분이 직접해줘야 합니다. 예를들어, Fave Flicks에 새로운 영화를 추가할때마다, 메인화면에 있는 영화 목록에 나오길 원합니다. 이런 일은 어렵지는 않지만, 앱에 새로운 기능들을 추가하다보면 까먹기 쉽습니다.

UI 데이터를 최신상태로 유지하는 것 외에도, 뷰를 로딩하는 동안 로딩 스피너를 표시해야 할지도 모릅니다. 영화 목록이 비어있는 경우에, 사용자에게 무언가를 추가하라는 메시지를 표시 할수 있습니다. 오류가 발생하는 경우에, 다른 모든것을 숨기고 오류 메시지만 보여줄수 있습니다. 이것을 데이터가 변경될때마다 해야합니다.

이러한 수동 업데이트 과정(UI를 업데이트 하기 위해 코드를 작성하고, 데이터가 변경됨에 따라 일부를 추가하거나 삭제합니다)을 명령형 프로그래밍(imperative programming)이라고 합니다. 이러한 패러다임에 의존하면 뷰 컨트롤러가 복잡해지고 거대해지며, iOS 개발의 문제입니다.

이를 해결을 위한 SwiftUI(SwiftUI to the Rescue)

명령형(imperative) 접근법 대신에(In place of), SwiftUI는 UI를 쉽게 업데이트하는 선언적 프로그래밍(declarative programming) 스타일을 채택했습니다. UI를 어떻게(how) 업데이트하는것에 집중하는 대신에, 무엇을(what) 업데이트 할 것인지에 집중합니다. SwiftUI의 데이터 흐름(data flow)이 나머지를 처리해줍니다. 

데이터가 변경될때마다, SwiftUI는 최신 데이터에 맞게 처음부터 모든 뷰를 다시 만듭니다. 이는 로딩 스피너, 결과 테이블과 오류 메시지를 한번에 모두 표시할 위험이 없다는 것을 의미합니다. 

이후에, 앱이 원하는대로 정확하게 응답하도록 만들기 위해 UI를 선언하고 데이터 흐름을 사용하는 방법을 알게 될 것입니다.

내부 상태로 동작하기(Working With Internal State)

첫번째 새로운 기능은, 프로필 페이지를 설정하는 것입니다. 여기에서 이름과 좋아하는 영화 장르를 설정할 수 있습니다. 빌드하고 실행하세요. 네비게이션 바 왼쪽에 있는 사용자 아이콘을 탭하세요. 

현재 이 화면은 약간 비어있습니다. 다음과 같은 것을 원할 것입니다:

  1. 이름을 추가할 수 있는 텍스트 필드를 보여줍니다.
  2. 이름을 변경하면, 네비게이션 바 제목도 업데이트 합니다.

UserView.swift을 여세요. 파일의 내용을 다음에 오는 것으로 교체하세요.

import SwiftUI

struct UserView: View {
  // 1
  @State private var userName = ""

  var body: some View {
    NavigationView {
      Form {
        Section(header: Text("User")) {
          // 2
          TextField("User Name", text: $userName)
        }
      }
    }
    // 3
    .navigationBarTitle(Text("\(userName) Info"), displayMode: .inline)
    .navigationBarItems(
      trailing:
        Button(action: updateUserInfo) {
          Text("Update")
        }
    )
  }

  // 4
  func updateUserInfo() {
  }
}

 

위 코드 설명:

1. state 프로퍼티로 사용자이름을 저장합니다. 이 의미는 나중에 자세히 알아봅니다.
2. TextField에 userName를 바인딩으로 전달합니다. 사용자가 텍스트 필드에 입력할때, 텍스트 필드는 userName을 업데이트 합니다. 이는 userName에 TextField를 바인딩(binds)합니다.
3. 네비게이션바에 userName을 보여줍니다. userName이 변경될때마다, 네비게이션 바 제목은 업데이트합니다.
4. 사용자가 네비게이션 바에 있는 업데이트(Update)를 탭할때 updateUserInfo()를 호출합니다. 나중에 내용을 추가할 것입니다. 

빌드하고 실행하세요. 네비게이션 바에 있는 사용자 버튼을 다시 탭하고, 새로운 뷰를 보게 될 것입니다! 텍스트 필드에 입력하고 입력한 각각의 문자로 네비게이션 바 제목을 업데이트합니다. 보세요! :]

이 마법은 userName 앞에 추가한 @state 프로퍼티 래퍼에 의해 동작합니다. 이는 SwiftUI가 자체적으로 userName을 소유하고 관리하는 것을 의미합니다. 상태 프로퍼티의 값이 변경될때, SwiftUI는 상태에 따라 뷰를 다시 만듬으로써 항상 최신의 상태로 유지합니다. 

SwiftUI에서의 프로퍼티 래퍼(Property Wrappers in SwiftUI)

이 시점에, @State가 어떻게 userName의 동작을 변경하는지 궁금해 할 것입니다. @State는 바인딩된 프로퍼티의 값이 변경될때마다 UI를 업데이트하는 프로퍼티 래퍼(property wrapper) 타입입니다. 

프로퍼티를 선언할때 @ 기호가 있으면, 이는 프로퍼티 래퍼(property wrapper)를 사용하고 있는 것을 의미합니다. SwiftUI에서 연관된 데이터 흐름(data flow) 프로퍼티 래퍼를 사용해서 데이터 흐름(data flow)을 제어합니다.

  • @State
  • @Binding
  • @StateObject
  • @ObservedObject
  • @EnvironmentObject
  • @Environment

참조 타입과 값 타입(Reference and Value Types)

@State 프로퍼티는 String, Int, Bool, struct, enum과 같은 값 타입(value type)에서만 유용합니다. 뷰가 클래스와 같은 참조 타입(reference type)을 소유하길 원하는 경우에, @State 대신 @ObservedObject 또는 @EnvironmentObject를 사용합니다. 이것들은 튜토리얼 뒷부분에서 배우게 될 것입니다.

빌드하고 실행하세요. 영화 추가(Add Movie) 뷰로 이동하기 위해 우측 상단 모서리에 있는 + 버튼을 탭하세요. 제목을 입력하고, 장르를 선택하고 별점을 추가하세요.

입력을 완료하면, 추가(Add)를 탭하세요. 영화 목록으로 돌아가지만, 새 영화는 어디에도 없습니다.

걱정하지 마세요. 항목을 성공적으로 만들었지만, 아직 보여지지 않습니다.

MoveList.swift를 여세요. 뷰의 상단에 movieStore가 정의되어 있는것을 보게 될 것입니다. 배열은 추가하거나 삭제한 모든 영화를 추적합니다. 다시 빌드하고 실행하면 예상했던 것처럼 새로운 영화가 나타날것입니다.

여기에서 문제는 새로운 영화를 추가할때 뷰가 자동으로 업데이트되는 것을 알지 못하는 것입니다. moveStore로 부터 최신 영화 목록을 가져오도록 하는게 없습니다. 이는 movieStore가 예전의 평범한 프로퍼티로 선언했기 때문입니다.

var movieStore = MovieStore()

 

객체 관찰하기(Observing Objects)

movieStore가 변경할때 뷰를 업데이트하기 위해서, 그것을 관찰(observe)해야 합니다. 적절한 프로퍼티 래퍼(property wrapper)를 추가하는 것은 데이터가 변경으로 UI 업데이트를 관리하는데 도움이 됩니다. MovieList.swift에서, movieStore에 @ObservedObject 프로퍼티 래퍼를 추가하면 다음과 같습니다.

@ObservedObject var movieStore = MovieStore()

 

관찰되는 객체(observed object) 관찰가능한(observable) 클래스에 대한 참조입니다. 관찰되는 @ObservedObject 프로퍼티 래퍼를 사용해서 객체가 업데이트할때마다 뷰에게 알려주므로써, 변경될때마다 뷰를 업데이트 합니다.

두가지 방식 모두 동작합니다. 관찰되는(observed) 객체인 프로퍼티에 텍스트 필드를 바인딩하는 경우, 텍스트 필드를 업데이트 해서 관찰되는(observed) 객체의 프로퍼티가 업데이트 될 것입니다.

movieStore를 관찰할 수 있는 것은 observable 이기 때문입니다. 이는 MovieStore이 ObservableObject 프로토콜을 준수하기 때문에 동작합니다. 

다시한번, 빌드하고 실행하세요, 새로운 영화를 추가하세요. 만세! 목록은 자동으로 업데이트 합니다. :]

여기에서 왜 @State는 동작하지 않을까?(Why @State Doesn’t Work Here)

 movieStore를 @State로 사용할 수 없는지 궁금해 할것입니다. 

@State는 값 타입전용이므로, 참조 타입인 movieStore에 사용할 수 없습니다. @ObservedObject는 @State 처럼 변경에 응답할 수 있지만, 한가지가 다릅니다.

데이터가 트리거(tragger) 될때마다 뷰를 업데이트하며, SwiftUI는 뷰의 body를 다시 계산하므로써, 뷰를 새로고침합니다. 이런 경우에, 뷰에 @ObservedObject가 선언되어 있다면, SwiftUI는 뷰를 새로고침할때 객체를 다시 만듭니다.

State 변수와는 다르게 동작합니다. @State를 사용할때, SwiftUI는 해당 프로퍼티의 생명주기를 제어합니다. 뷰가 새로고침할때 @State 프로퍼티는 값을 유지합니다. 

다행스럽게, @State는 참조 타입에 대한 지원을 함께 합니다:  @StateObject, @State와 같은 동작을 하지만, 특별히 참조타입에 사용합니다.

다시한변, movieStore의 선언을 다음과 같이 교체하세요.

@StateObject var movieStore = MovieStore()

 

빌드하고 실행하세요, 이전과 동일한 동작을 보게 될것이지만, 이제는, SwiftUI는 movieStore의 생명주기를 관리합니다. 

@State, @StateObject, 또는 @ObservedObject 사용을 결정하려는 경우에, 선언하는 프로퍼티에 대해 2개의 질문을 할 수 있습니다.

  • 값 타입 또는 참조 타입인가? 값 타입인 경우에는 @State를 사용하세요.
  • SwiftUI가 프로퍼티의 생명주기를 관리하길 원하는가? 뷰 안에서만 객체를 사용만 하는 경우에 프로퍼티는 @StateObject가 좋습니다. 하지만 뷰의 바깥쪽에서 생성하거나 사용하는 경우에, @ObservedObject가 훨씬 더 어울립니다.

단일 원천 자료(A Single Source of Truth)

SwiftUI 데이터 흐름(data flow)은 데이터가 단일 원천 자료(single source of truth)라는 개념을 기반으로 합니다. 단일 원천 자료는 데이터의 값을 결정하는 것은 하나이고 한곳에서만 합니다. 

예를들어, UserView에서, 사용자 이름을 설정합니다. 코드에서 사용자 이름의 값을 결정하는 하나의 원천 자료(source of truth)이며, 한 곳에서만 있어야 합니다. 사용자 이름이 필요한 다른 구성요소는 해당 원천 자료(source of truth)를 참조해야 합니다. 이는 동기화 상태를 유지하므로써, 원천 자료가 변경될때 모든 참조도 변경됩니다.

다음으로, 이런 개념을 보여주는 새로운 뷰를 추가할 것입니다. 영화는 언제나 장르가 있으므로, 사용자가 좋아하는 장르를 가지고 있으면 편리하지 않을까요? 따라서, 새 영화를 추가할때, 좋아하는 장르가 첫번째로 제안될 것입니다. 

Xcode에서 메뉴 바에 있는 File > New > File…로 새로운 뷰를 추가하세요. SwiftUI View를 선택했는지 확인하고 Next를 클릭하세요. 이름은 GenrePicker이고 Create를 클릭하세요. 새 뷰의 내용을 다음에 오는 것으로 교체하세요.

import SwiftUI

struct GenrePicker: View {
  // 1
  @Binding var genre: String

  var body: some View {
    // 2
    Picker(selection: $genre, label: Spacer()) {
      // 3
      ForEach(Movie.possibleGenres, id: \.self) {
        Text($0)
      }
    }
    .pickerStyle(WheelPickerStyle())
  }
}

struct GenrePicker_Previews: PreviewProvider {
  static var previews: some View {
    // 4
    GenrePicker(genre: .constant("Action"))
  }
}

 

위 코드에서, AddMovie로 부터 장르 선택기(genre picker)를 가져와서 재사용가능한 뷰에 넣었습니다. 하지만 하나가 변경되었습니다: genre에 @Binding 프로퍼티 래퍼가 추가 되었습니다.

다음은 무엇을 하는지 입니다.

  1. 선택된 장르는 genre 프로퍼티에 저장되며, @Binding 프로퍼티 래퍼가 추가 되었습니다.
  2. 선택기 휠(picker wheel)을 만듭니다. 사용자가 선택기의 선택된 행을 변경할때, genre를 설정할 것이며, 그 반대의 경우에도 마찬가지입니다. - genre를 설정하면 선택기의 선택된 행을 변경합니다.
  3. 선택기(picker)에서의 행들은 Movie.possibleGenres를 반복해서 생성되고 Text 뷰에서 각 값을 보여줍니다.
  4. 미리보기에서 genre에 대한 값을 전달해야 하지만, 일반 문자열을 전달해도 잘리지 않습니다. Binding이 필요합니다. 미리보기이기 때문에, .constant로 아무것도 하지 않는 바인딩을 생성할 수 있습니다. 

하나의 상태(state) 프로퍼티는 단일 원천 자료(source of truth)이며,  바인딩(binding)하는것은 다른 프로퍼티를 참조합니다. - 일반적으로 상태 프로퍼티는 다른곳에서 선언됩니다. 바인딩하면 원천 자료를 참조하고 업데이트 합니다.

GenrePicekr의 목적은 사용자가 좋아하는 장르를 선택하기위해  AddView 또는 다른곳에서 GenrePicker를 사용하도록 미리 설정할 수 있습니다. 이는 GenrePicker가 설정한 장르를 소유(own) 하지 않는 것을 의미하며, @Binding을 사용합니다. 

바인딩으로 상태 전달하기(Passing State as a Binding)

이제 GenrePicker가 준비되었으며, 사용할 때입니다.

AddMovie.swift를 다시 열어주세요. 텍스트 제목이 "Genre"인 Section을 찾으세요. 내용을 다음으로 교체하세요.

GenrePicker(genre: $genre)

 

Picker를 새로운 GenrePicker로 교체합니다. AddMovie에 있는, genre는 상태(state) 프로퍼티 입니다. GenrePicker에 $genre를 전달하면, 실제로는 프로퍼티에 바인딩(binding)을 전달합니다. 사용자가 GenrePicker에서 장르(genre)를 수정할때마다, AddMovie의 genre의 값이 변경될 것입니다.

빌드하고 실행하세요. 선택기(picker)는 이전과 같아보이지만, 이제 재사용 가능합니다.

좋아하는 장르 설정하기(Setting a Favorite Genre)

UserView.swift를 여세요. 뷰의 상단에, userName을 정의한 곳 뒤에 다음 줄을 추가하세요.

@State private var favoriteGenre = ""

 

다음으로, Form안쪽에, 기존에 있던 것 뒤에 새로운 Section을 추가하세요.

Section(header: Text("Favorite Genre")) {
  GenrePicker(genre: $favoriteGenre)
}

 

GenrePicker를 사용해서 사용자가 좋아하는 장르를 설정합니다.

빌드하고 실행하세요. Add Movie 화면에서와 마찬가지로 마찬가지로 사용자 뷰에서 선택기(picker)를 보게 될 것입니다.  GenrePicker의 값이 변경될때, favoriteGenre를 업데이트 할 것입니다.

UserView를 닫고 다시 돌아오는 경우에, 선택했던것이 유지되지 않는 것을 알게 될 것입니다. 잠시 뒤에 이를 처리할 것입니다.

외부 데이터로 작업하기(Working With External Data)

현재, UserView는 @State를 사용해서 사용자이름과 좋아하는 장르를 설정합니다. 앱의 다른 곳에 전달하길 원하므로, 현재 사용자 데이터를 추적하는 UserStore 클래스를 만들것입니다.

Xcode의 메뉴바에서 File > New > FIle…로 가서 새로운 뷰를 만드세요. Swift File을 선택하고 Next를 클릭하세요. 이름은 UserInfo.swift로 하고 Create를 클릭하세요. 내용을 다음에 오는 것으로 교체하세요.

import Foundation

struct UserInfo {
  let userName: String
  let favoriteGenre: String
}

 

UserInfo는 사용자를 표현하는 Swift 구조체입니다. 다음으로, 동일한 방법으로 다른 파일을 생성하세요. 이름은 UserStore.swift입니다. 내용을 다음에 오는 것으로 교체하세요.

import Combine

// 1
class UserStore: ObservableObject {
  // 2
  @Published var currentUserInfo: UserInfo?
}

 

UserStore는 여러분이 이전에 선언했던 UserInfo 구조체를 사용해서 현재 사용자를 추적합니다. Combine 프레임워크와 프로퍼티 래퍼(property wrappers) 덕분에, 위 코드에서 많은 새로운 일이 벌어집니다.

  1. UserStore는 ObservableObject를 준수합니다. 이것은 SwiftUI에 의해서 관찰되는(observed)는 것이 가능합니다. 
  2. @Published 프로퍼티 래퍼는 ObservableObject의 관찰자(observers)에서 업데이트를 시도합니다. 배포된(published) 프로퍼티가 변경될때마다, 관찰자(observers)에게 통지합니다. @Published 프로퍼티 래퍼로 currentUserInfo를 선언해서, 새로운 값을 설정하면 UserStore를 관찰하는(observing) 모든 뷰가 업데이트 할 것입니다.

관찰가능한 객체 관찰하기(Observing an ObservableObject)

때로는, 데이터의 원천 자료(source of truth)는 SwiftUI 뷰 내부에 존재하지 않습니다. 이 경우에는, 클래스가 SwiftUI와 상호작용하도록 ObservableObject 프로토콜을 사용합니다. ObservableObject는 SwiftUI 뷰에 업데이트를 보내는 원천 자료(source of truth)이고, UI 변경에 따라 업데이트 합니다. 

지금까지, SwiftUI 뷰에서 참조할 수 있는 ObservableObject를 준수하는 클래스를 만들엇씁니다: UserStore. 다음으로, 관찰해야하며, 다음에 오는 곳들에서 UserStore가 필요합니다. 

  • UserView : 사용자 이름과 좋아하는 장르 설정
  • MovieList : 사용자의 이름 표시
  • AddMovie : 기본으로 좋아하는 장르 사용하기

Environment 사용하기(Using the Environment)

SwiftUI에서, environment는 뷰와 그 자식간에 공유되는 변수와 객체를 저장합니다.

관찰가능한(observable) 객체를 참조해야 하고, MovieSotre에서 했던 것처럼 @ObservedObject를 사용하는 것이며, 필요할때마다 UserStore에 참조를 전달합니다.

많은 중첩된 뷰들로된 큰 앱에서는 재미가 없습니다. 이 때문에, UserStore는 environment object의 좋은 후보자(candidate)입니다. 필요한 모든 뷰에 객체를 전달하는 대신에, envrionment object는 원조 뷰에서 제공되고, 모든 하위 뷰에서 사용가능합니다.

이는 의미합니다. UserStore의 인스턴스를 만들고 MovieList에 environment object로 전달하는 경우에, MovieList의 모든 자식뷰들은 자동으로 UserStore를 가져올 것입니다.

environment object를 사용하기 위해서 다음에 오는 것을 수행해야합니다.

  1. ObservableObject를 준수하는 클래스 만들기
  2. 모든 관찰자를 업데이트하기 위해, 클래스에서 적어도 하나의 @Published 프로퍼티 래퍼가 있거나 수동으로 ObservableObject에서 요구된 objectWillChange 게시자(publisher)를 제공해야 합니다.
  3. 뷰를 생성할때 environmentObject() 뷰 수정자(modifier)를 사용해서 뷰에 관찰 가능한 클래스의 인스턴스를 전달합니다.

UserStore 클래스가 생성될때, 이미 처음 2단계가 완료되었습니다. 다음으로, MovieList의 environment로 UserStore를 전달해야 합니다. SceneDelegate.swift를 여세요.

scene(_:willConnectTo:options:)에서, contentView를 만드는 첫번째 줄을 다음에 오는 것으로 교체하세요.

let contentView = MovieList().environmentObject(UserStore())

 

MovieList를 생성하고나서, envrionmentObject(_:)를 사용해서 UserStore의 인스턴스를 envrionment로 전달하세요. 이제 영화 목록과 그 계층구조에서 사용할 수 있습니다.

다음으로, MovieList.swift를 여세요, UserStore를 사용하기 위해, movieStore를 선언하는 줄 바로 앞에, 구조체의 시작부분에 다음에 오는 것을 추가하세요: 

@EnvironmentObject var userStore: UserStore

 

@EnvironmentObject는 뷰 또는 원조(ancestor) 뷰에 전달된 envrionment 객체들을 사용합니다.

사용자 네비게이션 항목에 사용자 이름을 표시할 수 있습니다. MovieList.swift에서, person.fill 시스템 아이콘으로된 Image를 찾으세요. 해당 줄을 다음과 같이 교체하세요.

HStack {
  // 1
  userStore.currentUserInfo.map { Text($0.userName) }
  // 2
  Image(systemName: "person.fill")
}

 

위 코드 에서 하는 것:

  1. 만약 존재하는 경우에 Text로 보여주기 위해 현재 사용자의 userName 프로퍼티를 가져옵니다. currentUserInfo는 옵셔널(optional) 이지만, 뷰의 본문에서 if let을 사용할 수 없습니다. map를 사용해서 currentUserInfo가 존재하는 경우에 Text를 생성할 것입니다.
  2. HStack에 이전과 같은 이미지를 추가하세요.

잘했습니다! 빌드하고 실행하세요. UserStore에 사용자 이름을 전달하지 않았으며, 변경되는 것을 볼수 없습니다.

체인 아래에서 Envrionment Object를 사용하기(Using an Envrionment Object Down the Chain)

사용권한을 얻기 위해 UserView에 UserStore를 전달할 필요가 없습니다. environment는 이미 처리했습니다. 대신에, UserView.swift를 열고, MovieList와 마찬가지로 UserView의 상단에 다른 변수들과 함께 userStore를 추가하세요.

@EnvironmentObject var userStore: UserStore

 

비어 있는 updateUserInfo()에 다음에 오는 것을 추가하세요.

let newUserInfo = UserInfo(userName: userName, favoriteGenre: favoriteGenre)
userStore.currentUserInfo = newUserInfo

 

Update를 탭해서 updateUserInfo()를 호출합니다. 이 메소드는 이 뷰에서 사용하는 2개의 state 프로퍼티로 새로운 userInfo 객체를 만듭니다: userName과 favorityGenre. 그리고나서 userStore 개시된 프로퍼티를 업데이트함으로써 앱의 다른곳에서 사용할 수 있습니다. UserStore는 ObservableObject이므로, 모든 관찰자(observers)에서 업데이트를 시도할 것입니다. 

body의 끝부분에서, navigationBarItems 뷰 수정자 뒤에, 또 다른 뷰 수정자를 추가하세요.

.onAppear {
  userName = userStore.currentUserInfo?.userName ?? ""
  favoriteGenre = userStore.currentUserInfo?.favoriteGenre ?? ""
}

 

뷰가 나타날때 userStore로 부터 현재 userName과 favoriteGenre를 가져옵니다.

빌드하고 실행하세요. 사용자 버튼을 탭하고, 이름을 입력하고, Update를 탭하세요. 그리고나서 영확 목록으로 돌아가기위해 네비게이션바 왼쪽 버튼을 탭하세요. 사용자 버튼 옆에 이름이 보여야 합니다. 만세! :]

Update를 탭해도 사용자 뷰로 돌아가지 않는다는 것을 눈치 챘을수도 있습니다. 다음에 추가할 것입니다.

환경 변수(Envrionment Variables)

UserView.swift에서, 남은 프로퍼티 선언과 함께 다음 줄을 추가하세요.

@Environment(\.presentationMode) var presentationMode

 

이는 새로운 프로퍼티 래퍼(property wrapper)이며, 어떤 의미로는@EnvironmentObject와 비슷합니다: 둘다 뷰 계층구조에서 공유됩니다. 다른 점은 무엇이든 environment object로 추가할수 있지만, evnrionment values는 키-값(key-value)쌍과 더 비슷합니다.

@EnvrionmentObject와 @Environment 모두 환경(environment)을 공유하지만, 매우 다른 목적으로 사용합니다. 일반적으로 앱에서 종속성을 관리하기 위해 @EnvrionmentObject를 사용합니다. SwiftUI는 뷰와 그 자식들에 대한 설정을 관리하기 위해 @Environment를 사용합니다. 각각의 뷰는 계층구조의 뷰의 동작을 변경하는데 사용할 수 있는 환경 변수(envrionment values)를 제공합니다. 이런 값들 중 하나가 presentationMode입니다. 

updateUserInfo()의 끝부분에 다음에 오는 줄을 추가하세요.

presentationMode.wrappedValue.dismiss()

 

presentation mode는 뷰 계층구조간에 공유하므로, 환경 변수(envrionment variable)가 됩니다. SwiftUI는 현재 보여지는 것을 관리하기 위해 presentation mode를 사용합니다. dismiss()를 호출해서, 뷰 계층구조는 현재 뷰를 닫고, 이 경우에는 UserView입니다.

빌드하고 실행하세요. 사용자 정보를 업데이트하고 Update를 택하고 앱에서 사용자 뷰를 닫는 방법을 확인하세요.

관찰하는 객체(Observing Objects)

영화를 추가할때 기본 장르를 업데이트하기 위해, AddMovie에 있는 UserStore를 참조를 가져와야 합니다.

AddMovie.swift를 여세요. 뷰의 상단쪽, movieStore의 선언 바로 아래에, 다음 코드를 추가하세요.

@EnvironmentObject var userStore: UserStore

 

다음으로, AddMovie의 body내부에서 NavigationView의 닫기 괄호를 찾으세요. body의 끝부분 이전에 마지막 닫기 괄호여야 합니다. 다음에 오는 뷰 수정자를 추가하세요.

.onAppear { genre = userStore.currentUserInfo?.favoriteGenre ?? "" }

 

사용자의 좋아하는 장르가 하나 있다면, genre을 설정합니다. 데이터 흐름(data flow) 덕분에, GenrePicker를 선택하고나서 좋아하는 장르가 업데이트 됩니다.

마지막으로, 빌드하고 실애하세요. 사용자 아이콘을 클릭하고 좋아하는 장르를 설정하세요. 변경된 것을 저장하기 위해 Update를 클릭하세요.

이제, Add Movie화면으로 이동하세요. 선택기(picker)에서 선택했던 좋아하는 장르를 보게 될 것입니다. 만세!

올바른 프로퍼티 래퍼 선택하기(Choosing the Reight Property Wrapper)

데이터 흐름(data flow)은 처음에는 많은 부분을 차지하고, 작업에 적합한 도구를 선택하는 방법이 궁금 할 것입니다. 

업데이트에 반응하는 무언가를 관찰하는 것이 필요하지 않는 경우에, 일반 프로퍼티가 어울립니다. 무언가 변경 될때마다 뷰를 업데이트를 원하는 경우에, 다음에 오는 질문들을 사용해서 결정하세요.

여기에서 어디로 가야하나요?(Where to Go From Here?)

튜토리얼 상단이나 하단에 있는 자료 다운로드를 사용해서 최종 프로젝트를 다운로드 합니다.

축하합니다! SwiftUI에서 데이터 흐름(data flow)과 프로퍼티 래퍼를 마스터 했습니다. 원천 자료(sources of truth)의 개념에 대해 배웠으며, 이제 모든 데이터를 최신으로 유지할 수 있습니다. 또한 올바른 상황에 맞게 사용하는 프로퍼티 래퍼를 식별했습니다.

데이터 흐름(data flow)을 포함해서, SwiftUI에 대해 많은 것을 배우고자 하는 경우에, SwiftUI by Tutorials 책에 필요한 것이 있습니다.

또한 Apple 문서에 있는 State and Data Flow를 참조할 수 있습니다.

데이터 흐름(data flow)을 이해하면 SwiftUI에서의 데이터 작업이 쉽고, 이 튜토리얼로 더 명확해졌으면 합니다.

반응형
Posted by 까칠코더
,