SwiftUI로 핀터레스트 화면 구현하기

김가영·2023년 2월 8일
0

swift

목록 보기
3/3
post-thumbnail
post-custom-banner

UI

  • 기본 구조
ScrollView {
  HStack {
    LazyVStack { ... }
    LazyVStack { ... }
   }
}

Managing user interface state

뷰 내에서 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으로 선언하고 하위 뷰에 전달하는 형식을 이용하자

Managing model data in your app

데이터모델은 UI와 다른 로직들을 연결하는 통로이다. SwiftUI에서는 data model을 observable object로 정의하고, property들을 @Published 로 마크한 후 view 내에서 이용한다.

1. view model을 observable object로 만들기

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.
}

2. Monitor changes in observable objects

@ObservedObject attribute을 이용해서 observable object를 뷰 내에서 subscribe할 수 있다.

struct BookView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        Text(book.title)
    }
}

observed object도 하위 뷰에 전달할 수 있다.

3. Instantiate a model object in a view

SwiftUI에서는 뷰를 계속해서 만들고, 다시 만든다. 같은 input 내에서는 항상 같은 view가 나와야 하므로 view 내에서 observed object를 생성하는 것은 위험하다.

대신, StateObject 를 이용할 수 있다.

struct LibraryView: View {
    @StateObject private var book = Book()
    
    var body: some View {
        BookView(book: book)
    }
}

StateObjectObservedObject 와 동일하지만 view가 여러번 초기화되더라도 해당 view 내에서는 항상 동일한 하나의 instance를 생성한다. 다른 view의 ObservedObject 로 전달할 수 있다.

싱글톤 패턴과 함께 이용될 수 있다.

4. Share an object throughout your app

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
    
    // ...
}

5. Create a two-way connection using bindings

UI에서 data model 내의 값을 변경하기를 원한다면 역시 $ 를 이용한 binding에 접근하면 된다.

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

무한 스크롤 - LazyVStack 을 이용해서 scroll view가 끝까지 도달했는지 판단하기


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

profile
개발블로그
post-custom-banner

0개의 댓글