a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Repos/PagedListModel.swift

ui-smoke
gitbay-ios/gitbay/Repos/PagedListModel.swift history · blame · raw

68 lines · 2202 bytes

 1import Foundation
 2import Observation
 3
 4/// Accumulated pages of one list command (`repo list`, `issue list`,
 5/// `mr list`). Cursors are opaque and server-minted; the model only
 6/// hands back what `next` carried.
 7@Observable
 8@MainActor
 9final class PagedListModel<Element: Decodable & Sendable> {
10
11    private(set) var state: LoadState<[Element]> = .loading
12    private(set) var isLoadingMore = false
13    private var next: String?
14
15    var hasMore: Bool { next != nil }
16
17    private let client: GitbayClient
18    private let pageSize: Int
19    /// The list command minus paging flags. Set by the owning view model
20    /// when a filter changes; a change only takes effect via `reload()`.
21    var argv: [String]
22    var emptyMessage: String
23
24    init(
25        client: GitbayClient,
26        argv: [String],
27        emptyMessage: String,
28        pageSize: Int = 50
29    ) {
30        self.client = client
31        self.argv = argv
32        self.emptyMessage = emptyMessage
33        self.pageSize = pageSize
34    }
35
36    func reload() async {
37        state = .loading
38        next = nil
39        await fetch(cursor: nil, appendingTo: [])
40    }
41
42    /// Fetch the next page. Safe to call repeatedly from row-appear
43    /// triggers; it no-ops while a fetch is in flight or at the end.
44    func loadMore() async {
45        guard let cursor = next, !isLoadingMore, let loaded = state.value else { return }
46        isLoadingMore = true
47        defer { isLoadingMore = false }
48        await fetch(cursor: cursor, appendingTo: loaded)
49    }
50
51    private func fetch(cursor: String?, appendingTo existing: [Element]) async {
52        do {
53            let page = try await client.readPage(
54                argv, of: Element.self, limit: pageSize, cursor: cursor
55            )
56            next = page.next
57            let all = existing + page.items
58            state = all.isEmpty ? .empty(emptyMessage) : .loaded(all)
59        } catch {
60            // A failed first page is the screen's state; a failed later
61            // page keeps what is on screen and leaves the cursor for a
62            // retry from the same trigger.
63            if existing.isEmpty {
64                state = .from(error)
65            }
66        }
67    }
68}