Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
UIKitTableSource.hpp
Go to the documentation of this file.
1#pragma once
2
3// UIKitTableSource.hpp — bridge any aria list source onto UITableView.
4//
5// UIKit counterpart of `qt_list_model_adapter.hpp` and
6// `AppKitTableSource.hpp`. Accepts any source satisfying
7// `aria::ListSourceOf<L, T>` (ObservableList / FilteredList /
8// SortedList / MappedList) and turns `ListChange<T>` events into:
9//
10// Insert -> [UITableView insertRowsAtIndexPaths:...:]
11// Remove -> [UITableView deleteRowsAtIndexPaths:...:]
12// Replace -> [UITableView reloadRowsAtIndexPaths:...:]
13// ItemChanged -> [UITableView reloadRowsAtIndexPaths:...:]
14// Move -> [UITableView moveRowAtIndexPath:toIndexPath:]
15// Reset -> [UITableView reloadData]
16//
17// Header is .mm-only (UIKit imports). Header-only template — same
18// distribution model as the AppKit / Qt6 adapters.
19//
20// Construct and read on the main thread. Events own their payloads and
21// enter one FIFO queue, so a worker event cannot be overtaken by a later
22// main-thread event. Idle main-thread delivery remains synchronous.
23// Destruction retires pending work immediately; native data-source cleanup
24// is transferred to the main queue when destroyed from another thread.
25
27#include "aria/list_source.hpp"
29#include "aria/subscription.hpp"
30
31#import <UIKit/UIKit.h>
32
33#include <atomic>
34#include <cstddef>
35#include <functional>
36#include <deque>
37#include <mutex>
38#include <stdexcept>
39#include <memory>
40#include <utility>
41#include <vector>
42
43// ─── ObjC data-source / delegate ────────────────────────────────────────
44
45@interface AriaUITableDataSource : NSObject <UITableViewDataSource, UITableViewDelegate>
46- (instancetype)initWithRowCount:(std::function<NSInteger()>)rowCountFn
47 cellForFn:(std::function<UITableViewCell*(UITableView*,
48 NSIndexPath*)>)cellForFn;
49@end
50
51namespace aria::adapters::uikit {
52
53template<typename T>
55public:
60 using CellForRowFn = std::function<UITableViewCell*(UITableView*,
61 std::shared_ptr<T>,
62 NSIndexPath*)>;
63
67 template<class L>
68 requires ::aria::ListSourceOf<L, T>
69 ObservableTableSource(UITableView* tableView,
70 L& source,
71 CellForRowFn cell_for_row)
72 : state_(std::make_shared<State>()) {
73 if (![NSThread isMainThread]) throw std::logic_error("table bridge construction requires the main thread");
74 state_->events = std::make_shared<EventQueue>();
75 state_->events->target = state_;
76 state_->table = tableView;
77 state_->cell_for_row = std::move(cell_for_row);
78 state_->snapshot = source.snapshot();
79
80 std::weak_ptr<State> weak_state = state_;
81
82 auto row_count_fn = [weak_state]() -> NSInteger {
83 if (auto s = weak_state.lock(); s && !s->detached.load(std::memory_order_acquire)) {
84 return static_cast<NSInteger>(s->snapshot.size());
85 }
86 return 0;
87 };
88 auto cell_for_fn = [weak_state](UITableView* tv,
89 NSIndexPath* indexPath) -> UITableViewCell* {
90 auto s = weak_state.lock();
91 if (!s || s->detached.load(std::memory_order_acquire)) return nil;
92 const NSInteger row = indexPath.row;
93 if (row < 0
94 || static_cast<std::size_t>(row) >= s->snapshot.size()) {
95 return [[UITableViewCell alloc]
96 initWithStyle:UITableViewCellStyleDefault
97 reuseIdentifier:@"empty"];
98 }
99 auto item = s->snapshot[static_cast<std::size_t>(row)];
100 if (!item) {
101 return [[UITableViewCell alloc]
102 initWithStyle:UITableViewCellStyleDefault
103 reuseIdentifier:@"empty"];
104 }
105 return s->cell_for_row(tv, item, indexPath);
106 };
107
108 state_->ds = [[AriaUITableDataSource alloc]
109 initWithRowCount:std::move(row_count_fn)
110 cellForFn:std::move(cell_for_fn)];
111 sub_ = source.observe([events = state_->events](const ::aria::ListChange<T>& change) {
112 enqueue_(events, change);
113 });
114 tableView.dataSource = state_->ds;
115 tableView.delegate = state_->ds;
116 [tableView reloadData];
117 }
118
120 state_->detached.store(true, std::memory_order_release);
121 {
122 std::lock_guard lock(state_->events->mutex);
123 state_->events->stopped = true;
124 }
125 sub_.release();
126 if ([NSThread isMainThread]) {
127 cleanup_(*state_);
128 } else {
129 // Transfer the sole wrapper owner, not a temporary shared copy:
130 // the native data-source and renderer captures are released on main.
131 auto* owner = new std::shared_ptr<State>(std::move(state_));
132 dispatch_async_f(dispatch_get_main_queue(), owner, [](void* context) {
133 std::unique_ptr<std::shared_ptr<State>> state{static_cast<std::shared_ptr<State>*>(context)};
134 cleanup_(**state);
135 });
136 }
137 }
138
141
142 [[nodiscard]] std::size_t row_count() const noexcept {
143 return state_ ? state_->snapshot.size() : 0;
144 }
145
146 [[nodiscard]] std::shared_ptr<T> at(std::size_t i) const {
147 if (!state_) return nullptr;
148 if (i >= state_->snapshot.size()) return nullptr;
149 return state_->snapshot[i];
150 }
151
152private:
153 struct State;
154 struct EventQueue {
155 std::mutex mutex;
156 std::deque<::aria::ListChange<T>> changes;
157 std::weak_ptr<State> target;
158 bool scheduled = false;
159 bool stopped = false;
160 };
161 struct State {
162 UITableView* __weak table = nil;
163 AriaUITableDataSource* __strong ds = nil;
164 CellForRowFn cell_for_row;
165 std::vector<std::shared_ptr<T>> snapshot;
166 std::atomic<bool> detached{false};
167 std::shared_ptr<EventQueue> events;
168 };
169
170 static void cleanup_(State& state) {
171 std::deque<::aria::ListChange<T>> discarded;
172 {
173 std::lock_guard lock(state.events->mutex);
174 discarded.swap(state.events->changes);
175 }
176 UITableView* table = state.table;
177 if (table.dataSource == state.ds) table.dataSource = nil;
178 if (table.delegate == state.ds) table.delegate = nil;
179 }
180
181 static void enqueue_(const std::shared_ptr<EventQueue>& events, const ::aria::ListChange<T>& change) {
182 {
183 std::lock_guard lock(events->mutex);
184 if (events->stopped) return;
185 events->changes.push_back(change);
186 if (events->scheduled) return;
187 events->scheduled = true;
188 }
189 if ([NSThread isMainThread]) drain_(events);
190 else {
191 auto pending = events; // Copy ownership into the block, not the reference parameter.
192 dispatch_async(dispatch_get_main_queue(), ^{ drain_(pending); });
193 }
194 }
195
196 static void drain_(const std::shared_ptr<EventQueue>& events) {
197 auto state = events->target.lock(); // Native State is only retained on main.
198 if (!state) return;
199 for (;;) {
200 ::aria::ListChange<T> change{};
201 {
202 std::lock_guard lock(events->mutex);
203 if (events->stopped || events->changes.empty()) {
204 events->scheduled = false;
205 return;
206 }
207 change = std::move(events->changes.front());
208 events->changes.pop_front();
209 }
210 if (state->detached.load(std::memory_order_acquire)) return;
211 try { apply_change_(*state, change); }
212 catch (...) { ::aria::report_callback_failure("uikit.table", std::current_exception()); }
213 }
214 }
215
216 static NSIndexPath* ip_(std::size_t row) {
217 return [NSIndexPath indexPathForRow:static_cast<NSInteger>(row) inSection:0];
218 }
219
220 static void apply_change_(State& s,
221 const ::aria::ListChange<T>& ch) {
222 using K = ::aria::ListChangeKind;
223 switch (ch.kind) {
224 case K::Insert: apply_insert_(s, ch.index, ch.item); return;
225 case K::Remove: apply_remove_(s, ch.index); return;
226 case K::Replace: apply_replace_(s, ch.index, ch.item); return;
227 case K::ItemChanged: apply_item_changed_(s, ch.index); return;
228 case K::Move: apply_move_(s, ch.from_index, ch.index); return;
229 case K::Reset: apply_reset_(s, ch); return;
230 }
231 }
232
233 static void apply_insert_(State& s,
234 std::size_t idx,
235 const std::shared_ptr<T>& item) {
236 if (idx > s.snapshot.size()) idx = s.snapshot.size();
237 s.snapshot.insert(s.snapshot.begin() + static_cast<std::ptrdiff_t>(idx),
238 item);
239 if (!s.table) return;
240 [s.table insertRowsAtIndexPaths:@[ ip_(idx) ]
241 withRowAnimation:UITableViewRowAnimationFade];
242 }
243
244 static void apply_remove_(State& s, std::size_t idx) {
245 if (idx >= s.snapshot.size()) return;
246 s.snapshot.erase(s.snapshot.begin() + static_cast<std::ptrdiff_t>(idx));
247 if (!s.table) return;
248 [s.table deleteRowsAtIndexPaths:@[ ip_(idx) ]
249 withRowAnimation:UITableViewRowAnimationFade];
250 }
251
252 static void apply_replace_(State& s,
253 std::size_t idx,
254 const std::shared_ptr<T>& item) {
255 if (idx >= s.snapshot.size()) return;
256 s.snapshot[idx] = item;
257 if (!s.table) return;
258 [s.table reloadRowsAtIndexPaths:@[ ip_(idx) ]
259 withRowAnimation:UITableViewRowAnimationFade];
260 }
261
262 static void apply_item_changed_(State& s, std::size_t idx) {
263 if (idx >= s.snapshot.size()) return;
264 if (!s.table) return;
265 [s.table reloadRowsAtIndexPaths:@[ ip_(idx) ]
266 withRowAnimation:UITableViewRowAnimationNone];
267 }
268
269 static void apply_move_(State& s, std::size_t from, std::size_t to) {
270 if (from == to) return;
271 if (from >= s.snapshot.size() || to >= s.snapshot.size()) return;
272 auto moved = s.snapshot[from];
273 s.snapshot.erase(s.snapshot.begin() + static_cast<std::ptrdiff_t>(from));
274 s.snapshot.insert(s.snapshot.begin() + static_cast<std::ptrdiff_t>(to),
275 std::move(moved));
276 if (!s.table) return;
277 [s.table moveRowAtIndexPath:ip_(from) toIndexPath:ip_(to)];
278 }
279
280 static void apply_reset_(State& s, const ::aria::ListChange<T>& change) {
281 if (!change.snapshot) throw std::logic_error("Reset requires an owned snapshot");
282 s.snapshot = *change.snapshot;
283 if (s.table) [s.table reloadData];
284 }
285
286 std::shared_ptr<State> state_;
287 ::aria::Subscription sub_;
288};
289
290} // namespace aria::adapters::uikit
Definition UIKitTableSource.hpp:54
std::size_t row_count() const noexcept
Definition UIKitTableSource.hpp:142
ObservableTableSource & operator=(const ObservableTableSource &)=delete
~ObservableTableSource()
Definition UIKitTableSource.hpp:119
requires ::aria::ListSourceOf< L, T > ObservableTableSource(UITableView *tableView, L &source, CellForRowFn cell_for_row)
Construct a binding between an aria list source and a UITableView.
Definition UIKitTableSource.hpp:69
std::shared_ptr< T > at(std::size_t i) const
Definition UIKitTableSource.hpp:146
std::function< UITableViewCell *(UITableView *, std::shared_ptr< T >, NSIndexPath *)> CellForRowFn
Render callback: given a row's shared_ptr<T>, the table view and the index path being asked about,...
Definition UIKitTableSource.hpp:60
ObservableTableSource(const ObservableTableSource &)=delete
AriaUITableDataSource
Definition UIKitTableSource.hpp:46
Definition UIKitAdapter.hpp:74
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
ListChangeKind
Definition list_change.hpp:10
Definition validation_key.hpp:110