craft-native 0.0.202 → 0.0.203
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/dist/android/src/index.d.ts +1 -0
- package/dist/android/src/index.js +141 -85
- package/dist/android/src/testing.d.ts +2 -0
- package/dist/android/templates/MainActivityNative.kt.template +151 -55
- package/dist/android/templates/build.gradle.kts.app.template +2 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -1
- package/dist/ios/src/index.js +17 -4
- package/dist/ios/templates/CraftHybrid.swift +28 -3
- package/dist/ios/templates/CraftNativeFiles.swift +4 -2
- package/dist/ios/templates/CraftNativeFlatList.swift +62 -8
- package/dist/ios/templates/CraftNativeScreen.swift +38 -14
- package/package.json +1 -1
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
|
|
3
3
|
private final class CraftNativeFlatListLayout: UICollectionViewFlowLayout {
|
|
4
|
+
private var preparedExtent: CGFloat?
|
|
5
|
+
|
|
6
|
+
override func prepare() {
|
|
7
|
+
// Content-size queries can prepare layout before layoutSubviews runs.
|
|
8
|
+
(collectionView as? CraftNativeFlatList)?.updateItemEstimate()
|
|
9
|
+
super.prepare()
|
|
10
|
+
preparedExtent = collectionView.map { extent(of: $0.bounds) }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private func extent(of bounds: CGRect) -> CGFloat {
|
|
14
|
+
scrollDirection == .vertical ? bounds.width : bounds.height
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
5
|
-
guard let
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
guard let preparedExtent else { return true }
|
|
19
|
+
return abs(preparedExtent - extent(of: newBounds)) > .ulpOfOne
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
|
|
23
|
+
let context = super.invalidationContext(forBoundsChange: newBounds)
|
|
24
|
+
if shouldInvalidateLayout(forBoundsChange: newBounds), let flow = context as? UICollectionViewFlowLayoutInvalidationContext {
|
|
25
|
+
flow.invalidateFlowLayoutDelegateMetrics = true
|
|
26
|
+
flow.invalidateFlowLayoutAttributes = true
|
|
27
|
+
// Self-sized item dimensions are cached separately from flow
|
|
28
|
+
// metrics. Clear them before a narrower viewport validates old
|
|
29
|
+
// widths, while keeping the cells and their rendered state.
|
|
30
|
+
if let collectionView {
|
|
31
|
+
let paths = (0..<collectionView.numberOfSections).flatMap { section in
|
|
32
|
+
(0..<collectionView.numberOfItems(inSection: section)).map { IndexPath(item: $0, section: section) }
|
|
33
|
+
}
|
|
34
|
+
flow.invalidateItems(at: paths)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return context
|
|
9
38
|
}
|
|
10
39
|
}
|
|
11
40
|
|
|
@@ -190,9 +219,30 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
190
219
|
/// room again as they are.
|
|
191
220
|
func fittingHeight(width: CGFloat) -> CGFloat {
|
|
192
221
|
guard !isScrollEnabled else { return 0 }
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
// Flex measurement proposes several widths before committing a frame.
|
|
223
|
+
// Mutating the collection here lets self-sizing cache a provisional
|
|
224
|
+
// width and can reenter parent measurement through contentSize.didSet.
|
|
225
|
+
// Read the committed measurement, or estimate until layout measures
|
|
226
|
+
// rows at their actual width and onFittingHeightChanged asks for room.
|
|
227
|
+
let content: CGFloat
|
|
228
|
+
if abs(bounds.width - width) <= 0.5, contentSize.height > 0 {
|
|
229
|
+
content = contentSize.height
|
|
230
|
+
} else if flowLayout.scrollDirection == .horizontal {
|
|
231
|
+
content = items.isEmpty ? 0 : 44
|
|
232
|
+
} else {
|
|
233
|
+
var rows = 0
|
|
234
|
+
var pending = 0
|
|
235
|
+
for item in items {
|
|
236
|
+
if item.isChrome {
|
|
237
|
+
rows += (pending + columns - 1) / columns + 1
|
|
238
|
+
pending = 0
|
|
239
|
+
} else {
|
|
240
|
+
pending += 1
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
rows += (pending + columns - 1) / columns
|
|
244
|
+
content = CGFloat(rows) * 44 + CGFloat(max(0, rows - 1)) * flowLayout.minimumLineSpacing
|
|
245
|
+
}
|
|
196
246
|
return max(0, ceil(content + contentInset.top + contentInset.bottom))
|
|
197
247
|
}
|
|
198
248
|
|
|
@@ -208,13 +258,17 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
208
258
|
return max(1, (width - spacing) / CGFloat(count))
|
|
209
259
|
}
|
|
210
260
|
|
|
211
|
-
|
|
261
|
+
fileprivate func updateItemEstimate() {
|
|
212
262
|
let estimate = flowLayout.scrollDirection == .horizontal
|
|
213
263
|
? CGSize(width: 44, height: itemExtent(isChrome: true))
|
|
214
264
|
: CGSize(width: itemExtent(isChrome: false), height: 44)
|
|
215
265
|
if flowLayout.estimatedItemSize != estimate, bounds.width > 0 {
|
|
216
266
|
flowLayout.estimatedItemSize = estimate
|
|
217
267
|
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
override func layoutSubviews() {
|
|
271
|
+
updateItemEstimate()
|
|
218
272
|
super.layoutSubviews()
|
|
219
273
|
craftApplyCornerRadius()
|
|
220
274
|
}
|
|
@@ -4650,16 +4650,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4650
4650
|
}
|
|
4651
4651
|
/// The scroll view the screen starts with, when nothing sits above it:
|
|
4652
4652
|
/// the root's first child, its first, and so on, none padded on top.
|
|
4653
|
-
func leading(_ node: RenderedNode) ->
|
|
4654
|
-
if let scroll = node.view as? CraftNativeScrollView { return scroll.isVertical && scroll.isScrollEnabled ?
|
|
4653
|
+
func leading(_ node: RenderedNode) -> RenderedNode? {
|
|
4654
|
+
if let scroll = node.view as? CraftNativeScrollView { return scroll.isVertical && scroll.isScrollEnabled ? node : nil }
|
|
4655
4655
|
if let list = node.view as? CraftNativeFlatList {
|
|
4656
|
-
return list.isScrollEnabled && (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal ?
|
|
4656
|
+
return list.isScrollEnabled && (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal ? node : nil
|
|
4657
4657
|
}
|
|
4658
4658
|
guard paddingInsets(node.style).top == 0,
|
|
4659
4659
|
let first = node.children.first(where: { $0.type != "Modal" && !$0.view.isHidden }) else { return nil }
|
|
4660
4660
|
return leading(first)
|
|
4661
4661
|
}
|
|
4662
4662
|
let scroll = find(renderedRoot)
|
|
4663
|
+
let leadingScroll = leading(renderedRoot)
|
|
4664
|
+
// Only the screen's leading scroller draws beneath floating chrome.
|
|
4665
|
+
// A bounded list inside a form must clip its rows to its viewport.
|
|
4666
|
+
if let leadingScroll, leadingScroll.style["overflow"] as? String != "hidden" {
|
|
4667
|
+
leadingScroll.view.clipsToBounds = false
|
|
4668
|
+
}
|
|
4663
4669
|
// Under a large title the screen runs edge to edge, as UIKit's own
|
|
4664
4670
|
// screens do: the scroll view starts under the bar and insets its
|
|
4665
4671
|
// content by it, so the title collapses and pull to refresh draws
|
|
@@ -4667,7 +4673,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4667
4673
|
// view: its content scrolls up under the bar behind iOS's scroll edge
|
|
4668
4674
|
// effect, rather than over the bar's back button. Other screens stay
|
|
4669
4675
|
// inside the safe area.
|
|
4670
|
-
let edgeToEdge = scroll != nil && (prefersLargeTitle == true || scroll ===
|
|
4676
|
+
let edgeToEdge = scroll != nil && (prefersLargeTitle == true || scroll === leadingScroll?.view)
|
|
4671
4677
|
let fullEdgeToEdge = rootIsSafeAreaView
|
|
4672
4678
|
let topEdgeToEdge = fullEdgeToEdge || edgeToEdge
|
|
4673
4679
|
rootTopToSafeArea?.isActive = !topEdgeToEdge
|
|
@@ -5801,6 +5807,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5801
5807
|
case "sectionlist": return "SectionList"
|
|
5802
5808
|
case "flatlist": return "FlatList"
|
|
5803
5809
|
case "svg": return "Svg"
|
|
5810
|
+
case "g": return "G"
|
|
5811
|
+
case "svgtext", "svg-text": return "SvgText"
|
|
5804
5812
|
case "touchableopacity": return "TouchableOpacity"
|
|
5805
5813
|
case "touchablehighlight": return "TouchableHighlight"
|
|
5806
5814
|
case "pressable": return "Pressable"
|
|
@@ -5848,7 +5856,20 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5848
5856
|
stack.removeArrangedSubview(spacer)
|
|
5849
5857
|
spacer.removeFromSuperview()
|
|
5850
5858
|
}
|
|
5851
|
-
|
|
5859
|
+
// STX permits text directly inside containers (including HTML links).
|
|
5860
|
+
// Give it a native label, inheriting typography without duplicating
|
|
5861
|
+
// the parent's padding, dimensions or flex layout.
|
|
5862
|
+
let typography = style.filter { key, _ in
|
|
5863
|
+
key == "color" || key == "letterSpacing" || key == "lineHeight"
|
|
5864
|
+
|| key.hasPrefix("font") || key.hasPrefix("text")
|
|
5865
|
+
}
|
|
5866
|
+
let nodes = children.compactMap { child -> [String: Any]? in
|
|
5867
|
+
if let node = child as? [String: Any] { return node }
|
|
5868
|
+
if let text = child as? String {
|
|
5869
|
+
return ["type": "Text", "style": typography, "children": [text]]
|
|
5870
|
+
}
|
|
5871
|
+
return nil
|
|
5872
|
+
}
|
|
5852
5873
|
let keys = nodes.compactMap { explicitKey($0, props: $0["props"] as? [String: Any] ?? [:]) }
|
|
5853
5874
|
let counts = Dictionary(keys.map { ($0, 1) }, uniquingKeysWith: +)
|
|
5854
5875
|
let old = Dictionary(uniqueKeysWithValues: parent.children.map { ($0.identity, $0) })
|
|
@@ -6157,10 +6178,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6157
6178
|
}
|
|
6158
6179
|
|
|
6159
6180
|
/// Views that take their own touches whatever handlers the screen gives
|
|
6160
|
-
/// them: a scroll view, a modal, and a video's
|
|
6181
|
+
/// them: native controls, a scroll view, a modal, and a video's controls (with
|
|
6161
6182
|
/// interaction off, its play, scrubber and full screen never answered).
|
|
6162
6183
|
private func ownsTouches(_ view: UIView) -> Bool {
|
|
6163
|
-
view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
6184
|
+
view is UIControl || view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
6164
6185
|
}
|
|
6165
6186
|
|
|
6166
6187
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|
|
@@ -6172,7 +6193,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6172
6193
|
let recognizer = UITapGestureRecognizer(target: self, action: #selector(viewPressed(_:)))
|
|
6173
6194
|
tapRecognizers[id] = recognizer
|
|
6174
6195
|
view.addGestureRecognizer(recognizer)
|
|
6175
|
-
} else if
|
|
6196
|
+
} else if !actionable, let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
6176
6197
|
view.removeGestureRecognizer(recognizer)
|
|
6177
6198
|
}
|
|
6178
6199
|
// A container keeps receiving touches without a handler of its own,
|
|
@@ -6420,11 +6441,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6420
6441
|
)
|
|
6421
6442
|
// A label paints its background inside its own bounds, so a rounded
|
|
6422
6443
|
// chip of text clips to its corners.
|
|
6423
|
-
//
|
|
6424
|
-
//
|
|
6425
|
-
//
|
|
6426
|
-
|
|
6427
|
-
view.clipsToBounds =
|
|
6444
|
+
// Native scrollers clip their viewport unless explicitly made
|
|
6445
|
+
// visible. trackContentScrollView opens only the leading scroller
|
|
6446
|
+
// beneath the screen's floating chrome.
|
|
6447
|
+
let overflow = style["overflow"] as? String
|
|
6448
|
+
view.clipsToBounds = overflow == "hidden"
|
|
6449
|
+
|| (view is UIScrollView && overflow != "visible")
|
|
6428
6450
|
|| (view is UILabel && (view.craftRequestedCornerRadius ?? 0) > 0)
|
|
6429
6451
|
view.layer.masksToBounds = view.clipsToBounds
|
|
6430
6452
|
view.craftLayoutStyle = CraftNativeLayoutStyle(style)
|
|
@@ -6809,7 +6831,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6809
6831
|
} else if type != "TextInput" {
|
|
6810
6832
|
view.accessibilityValue = nil
|
|
6811
6833
|
}
|
|
6812
|
-
let
|
|
6834
|
+
let hasLinkTarget = ["href", "to"].contains { !(props[$0] as? String ?? "").isEmpty }
|
|
6835
|
+
let linkedControl = ["Link", "TouchableOpacity", "TouchableHighlight", "Pressable"].contains(type) && hasLinkTarget
|
|
6836
|
+
let role = (props["accessibilityRole"] as? String) ?? (linkedControl ? "link" : nil)
|
|
6813
6837
|
// A scroll view, a list or a group of controls holds what VoiceOver
|
|
6814
6838
|
// reads: its label names the group, as a tab strip or a pager's
|
|
6815
6839
|
// name, and never stands in for its content. As an element of its
|