ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Discover Observation in SwiftUI (feat. WWDC 2023)
    SwiftUI 2023. 6. 8. 10:35

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 이번 WWDC 2023에서 소개된 SwiftUI에서 데이터 플로우 변화를 가져가면서 새로 생긴 매크로와 기존 데이터 바인딩 등 방식의 변화들을 다뤄볼까해요🙋🏻

    충분히 SwiftUI 데이터 모델이 Observation을 통해 간소화 되었고 매크로가 또 모델을 단순화하고 앱 성능을 개선해줍니다.

     

    자 그럼 얼마나 더 편리해졌고 유용해졌는지 같이 살펴보시죠🕺🏻

     

    What is Observation?

    새로 나온 Observation은 과연 무엇일까요?

    Observation은 프로퍼티의 변경 사항을 추적하기 위한 새로운 Swift 기능입니다.

    매크로의 대단한 기능을 통해 이뤄지죠.

    우리가 평소 꾸리는 모델에 @Observable을 추가하는 것만으로 UI가 데이터 모델의 변경 사항에 응답하도록 만드는데 필요한 전부가 됩니다.

    이러한것들을 바로 Swift 5.9의 매크로 시스템을 통해 모두 가능하게 된 것이죠🙌

    @Observable class FoodTruckModel {
      var orders: [Order] = []
      var donuts = Donut.all
    }

    @Observable은 Swift 컴파일러에게 작성한 코드를 타입을 관찰할 수 있는 확장된 형식으로 변환하도록 지시합니다.

    그럼 매크로로 정해놓은것처럼 형식을 변환해주는것이죠.

     

    이를 통해 Swift 뷰를 더 강화해볼 수 있어요.

    가장 놀라운 점은 이러한걸 구동시키는데 더이상 프로퍼티 래퍼가 필요없다는 겁니다.

    struct DonutMenu: View {
      let model: FoodTruckModel
      
      var body: some View {
        List {
          Section("Donuts") {
            ForEach(model.donuts) { donut in
              Text(donut.name)
            }
            Button("Add new donut") {
              model.addDonut()
            }
          }
        }
      }
    }

    단순히 이처럼 올리기만하면 model 프로퍼티에 액세스함을 뷰는 감지할 수 있습니다.

    뷰가 실행될 때 SwiftUI가 Observable 타입에서 사용되는 프로퍼티에 대한 모든 액세스를 추적합니다.

    그 후 해당 추적 정보를 가져와 특정 인스턴스의 프로퍼티에 대한 다음 변경 사항이 변경되는 시기를 경정하는데 사용합니다.

    위 코드를 보시면 버튼을 눌러 도넛을 추가할때 도넛 배열을 변경하면 도넛 메뉴 뷰가 무효화되고 이에 따라 UI가 다시 업데이트 됩니다.

     

    그럼 모델에서 저장 프로퍼티말고 연산 프로퍼티를 사용한다면 어떨까요?

     

    computed property

    연산 프로퍼티를 추가해도 이전과 동일한 규칙을 따릅니다.

    @Observable class FoodTruckModel {
      var orders: [Order] = []
      var donuts = Donut.all
      var orderCount: Int { orders.count }
    }

    즉 해당 사용되는 연산 프로퍼티가 변경되면 당연하게도 UI가 업데이트 되죠.

    struct DonutMenu: View {
      let model: FoodTruckModel
      
      var body: some View {
        List {
          Section("Donuts") {
            ForEach(model.donuts) { donut in
              Text(donut.name)
            }
            Button("Add new donut") {
              model.addDonut()
            }
          }
          
          // 새로 추가된 연산 프로퍼티에 대한 뷰
          Section("Orders") {
            LabeldContent("Count", value: "\(model.orderCount)")
          }
        }
      }
    }

    마찬 가지로 뷰에서 해당 프로퍼티에 액세스하는 모델의 orderCount가 호출됩니다.

    즉 쉽게 말해 order값이 변경되면 orderCount가 order 프로퍼티에 액세스 하기에 뷰에서 해당 텍스트가 업데이트 되는 것입니다😁

     

    @Observable 매크로를 사용하여 SwiftUI는 해당 프로퍼티에 대한 액세스를 추적하고 다음 프로퍼티가 Observation에서 변경되는 시기를 관찰할 수 있습니다.

    이와 같이 추적을 통해 특정 프로퍼티가 변경될 때만 UI가 뷰를 다시 계산함으로 성능 향상을 기대할 수 있습니다.

     

    요약하여 아래와 같은 이점을 지닙니다.

     1️⃣ Macro

     2️⃣ Tracks access

     3️⃣ Property changes cause UI updates

     

    매크로에 대해 더 자세한 내용을 보고 싶다면 Write Swift macros 섹션이나 Expand on Swift macros 섹션을 봐야될것 같네요🙋🏻

     

    SwiftUI Property wrappers

    SwiftUI에서 프로퍼티 래퍼를 사용해야 하는 경우에 대한 일련의 편리한 규칙들을 살펴보겠습니다.

    Observable을 사용하면 SwiftUI의 프로퍼티 래퍼 사용이 훨씬 쉬워집니다.

    State, environment, bindable은 SwiftUI 작업을 위한 기본적인 세가지 프로퍼티 래퍼입니다.

     

    위에서 이미 SwiftUI를 통해 이러한 프로퍼티 래퍼의 사용을 하지 않아도 되는 경우를 다뤘지만 이제는 필요한 경우에 대해 자세히 보려합니다😳

     

    @State

    모델 프로퍼티를 뷰에 선언할때 해당 프로퍼티 래퍼를 붙여야되는 경우는 이제 뷰가 모델에 저장된 자체 상태를 가져야하는 경우에 붙여줍니다.

    조금 이해가 안갈 수 있으니 예시로 보시죠!

    import SwiftUI
    import Observation
    
    struct ContentView: View {
      var dogList: DogList
      @State private var dogToAdd: Dog?
      
      var body: some View {
        List(dogList.dogs) { DogView(dog: $0) }
        
        Button("Add Dog") { dogToAdd = Dog(name: "Dog\(Int.random(in: 1...100))", age: Int.random(in: 1...100)) }
          .sheet(item: $dogToAdd) { _ in
            Button("Save") {
              dogList.dogs.append(dogToAdd ?? .init(name: "Puppy", age: 3))
              dogToAdd = nil
            }
            
            Button("Cancel") { dogToAdd = nil }
          }
      }
    }
    
    // MARK: - 강아지 셀 뷰
    private struct DogView: View {
      let dog: Dog
      
      init(dog: Dog) {
        self.dog = dog
      }
      
      var body: some View {
        HStack(spacing: 10) {
          Spacer()
          
          Text(dog.name)
            .font(.title)
          
          Text("\(dog.age)")
            .font(.caption)
          
          Spacer()
        }
      }
    }
    
    // MARK: - 강아지 모델
    @Observable
    class Dog: Identifiable {
      var name: String = ""
      var age: Int = 0
      
      init(name: String, age: Int) {
        self.name = name
        self.age = age
      }
    }
    
    @Observable
    class DogList {
      var dogs: [Dog] = []
      
      init(dogs: [Dog]) {
        self.dogs = dogs
      }
    }

    요런 코드가 있다고 가정해볼께요.

    여기서 보면 시트를 띄우기 위해서 dogToAdd 상태 프로퍼티를 이용합니다.

    즉, 해당 뷰에서 자체적으로 Dog 모델의 저장된 상태를 가지고 시트를 컨트롤 해주니까 @State가 필요합니다.

    만약 @State가 없다면 sheet에 바인딩 될 수 없음으로 @State 상태 값이 필요한것입니다.

    TextField를 구성하더라도 Dog 모델에 존재하는 프로퍼티를 그대로 쓸 수 없습니다.

    왜냐하면 dogToAdd는 @State 프로퍼티라 바인딩이 되지만 그 내에 있는 name은 Binding되지 않기에 TextField를 갖는 하위 뷰를 만들어 거기에 @Bindable 속성을 갖는 프로퍼티를 만들고 넘겨주어 처리해줘야 합니다.🥹

    왜냐하면 지금은 dogToAdd가 옵셔널이기 때문이죠.

    옵셔널하지 않다면 바로도 가능합니다.

    위에서도 dogToAdd 프로퍼티는 포함된 뷰의 수명에 따라 관리됩니다.

     

    그 다음으로 @Environment를 살펴볼까요?

     

    @Environment

    Environment를 통해 값을 전역적으로 액세스 가능한 값으로 전파할 수 있죠.

    import SwiftUI
    
    @main
    struct Observation_PracticeApp: App {
      @State var dog: Dog?
      
      var body: some Scene {
        WindowGroup {
          ContentView(dogList: .init(dogs: []))
            .environment(dog)
        }
      }
    }
    
    struct ContentView: View {
      var dogList: DogList
      @State private var dogToAdd: Dog?
      @Environment(Dog.self) var dog: Dog?
      
      var body: some View {
        List(dogList.dogs) { DogView(dog: $0) }
        
        Button("Add Dog") { dogToAdd = Dog(name: "Dog\(Int.random(in: 1...100))", age: Int.random(in: 1...100)) }
          .sheet(item: $dogToAdd) { dog in
            Button("Save") {
              dog.name = "Green"
              
              dogList.dogs.append(dogToAdd ?? .init(name: "Puppy", age: 3))
              dogToAdd = nil
            }
            
            Button("Cancel") { dogToAdd = nil }
          }
      }
    }

    App에서 환경 변수로 사용할 모델을 주입해줍니다.

    그리고 필요한 뷰에서 해당 @Environment 프로퍼티로 가져옵니다.

    그럼 이제 전역적으로 싱글턴처럼 사용할 수 있죠.

    즉, 어떤 뷰든 해당 값이 변하면 같이 변하게 됩니다.

     

    그 다음으로 @Bindable입니다.

     

    @Bindable

    이번 WWDC 2023에서 Observation이 나오면서 새로 나온 프로퍼티 래퍼입니다.

    해당 프로퍼티 래퍼는 정말 가볍습니다.

    바인딩 가능하도록 만들어줍니다.

    @Binding에서 익숙하듯이 $를 사용해 해당 속성에 바인딩을 가져옵니다.

    대분의 경우 Observable 타입에 대한 바인딩입니다.

    예를들어 TextField를 구성해야 한다고 생각해볼께요.

    그럼 아래와 같이 사용할 수 있습니다.

    struct ContentView: View {
      @Bindable var dog: Dog = .init(name: "puppy", age: 3)
      
      var body: some View {
        TextField("Name", text: $dog.name)
      }
    }

    요렇게 사용할 수 있죠.

    그런데 아직 더 봐야할것 같은건 사실 @State로도 지금 예제정도는 다 가능한데 어떤 이점이 더 크게 있는지는 잘 모르겠네요🥹

    아마 @State와는 다르게 @Bindable은 딱 바인딩만 할때 사용하고 뷰의 상태의 일부분이여야 한다면 @State를 사용하는것이 맞아보여요.

     

    자 정리해볼까요?

     

    SwiftUI Property wrappers 사용 기준

    우선 위 흐름을 따라 결정할 수 있을것 같아요.

    1️⃣ 해당 모델이 뷰의 상태여야 한다면 @State를 사용

    2️⃣ 앱 전역적으로 사용되어야 한다면 @Environment 사용

    3️⃣ 바인딩만 딱 필요하다면 @Bindable 사용

    4️⃣ 위 항목에 해당하지 않는다면 그냥 프로퍼티 래퍼 없이 사용

     

    총 이렇게 이제는 @Binding 변수를 사용하거나 @ObservedObject, @StateObject 사용없이 프로퍼티 래퍼는 저 3가지만 사용할 수 있을것 같네요🙌

     

    그럼 더 좋은 사용을 위해 진보된 사용에 대해 알아볼까요?

     

    Advanced uses

    observable의 몇가지 고급 버전의 사용법을 알아보겠습니다.

    SwiftUI에서는 Observable된 모델을 가지고 해당 모델의 인스턴스를 추적하여 변경되면 뷰를 업데이트 한다고 했습니다.

    그런데 이것이 지켜지지 않는 경우도 더러 있습니다.

    만약 연산 프로퍼티에 포함된 저장 프로퍼티가 없는 경우에 Observation과 함께 작동하려면 두가지 단계를 추가해야 한다고 합니다.

    Observable 타입의 저장 프로퍼티 구성을 통해서 관찰되는 프로퍼티가 변경되지 않는 경우에만 수행하면 충분합니다.

     

    이럴때는 프로퍼티에 액세스하거나 프로퍼티가 변경될 때 Observation에 노티만 주면 됩니다.

    그런데 사실 대부분의 경우는 해당 모델의 저장 프로퍼티를 가지고 구성되기에 이러한 수작업은 필요하지 않긴 합니다.

    다만 이걸 소개하는 이유는 이렇게도 수동으로 커스텀하고 고급화하여 사용할 수 있다는 유연성을 보여줍니다.

    즉 Observation을 직접 쉽게 사용할 수 있다는걸 보여줍니다😃

     

    자 마지막으로 기존 SwiftUI에서 ObservableObject로 사용되던 모델들을 변경해볼까요?

     

    ObservableObject > @Observable

    기존 ObservableObject를 사용하는 코드를 새로 나온 @Observable 매크로로 업데이트하는 방법에 대해 알아보시죠.

     

    기존에 ObservableObject 타입의 모델이 있다고 볼께요.

    class FoodTruckModel: ObservableObject {
      @Published var orders: [Order] = []
      @Published var donuts = Donut.all
      var orderCount: Int { orders.count }
    }

    여기서 단순하게 아래와 같이 ObservableObject과 @Published를 없애고 @Observable로 붙여주면 끝입니다.

    @Observable class FoodTruckModel {
      var orders: [Order] = []
      var donuts = Donut.all
      var orderCount: Int { orders.count }
    }

    아주 쉽죠!?

     

    그럼 뷰를 볼까요?

     

    뷰에서도 기존 @ObservableObject와 @EnvironmentObject를 사용하고 있었죠?

    이제 필요없습니다.

    필요하다면 @State나 @Bindable로 대체하면 됩니다.

     

    ObservableObject에서 @Observable 매크로로 변경하게 됨으로 대부분의 어노테이션을 제거할 수 있게 되었습니다.

    또한 기본 세가지의 프로퍼티 래퍼만 남겨두었고 그 래퍼들 또한 아주 간단하고 명확합니다.

    고려할 옵션 자체가 적으니 새로운 기능을 구현할 때 많은 고민이 필요없습니다.

     

    마무리

    진짜 혁명까지는 모르겠는데 솔직히 너무 편해지고 단순해지긴 했네요!

    조금 더 명확한 기준을 가질 수 있을것 같아요🙌

    위 테스트를 해본 코드들은 아래 깃헙 레포에서 확인해보실 수 있습니다.

    https://github.com/GREENOVER/playground/tree/main/Observation_Practice

     

    GitHub - GREENOVER/playground: 학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터

    학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터. Contribute to GREENOVER/playground development by creating an account on GitHub.

    github.com

     

    참고 자료

    https://developer.apple.com/wwdc23/10149

     

    Discover Observation in SwiftUI - WWDC23 - Videos - Apple Developer

    Simplify your SwiftUI data models with Observation. We'll share how the Observable macro can help you simplify models and improve your...

    developer.apple.com

Designed by Tistory.