ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Ch9. State & Data Flow
    Ray Wenderlich/SwiftUI 2021. 2. 7. 20:26

    Ch9. State & Data Flow

    SwiftUI는 많은 장점을 가지고 있는데

    • Declarative: View를 구현하지 않고 선언하는 방식으로 구현한다.
    • Functional: 같은 State에서 항상 같은 UI 결과물을 갖는다.
    • Reactive: State가 변경되면 SwiftUI가 자동으로 UI를 업데이트한다.

    State

    이 책에서 @State라는 문법을 많이 봤다.

    var numberOfAnswered = 0
    var numberOfQuestions = 5
    
    var body: some View {
      // 1
      Button(action: {
        // 2
        self.numberOfAnswered += 1
      }) {
        // 3
        HStack {
          Text("\(numberOfAnswered)/\(numberOfQuestions)")
            .font(.caption)
            .padding(4)
          Spacer()
        }
      }
    }
    

    //2 에서 에러가 발생한다.

    Why?? body내에서 view의 State를 변경할 수 없다.

     

    그렇다면 numberOfAnswered를 별도의 Struct에 집어 넣는다면?

    struct ScoreView: View {
      var numberOfQuestions = 5
    
      // 1
      struct State {
        var numberOfAnswered = 0
      }
    
      // 2
      var state = State()
      
      var body: some View {
        ...
      }
    }
    

    똑같은 에러가 발생한다. Struct는 Value type이기 때문에 예상했던 결과다.

     

    Embeding the state into a class

    Wrapper class를 만들어보자.

    class Box<T> {
      var wrappedValue: T
      init(initialValue value: T) { self.wrappedValue = value }
    }
    
    struct State {
      var numberOfAnswered = Box<Int>(initialValue: 0)
    }
    

    작동은 할 것이다.

     

    The real State

    이와 유사한 기능을 SwiftUI에서 제공하고 있다.

    var _numberOfAnswered = State<Int>(initialValue: 0)
    

    변수명앞에 _를 사용했다. 전체 코드를 한 번 보자.

    struct ScoreView: View {
      var numberOfQuestions = 5
    
      var _numberOfAnswered = State<Int>(initialValue: 0)
    
      var body: some View {
        Button(action: {
          self._numberOfAnswered.wrappedValue += 1
          print("Answered: \(self._numberOfAnswered.wrappedValue)")
        }) {
          HStack {
            Text("\(_numberOfAnswered.wrappedValue)/\(numberOfQuestions)")
              .font(.caption)
              .padding(4)
            Spacer()
          }
        }
      }
    }
    

    State는 무엇인가?

    애플 문서에는 이렇게 나온다.

    A property wrapper type that can read and write a value managed by SwiftUI.
    SwiftUI managaes the storage of any property you declare as a state. When the state value changes the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view.

    그렇다면 State<Value>, @State, $간에는 무슨 관계가 있는걸까?

    var _numberOfAnswered = State<Int>(initialValue: 0)
    

    위 코드를 이렇게 바꿔보자.

    @State var numberOfAnswered = 0
    

    뭔가 익숙하다. 이 코드만 변경하여 컴파일해도 잘 된다.

    @State로 선언된 프로퍼티는 프로퍼티 래퍼(wrapper)이다. 컴파일러는 State<Int>타입의 프로퍼티를 _(underscore)를 붙인 채 하나 생성한다.

     

    위 전체코드 중 Button의 핸들러 내부 코드를 보자.

    self._numberOfAnswered.wrappedValue += 1
    print("Answered: \(self._numberOfAnswered.wrappedValue)")
    

    위 코드는 이렇게 바꿀 수 있다.

    self.numberOfAnswered += 1
    print("Answered: \(self.numberOfAnswered)")
    

    이렇게 바꿔도 똑같이 작동한다. 컴파일러가 알아서 위의 코드 형태로 변경할 것이다.

     

    정리해보자.

    View내부에 프로퍼티가 있고, 이 프로퍼티를 바꾼다고 해서 View가 업데이트 되지는 않는다. 이 프로퍼티를 @State로 선언함으로써, View는 프로퍼티 값의 변경에 반응하여 View를 업데이트한다.

     

    Using binding for two-way reactions

    @State가 View를 업데이트하는 역할만 하는 것이 아니다.

    SwiftUI의 컴포넌트들은 데이터를 갖지 않는다. (don't own the data). 대신에 데이터에 대한 레퍼런스를 갖는다. 이는 모델이 변경될 때, 알아서 UI가 업데이트 되는 것을 가능하게 해준다.

    이 과정에서 Binding이 사용된다.

     

    Binding이 뭘까? 애플 문서에는 이렇게 나온다.

    A binding is a two-way connection between a property that stores data, and a view that displays and changes the data. A binding connects a property to a source of truth stored elsewhere, instead of storing data directly.

    그니까 데이터를 갖고 있는 곳과, 이를 보여주기 위한 View를 연결하는 도구다. 이말이야

     

    Defining the single source of truth

    Single source of trugh. SwiftUI에서 하루종일 듣는 말이다.

    데이터가 오직 하나의 엔티티에서만 존재하고 다른 모든 엔티티에서는 데이터를 소유하는 엔티티로 접근해야 한다. 복사가 아니라!

     

    The art of observation

    Binding은 single source of truth를 구현하기 위해 사용되는 것이다.

    State값을 전달할 때, State의 value가 아닌 State의 binding을 전달하자

     

    여러 @State프로퍼티를 갖고 있는 모델(struct)을 생각해보자.

    이 모델의 프로퍼티 중 하나의 값이 바뀌면 그 값에 해당하는 UI만 업데이트 되기를 원할 것이다. 하지만 이 struct를 참조하는 모든 UI가 업데이트 된다.

    struct를 쓰면 안된다는 건 아니다. 하나의 모델에 관계없는 여러 프로퍼티를 넣어서는 안된다는 것을 알아두자.

     

    만약 class타입으로 모델을 생성한다면, 이런 일을 예방할 수 있다. 참조타입의 모델이라면, 모델이 생성될 때만 state가 변경된다. 그 뒤로는 모델의 값이 바뀌어도 UI업데이트가 일어나지 않는다.

     

    위 상황을 고려할 때, 여러분의 모델은

    • 참조타입이어야 한다.
    • 어떤 프로퍼티가 UI를 업데이트 시켜야 하는지 알 수 있어야 한다.

    이를 하기 위해

    • class를 observable로 선언하라. 이는 State프로퍼티와 유사한 것이다.
    • 클래스의 프로퍼티를 Observable로 선언하라.
    • Observable클래스 타입의 인스턴스 프로퍼티는 observed로 선언하라. Observable클래스를 뷰 내에서 Observed 프로퍼티로 사용할 수 있게 해준다.

    클래스를 Observable하게 만들기 위해서는 ObservableObject 프로토콜을 구현해야 한다. 이 프로토콜은 objectWillChange프로퍼티 하나를 갖고 있다. 컴파일러가 알아서 연결하는 값이니 직접 구현할 필요는 없다.

    // 1
    final class UserManager: ObservableObject {
      // 2
      @Published
      var profile: Profile = Profile()
      
      @Published
      var settings: Settings = Settings()
      
      // 3
      var isRegistered: Bool {
        return profile.name.isEmpty == false
      }
      ...
    }
    1. ObservableObject를 따르는 클래스 생성
    2. 2개의 프로퍼티가 @Published타입으로 생성되었다. 이 값은 뷰에서 State프로퍼티의 역할을 하게 된다.
    3. 그냥 Computed 프로퍼티다. 이 값이 Published 프로퍼티에 관련된 값이라면, 그 기능을 따라한다. 즉, 이 Computed 프로퍼티를 뷰에서 사용하면, UI가 업데이트된다.

    @Published를 생성할 때는 @State타입을 사용할 때와 똑같은 사항이 적용된다.

    • 값 타입이어야 한다. 기본 데이터타입 또는 structure

    @Published는 @State타입과 마찬가지로

    • Single source of trugh를 정의한다.
    • Binding을 갖고 있다.
    • 값이 업데이트되면 UI가 업데이트 된다.

    뷰에서 ObservableObject의 @Published 프로퍼티를 사용하기 위해서는 @ObservedObject타입으로 선언해야 한다.

     

    Sharing in the environment

    View의 hierarchy에서 하나의 인스턴스를 같이 참조하려면?

    싱글턴을 사용하는게 편했다. 하지만 SwiftUI에서는 Environment object를 제공한다.

    • environmentObject(_:)를 사용하여 environment object를 주입한다.
    • @EnvironmentObject 선언을 통해 주입한 인스턴스를 사용할 수 있다.

    Environment에 주입한 인스턴스는 주입받은 뷰 및 그 서브뷰 모두가 사용할 수 있다.

    window.rootViewController = UIHostingController(
      rootView: StarterView()
        .environmentObject(userManager)
        // 1
        .environmentObject(ChallengesViewModel())
    )
    
    1. ChallengesViewModel클래스를 environment에 주입했다. 이제 StarterView 및 그 모든 서브뷰들이 이 하나의 인스턴스를 같이 사용할 수 있다.

    근데 environment에 주입하면서 변수이름은 지정하지 않고 있다. 오직 타입만 전달될 뿐!

    그러면 StarterView 및 서브뷰들은 어떻게 전달받은 인스턴스를 알 수 있을까?

    간단하다. EnvironmentObject는 타입별로 하나만 주입할 수 있다.

    Understanding environment properties

    SwiftUI는 시스템에서 관리하는 환경변수를 이미 많이 제공하고 있다.

    예를 들어, 사용하고 있는 컬러스킴(dark/light)를 가져올 수 있다.

     

    이런식으로 사용할 수 있다.

    @Environment(\.verticalSizeClass) var verticalSizeClass
    
    var body: some View {
    	if verticalSizeClass == .compact {
    ...
    

    Creating custom environment properties

    2가지 단계가 있다.

    1. EnvironmentKey 프로토콜을 따르는 structure를 생성한다.
    2. EnvironmentValuesextension에 따르는 computed property를 구현한다.
    struct QuestionsPerSessionKey: EnvironmentKey {
      static var defaultValue: Int = 5
    }
    // 1
    extension EnvironmentValues {
      // 2
      var questionsPerSession: Int {
        // 3
        get { self[QuestionsPerSessionKey.self] }
        set { self[QuestionsPerSessionKey.self] = newValue }
      }
    }
    
    // 사용
    @Environment(\.questionsPerSession) var questionsPerSession
    
    ScoreView(
      numberOfQuestions: questionsPerSession, 
      numberOfAnswered: $numberOfAnswered
    )
    

    'Ray Wenderlich > SwiftUI' 카테고리의 다른 글

    Ch12. Conditional Views  (0) 2021.02.14
    Ch11. Lists & Navigation  (0) 2021.02.07
    Ch8. Introducint Stacks & Containers  (0) 2021.02.07
    Ch7. Controls & User Input  (0) 2021.02.07
    Ch6. Intro to controls: Text & Image  (0) 2021.02.07
Designed by Tistory.