iOS 응용 (SwiftUI)

[iOS - SwiftUI] 2. List 형태 UI - pull to refresh, 페이지네이션 구현 방법 (List, refreshable, @Sendable)

jake-kim 2024. 4. 24. 01:33

1. List 형태 UI - @State를 활용하여 로딩 상태, 로드 상태, 실패 상태 띄우기

2. List 형태 UI - pull to refresh, 페이지네이션 구현 방법 (List, refreshable, pagination, @Sendable)

3. List 형태 UI - 검색된 결과 UI에 보여지게 하는 방법 (searchable)

복습) 1번 글까지 한 것

  • @State로 선언된 프로퍼티를 활용하면 매우 쉽게 상태 표현이 가능
    • 뷰 코드에서 이 프로퍼티에 관한 switch문을 활용하여 상태에 따라 뷰를 다르게 보여주는 코드를 준비해 놓고, 프로퍼티만 변경되면 자동으로 바인딩되어 뷰가 변경됨
    • 코드: https://github.com/JK0369/ExListSwiftUI.git
struct ContentView: View {
    enum ViewState {
        case loading
        case success
        case failed
    }
    
    @State private var state = ViewState.loading
    
    var body: some View {
        Group {
            switch state {
            case .loading:
                ...
            case .success:
                ...
            case .failed:
                ...
            }
        }
        ...
    }
}

List와 pull to refresh (refreshable)

  • SwiftUI에서는 refreshable를 사용하면 간편하게 리프레시 구현이 가능
  • 위 코드의 success부분에 List를 띄우기 위해서 필요한 데이터 Person 정의
struct Person: Identifiable {
    let id: String
    let age: Int
    let name: String
}
  • 이 데이터를 List에 뿌려주기
    • List에는 rowContent 인자에 별도의 뷰를 주입해주어야 하므로 이 값을 정의 (closure형태로 구현해도 되지만 뷰를 나누기 위해서 PersonRowView로 따로 만들기)
struct ContentView: View {
...
    @State private var items = [Person]()
    
    var body: some View {
        Group {
            switch state {
            case .loading:
                ...
            case .success:
                List(items, rowContent: <#T##(Person) -> RowContent#>)
			...
            }
        }
...
    }
}
  • PersonRowView라는 별도의 뷰로 만들어놓으면 아래처럼 심플하게 표현이 가능
List(items, rowContent: PersonRowView.init)
  • PersonRowView 정의
import SwiftUI

struct PersonRowView: View {
    let item: Person
    
    var body: some View {
        NavigationLink {
            Text("detail")
        } label: {
            HStack {
                Text("age: \(item.age), name: \(item.name)")
                    .font(.caption.weight(.heavy))
            }
        }
    }
}

List형태의 뷰 완성)

struct ContentView: View {
    ...
    
    @State private var items = [Person(id: "1", age: 10, name: "jake")]
    @State private var state = ViewState.loading
    
    var body: some View {
        Group {
            switch state {
            ...
            case .success:
                List(items, rowContent: PersonRowView.init)
		...
        }
    }
}

  • refershable을 추가하여, 여기에 값을 더하는 코드를 추가
    • loadMoreItems는 @Sendable타입으로 정의 (@Sendable 타입은 아래에서 설명)
List(items, rowContent: PersonRowView.init)
    .refreshable(action: loadMoreItems)
    
@Sendable func loadMoreItems() async {
    try? await Task.sleep(nanoseconds: 500_000_000)
    let mocks = [
        Person(id: Date.now.description, age: 10, name: "jake1"),
        Person(id: Date.now.description + "1", age: 20, name: "jake2"),
        Person(id: Date.now.description + "2", age: 30, name: "jake3"),
        Person(id: Date.now.description + "3", age: 40, name: "jake4"),
        Person(id: Date.now.description + "4", age: 50, name: "jake5"),
        Person(id: Date.now.description + "5", age: 60, name: "jake6"),
        Person(id: Date.now.description + "6", age: 20, name: "jake7"),
        Person(id: Date.now.description + "7", age: 30, name: "jake8"),
        Person(id: Date.now.description + "8", age: 40, name: "jake9"),
        Person(id: Date.now.description + "9", age: 50, name: "jake10"),
        Person(id: Date.now.description + "10", age: 60, name: "jake11"),
        Person(id: Date.now.description + "11", age: 20, name: "jake12"),
        Person(id: Date.now.description + "12", age: 30, name: "jake13"),
        Person(id: Date.now.description + "13", age: 40, name: "jake14"),
        Person(id: Date.now.description + "14", age: 50, name: "jake15"),
        Person(id: Date.now.description + "15", age: 60, name: "jake16"),
    ]
    items.append(contentsOf: mocks)
}
  • @Sendable타입이란?
    • concurrency 환경에서 value type과 같은 곳에 동시에 수정과 읽기가 일어나면 크래시가 발생하는데, 이를 안전하게 처리해주는 것
    • 개발자 입장에서 concurrency 상황에서의 문제들에 관해 고민할 필요 없도록 @Sendable을 선언하여 데이터 업데이트를 편하게 사용

  • pull to refresh 완성

페이지네이션 구현 ()

  • Swift에서 UITableView를 사용할 때 페이지네이션 구현 방법
    • 내장된 델리게이트 함수 prefetchRows 사용
    • contentSize.height와 contentOffset.y을 계산하여, contentSize.height값이 얼마 남지 않았을때 loadMoreData 호출
    • UITableView에서 빈 footerView를 놓고 이 footerView가 보일 때 loadMoreData와 같은 함수 호출
  • SwiftUI에서도 위 방법들 모두 다 가능하지만 가장 간편한 방법은 UITableView의 footerView를 놓는 것과 유사하게 마지막 Row가 보일때 loadMoreData를 호출하는 것
    • SwiftUI의 onAppear를 사용하면 매우 쉽게 구현이 가능
  • List 코드에서 클로저를 열고, 위에서 구현했던 PersonRowView 삽입
List(items) { person in
    PersonRowView(item: person)
}
  • onAppear를 활용하여 마지막 id의 아이템이 보일 경우 loadMoreData를 호출
List(items) { person in
    PersonRowView(item: person)
        .onAppear {
            if person.id == items.last?.id {
                Task {
                    await loadMoreItems()
                }
            }
        }
}

페이지네이션 완성)

 

* 전체 코드: https://github.com/JK0369/ExListSwiftUI

* 참고

- https://developer.apple.com/documentation/swift/sendable