craft-native 0.0.164 → 0.0.167
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/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/MainActivityNative.kt.template +416 -36
- package/dist/api/mobile.d.ts +25 -1
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -0
- package/dist/ios/src/index.js +138 -88
- package/dist/ios/src/source-audit.d.ts +8 -0
- package/dist/ios/templates/CraftApp.swift +102 -13
- package/dist/ios/templates/CraftHealth.swift +214 -39
- package/dist/ios/templates/CraftHeartRate.swift +34 -4
- package/dist/ios/templates/CraftHybrid.swift +140 -17
- package/dist/ios/templates/CraftNativeScreen.swift +338 -61
- package/dist/ios/templates/CraftNativeServices.swift +76 -23
- package/dist/mobile.js +29 -0
- package/package.json +1 -1
|
@@ -550,6 +550,27 @@ private final class CraftNativeLayoutStyleBox {
|
|
|
550
550
|
|
|
551
551
|
private var craftNativeLayoutStyleKey: UInt8 = 0
|
|
552
552
|
private var craftNativeCornerRadiusKey: UInt8 = 0
|
|
553
|
+
private var craftNativeCornerRadiiKey: UInt8 = 0
|
|
554
|
+
private var craftNativeBorderLayerKey: UInt8 = 0
|
|
555
|
+
private var craftNativeSideBorderLayersKey: UInt8 = 0
|
|
556
|
+
|
|
557
|
+
private func craftRoundedPath(_ rect: CGRect, _ rawRadii: [CGFloat]) -> CGPath {
|
|
558
|
+
let limit = min(rect.width, rect.height) / 2
|
|
559
|
+
let radii = (0..<4).map { min(max(0, rawRadii.count > $0 ? rawRadii[$0] : 0), limit) }
|
|
560
|
+
let path = CGMutablePath()
|
|
561
|
+
let minX = rect.minX, maxX = rect.maxX, minY = rect.minY, maxY = rect.maxY
|
|
562
|
+
path.move(to: CGPoint(x: minX + radii[0], y: minY))
|
|
563
|
+
path.addLine(to: CGPoint(x: maxX - radii[1], y: minY))
|
|
564
|
+
path.addArc(center: CGPoint(x: maxX - radii[1], y: minY + radii[1]), radius: radii[1], startAngle: -.pi / 2, endAngle: 0, clockwise: false)
|
|
565
|
+
path.addLine(to: CGPoint(x: maxX, y: maxY - radii[2]))
|
|
566
|
+
path.addArc(center: CGPoint(x: maxX - radii[2], y: maxY - radii[2]), radius: radii[2], startAngle: 0, endAngle: .pi / 2, clockwise: false)
|
|
567
|
+
path.addLine(to: CGPoint(x: minX + radii[3], y: maxY))
|
|
568
|
+
path.addArc(center: CGPoint(x: minX + radii[3], y: maxY - radii[3]), radius: radii[3], startAngle: .pi / 2, endAngle: .pi, clockwise: false)
|
|
569
|
+
path.addLine(to: CGPoint(x: minX, y: minY + radii[0]))
|
|
570
|
+
path.addArc(center: CGPoint(x: minX + radii[0], y: minY + radii[0]), radius: radii[0], startAngle: .pi, endAngle: .pi * 1.5, clockwise: false)
|
|
571
|
+
path.closeSubpath()
|
|
572
|
+
return path
|
|
573
|
+
}
|
|
553
574
|
|
|
554
575
|
extension UIView {
|
|
555
576
|
/// The layout style this view's parent reads. It lives on the view, so an
|
|
@@ -571,11 +592,98 @@ extension UIView {
|
|
|
571
592
|
}
|
|
572
593
|
}
|
|
573
594
|
|
|
595
|
+
var craftRequestedCornerRadii: [CGFloat]? {
|
|
596
|
+
get { (objc_getAssociatedObject(self, &craftNativeCornerRadiiKey) as? [NSNumber])?.map { CGFloat(truncating: $0) } }
|
|
597
|
+
set {
|
|
598
|
+
objc_setAssociatedObject(self, &craftNativeCornerRadiiKey, newValue?.map { NSNumber(value: Double($0)) }, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
599
|
+
craftApplyCornerRadius()
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
574
603
|
func craftApplyCornerRadius() {
|
|
575
604
|
let requested = craftRequestedCornerRadius ?? 0
|
|
576
605
|
let limit = min(bounds.width, bounds.height) / 2
|
|
577
606
|
let radius = limit > 0 ? min(requested, limit) : min(requested, 9_998)
|
|
578
|
-
if
|
|
607
|
+
if let radii = craftRequestedCornerRadii {
|
|
608
|
+
let mask = (layer.mask as? CAShapeLayer) ?? CAShapeLayer()
|
|
609
|
+
mask.frame = bounds
|
|
610
|
+
mask.path = craftRoundedPath(bounds, radii)
|
|
611
|
+
layer.mask = mask
|
|
612
|
+
layer.cornerRadius = 0
|
|
613
|
+
} else {
|
|
614
|
+
layer.mask = nil
|
|
615
|
+
if layer.cornerRadius != radius { layer.cornerRadius = radius }
|
|
616
|
+
}
|
|
617
|
+
if let border = objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer {
|
|
618
|
+
border.frame = bounds
|
|
619
|
+
let inset = border.lineWidth / 2
|
|
620
|
+
border.path = UIBezierPath(
|
|
621
|
+
roundedRect: bounds.insetBy(dx: inset, dy: inset),
|
|
622
|
+
cornerRadius: max(0, radius - inset)
|
|
623
|
+
).cgPath
|
|
624
|
+
}
|
|
625
|
+
if let sides = objc_getAssociatedObject(self, &craftNativeSideBorderLayersKey) as? [CALayer], sides.count == 4 {
|
|
626
|
+
let top = sides[0].frame.height
|
|
627
|
+
let right = sides[1].frame.width
|
|
628
|
+
let bottom = sides[2].frame.height
|
|
629
|
+
let left = sides[3].frame.width
|
|
630
|
+
sides[0].frame = CGRect(x: 0, y: 0, width: bounds.width, height: top)
|
|
631
|
+
sides[1].frame = CGRect(x: max(0, bounds.width - right), y: 0, width: right, height: bounds.height)
|
|
632
|
+
sides[2].frame = CGRect(x: 0, y: max(0, bounds.height - bottom), width: bounds.width, height: bottom)
|
|
633
|
+
sides[3].frame = CGRect(x: 0, y: 0, width: left, height: bounds.height)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fileprivate func craftApplyBorderStyle(_ style: String?, width: CGFloat, color: UIColor) {
|
|
638
|
+
guard width > 0, style == "dashed" || style == "dotted" else {
|
|
639
|
+
(objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer)?.removeFromSuperlayer()
|
|
640
|
+
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
641
|
+
layer.borderWidth = width
|
|
642
|
+
layer.borderColor = color.cgColor
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
layer.borderWidth = 0
|
|
646
|
+
let border = (objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer) ?? CAShapeLayer()
|
|
647
|
+
border.strokeColor = color.cgColor
|
|
648
|
+
border.fillColor = nil
|
|
649
|
+
border.lineWidth = width
|
|
650
|
+
border.lineDashPattern = style == "dotted"
|
|
651
|
+
? [NSNumber(value: Double(width)), NSNumber(value: Double(width * 1.5))]
|
|
652
|
+
: [NSNumber(value: Double(width * 3)), NSNumber(value: Double(width * 2))]
|
|
653
|
+
border.contentsScale = window?.screen.scale ?? traitCollection.displayScale
|
|
654
|
+
if border.superlayer !== layer { layer.addSublayer(border) }
|
|
655
|
+
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, border, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
656
|
+
craftApplyCornerRadius()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fileprivate func craftApplySideBorders(_ hasSideBorders: Bool, style: String?, widths: [CGFloat], colors: [UIColor]) {
|
|
660
|
+
guard hasSideBorders else {
|
|
661
|
+
(objc_getAssociatedObject(self, &craftNativeSideBorderLayersKey) as? [CALayer])?.forEach { $0.removeFromSuperlayer() }
|
|
662
|
+
objc_setAssociatedObject(self, &craftNativeSideBorderLayersKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
663
|
+
craftApplyBorderStyle(style, width: widths.first ?? 0, color: colors.first ?? .clear)
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
(objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer)?.removeFromSuperlayer()
|
|
667
|
+
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
668
|
+
layer.borderWidth = 0
|
|
669
|
+
let top = max(0, widths.count > 0 ? widths[0] : 0)
|
|
670
|
+
let right = max(0, widths.count > 1 ? widths[1] : 0)
|
|
671
|
+
let bottom = max(0, widths.count > 2 ? widths[2] : 0)
|
|
672
|
+
let left = max(0, widths.count > 3 ? widths[3] : 0)
|
|
673
|
+
let frames = [
|
|
674
|
+
CGRect(x: 0, y: 0, width: bounds.width, height: top),
|
|
675
|
+
CGRect(x: max(0, bounds.width - right), y: 0, width: right, height: bounds.height),
|
|
676
|
+
CGRect(x: 0, y: max(0, bounds.height - bottom), width: bounds.width, height: bottom),
|
|
677
|
+
CGRect(x: 0, y: 0, width: left, height: bounds.height),
|
|
678
|
+
]
|
|
679
|
+
let layers = frames.enumerated().map { index, frame in
|
|
680
|
+
let side = CALayer()
|
|
681
|
+
side.frame = frame
|
|
682
|
+
side.backgroundColor = (colors.count > index ? colors[index] : .clear).cgColor
|
|
683
|
+
layer.addSublayer(side)
|
|
684
|
+
return side
|
|
685
|
+
}
|
|
686
|
+
objc_setAssociatedObject(self, &craftNativeSideBorderLayersKey, layers, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
579
687
|
}
|
|
580
688
|
}
|
|
581
689
|
|
|
@@ -670,6 +778,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
670
778
|
var gridColumns = 1 { didSet { if gridColumns != oldValue { invalidateMeasurements() } } }
|
|
671
779
|
var gridAutoRows: CraftNativeLength? { didSet { if gridAutoRows != oldValue { invalidateMeasurements() } } }
|
|
672
780
|
var reverse = false { didSet { if reverse != oldValue { invalidateMeasurements() } } }
|
|
781
|
+
var wrapReverse = false { didSet { if wrapReverse != oldValue { invalidateMeasurements() } } }
|
|
673
782
|
var padding = UIEdgeInsets.zero {
|
|
674
783
|
didSet {
|
|
675
784
|
layoutMargins = padding
|
|
@@ -869,6 +978,29 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
869
978
|
return max(0, result)
|
|
870
979
|
}
|
|
871
980
|
|
|
981
|
+
/// The first baseline is the cross-axis anchor for a horizontal baseline
|
|
982
|
+
/// flex line. Views without a baseline participate at their bottom edge,
|
|
983
|
+
/// matching the CSS fallback for replaced and non-text elements.
|
|
984
|
+
private static func firstBaseline(_ view: UIView, cross: CGFloat) -> CGFloat {
|
|
985
|
+
let baseline: CGFloat?
|
|
986
|
+
if let label = view as? CraftNativeLabel {
|
|
987
|
+
// CraftNativeLabel draws inside its inset rect, so include the
|
|
988
|
+
// authored top padding before the font's own baseline (UIKit's
|
|
989
|
+
// labels expose no measured first-baseline offset).
|
|
990
|
+
baseline = label.insets.top + label.font.ascender
|
|
991
|
+
} else if let label = view as? UILabel {
|
|
992
|
+
baseline = label.font.ascender
|
|
993
|
+
} else if let field = view as? UITextField {
|
|
994
|
+
baseline = field.font?.ascender
|
|
995
|
+
} else if let textView = view as? UITextView {
|
|
996
|
+
baseline = textView.font?.ascender
|
|
997
|
+
} else {
|
|
998
|
+
baseline = nil
|
|
999
|
+
}
|
|
1000
|
+
guard let baseline, baseline.isFinite else { return cross }
|
|
1001
|
+
return min(max(0, baseline), cross)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
872
1004
|
/// Children that take part in the flow: shown, not a modal overlay.
|
|
873
1005
|
private var visibleChildren: [UIView] {
|
|
874
1006
|
flowChildren.filter { $0.superview === self && !$0.isHidden && !($0 is CraftNativeModalView) }
|
|
@@ -1255,31 +1387,53 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1255
1387
|
}
|
|
1256
1388
|
let lineCross = lineCrosses[lineIndex]
|
|
1257
1389
|
let lineOffset = sharedCrossOffsets?[lineIndex] ?? crossOffset
|
|
1390
|
+
let crossReversed = wrapReverse && wrap
|
|
1391
|
+
let physicalLineOffset = crossReversed ? contentCross - lineOffset - lineCross : lineOffset
|
|
1258
1392
|
let sharedLine = sharedLines[lineIndex]
|
|
1259
1393
|
let useSharedPositions = !reverse && sharedLine != nil
|
|
1394
|
+
let baseline = horizontal
|
|
1395
|
+
? line.compactMap { index -> CGFloat? in
|
|
1396
|
+
let item = items[index]
|
|
1397
|
+
guard item.align == "baseline" else { return nil }
|
|
1398
|
+
return item.crossLeading + Self.firstBaseline(item.view, cross: item.cross)
|
|
1399
|
+
}.max()
|
|
1400
|
+
: nil
|
|
1260
1401
|
var mainOffset = leading
|
|
1261
1402
|
for (offset, index) in line.enumerated() {
|
|
1262
1403
|
let item = items[index]
|
|
1263
|
-
let
|
|
1264
|
-
let
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1404
|
+
let crossPosition: CGFloat
|
|
1405
|
+
if item.align == "baseline", let baseline {
|
|
1406
|
+
let childBaseline = Self.firstBaseline(item.view, cross: item.cross)
|
|
1407
|
+
crossPosition = CraftNativeCrossAxisBridge.position(
|
|
1408
|
+
available: baseline,
|
|
1409
|
+
size: childBaseline,
|
|
1410
|
+
leading: item.crossLeading,
|
|
1411
|
+
trailing: item.crossTrailing,
|
|
1412
|
+
alignment: 3
|
|
1413
|
+
) ?? max(item.crossLeading, baseline - childBaseline)
|
|
1414
|
+
} else {
|
|
1415
|
+
let alignment = item.align == "center" ? UInt32(1) : item.align == "flex-end" ? UInt32(2) : UInt32(0)
|
|
1416
|
+
crossPosition = CraftNativeCrossAxisBridge.position(
|
|
1417
|
+
available: lineCross,
|
|
1418
|
+
size: item.cross,
|
|
1419
|
+
leading: item.crossLeading,
|
|
1420
|
+
trailing: item.crossTrailing,
|
|
1421
|
+
alignment: alignment
|
|
1422
|
+
) ?? {
|
|
1423
|
+
switch alignment {
|
|
1424
|
+
case 1: return item.crossLeading + (lineCross - item.crossMargins - item.cross) / 2
|
|
1425
|
+
case 2: return lineCross - item.crossTrailing - item.cross
|
|
1426
|
+
default: return item.crossLeading
|
|
1427
|
+
}
|
|
1428
|
+
}()
|
|
1429
|
+
}
|
|
1430
|
+
let physicalCrossPosition = crossReversed ? lineCross - crossPosition - item.cross : crossPosition
|
|
1277
1431
|
let mainPosition = useSharedPositions
|
|
1278
1432
|
? (sharedLine?[offset].position ?? (mainOffset + item.mainLeading))
|
|
1279
1433
|
: mainOffset + item.mainLeading
|
|
1280
1434
|
let frame = horizontal
|
|
1281
|
-
? CGRect(x: padding.left + mainPosition, y: padding.top +
|
|
1282
|
-
: CGRect(x: padding.left +
|
|
1435
|
+
? CGRect(x: padding.left + mainPosition, y: padding.top + physicalLineOffset + physicalCrossPosition, width: item.main, height: item.cross)
|
|
1436
|
+
: CGRect(x: padding.left + physicalLineOffset + physicalCrossPosition, y: padding.top + mainPosition, width: item.cross, height: item.main)
|
|
1283
1437
|
frames.append((item.view, frame))
|
|
1284
1438
|
if !useSharedPositions { mainOffset += item.mainMargins + item.main + mainGap + between }
|
|
1285
1439
|
}
|
|
@@ -2448,9 +2602,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2448
2602
|
|
|
2449
2603
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
2450
2604
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
2451
|
-
|
|
2605
|
+
let colorChanged = previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle
|
|
2606
|
+
let textSizeChanged = previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory
|
|
2607
|
+
guard colorChanged || textSizeChanged else { return }
|
|
2452
2608
|
refreshTraitDefaults()
|
|
2453
|
-
|
|
2609
|
+
sendAppearance()
|
|
2454
2610
|
}
|
|
2455
2611
|
|
|
2456
2612
|
override func viewWillAppear(_ animated: Bool) {
|
|
@@ -2538,7 +2694,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2538
2694
|
}
|
|
2539
2695
|
|
|
2540
2696
|
private func refreshTraitDefaults() {
|
|
2541
|
-
view.backgroundColor = config.resolvedBackgroundColor
|
|
2697
|
+
view.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
2542
2698
|
if let document = mutationDocument.node("root") {
|
|
2543
2699
|
renderCommitted(document)
|
|
2544
2700
|
} else {
|
|
@@ -2621,6 +2777,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2621
2777
|
capabilities: \(capabilityJSON),
|
|
2622
2778
|
initialAppState: "\(CraftNativeActions.currentAppState())",
|
|
2623
2779
|
colorScheme: "\(colorScheme)",
|
|
2780
|
+
appearance: \(appearanceJSON()),
|
|
2624
2781
|
appURL: \(appURL),
|
|
2625
2782
|
appVersion: \(appVersion),
|
|
2626
2783
|
appBuild: \(appBuild),
|
|
@@ -2701,6 +2858,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2701
2858
|
return config.appearance == "dark" ? "dark" : "light"
|
|
2702
2859
|
}
|
|
2703
2860
|
|
|
2861
|
+
private func sendAppearance() {
|
|
2862
|
+
let style: UIUserInterfaceStyle = colorScheme == "dark" ? .dark : .light
|
|
2863
|
+
send(type: "APPEARANCE", payload: CraftPageAppearance.snapshot(style: style))
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
private func appearanceJSON() -> String {
|
|
2867
|
+
let style: UIUserInterfaceStyle = colorScheme == "dark" ? .dark : .light
|
|
2868
|
+
guard let data = try? JSONSerialization.data(withJSONObject: CraftPageAppearance.snapshot(style: style)),
|
|
2869
|
+
let json = String(data: data, encoding: .utf8) else { return "{}" }
|
|
2870
|
+
return json
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2704
2873
|
/// The screen's name as its script knows it: the route the host opened,
|
|
2705
2874
|
/// else the one a route bundle selected for itself.
|
|
2706
2875
|
private func screenName(in context: JSContext? = nil) -> String {
|
|
@@ -2726,8 +2895,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2726
2895
|
/// - `craft.secureStorage.getSync(key)`: the Keychain item the web page's
|
|
2727
2896
|
/// `craft.secureStorage.set` wrote.
|
|
2728
2897
|
/// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
|
|
2729
|
-
/// tabBarHidden, contentSwipeBack, tabSwipeEnabled, rightButtons })`;
|
|
2730
|
-
/// tap is the event `navButton`.
|
|
2898
|
+
/// tabBarHidden, contentSwipeBack, tabSwipeEnabled, backgroundColor, rightButtons })`;
|
|
2899
|
+
/// a right button's tap is the event `navButton`.
|
|
2731
2900
|
///
|
|
2732
2901
|
/// A runtime that assigns `craft.storage` (or the others) afterwards keeps
|
|
2733
2902
|
/// these: each namespace is a property whose setter adds them back to the
|
|
@@ -2979,6 +3148,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2979
3148
|
/// own horizontal pager or chart gesture.
|
|
2980
3149
|
private(set) var tabSwipeEnabled = true
|
|
2981
3150
|
private var prefersLargeTitle: Bool?
|
|
3151
|
+
/// The screen's own colour (`backgroundColor`), under the status bar and
|
|
3152
|
+
/// the home indicator too, where its content never reaches: a black
|
|
3153
|
+
/// full-screen timer is black edge to edge. nil is the app's.
|
|
3154
|
+
private var screenBackgroundColor: UIColor?
|
|
2982
3155
|
|
|
2983
3156
|
/// Whether this screen asked for a bar of its own through `setOptions`: a
|
|
2984
3157
|
/// title, a large title or buttons, and not `hidden`. A tab's root screen
|
|
@@ -3019,6 +3192,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3019
3192
|
if let tabSwipeEnabled = options["tabSwipeEnabled"] as? Bool {
|
|
3020
3193
|
self.tabSwipeEnabled = tabSwipeEnabled
|
|
3021
3194
|
}
|
|
3195
|
+
if options.keys.contains("backgroundColor") {
|
|
3196
|
+
screenBackgroundColor = color(options["backgroundColor"])
|
|
3197
|
+
view.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
3198
|
+
}
|
|
3022
3199
|
if let hidden = options["hidden"] as? Bool {
|
|
3023
3200
|
navigationBarHidden = hidden
|
|
3024
3201
|
if navigationController?.topViewController === self {
|
|
@@ -3203,6 +3380,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3203
3380
|
observe(UIApplication.didBecomeActiveNotification, state: "active")
|
|
3204
3381
|
observe(UIApplication.willResignActiveNotification, state: "inactive")
|
|
3205
3382
|
observe(UIApplication.didEnterBackgroundNotification, state: "background")
|
|
3383
|
+
for name in [UIContentSizeCategory.didChangeNotification, UIAccessibility.reduceMotionStatusDidChangeNotification, UIAccessibility.reduceTransparencyStatusDidChangeNotification, .craftColorSchemeChanged] {
|
|
3384
|
+
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
3385
|
+
forName: name,
|
|
3386
|
+
object: nil,
|
|
3387
|
+
queue: .main
|
|
3388
|
+
) { [weak self] _ in
|
|
3389
|
+
self?.refreshTraitDefaults()
|
|
3390
|
+
self?.sendAppearance()
|
|
3391
|
+
})
|
|
3392
|
+
}
|
|
3206
3393
|
// The app script has no screen for the keyboard to cover.
|
|
3207
3394
|
if !isAppScript {
|
|
3208
3395
|
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
@@ -4674,7 +4861,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4674
4861
|
let direction = style["flexDirection"] as? String
|
|
4675
4862
|
stack.axis = direction == "row" || direction == "row-reverse" ? .horizontal : .vertical
|
|
4676
4863
|
stack.reverse = direction?.hasSuffix("-reverse") == true
|
|
4677
|
-
|
|
4864
|
+
let flexWrap = style["flexWrap"] as? String
|
|
4865
|
+
stack.wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || style["flexWrap"] as? Bool == true
|
|
4866
|
+
stack.wrapReverse = flexWrap == "wrap-reverse"
|
|
4678
4867
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
4679
4868
|
stack.alignContent = style["alignContent"] as? String ?? "flex-start"
|
|
4680
4869
|
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
@@ -4710,17 +4899,45 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4710
4899
|
)
|
|
4711
4900
|
view.alpha = number(style["opacity"]) ?? 1
|
|
4712
4901
|
view.isHidden = style["display"] as? String == "none"
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4902
|
+
let borderRadius = number(style["borderRadius"]) ?? 0
|
|
4903
|
+
let cornerRadiusKeys = ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"]
|
|
4904
|
+
let hasCornerRadii = cornerRadiusKeys.contains { style[$0] != nil }
|
|
4905
|
+
view.craftRequestedCornerRadius = borderRadius
|
|
4906
|
+
view.craftRequestedCornerRadii = hasCornerRadii
|
|
4907
|
+
? cornerRadiusKeys.map { number(style[$0]) ?? borderRadius }
|
|
4908
|
+
: nil
|
|
4909
|
+
let borderWidth = number(style["borderWidth"]) ?? 0
|
|
4910
|
+
let borderColor = color(style["borderColor"]) ?? .clear
|
|
4911
|
+
let sideWidthKeys = ["borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"]
|
|
4912
|
+
let sideColorKeys = ["borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"]
|
|
4913
|
+
view.craftApplySideBorders(
|
|
4914
|
+
(sideWidthKeys + sideColorKeys).contains { style[$0] != nil },
|
|
4915
|
+
style: style["borderStyle"] as? String,
|
|
4916
|
+
widths: sideWidthKeys.map { number(style[$0]) ?? borderWidth },
|
|
4917
|
+
colors: sideColorKeys.map { color(style[$0]) ?? borderColor }
|
|
4918
|
+
)
|
|
4919
|
+
view.layer.zPosition = number(style["zIndex"]) ?? 0
|
|
4920
|
+
applyTransform(style["transform"], to: view)
|
|
4716
4921
|
let elevation = max(0, number(style["elevation"]) ?? 0)
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4922
|
+
let shadowColor = color(style["shadowColor"]) ?? .black
|
|
4923
|
+
let shadowOpacity = min(1, max(0, number(style["shadowOpacity"]) ?? (elevation > 0 ? min(0.28, 0.12 + elevation * 0.02) : 0)))
|
|
4924
|
+
let shadowRadius = max(0, number(style["shadowRadius"]) ?? elevation * 0.5)
|
|
4925
|
+
let shadowOffset = style["shadowOffset"] as? [String: Any]
|
|
4926
|
+
view.layer.shadowColor = shadowColor.cgColor
|
|
4927
|
+
view.layer.shadowOpacity = Float(shadowOpacity)
|
|
4928
|
+
view.layer.shadowRadius = shadowRadius
|
|
4929
|
+
view.layer.shadowOffset = CGSize(
|
|
4930
|
+
width: number(shadowOffset?["width"]) ?? 0,
|
|
4931
|
+
height: number(shadowOffset?["height"]) ?? (elevation * 0.25)
|
|
4932
|
+
)
|
|
4721
4933
|
// A label paints its background inside its own bounds, so a rounded
|
|
4722
|
-
// chip of text clips to its corners.
|
|
4723
|
-
|
|
4934
|
+
// chip of text clips to its corners. A scroll view clips its content
|
|
4935
|
+
// unless told `overflow: visible`, as UIKit's and React Native's do:
|
|
4936
|
+
// unclipped, a screen's content scrolled up over its bar's back
|
|
4937
|
+
// button and down into the home indicator.
|
|
4938
|
+
let overflow = style["overflow"] as? String
|
|
4939
|
+
view.clipsToBounds = overflow == "hidden"
|
|
4940
|
+
|| (view is UIScrollView && overflow != "visible")
|
|
4724
4941
|
|| (view is UILabel && (view.craftRequestedCornerRadius ?? 0) > 0)
|
|
4725
4942
|
view.layer.masksToBounds = view.clipsToBounds
|
|
4726
4943
|
view.craftLayoutStyle = CraftNativeLayoutStyle(style)
|
|
@@ -4740,6 +4957,49 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4740
4957
|
)
|
|
4741
4958
|
}
|
|
4742
4959
|
|
|
4960
|
+
private func applyTransform(_ raw: Any?, to view: UIView) {
|
|
4961
|
+
view.transform = .identity
|
|
4962
|
+
view.layer.transform = CATransform3DIdentity
|
|
4963
|
+
guard let transforms = raw as? [[String: Any]] else { return }
|
|
4964
|
+
var affine = CGAffineTransform.identity
|
|
4965
|
+
var threeDimensional = CATransform3DIdentity
|
|
4966
|
+
var hasThreeDimensionalRotation = false
|
|
4967
|
+
for transform in transforms {
|
|
4968
|
+
if let value = number(transform["translateX"]) { affine = affine.translatedBy(x: value, y: 0) }
|
|
4969
|
+
if let value = number(transform["translateY"]) { affine = affine.translatedBy(x: 0, y: value) }
|
|
4970
|
+
if let value = number(transform["scale"]) { affine = affine.scaledBy(x: value, y: value) }
|
|
4971
|
+
if let value = number(transform["scaleX"]) { affine = affine.scaledBy(x: value, y: 1) }
|
|
4972
|
+
if let value = number(transform["scaleY"]) { affine = affine.scaledBy(x: 1, y: value) }
|
|
4973
|
+
if let value = transformAngle(transform["rotate"] ?? transform["rotateZ"]) { affine = affine.rotated(by: value) }
|
|
4974
|
+
if let value = transformAngle(transform["skewX"]) { affine = affine.concatenating(CGAffineTransform(a: 1, b: 0, c: tan(value), d: 1, tx: 0, ty: 0)) }
|
|
4975
|
+
if let value = transformAngle(transform["skewY"]) { affine = affine.concatenating(CGAffineTransform(a: 1, b: tan(value), c: 0, d: 1, tx: 0, ty: 0)) }
|
|
4976
|
+
if let value = transformAngle(transform["rotateX"]) {
|
|
4977
|
+
threeDimensional = CATransform3DRotate(threeDimensional, value, 1, 0, 0)
|
|
4978
|
+
hasThreeDimensionalRotation = true
|
|
4979
|
+
}
|
|
4980
|
+
if let value = transformAngle(transform["rotateY"]) {
|
|
4981
|
+
threeDimensional = CATransform3DRotate(threeDimensional, value, 0, 1, 0)
|
|
4982
|
+
hasThreeDimensionalRotation = true
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
if hasThreeDimensionalRotation {
|
|
4986
|
+
view.layer.transform = CATransform3DConcat(CATransform3DMakeAffineTransform(affine), threeDimensional)
|
|
4987
|
+
} else {
|
|
4988
|
+
view.transform = affine
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
private func transformAngle(_ raw: Any?) -> CGFloat? {
|
|
4993
|
+
if let value = number(raw) { return value }
|
|
4994
|
+
guard let text = raw as? String else { return nil }
|
|
4995
|
+
if text.hasSuffix("rad") { return CGFloat(Double(text.dropLast(3)) ?? 0) }
|
|
4996
|
+
if text.hasSuffix("deg") { return CGFloat((Double(text.dropLast(3)) ?? 0) * .pi / 180) }
|
|
4997
|
+
// Keep the conversion explicit: newer Swift toolchains see both the
|
|
4998
|
+
// CGFloat(Double) and CGFloat(Float) initializers through map and
|
|
4999
|
+
// reject the overloaded constructor reference.
|
|
5000
|
+
return Double(text).map { CGFloat($0) }
|
|
5001
|
+
}
|
|
5002
|
+
|
|
4743
5003
|
private func configureText(_ label: UILabel, text: String, style: [String: Any]) {
|
|
4744
5004
|
let transformed = transformedText(text, style: style)
|
|
4745
5005
|
label.textColor = color(style["color"]) ?? .label
|
|
@@ -4747,14 +5007,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4747
5007
|
label.textAlignment = textAlignment(style["textAlign"])
|
|
4748
5008
|
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
4749
5009
|
if let spacing = number(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4753
|
-
case "underline line-through":
|
|
4754
|
-
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
4755
|
-
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4756
|
-
default: break
|
|
4757
|
-
}
|
|
5010
|
+
addTextDecorationAttributes(style, to: &attributes)
|
|
5011
|
+
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
4758
5012
|
if let lineHeight = number(style["lineHeight"]) {
|
|
4759
5013
|
let paragraph = NSMutableParagraphStyle()
|
|
4760
5014
|
paragraph.minimumLineHeight = lineHeight
|
|
@@ -4799,14 +5053,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4799
5053
|
paragraph.maximumLineHeight = lineHeight
|
|
4800
5054
|
attributes[.paragraphStyle] = paragraph
|
|
4801
5055
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4805
|
-
case "underline line-through":
|
|
4806
|
-
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
4807
|
-
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4808
|
-
default: break
|
|
4809
|
-
}
|
|
5056
|
+
addTextDecorationAttributes(style, to: &attributes)
|
|
5057
|
+
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
4810
5058
|
if forceUnderline { attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue }
|
|
4811
5059
|
return attributes
|
|
4812
5060
|
}
|
|
@@ -4828,17 +5076,46 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4828
5076
|
paragraph.alignment = alignment
|
|
4829
5077
|
attributes[.paragraphStyle] = paragraph
|
|
4830
5078
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4834
|
-
case "underline line-through":
|
|
4835
|
-
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
4836
|
-
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4837
|
-
default: break
|
|
4838
|
-
}
|
|
5079
|
+
addTextDecorationAttributes(style, to: &attributes)
|
|
5080
|
+
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
4839
5081
|
return attributes
|
|
4840
5082
|
}
|
|
4841
5083
|
|
|
5084
|
+
private func textShadow(_ style: [String: Any]) -> NSShadow? {
|
|
5085
|
+
guard let shadowColor = color(style["textShadowColor"]) else { return nil }
|
|
5086
|
+
let offset = style["textShadowOffset"] as? [String: Any]
|
|
5087
|
+
let shadow = NSShadow()
|
|
5088
|
+
shadow.shadowColor = shadowColor
|
|
5089
|
+
shadow.shadowOffset = CGSize(
|
|
5090
|
+
width: number(offset?["width"]) ?? 0,
|
|
5091
|
+
height: number(offset?["height"]) ?? 0
|
|
5092
|
+
)
|
|
5093
|
+
shadow.shadowBlurRadius = max(0, number(style["textShadowRadius"]) ?? 0)
|
|
5094
|
+
return shadow
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
private func addTextDecorationAttributes(_ style: [String: Any], to attributes: inout [NSAttributedString.Key: Any]) {
|
|
5098
|
+
let line = style["textDecorationLine"] as? String
|
|
5099
|
+
let decorationStyle: NSUnderlineStyle = {
|
|
5100
|
+
switch style["textDecorationStyle"] as? String {
|
|
5101
|
+
case "double": return .double
|
|
5102
|
+
case "dotted": return [.single, .patternDot]
|
|
5103
|
+
case "dashed": return [.single, .patternDash]
|
|
5104
|
+
default: return .single
|
|
5105
|
+
}
|
|
5106
|
+
}()
|
|
5107
|
+
let rawStyle = decorationStyle.rawValue
|
|
5108
|
+
let decorationColor = color(style["textDecorationColor"])
|
|
5109
|
+
if line == "underline" || line == "underline line-through" {
|
|
5110
|
+
attributes[.underlineStyle] = rawStyle
|
|
5111
|
+
if let decorationColor { attributes[.underlineColor] = decorationColor }
|
|
5112
|
+
}
|
|
5113
|
+
if line == "line-through" || line == "underline line-through" {
|
|
5114
|
+
attributes[.strikethroughStyle] = rawStyle
|
|
5115
|
+
if let decorationColor { attributes[.strikethroughColor] = decorationColor }
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
|
|
4842
5119
|
private func transformedText(_ text: String, style: [String: Any]) -> String {
|
|
4843
5120
|
switch style["textTransform"] as? String {
|
|
4844
5121
|
case "uppercase": return text.uppercased()
|
|
@@ -5237,11 +5514,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5237
5514
|
if let number = value as? NSNumber, !(value is Bool) { return min(maximum, max(0, number.doubleValue)) }
|
|
5238
5515
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
5239
5516
|
if let seconds = Double(text), seconds.isFinite { return min(maximum, max(0, seconds)) }
|
|
5240
|
-
let parts = text.split(separator: ":")
|
|
5241
|
-
guard parts.count == 2 || parts.count == 3
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
let seconds = parts.count == 3 ? parts[2] : 0
|
|
5517
|
+
let parts = text.split(separator: ":")
|
|
5518
|
+
guard parts.count == 2 || parts.count == 3,
|
|
5519
|
+
let hours = Double(parts[0]),
|
|
5520
|
+
let minutes = Double(parts[1]) else { return nil }
|
|
5521
|
+
let seconds = parts.count == 3 ? (Double(parts[2]) ?? -1) : 0
|
|
5245
5522
|
guard hours >= 0, minutes >= 0, minutes < 60, seconds >= 0, seconds < 60 else { return nil }
|
|
5246
5523
|
return min(maximum, hours * 3600 + minutes * 60 + seconds)
|
|
5247
5524
|
}
|