SwiftUI의 데이터 흐름

  • SwiftUI는 Apple 앱을 개발하는 완전히 새로운 방식입니다.
  • 이번에는 SwiftUI에서의 데이터 흐름과 라이프사이클을 알아보고자 합니다.
  • SwiftUI 데이터 흐름에 대해서는 애플의 WWDC19WWDC20 영상이 있습니다.
  • SwiftUI 데이터 흐름에 대한 애플의 공식 문서도 존재합니다.
  • WWDC23 이후 완전히 개편된 간편해진 데이터 사용 방법에 공식 문서도 있습니다.

WWDC 23 이후

Xcode 15와 Swift 5.9 이후, Observable Macro를 통해 더 쉽게 SwiftUI의 데이터를 다룰 수 있게 되었습니다.

  • Observable 프로토콜을 준수하는 대신, @Observable Macro를 표시합니다.
  • 이로 인해, @Published 프로퍼티 래퍼도 이제 필요하지 않게 되었습니다.
// BEFORE
class Library: ObservableObject {
    @Published var books: [Book] = [Book(), Book(), Book()]
}

// AFTER
@Observable class Library {
    var books: [Book] = [Book(), Book(), Book()]
}
  • 이전에는 @State, @StateObject, @ObservedObject, @EnvironmentObject와 같은 프로퍼티 래퍼들이 있었습니다.
  • 그러나 이제는 훨씬 간단하게, 값 타입과 참조 타입 모두 @State 프로퍼티 래퍼만을 사용하면 됩니다.
// BEFORE
@main
struct BookReaderApp: App {
    @StateObject private var library = Library()

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}

// AFTER
@main
struct BookReaderApp: App {
    @State private var library = Library()

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}
  • @EnvironmentObject 역시, @Environment로 통일할수 있게 되었습니다.
// BEFORE
struct LibraryView: View {
    @EnvironmentObject var library: Library

    var body: some View {
        List(library.books) { book in
            BookView(book: book)
        }
    }
}

// AFTER
struct LibraryView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        List(library.books) { book in
            BookView(book: book)
        }
    }
}
  • @ObservedObject 역시, 필요가 없어졌습니다.
// BEFORE
struct BookView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        Text(book.title)
    }
}

// AFTER
struct BookView: View {
    var book: Book
    
    var body: some View {
        Text(book.title)
    }
}
  • Observable을 준수하는 오브젝트에 대해 Binding이 필요한 경우에만 @Bindable을 사용하면 됩니다.
// BEFORE
struct BookEditView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        TextField("Title", text: $book.title)
    }
}

// AFTER
struct BookEditView: View {
    @Bindable var book: Book
    
    var body: some View {
        TextField("Title", text: $book.title)
    }
}

SwiftUI 라이프사이클

  • SwiftUI에는 View의 상태를 나타내는 함수가 즉, 라이프사이클이 아래와 같이 단 두가지 밖에 없습니다.
  • 대신 상태를 나타내는 다양한 Property Wrapper가 존재해 Data 흐름에 대한 여러 상태에 대응할 수 있습니다.
.onAppear {
    print("View appeared")
}

.onDisappear {
    print("View disappeared")
}

@State

  • 일반적으로 struct는 값 타입이여서 struct내의 값을 변경할 수 없습니다.
  • SwiftUI는 @State를 제공해 struct내의 값을 변경할 수 있게 해줍니다.
struct ContentView: View {
  @State private var number = 0
}
  • SwiftUI의 view는 struct이고, 이는 언제든 소멸되거나 재생성됩니다.
  • 그렇기 때문에 @State를 사용해 지속적으로 변형 가능한 변수를 만드는 것입니다.
  • 단, @State는 String, Int, Bool과 같은 값 타입에만 사용되는 것이 좋습니다.
  • 일반적으로 @State 변수는 private으로 선언되고, 다른 view와 공유되지 않습니다.
  • 다른 view와 값을 공유하고 싶다면, @Binding이나 @ObservedObject를 사용하면 됩니다.

