ScrollView {
HStack {
LazyVStack { ... }
LazyVStack { ... }
}
}
뷰 내에서 view model을 source of truth로 이용하기 위해서는 @State
, @Binding
과 같은 property wrapper를 이용해야 한다.
@State
→ A property wrapper type that can read and write a value managed by SwiftUI.
wrapper는 value 그 자체가 아님을 기억하자. 뷰 내에 @State
로 마크된 변수들이 존재하면, SwiftUI는 해당 변수들이 가지고 있는 값이 변화할 때마다 해당 값과 연관된 view hierarchy들을 갱신해준다.
property name을 통해 값에 접근할 수 있고, 하위 view 에 전달할 수도 있다. 전달받은 하위 view에서는 property 값을 변경할 수는 없지만, 해당 property 값을 이용할 수 있고 그 값이 상위 view에서 변경되면 역시 SwiftUI가 하위 view를 갱신해준다.
하위 view에서도 값을 변경하기 원한다면 state를 그대로 전달하는 대신 binding(to the state)을 전달해야 한다. property name 앞에 $
를 붙여서 binding에 접근할 수 있고, binding을 하위 뷰에 전달하면 하위 뷰에서도 해당 state의 값을 변경할 수 있다.
하위 뷰에서는 @Binding
wrapper로 전달 받을 값을 정의해야한다. binding으로 정의하더라도 state와 처럼 property name을 통해 값에 바로 접근할 수 있고 $
를 통해 binding에 접근할 수 있다.
*view를 인스턴스화하면서 state를 초기화하면 SwiftUI의 storage management와 충돌할 수 있으므로, 해당 값을 이용하는 view hierarchy의 최상단 view에서 private으로 선언하고 하위 뷰에 전달하는 형식을 이용하자
데이터모델은 UI와 다른 로직들을 연결하는 통로이다. SwiftUI에서는 data model을 observable object로 정의하고, property들을 @Published
로 마크한 후 view 내에서 이용한다.
ObservableObject
는 @Published
로 마크된 변수를 가지면서, 마크된 변수 중 하나라도 변경되면 (변경되기 전에) 값을 emit 하는 publisher를 가지고 있는 object type이다 (AnyObject
를 구현하기 때문에 class로만 이용 가능하다).
class Book: ObservableObject {
@Published var title = "Great Expectations"
let identifier = UUID() // A unique identifier that never changes.
}
@ObservedObject
attribute을 이용해서 observable object를 뷰 내에서 subscribe할 수 있다.
struct BookView: View {
@ObservedObject var book: Book
var body: some View {
Text(book.title)
}
}
observed object도 하위 뷰에 전달할 수 있다.
SwiftUI에서는 뷰를 계속해서 만들고, 다시 만든다. 같은 input 내에서는 항상 같은 view가 나와야 하므로 view 내에서 observed object를 생성하는 것은 위험하다.
대신, StateObject
를 이용할 수 있다.
struct LibraryView: View {
@StateObject private var book = Book()
var body: some View {
BookView(book: book)
}
}
StateObject
는 ObservedObject
와 동일하지만 view가 여러번 초기화되더라도 해당 view 내에서는 항상 동일한 하나의 instance를 생성한다. 다른 view의 ObservedObject
로 전달할 수 있다.
싱글톤 패턴과 함께 이용될 수 있다.
data model을 앱 내에서 전반적으로 이용하고 싶을 때는 environmentObject
로 정의해서 매번 data model을 전달하지 않고도 하위 뷰에서 이용 가능하게 만들 수 있다.
@main
struct BookReader: App {
@StateObject private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
하위 뷰에서는 다음과 같이 이용할 수 있게 된다.
struct LibraryView: View {
@EnvironmentObject var library: Library
// ...
}
UI에서 data model 내의 값을 변경하기를 원한다면 역시 $
를 이용한 binding에 접근하면 된다.
struct BookEditView: View {
@ObservedObject var book: Book
var body: some View {
TextField("Title", text: $book.title)
}
}
struct PinterestLazyStackView: View {
private let spacing = 20.0
@Binding var didReachEnd: Bool
var body: some View {
LazyVStack {
ForEach {
...
}
Color.clear
.frame(width: 0, height: 0)
.onAppear {
didReachEnd = true
}
}
}
}
size.zero
인 clear 뷰를 가장 아래에 놓고, didReachEnd
바인딩에 도달했음을 전달한다.
상위 뷰에서는
.onChange(of: didReachEnd, perform: { value in
guard didReachEnd else { return }
pinListViewModel.request() {
// completion handler
didReachEnd = false
}
})
.onChange
를 이용해서 didReachEnd
가 true인 경우 네트워크 요청을 보낸다. 네트워크 요청이 종료되면 didReachEnd
값을 다시 false로 변경해주어 다시 스크롤이 끝까지 도달할 것에 대비한다.
소스 https://github.com/I-Swift-UI/jujube0/tree/main/03-pinterest