craft-native 0.0.123 → 0.0.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/android/src/index.d.ts +3 -0
- package/dist/android/src/index.js +22 -3
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/CraftNativeFlatList.kt.template +151 -7
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +29 -0
- package/dist/android/templates/MainActivity.kt.template +17 -0
- package/dist/android/templates/MainActivityNative.kt.template +1085 -84
- package/dist/android/templates/build.gradle.kts.app.template +1 -0
- package/dist/api/mobile.d.ts +250 -2
- package/dist/cli.js +3 -3
- package/dist/index.cjs +22 -1
- package/dist/index.js +22 -1
- package/dist/ios/src/index.d.ts +125 -3
- package/dist/ios/src/index.js +284 -47
- package/dist/ios/templates/CraftApp.swift +1186 -69
- package/dist/ios/templates/CraftHybrid.swift +1196 -0
- package/dist/ios/templates/CraftNativeActions.swift +310 -10
- package/dist/ios/templates/CraftNativeFlatList.swift +226 -17
- package/dist/ios/templates/CraftNativeScreen.swift +2217 -220
- package/dist/ios/templates/CraftNativeUI.swift +562 -0
- package/dist/ios/templates/Info.plist.template +2 -1
- package/dist/mobile.js +204 -0
- package/package.json +1 -1
|
@@ -9,10 +9,10 @@ private final class CraftNativeFlatListLayout: UICollectionViewFlowLayout {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/// A keyed, recycling native list used by
|
|
12
|
+
/// A keyed, recycling native list used by stx's native FlatList primitive.
|
|
13
13
|
/// The screen controller owns rendered row state; this view owns collection
|
|
14
14
|
/// diffs, viewport state, and UICollectionView cell reuse.
|
|
15
|
-
final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowLayout {
|
|
15
|
+
final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowLayout, UIGestureRecognizerDelegate {
|
|
16
16
|
typealias RenderItem = (_ node: [String: Any], _ identity: String, _ previous: UIView?) -> UIView
|
|
17
17
|
|
|
18
18
|
private final class Cell: UICollectionViewCell {
|
|
@@ -21,6 +21,9 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
21
21
|
private(set) var representedIdentity: String?
|
|
22
22
|
private(set) var hostedView: UIView?
|
|
23
23
|
var onRecycle: ((String) -> Void)?
|
|
24
|
+
/// The width (or, sideways, height) the list gives this row, so a row
|
|
25
|
+
/// is measured at the list's width rather than the layout's estimate.
|
|
26
|
+
var fittingExtent: CGFloat?
|
|
24
27
|
|
|
25
28
|
override func prepareForReuse() {
|
|
26
29
|
super.prepareForReuse()
|
|
@@ -56,13 +59,33 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
56
59
|
) -> UICollectionViewLayoutAttributes {
|
|
57
60
|
guard let hostedView = hostedView else { return layoutAttributes }
|
|
58
61
|
let attributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
// A cell has no collectionView property; the list is its nearest
|
|
63
|
+
// collection view ancestor.
|
|
64
|
+
var ancestor = superview
|
|
65
|
+
while let view = ancestor, !(view is UICollectionView) { ancestor = view.superview }
|
|
66
|
+
if let collectionView = ancestor as? UICollectionView,
|
|
67
|
+
let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout,
|
|
68
|
+
flowLayout.scrollDirection == .horizontal {
|
|
69
|
+
let height = fittingExtent ?? layoutAttributes.size.height
|
|
70
|
+
let measured = CraftNativeFlexLayout.measure(
|
|
71
|
+
hostedView,
|
|
72
|
+
width: nil,
|
|
73
|
+
height: height,
|
|
74
|
+
maxWidth: CraftNativeFlexLayout.unbounded,
|
|
75
|
+
maxHeight: height
|
|
76
|
+
)
|
|
77
|
+
attributes.size = CGSize(width: max(1, ceil(measured.width)), height: max(1, height))
|
|
78
|
+
} else {
|
|
79
|
+
let width = fittingExtent ?? layoutAttributes.size.width
|
|
80
|
+
let measured = CraftNativeFlexLayout.measure(
|
|
81
|
+
hostedView,
|
|
82
|
+
width: width,
|
|
83
|
+
height: nil,
|
|
84
|
+
maxWidth: width,
|
|
85
|
+
maxHeight: CraftNativeFlexLayout.unbounded
|
|
86
|
+
)
|
|
87
|
+
attributes.size = CGSize(width: max(1, width), height: max(1, ceil(measured.height)))
|
|
88
|
+
}
|
|
66
89
|
return attributes
|
|
67
90
|
}
|
|
68
91
|
}
|
|
@@ -86,11 +109,24 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
86
109
|
private var renderItem: RenderItem?
|
|
87
110
|
private var recycleItem: ((String) -> Void)?
|
|
88
111
|
private var endReached: (() -> Void)?
|
|
112
|
+
var onScrollEvent: ((UIScrollView) -> Void)?
|
|
113
|
+
/// Called as the visible rows move, for a screen that lays out around them.
|
|
114
|
+
var onLayoutChanged: (() -> Void)?
|
|
115
|
+
var onScrollBeginDrag: ((UIScrollView) -> Void)?
|
|
116
|
+
var onScrollEndDrag: ((UIScrollView) -> Void)?
|
|
117
|
+
var onMomentumScrollBegin: ((UIScrollView) -> Void)?
|
|
118
|
+
var onMomentumScrollEnd: ((UIScrollView) -> Void)?
|
|
89
119
|
private var endReachedThreshold = 0.1
|
|
90
120
|
private var endReachedSignature: String?
|
|
121
|
+
private var dataContentSignature = ""
|
|
91
122
|
private var columns = 1
|
|
92
123
|
private var isApplyingSnapshot = false
|
|
93
124
|
private var pendingApply: (() -> Void)?
|
|
125
|
+
private var pendingThemeRefresh = false
|
|
126
|
+
private let nativeRefreshControl = UIRefreshControl()
|
|
127
|
+
private var refreshHandler: (() -> Void)?
|
|
128
|
+
private let keyboardTapGesture = UITapGestureRecognizer()
|
|
129
|
+
private var keyboardShouldPersistTaps = "never"
|
|
94
130
|
|
|
95
131
|
init() {
|
|
96
132
|
let layout = CraftNativeFlatListLayout()
|
|
@@ -102,7 +138,12 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
102
138
|
backgroundColor = .clear
|
|
103
139
|
alwaysBounceVertical = true
|
|
104
140
|
delegate = self
|
|
141
|
+
keyboardTapGesture.cancelsTouchesInView = false
|
|
142
|
+
keyboardTapGesture.delegate = self
|
|
143
|
+
keyboardTapGesture.addTarget(self, action: #selector(keyboardTap))
|
|
144
|
+
addGestureRecognizer(keyboardTapGesture)
|
|
105
145
|
register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
|
|
146
|
+
nativeRefreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
|
|
106
147
|
diffableDataSource = UICollectionViewDiffableDataSource<Int, String>(collectionView: self) {
|
|
107
148
|
[weak self] collectionView, indexPath, identity in
|
|
108
149
|
guard let self = self,
|
|
@@ -114,6 +155,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
114
155
|
let renderItem = self.renderItem else { return UICollectionViewCell() }
|
|
115
156
|
let view = renderItem(item.node, identity, cell.hostedView)
|
|
116
157
|
cell.onRecycle = self.recycleItem
|
|
158
|
+
cell.fittingExtent = self.itemExtent(isChrome: item.isChrome)
|
|
117
159
|
cell.host(view, identity: identity)
|
|
118
160
|
return cell
|
|
119
161
|
}
|
|
@@ -121,6 +163,123 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
121
163
|
|
|
122
164
|
required init?(coder: NSCoder) { nil }
|
|
123
165
|
|
|
166
|
+
/// Called when a list that does not scroll changes height, so the
|
|
167
|
+
/// containers around it make room.
|
|
168
|
+
var onFittingHeightChanged: (() -> Void)?
|
|
169
|
+
|
|
170
|
+
override var contentSize: CGSize {
|
|
171
|
+
didSet {
|
|
172
|
+
guard !isScrollEnabled, abs(oldValue.height - contentSize.height) > 0.5 else { return }
|
|
173
|
+
invalidateIntrinsicContentSize()
|
|
174
|
+
onFittingHeightChanged?()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override var isScrollEnabled: Bool {
|
|
179
|
+
didSet {
|
|
180
|
+
guard isScrollEnabled != oldValue else { return }
|
|
181
|
+
alwaysBounceVertical = isScrollEnabled && flowLayout.scrollDirection == .vertical
|
|
182
|
+
onFittingHeightChanged?()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// The height of every row at `width`: what a list that does not scroll
|
|
187
|
+
/// (`scrollEnabled={false}`) takes inside a scroll view or a column.
|
|
188
|
+
/// Rows not measured yet count at their estimate, and the list asks for
|
|
189
|
+
/// room again as they are.
|
|
190
|
+
func fittingHeight(width: CGFloat) -> CGFloat {
|
|
191
|
+
guard !isScrollEnabled else { return 0 }
|
|
192
|
+
if abs(bounds.width - width) > 0.5 { frame.size.width = width }
|
|
193
|
+
layoutIfNeeded()
|
|
194
|
+
let content = flowLayout.collectionViewContentSize.height
|
|
195
|
+
return max(0, ceil(content + contentInset.top + contentInset.bottom))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// The width a row gets: the list's own width inside its content padding,
|
|
199
|
+
/// shared between columns for data rows.
|
|
200
|
+
private func itemExtent(isChrome: Bool) -> CGFloat {
|
|
201
|
+
if flowLayout.scrollDirection == .horizontal {
|
|
202
|
+
return max(1, bounds.height - adjustedContentInset.top - adjustedContentInset.bottom)
|
|
203
|
+
}
|
|
204
|
+
let width = max(1, bounds.width - adjustedContentInset.left - adjustedContentInset.right)
|
|
205
|
+
let count = isChrome ? 1 : columns
|
|
206
|
+
let spacing = flowLayout.minimumInteritemSpacing * CGFloat(count - 1)
|
|
207
|
+
return max(1, (width - spacing) / CGFloat(count))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
override func layoutSubviews() {
|
|
211
|
+
let estimate = flowLayout.scrollDirection == .horizontal
|
|
212
|
+
? CGSize(width: 44, height: itemExtent(isChrome: true))
|
|
213
|
+
: CGSize(width: itemExtent(isChrome: false), height: 44)
|
|
214
|
+
if flowLayout.estimatedItemSize != estimate, bounds.width > 0 {
|
|
215
|
+
flowLayout.estimatedItemSize = estimate
|
|
216
|
+
}
|
|
217
|
+
super.layoutSubviews()
|
|
218
|
+
craftApplyCornerRadius()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func setContentContainerStyle(_ raw: [String: Any]?) {
|
|
222
|
+
func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
223
|
+
let padding = number(raw?["padding"]) ?? 0
|
|
224
|
+
let horizontal = number(raw?["paddingHorizontal"]) ?? padding
|
|
225
|
+
let vertical = number(raw?["paddingVertical"]) ?? padding
|
|
226
|
+
contentInset = UIEdgeInsets(
|
|
227
|
+
top: number(raw?["paddingTop"]) ?? vertical,
|
|
228
|
+
left: number(raw?["paddingLeft"]) ?? horizontal,
|
|
229
|
+
bottom: number(raw?["paddingBottom"]) ?? vertical,
|
|
230
|
+
right: number(raw?["paddingRight"]) ?? horizontal
|
|
231
|
+
)
|
|
232
|
+
scrollIndicatorInsets = contentInset
|
|
233
|
+
collectionViewLayout.invalidateLayout()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func setKeyboardShouldPersistTaps(_ value: Any?) {
|
|
237
|
+
let requested = value as? String
|
|
238
|
+
keyboardShouldPersistTaps = ["always", "handled", "never"].contains(requested) ? requested! : "never"
|
|
239
|
+
keyboardTapGesture.isEnabled = keyboardShouldPersistTaps != "always"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private var refreshingProp = false
|
|
243
|
+
|
|
244
|
+
/// `onRefresh` and `refreshing`, controlled as in React Native: a pull
|
|
245
|
+
/// the screen does not answer with `refreshing` true ends at once.
|
|
246
|
+
func setRefreshHandler(_ handler: (() -> Void)?, refreshing: Bool) {
|
|
247
|
+
refreshHandler = handler
|
|
248
|
+
refreshingProp = refreshing
|
|
249
|
+
let control = handler == nil ? nil : nativeRefreshControl
|
|
250
|
+
if refreshControl !== control { refreshControl = control }
|
|
251
|
+
if handler == nil || !refreshing {
|
|
252
|
+
if nativeRefreshControl.isRefreshing { nativeRefreshControl.endRefreshing() }
|
|
253
|
+
} else if !nativeRefreshControl.isRefreshing {
|
|
254
|
+
nativeRefreshControl.beginRefreshing()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@objc private func refreshTriggered() {
|
|
259
|
+
refreshHandler?()
|
|
260
|
+
DispatchQueue.main.async { [weak self] in
|
|
261
|
+
guard let self, !self.refreshingProp, self.nativeRefreshControl.isRefreshing else { return }
|
|
262
|
+
self.nativeRefreshControl.endRefreshing()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@objc private func keyboardTap() {
|
|
267
|
+
guard keyboardShouldPersistTaps != "always" else { return }
|
|
268
|
+
if keyboardShouldPersistTaps == "handled" {
|
|
269
|
+
let point = keyboardTapGesture.location(in: self)
|
|
270
|
+
var hit = hitTest(point, with: nil)
|
|
271
|
+
while let view = hit, view !== self {
|
|
272
|
+
if view is UIControl || !(view.gestureRecognizers ?? []).isEmpty { return }
|
|
273
|
+
hit = view.superview
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
window?.endEditing(true)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
280
|
+
true
|
|
281
|
+
}
|
|
282
|
+
|
|
124
283
|
var visibleItemIdentities: [String] {
|
|
125
284
|
indexPathsForVisibleItems
|
|
126
285
|
.sorted { $0.item < $1.item }
|
|
@@ -220,7 +379,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
220
379
|
}
|
|
221
380
|
|
|
222
381
|
let dataItems = next.filter { !$0.isChrome }
|
|
223
|
-
let contentSignature =
|
|
382
|
+
let contentSignature = dataItems.map {
|
|
383
|
+
let encoded = $0.signature.base64EncodedString()
|
|
384
|
+
return "\($0.identity.utf8.count):\($0.identity)\(encoded.utf8.count):\(encoded)"
|
|
385
|
+
}.joined()
|
|
386
|
+
dataContentSignature = contentSignature
|
|
224
387
|
if contentSignature != endReachedSignature { endReachedSignature = nil }
|
|
225
388
|
}
|
|
226
389
|
|
|
@@ -232,9 +395,53 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
232
395
|
renderItem = nil
|
|
233
396
|
recycleItem = nil
|
|
234
397
|
endReached = nil
|
|
398
|
+
dataContentSignature = ""
|
|
399
|
+
onScrollEvent = nil
|
|
400
|
+
onLayoutChanged = nil
|
|
401
|
+
onScrollBeginDrag = nil
|
|
402
|
+
onScrollEndDrag = nil
|
|
403
|
+
onMomentumScrollBegin = nil
|
|
404
|
+
onMomentumScrollEnd = nil
|
|
405
|
+
pendingThemeRefresh = false
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func refreshThemeDefaults() {
|
|
409
|
+
if isApplyingSnapshot {
|
|
410
|
+
pendingThemeRefresh = true
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
var snapshot = diffableDataSource.snapshot()
|
|
414
|
+
let identities = snapshot.itemIdentifiers
|
|
415
|
+
guard !identities.isEmpty else { return }
|
|
416
|
+
pendingThemeRefresh = false
|
|
417
|
+
snapshot.reconfigureItems(identities)
|
|
418
|
+
isApplyingSnapshot = true
|
|
419
|
+
diffableDataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
|
|
420
|
+
self?.finishApplyingSnapshot()
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
425
|
+
onScrollEvent?(scrollView)
|
|
426
|
+
onLayoutChanged?()
|
|
427
|
+
evaluateEndReached()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
431
|
+
onScrollBeginDrag?(scrollView)
|
|
235
432
|
}
|
|
236
433
|
|
|
237
|
-
func
|
|
434
|
+
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
435
|
+
onScrollEndDrag?(scrollView)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
|
439
|
+
onMomentumScrollBegin?(scrollView)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
443
|
+
onMomentumScrollEnd?(scrollView)
|
|
444
|
+
}
|
|
238
445
|
|
|
239
446
|
func collectionView(
|
|
240
447
|
_ collectionView: UICollectionView,
|
|
@@ -244,11 +451,9 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
244
451
|
guard items.indices.contains(indexPath.item) else { return CGSize(width: bounds.width, height: 44) }
|
|
245
452
|
let item = items[indexPath.item]
|
|
246
453
|
if flowLayout.scrollDirection == .horizontal {
|
|
247
|
-
return CGSize(width:
|
|
454
|
+
return CGSize(width: 44, height: itemExtent(isChrome: item.isChrome))
|
|
248
455
|
}
|
|
249
|
-
|
|
250
|
-
let spacing = flowLayout.minimumInteritemSpacing * CGFloat(count - 1)
|
|
251
|
-
return CGSize(width: max(1, (bounds.width - spacing) / CGFloat(count)), height: 44)
|
|
456
|
+
return CGSize(width: itemExtent(isChrome: item.isChrome), height: 44)
|
|
252
457
|
}
|
|
253
458
|
|
|
254
459
|
private func evaluateEndReached() {
|
|
@@ -261,8 +466,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
261
466
|
let remaining = dataIndices.count - lastDataOrdinal - 1
|
|
262
467
|
let thresholdItems = max(1, Int(ceil(Double(dataIndices.count) * endReachedThreshold)))
|
|
263
468
|
guard remaining <= thresholdItems else { return }
|
|
264
|
-
let
|
|
265
|
-
let signature = "\(dataIndices.count):\(lastIdentity)"
|
|
469
|
+
let signature = dataContentSignature
|
|
266
470
|
guard endReachedSignature != signature else { return }
|
|
267
471
|
endReachedSignature = signature
|
|
268
472
|
endReached()
|
|
@@ -277,6 +481,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
277
481
|
pendingApply()
|
|
278
482
|
return
|
|
279
483
|
}
|
|
484
|
+
if self.pendingThemeRefresh {
|
|
485
|
+
self.pendingThemeRefresh = false
|
|
486
|
+
self.refreshThemeDefaults()
|
|
487
|
+
return
|
|
488
|
+
}
|
|
280
489
|
self.evaluateEndReached()
|
|
281
490
|
}
|
|
282
491
|
}
|