Skip to content

Commit ed67c30

Browse files
committed
Add pagination demo and reduce diff churn
1 parent e4a11a2 commit ed67c30

7 files changed

Lines changed: 572 additions & 329 deletions
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//
2+
// ApproachingBottomPaginationViewController.swift
3+
// Demo
4+
//
5+
// Created by OpenAI Codex on 2026-04-24.
6+
//
7+
8+
import ListableUI
9+
import BlueprintUILists
10+
import BlueprintUI
11+
import BlueprintUICommonControls
12+
import UIKit
13+
14+
15+
final class ApproachingBottomPaginationViewController : ListViewController
16+
{
17+
private let actions = ListActions()
18+
19+
private let pageSize = 20
20+
private let maxPageCount = 5
21+
22+
private var items : [DemoItem] = []
23+
private var nextItemNumber = 1
24+
private var loadedPageCount = 0
25+
private var approachingBottomCallCount = 0
26+
private var isLoadingNextPage = false
27+
28+
private var loadTask : Task<Void, Never>?
29+
30+
private var hasMorePages : Bool {
31+
self.loadedPageCount < self.maxPageCount
32+
}
33+
34+
deinit {
35+
self.loadTask?.cancel()
36+
}
37+
38+
override func viewDidLoad()
39+
{
40+
super.viewDidLoad()
41+
42+
self.title = "Approaching Bottom"
43+
44+
self.navigationItem.rightBarButtonItems = [
45+
UIBarButtonItem(title: "Reset", style: .plain, target: self, action: #selector(resetDemo)),
46+
UIBarButtonItem(title: "Bottom", style: .plain, target: self, action: #selector(scrollToBottom)),
47+
]
48+
49+
self.reset()
50+
}
51+
52+
override func configure(list : inout ListProperties)
53+
{
54+
list.appearance = .demoAppearance
55+
list.layout = .demoLayout()
56+
list.actions = self.actions
57+
58+
list.content.header = DemoHeader(
59+
title: "Approaching Bottom Pagination",
60+
detail: """
61+
Uses `onApproachingBottom(within: .screens(1.0))` to load the next page as the list nears its rendered end.
62+
Pages: \(loadedPageCount)/\(maxPageCount)
63+
Observer Calls: \(approachingBottomCallCount)
64+
Loading: \(isLoadingNextPage ? "Yes" : "No")
65+
"""
66+
)
67+
68+
list.stateObserver = ListStateObserver { observer in
69+
observer.onApproachingBottom(
70+
within: .screens(1.0),
71+
shouldPerform: { [weak self] _ in
72+
guard let self = self else { return false }
73+
return self.isLoadingNextPage == false && self.hasMorePages
74+
}
75+
) { [weak self] _ in
76+
self?.approachingBottomCallCount += 1
77+
self?.loadNextPage()
78+
}
79+
}
80+
81+
list("items") { section in
82+
for item in self.items {
83+
section += item
84+
}
85+
86+
if self.isLoadingNextPage {
87+
section += PaginationLoadingItem(identifierValue: "loading-next-page")
88+
} else if self.hasMorePages == false {
89+
section.footer = DemoFooter(text: "Reached the end of the demo list.")
90+
}
91+
}
92+
}
93+
94+
@objc private func resetDemo()
95+
{
96+
self.reset()
97+
}
98+
99+
@objc private func scrollToBottom()
100+
{
101+
self.actions.scrolling.scrollToLastItem(animated: true)
102+
}
103+
104+
private func reset()
105+
{
106+
self.loadTask?.cancel()
107+
self.loadTask = nil
108+
109+
self.items = []
110+
self.nextItemNumber = 1
111+
self.loadedPageCount = 0
112+
self.approachingBottomCallCount = 0
113+
self.isLoadingNextPage = false
114+
115+
self.appendPage()
116+
self.reload(animated: false)
117+
}
118+
119+
private func loadNextPage()
120+
{
121+
guard self.isLoadingNextPage == false else { return }
122+
guard self.hasMorePages else { return }
123+
124+
self.isLoadingNextPage = true
125+
self.reload(animated: true)
126+
127+
self.loadTask = Task { @MainActor [weak self] in
128+
try? await Task.sleep(nanoseconds: 1_000_000_000)
129+
130+
guard let self = self else { return }
131+
guard Task.isCancelled == false else { return }
132+
133+
self.isLoadingNextPage = false
134+
self.appendPage()
135+
self.reload(animated: true)
136+
self.loadTask = nil
137+
}
138+
}
139+
140+
private func appendPage()
141+
{
142+
let end = self.nextItemNumber + self.pageSize
143+
144+
for itemNumber in self.nextItemNumber..<end {
145+
self.items.append(DemoItem(text: "Item #\(itemNumber)"))
146+
}
147+
148+
self.nextItemNumber = end
149+
self.loadedPageCount += 1
150+
}
151+
}
152+
153+
154+
fileprivate struct PaginationLoadingItem : BlueprintItemContent, Equatable
155+
{
156+
var identifierValue : String
157+
158+
func element(with info : ApplyItemContentInfo) -> Element
159+
{
160+
Row(alignment: .center, minimumSpacing: 10.0) {
161+
PaginationActivityIndicatorElement()
162+
Label(text: "Loading next page…") {
163+
$0.font = .systemFont(ofSize: 17.0, weight: .medium)
164+
$0.color = .darkGray
165+
}
166+
}
167+
.inset(horizontal: 15.0, vertical: 13.0)
168+
}
169+
170+
func backgroundElement(with info: ApplyItemContentInfo) -> Element?
171+
{
172+
Box(
173+
backgroundColor: .white,
174+
cornerStyle: .rounded(radius: 8.0)
175+
)
176+
}
177+
}
178+
179+
fileprivate struct PaginationActivityIndicatorElement : UIViewElement {
180+
func makeUIView() -> UIActivityIndicatorView
181+
{
182+
UIActivityIndicatorView(style: .medium)
183+
}
184+
185+
func updateUIView(_ view: UIActivityIndicatorView, with context: UIViewElementContext)
186+
{
187+
if context.isMeasuring == false && view.isAnimating == false {
188+
view.startAnimating()
189+
}
190+
}
191+
}

Development/Sources/Demos/DemosRootViewController.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ public final class DemosRootViewController : ListViewController
105105
self?.push(ListStateViewController())
106106
}
107107
)
108+
109+
Item(
110+
DemoItem(text: "Approaching Bottom Pagination"),
111+
selectionStyle: .selectable(),
112+
onSelect: { _ in
113+
self?.push(ApproachingBottomPaginationViewController())
114+
}
115+
)
108116

109117
Item(
110118
DemoItem(text: "Itemization Editor"),

0 commit comments

Comments
 (0)