관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - SwiftUI] 튜토리얼 - 10. @State (달러 prefix), ObservableObject, @Published, @EnvironmentObject 본문

iOS 튜토리얼 (SwiftUI)

[iOS - SwiftUI] 튜토리얼 - 10. @State (달러 prefix), ObservableObject, @Published, @EnvironmentObject

jake-kim 2022. 7. 12. 21:46

@State

  • View를 상속받는 뷰에서, 값을 변경할때 사용하는 프로퍼티 어노테이션
@State var n = 0
  • 내부 구현부는 propertyWrapper를 사용한 형태

ex) @State를 붙이지 않은 경우 컴파일 에러 발생

struct ContentView: View {
  var n = 0
  
  var body: some View {
    VStack {
      Text("\(n)")
      Button("Tap!!") {
        n += 1 // Error: Left side of mutating operator isn't mutable: 'self' is immutable
      }
    }
  }
}

ex) @State를 프로퍼티 앞에 붙일 경우 컴파일 에러 해결

struct ContentView: View {
  @State var n = 0 // <-
  
  var body: some View {
    VStack {
      Text("\(n)")
      Button("Tap!!") {
        n += 1
      }
    }
  }
}

@State와 달러 prefix

ex) TabView구현 시 @State 프로퍼티와 달러 접두사 이용

struct ExampleView: View {
  @State private var selection = Tab.home // <- State 사용
  
  enum Tab {
    case home
    case setting
  }
  
  var body: some View {
    TabView(selection: $selection) { // <- 달러 접두사
      List {
        ForEach([Int](0...20), id: \.self) {
          Text("\($0)")
        }
      }
      .tabItem {
        Label("Home", systemImage: "house")
      }
      .tag(Tab.home)
      
      Text("Tab page 2")
        .tabItem {
          Label("List", systemImage: "list.bullet")
        }
        .tag(Tab.setting)
    }
  }
}

projectedValue이란?)

  • propertyWrapper 내부에서 다른 값을 정의하여 사용하는쪽에서 달러 `$` 키워드로 해당 값에 접근할 수 있는 기능
  • propertyWrapper에서 부가적인 프로퍼티 접근에 사용

propertyWrapper를 이용하여 RxSwift, RxCocoa를 구현한 @State 예시

import RxSwift
import RxCocoa

@propertyWrapper
struct State<T> {
  private var relay: BehaviorRelay<T>
  
  var wrappedValue: T {
    get { self.relay.value }
    set { self.relay.accept(newValue) }
  }
  var projectedValue: Observable<T> { self.relay.asObservable() }
  
  init(wrappedValue initialValue: T) {
    self.relay = .init(value: initialValue)
  }
}

// 사용하는 쪽 - 초기화
@Behavior var someProperty = false

// 사용하는 쪽 - 구독
$someProperty
  .subscribe { print($0) } 
  .disposeBag(disposeBag)

ObservableObject

  • ObservableObject와 아래에서 알아볼 @Published는 SwiftUI부터 사용할 수 있는 Combine 프레임워크에서 제공
  • AnyObject를 상속받고 있기 때문에 클래스형만 사용이 가능
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {
  
  /// The type of publisher that emits before the object has changed.
  associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
  
  /// A publisher that emits before the object has changed.
  var objectWillChange: Self.ObjectWillChangePublisher { get }
}
  • 아래에서 알아볼 @Published와 같이 사용되며, ObservableObject를 상속받아서 구현된 클래스의 값이 변경되면 이 값이 변경되었을때 처리를 손쉽게 구현이 가능

ex) ObservableObject를 서브클래싱하여 정의한 Contact 인스턴스는 objectWillChange 메소드 사용이 가능

class Contact: ObservableObject {
  @Published var name: String
  @Published var age: Int
  
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
  
  func haveBirthday() -> Int {
    age += 1
    return age
  }
}

let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
  .sink { _ in
    print("\(john.age) will change")
  }
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

@Published

  • propertyWrapper이며, projectedValue가 존재하여, 사용하는 쪽에서 dollor '$'기호를 붙여서 사용이 가능
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct Published<Value> {
  
  public init(wrappedValue: Value)
  public init(initialValue: Value)
  
  public struct Publisher : Publisher {
    public typealias Output = Value
    public typealias Failure = Never
    
    public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
  }
  
  public var projectedValue: Published<Value>.Publisher { mutating get set }
}
  • @Published도 class에서만 사용이 가능

ex) @Published 사용

  • 2번 출력되고 첫 번째 출력은 처음 초기화 할때 실행
import Combine

class Person { // <- class 만 사용 가능
  @Published var age: Int
  init(age: Int) {
    self.age = age
  }
}

let person = Person(age: 20)
person.$age
  .sink { age in
    print(age)
  }
person.age = 10

// 20
// 10

@EnvironmentObject

  • 마치 싱글톤처럼 한 인스턴스를 가지고, 모든 하위 뷰에서 접근이 가능한 인스턴스
  • 하위 뷰라는 것은, body부분에 선언한 뷰들을 의미

ex) EnviormentObject

  • EnviormentObject를 사용할땐 ObservableObject를 상속받는 클래스만 가능
    • 해당 인스턴스를 ContentView에서 선언하고, 하위 뷰에서도 사용이 가능한지 테스트
class Configuration: ObservableObject {
  @Published var version = 0 // 구독할 변수
}
  • 하위 뷰 정의
    • @EnviormentObject를 사용하여, 상위 뷰로부터 동기화될 변수 선언
struct SubView: View {
  @EnvironmentObject var config: Configuration
  
  var body: some View {
    Text("version \(config.version)")
  }
}
  • ContentView에서 SubView()를 body안에 선언하면 서브뷰가 되므로, 자동으로 config 변수가 하위 뷰에게도 전달
    • 단, 상위 뷰에서 .environmentObject를 선언
struct ContentView: View {
  @StateObject var config = Configuration()
  
  var body: some View {
    NavigationView {
      VStack {
        Text("version = \(self.config.version)")
          .padding()
        Button("Up version") {
          self.config.version += 1
        }
        .padding()
        NavigationLink(destination: SubView()) {
          Text("Go to SubView")
        }
      }
    }
    .environmentObject(self.config)
  }
}

상위 뷰에서 값을 변경하면 자동으로 하위 뷰에서도 변경

* 참고

https://stackoverflow.com/questions/56551131/what-does-the-dollar-sign-do-in-swift-swiftui

https://developer.apple.com/documentation/swiftui/state

Comments