@Binding

  • @Binding은 부모 view의 @State와 같은 값을 양방향으로 연결되도록 해줍니다.
  • 아래 코드에서 isPresentedshowAddView를 바인딩 시켜줘서 값을 변경해줍니다.
struct ContentView: View {
  @State private var showAddView = false

  var body: some View {
    VStack {
      Button("Trigger") {
        showAddView = true
      }
    }
    .sheet(isPresented: $showAddView) {
      AddView(isPresented: self.$showAddView)
    }
  }
}

struct AddView: View {
  @Binding var isPresented: Bool

  var body: some View {
    Button("Dismiss") {
      self.isPresented = false
    }
  }
}

ObservableObject

  • ObservableObject는 Protocol으로 Combine 프레임워크의 일부입니다.
  • 이것을 사용하기 위해서는, Protocol을 준수하고 @Published를 사용하면 됩니다.
  • @Published를 사용하면 변수의 값이 추가되거나 삭제 되었다는 것을 View가 알 수 있게 해줍니다.
  • ObservableObject는 MVVM 아키텍쳐의 ViewModel에 적용하기 좋은 프로토콜입니다.
class MyViewModel: ObservableObject {
  @Published var dataSource: MyModel
  
  init(dataSource: MyModel) {
    self.dataSource = dataSource
  }
}

@StateObject

  • WWDC 2020에서 애플은 @StateObject를 추가로 공개했습니다.
  • @ObservedObject와 거의 같은 방식으로 작동하는데요.
  • SwiftUI가 View를 다시 랜더링 할 때, 실수록 취소되는 것을 방지해줍니다.
struct ContentView: View {
  @StateObject var user = User()
}

@ObservedObject

  • SwiftUI는 @ObservedObject를 통해 view가 외부 객체를 감지하게 해줍니다.
  • 아래 코드에서 User class는 ObservableObject를 준수하고 @Published 변수를 갖고 있습니다.
  • @ObservedObject user 변수는 이러한 User class 객체를 담고 있습니다.
  • SwiftUI는 이러한 user 객체의 @Published 변수 값이 변경될 때 view를 refresh합니다.
class User: ObservableObject {
  @Published var name = "Hohyeon Moon"
}

struct ContentView: View {
  @ObservedObject var user = User()

  var body: some View {
    VStack {
      Text("Your name is \(user.name).")
    }
  }
}

@EnvironmentObject

  • @EnvironmentObject는 보통 앱 전반에 걸쳐 공유되는 데이터에 사용됩니다.
  • @EnvironmentObject.environmentObject()를 통해 값을 전달할 수 있습니다.
  • 전달하는 object는 ObservableObject 프로토콜을 준수해야 합니다.
  • 아래 코드와 같이 root view를 제공하면, 어떠한 view에서도 사용이 가능합니다.
// MySettings.swift
class Settings: ObservableObject {
  @Published var version = 0
}

// SceneDelegate.swift
var settings = UserSettings() 
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(settings))

// MyViews.swift
struct ContentView: View {
  @EnvironmentObject var settings: UserSettings

  var body: some View {
    NavigationView {
      VStack {
        Button(action: {
          self.settings.version += 1
        }) {
          Text("Increase version")
        }

        NavigationLink(destination: DetailView()) {
          Text("Show Detail View")
        }
      }
    }
  }
}

struct DetailView: View {
  @EnvironmentObject var settings: UserSettings

  var body: some View {
    Text("Version: \(settings.version)")
  }
}

총 정리

  • objc.io의 Chris Eidhof 이미지를 빌려와 총 정리를 해보면 이렇습니다.
swiftui-data-flow

마무리

  • 이렇게 해서 SwiftUI에서는 라이프사이클과 데이터 흐름을 어떻게 처리하는지 알아봤습니다.