@craft-native/ios 0.0.395 → 0.0.408
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/index.d.ts +7 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +268 -12
- package/templates/CraftHybrid.swift +1 -0
- package/templates/CraftNativeActions.swift +3 -1
- package/templates/CraftNativeFlatList.swift +4 -0
- package/templates/CraftNativeMutation.swift +27 -2
- package/templates/CraftNativeScreen.swift +293 -44
- package/templates/CraftNativeUI.swift +89 -0
- package/templates/testing/DriverTests.swift.template +17 -1
|
@@ -1108,6 +1108,10 @@ private func craftNativeAlignContentCode(_ value: String) -> UInt32 {
|
|
|
1108
1108
|
|
|
1109
1109
|
/// Keep the indicator's intrinsic size separate from authored style transforms.
|
|
1110
1110
|
private final class CraftNativeActivityIndicatorView: UIActivityIndicatorView {
|
|
1111
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
1112
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1111
1115
|
var indicatorScale: CGFloat = 1
|
|
1112
1116
|
}
|
|
1113
1117
|
|
|
@@ -1122,6 +1126,10 @@ private final class CraftNativeActivityIndicatorView: UIActivityIndicatorView {
|
|
|
1122
1126
|
/// `enableBackgroundAudio` writes), and comes back into its place when the
|
|
1123
1127
|
/// app does.
|
|
1124
1128
|
private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate {
|
|
1129
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
1130
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1125
1133
|
private let playerController = AVPlayerViewController()
|
|
1126
1134
|
private var player: AVPlayer?
|
|
1127
1135
|
private var sourceURI = ""
|
|
@@ -1454,6 +1462,10 @@ private final class CraftMarkerPhotoRedirects: NSObject, URLSessionTaskDelegate
|
|
|
1454
1462
|
}
|
|
1455
1463
|
|
|
1456
1464
|
private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
1465
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
1466
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1457
1469
|
private var annotationsByID: [String: MKPointAnnotation] = [:]
|
|
1458
1470
|
private var overlaysByID: [String: MKPolyline] = [:]
|
|
1459
1471
|
private var overlayCoordinatesByID: [String: [CLLocationCoordinate2D]] = [:]
|
|
@@ -2394,6 +2406,46 @@ private final class CraftNativeLayoutStyleBox {
|
|
|
2394
2406
|
init(_ style: CraftNativeLayoutStyle) { self.style = style }
|
|
2395
2407
|
}
|
|
2396
2408
|
|
|
2409
|
+
/// Pointer policy applies to authored nodes, not UIKit's private control views.
|
|
2410
|
+
enum CraftNativePointerEvents: String {
|
|
2411
|
+
case auto, none
|
|
2412
|
+
case boxNone = "box-none"
|
|
2413
|
+
case boxOnly = "box-only"
|
|
2414
|
+
|
|
2415
|
+
static func resolve(props: [String: Any], style: [String: Any]) -> Self {
|
|
2416
|
+
let prop = props["pointerEvents"]
|
|
2417
|
+
let value = prop == nil || prop is NSNull ? style["pointerEvents"] : prop
|
|
2418
|
+
return (value as? String).flatMap(Self.init(rawValue:)) ?? .auto
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
private var craftNativePointerEventsKey: UInt8 = 0
|
|
2423
|
+
|
|
2424
|
+
extension UIView {
|
|
2425
|
+
// nil identifies an internal view; even authored `auto` nodes are marked.
|
|
2426
|
+
var craftPointerEvents: CraftNativePointerEvents? {
|
|
2427
|
+
get { (objc_getAssociatedObject(self, &craftNativePointerEventsKey) as? String).flatMap(CraftNativePointerEvents.init(rawValue:)) }
|
|
2428
|
+
set { objc_setAssociatedObject(self, &craftNativePointerEventsKey, newValue?.rawValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) }
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
func craftPointerHitTest(_ point: CGPoint, with event: UIEvent?, native: () -> UIView?) -> UIView? {
|
|
2432
|
+
guard let policy = craftPointerEvents, policy != .auto else { return native() }
|
|
2433
|
+
guard policy != .none, !isHidden, alpha > 0.01, isUserInteractionEnabled else { return nil }
|
|
2434
|
+
if policy == .boxOnly && !self.point(inside: point, with: event) { return nil }
|
|
2435
|
+
let hit = native()
|
|
2436
|
+
var candidate = hit
|
|
2437
|
+
var authoredChild = false
|
|
2438
|
+
while let view = candidate, view !== self {
|
|
2439
|
+
if view.craftPointerEvents != nil { authoredChild = true; break }
|
|
2440
|
+
candidate = view.superview
|
|
2441
|
+
}
|
|
2442
|
+
if policy == .boxNone { return authoredChild ? hit : nil }
|
|
2443
|
+
// Preserve private controls (picker wheels, text selection, map gestures),
|
|
2444
|
+
// while excluding authored descendants of a box-only container.
|
|
2445
|
+
return authoredChild ? self : hit ?? self
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2397
2449
|
private var craftNativeLayoutStyleKey: UInt8 = 0
|
|
2398
2450
|
private var craftNativeCornerRadiusKey: UInt8 = 0
|
|
2399
2451
|
private var craftNativeCornerRadiiKey: UInt8 = 0
|
|
@@ -3021,12 +3073,14 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
3021
3073
|
/// is inside the view, so a floating button absolutely positioned out of a
|
|
3022
3074
|
/// zero-height parent (a component's wrapper) drew but could not be tapped.
|
|
3023
3075
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3076
|
+
return craftPointerHitTest(point, with: event) {
|
|
3077
|
+
if let hit = super.hitTest(point, with: event) { return hit }
|
|
3078
|
+
guard !clipsToBounds, !isHidden, alpha > 0.01, isUserInteractionEnabled else { return nil }
|
|
3079
|
+
for child in subviews.reversed() where !child.isHidden && child.alpha > 0.01 && child.isUserInteractionEnabled {
|
|
3080
|
+
if let hit = child.hitTest(child.convert(point, from: self), with: event) { return hit }
|
|
3081
|
+
}
|
|
3082
|
+
return nil
|
|
3028
3083
|
}
|
|
3029
|
-
return nil
|
|
3030
3084
|
}
|
|
3031
3085
|
|
|
3032
3086
|
// MARK: Children
|
|
@@ -4100,10 +4154,107 @@ private final class CraftNativeKeyboardAvoidingView: CraftNativeFlowView {
|
|
|
4100
4154
|
}
|
|
4101
4155
|
}
|
|
4102
4156
|
|
|
4157
|
+
// Preserve source dimensions when the loader downsamples decoded pixels. CSS
|
|
4158
|
+
// scale-down/none use intrinsic dimensions, never the thumbnail's display size.
|
|
4159
|
+
private var craftImageIntrinsicKey: UInt8 = 0
|
|
4160
|
+
private extension UIImage {
|
|
4161
|
+
var craftIntrinsicSize: CGSize {
|
|
4162
|
+
get { (objc_getAssociatedObject(self, &craftImageIntrinsicKey) as? NSValue)?.cgSizeValue ?? size }
|
|
4163
|
+
set { objc_setAssociatedObject(self, &craftImageIntrinsicKey, NSValue(cgSize: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
/// Explicit CSS-facing image geometry; the loader and semantic view stay retained.
|
|
4168
|
+
class CraftNativePositionedImageView: UIImageView {
|
|
4169
|
+
private var storedImage: UIImage?
|
|
4170
|
+
private var fittedImage: UIImageView?
|
|
4171
|
+
private var fit: String?
|
|
4172
|
+
private var position = CGPoint(x: 0.5, y: 0.5)
|
|
4173
|
+
|
|
4174
|
+
override var image: UIImage? {
|
|
4175
|
+
get { storedImage }
|
|
4176
|
+
set {
|
|
4177
|
+
storedImage = newValue
|
|
4178
|
+
super.image = fit == nil ? newValue : nil
|
|
4179
|
+
fittedImage?.image = newValue
|
|
4180
|
+
invalidateIntrinsicContentSize()
|
|
4181
|
+
setNeedsLayout()
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
override var intrinsicContentSize: CGSize {
|
|
4186
|
+
fit == nil ? super.intrinsicContentSize : storedImage?.craftIntrinsicSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
func configureObjectGeometry(props: [String: Any], style: [String: Any]) {
|
|
4190
|
+
func value(_ name: String) -> Any? {
|
|
4191
|
+
if let raw = props[name], !(raw is NSNull) { return raw }
|
|
4192
|
+
if let raw = style[name], !(raw is NSNull) { return raw }
|
|
4193
|
+
return nil
|
|
4194
|
+
}
|
|
4195
|
+
let rawFit = value("objectFit"), rawPosition = value("objectPosition")
|
|
4196
|
+
if rawFit != nil || rawPosition != nil {
|
|
4197
|
+
let requested = rawFit as? String ?? "fill"
|
|
4198
|
+
fit = ["fill", "contain", "cover", "none", "scale-down"].contains(requested) ? requested : "fill"
|
|
4199
|
+
position = Self.objectPosition(rawPosition)
|
|
4200
|
+
if fittedImage == nil {
|
|
4201
|
+
let child = UIImageView()
|
|
4202
|
+
child.isUserInteractionEnabled = false
|
|
4203
|
+
child.isAccessibilityElement = false
|
|
4204
|
+
child.contentMode = .scaleToFill
|
|
4205
|
+
insertSubview(child, at: 0)
|
|
4206
|
+
fittedImage = child
|
|
4207
|
+
}
|
|
4208
|
+
} else { fit = nil }
|
|
4209
|
+
fittedImage?.isHidden = fit == nil
|
|
4210
|
+
super.image = fit == nil ? storedImage : nil
|
|
4211
|
+
fittedImage?.image = storedImage
|
|
4212
|
+
invalidateIntrinsicContentSize()
|
|
4213
|
+
setNeedsLayout()
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
var usesObjectGeometry: Bool { fit != nil }
|
|
4217
|
+
var objectCovers: Bool { fit == "cover" }
|
|
4218
|
+
|
|
4219
|
+
private static func objectPosition(_ raw: Any?) -> CGPoint {
|
|
4220
|
+
guard let text = raw as? String else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4221
|
+
var parts = text.lowercased().split(whereSeparator: { $0.isWhitespace }).map(String.init)
|
|
4222
|
+
guard (1...2).contains(parts.count) else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4223
|
+
if parts.count == 1 {
|
|
4224
|
+
parts = ["top", "bottom"].contains(parts[0]) ? ["center", parts[0]] : [parts[0], "center"]
|
|
4225
|
+
} else if ["top", "bottom"].contains(parts[0]) || ["left", "right"].contains(parts[1]) { parts.swapAt(0, 1) }
|
|
4226
|
+
func fraction(_ part: String, low: String, high: String) -> CGFloat? {
|
|
4227
|
+
if part == low { return 0 }; if part == high { return 1 }; if part == "center" { return 0.5 }
|
|
4228
|
+
guard part.hasSuffix("%"), let number = Double(part.dropLast()), number.isFinite else { return nil }
|
|
4229
|
+
return CGFloat(number / 100)
|
|
4230
|
+
}
|
|
4231
|
+
guard let x = fraction(parts[0], low: "left", high: "right"), let y = fraction(parts[1], low: "top", high: "bottom") else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4232
|
+
return CGPoint(x: x, y: y)
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
override func layoutSubviews() {
|
|
4236
|
+
super.layoutSubviews()
|
|
4237
|
+
guard let fit, let child = fittedImage else { return }
|
|
4238
|
+
let source = storedImage?.craftIntrinsicSize ?? .zero
|
|
4239
|
+
guard source.width > 0, source.height > 0 else { child.frame = .zero; return }
|
|
4240
|
+
let contain = min(bounds.width / source.width, bounds.height / source.height)
|
|
4241
|
+
let scale: CGFloat = fit == "cover" ? max(bounds.width / source.width, bounds.height / source.height)
|
|
4242
|
+
: fit == "none" ? 1 : fit == "scale-down" ? min(1, contain) : contain
|
|
4243
|
+
let size = fit == "fill" ? bounds.size : CGSize(width: source.width * scale, height: source.height * scale)
|
|
4244
|
+
child.frame = CGRect(x: bounds.minX + (bounds.width - size.width) * position.x,
|
|
4245
|
+
y: bounds.minY + (bounds.height - size.height) * position.y, width: size.width, height: size.height)
|
|
4246
|
+
child.tintColor = tintColor
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4103
4250
|
/// An image-backed flex container. The image stays behind ordinary native
|
|
4104
4251
|
/// children so `ImageBackground` keeps the same layout and interaction
|
|
4105
4252
|
/// semantics as a `View` while using the shared image loader.
|
|
4106
|
-
private final class CraftNativeImageBackgroundView:
|
|
4253
|
+
private final class CraftNativeImageBackgroundView: CraftNativePositionedImageView {
|
|
4254
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4255
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4107
4258
|
let contentStack = CraftNativeFlowView()
|
|
4108
4259
|
|
|
4109
4260
|
override init(frame: CGRect) {
|
|
@@ -4142,6 +4293,10 @@ private final class CraftNativeSegmentElement: UIAccessibilityElement {
|
|
|
4142
4293
|
}
|
|
4143
4294
|
|
|
4144
4295
|
private final class CraftNativeSegmentedControl: UISegmentedControl {
|
|
4296
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4297
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4145
4300
|
var values: [String] = []
|
|
4146
4301
|
private var segments: [CraftNativeSegmentElement] = []
|
|
4147
4302
|
override var accessibilityElements: [Any]? {
|
|
@@ -4391,28 +4546,30 @@ private final class CraftNativeModalView: UIView, UIGestureRecognizerDelegate, U
|
|
|
4391
4546
|
}
|
|
4392
4547
|
|
|
4393
4548
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4549
|
+
return craftPointerHitTest(point, with: event) {
|
|
4550
|
+
guard !isHidden, alpha > 0.01, isUserInteractionEnabled else { return nil }
|
|
4551
|
+
let contentPoint = contentStack.convert(point, from: self)
|
|
4552
|
+
if contentStack.bounds.contains(contentPoint), let hit = contentStack.hitTest(contentPoint, with: event), hit !== contentStack {
|
|
4553
|
+
// Flow/layout views occupy the modal's entire content rect, but a
|
|
4554
|
+
// blank part of one must still be a scrim tap. Preserve touches
|
|
4555
|
+
// only when the hit view (or one of its ancestors) actually owns
|
|
4556
|
+
// a control or gesture recognizer.
|
|
4557
|
+
var candidate: UIView? = hit
|
|
4558
|
+
while let view = candidate, view !== contentStack {
|
|
4559
|
+
if view is UIControl || !(view.gestureRecognizers?.isEmpty ?? true) { return hit }
|
|
4560
|
+
candidate = view.superview
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
if !blocksBackground {
|
|
4564
|
+
// A dismissible popup consumes its outside hit so the close event
|
|
4565
|
+
// cannot also activate the screen below. Otherwise it owns the
|
|
4566
|
+
// hit only when a blocking modal is underneath it; with no modal
|
|
4567
|
+
// below, the screen remains pass-through.
|
|
4568
|
+
if dismissOnOutsideTap { return self }
|
|
4569
|
+
return craftScreenController?.hasBlockingModalBelow(self) == true ? self : nil
|
|
4570
|
+
}
|
|
4571
|
+
return blocker.hitTest(blocker.convert(point, from: self), with: event)
|
|
4572
|
+
}
|
|
4416
4573
|
}
|
|
4417
4574
|
|
|
4418
4575
|
override func layoutSubviews() {
|
|
@@ -4513,6 +4670,10 @@ func craftHandleKeyboardTap(in container: UIView, hit: UIView?, persistTaps: Str
|
|
|
4513
4670
|
}
|
|
4514
4671
|
|
|
4515
4672
|
private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDelegate {
|
|
4673
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4674
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4516
4677
|
let contentStack = CraftNativeFlowView()
|
|
4517
4678
|
private let keyboardTapGesture = UITapGestureRecognizer()
|
|
4518
4679
|
private var keyboardShouldPersistTaps = "never"
|
|
@@ -4877,6 +5038,10 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4877
5038
|
/// inherited down the tree as SVG does; `dark:` classes arrive already
|
|
4878
5039
|
/// resolved for the appearance, so a redraw is all a theme change needs.
|
|
4879
5040
|
final class CraftNativeSvgView: UIView {
|
|
5041
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5042
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5043
|
+
}
|
|
5044
|
+
|
|
4880
5045
|
var drawing: [String: Any] = [:] {
|
|
4881
5046
|
didSet {
|
|
4882
5047
|
animates = Self.hasAnimation(drawing["children"] as? [Any] ?? [])
|
|
@@ -5282,6 +5447,10 @@ private final class CraftNativeGradientLayer: CAGradientLayer {
|
|
|
5282
5447
|
/// pill with its label touching the edges. The padding is added to its size
|
|
5283
5448
|
/// and the label laid out inside it, at the alignment the style asked for.
|
|
5284
5449
|
private final class CraftNativeButton: UIButton {
|
|
5450
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5451
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5285
5454
|
var padding: UIEdgeInsets = .zero {
|
|
5286
5455
|
didSet {
|
|
5287
5456
|
guard padding != oldValue else { return }
|
|
@@ -5323,6 +5492,10 @@ private final class CraftNativeButton: UIButton {
|
|
|
5323
5492
|
/// Opt-in solid switch. Draws only public UIControl content; it neither walks
|
|
5324
5493
|
/// UISwitch's private views nor changes UIKit animations elsewhere in the app.
|
|
5325
5494
|
final class CraftNativeSolidSwitch: UIControl {
|
|
5495
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5496
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5326
5499
|
private(set) var isOn = false
|
|
5327
5500
|
var onColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
5328
5501
|
var offColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
@@ -5433,10 +5606,18 @@ final class CraftNativeSolidSwitch: UIControl {
|
|
|
5433
5606
|
/// square/checkmark symbols while keeping the control's title and accessibility
|
|
5434
5607
|
/// state native and keyboard/touch reachable.
|
|
5435
5608
|
private final class CraftNativeCheckboxButton: UIButton {
|
|
5609
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5610
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5436
5613
|
var indeterminate = false
|
|
5437
5614
|
}
|
|
5438
5615
|
|
|
5439
5616
|
private final class CraftNativeLabel: UILabel {
|
|
5617
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5618
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5440
5621
|
var insets = UIEdgeInsets.zero {
|
|
5441
5622
|
didSet {
|
|
5442
5623
|
guard insets != oldValue else { return }
|
|
@@ -5470,6 +5651,10 @@ private final class CraftNativeLabel: UILabel {
|
|
|
5470
5651
|
/// selection. UILabel has no selection model; UITextView supplies UIKit's
|
|
5471
5652
|
/// standard loupe, menu and copy behavior while remaining a plain text leaf.
|
|
5472
5653
|
private final class CraftNativeSelectableTextView: UITextView {
|
|
5654
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5655
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5473
5658
|
convenience init() {
|
|
5474
5659
|
self.init(frame: .zero, textContainer: nil)
|
|
5475
5660
|
}
|
|
@@ -5498,6 +5683,10 @@ private final class CraftNativeSelectableTextView: UITextView {
|
|
|
5498
5683
|
/// A one-line TextInput. Its style's padding is where the text and the
|
|
5499
5684
|
/// placeholder start, as in a web input; UITextField has no padding of its own.
|
|
5500
5685
|
final class CraftNativeTextField: UITextField {
|
|
5686
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5687
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5501
5690
|
var padding = UIEdgeInsets.zero {
|
|
5502
5691
|
didSet {
|
|
5503
5692
|
guard padding != oldValue else { return }
|
|
@@ -5529,6 +5718,10 @@ private final class CraftNativeFlexSpacer: UIView {}
|
|
|
5529
5718
|
/// An SF Symbol leaf. Its natural size is the symbol's, or a 20 pt square
|
|
5530
5719
|
/// when the style gives it no size at all.
|
|
5531
5720
|
private final class CraftNativeIconView: UIImageView {
|
|
5721
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5722
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5532
5725
|
var defaultBox: CGFloat? { didSet { if defaultBox != oldValue { invalidateIntrinsicContentSize() } } }
|
|
5533
5726
|
|
|
5534
5727
|
/// An icon says only what the screen labels it. iOS gives a symbol image
|
|
@@ -5554,6 +5747,10 @@ private final class CraftNativeIconView: UIImageView {
|
|
|
5554
5747
|
}
|
|
5555
5748
|
|
|
5556
5749
|
private final class CraftNativeTextView: UITextView {
|
|
5750
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5751
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5557
5754
|
private let placeholderLabel = UILabel()
|
|
5558
5755
|
private var nativeTextContainerInset: UIEdgeInsets?
|
|
5559
5756
|
private var nativeLineFragmentPadding: CGFloat?
|
|
@@ -5869,6 +6066,41 @@ enum CraftNativeBundle {
|
|
|
5869
6066
|
}
|
|
5870
6067
|
}
|
|
5871
6068
|
|
|
6069
|
+
private final class CraftNativePickerView: UIPickerView {
|
|
6070
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6071
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6072
|
+
}
|
|
6073
|
+
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
private final class CraftNativeDatePickerView: UIDatePicker {
|
|
6077
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6078
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6079
|
+
}
|
|
6080
|
+
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
private final class CraftNativeSwitch: UISwitch {
|
|
6084
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6085
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
private final class CraftNativeSlider: UISlider {
|
|
6091
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6092
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
}
|
|
6096
|
+
|
|
6097
|
+
private final class CraftNativeImageView: CraftNativePositionedImageView {
|
|
6098
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6099
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6100
|
+
}
|
|
6101
|
+
|
|
6102
|
+
}
|
|
6103
|
+
|
|
5872
6104
|
private final class CraftNativeScreenRootView: UIView {
|
|
5873
6105
|
weak var controller: CraftNativeScreenController?
|
|
5874
6106
|
|
|
@@ -5879,7 +6111,8 @@ private final class CraftNativeScreenRootView: UIView {
|
|
|
5879
6111
|
// overlays remain pass-through.
|
|
5880
6112
|
if let controller {
|
|
5881
6113
|
for modal in controller.view.subviews.compactMap({ $0 as? CraftNativeModalView }).reversed()
|
|
5882
|
-
where !modal.blocksBackground && !modal.isHidden && modal.alpha > 0.01
|
|
6114
|
+
where !modal.blocksBackground && !modal.isHidden && modal.alpha > 0.01
|
|
6115
|
+
&& modal.craftPointerEvents != CraftNativePointerEvents.none && modal.craftPointerEvents != .boxNone {
|
|
5883
6116
|
let local = modal.convert(point, from: self)
|
|
5884
6117
|
if !modal.bounds.contains(local) && (modal.dismissOnOutsideTap || controller.hasBlockingModalBelow(modal)) { return modal }
|
|
5885
6118
|
}
|
|
@@ -6371,7 +6604,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6371
6604
|
appliedTraitDefaults = traits
|
|
6372
6605
|
view.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
6373
6606
|
applyNavigationBarAppearance()
|
|
6374
|
-
if let document = mutationDocument.
|
|
6607
|
+
if let document = mutationDocument.document {
|
|
6375
6608
|
renderCommitted(document)
|
|
6376
6609
|
} else {
|
|
6377
6610
|
rootStack.setNeedsLayout()
|
|
@@ -6469,6 +6702,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6469
6702
|
return view.subviews[..<index].contains { candidate in
|
|
6470
6703
|
guard let other = candidate as? CraftNativeModalView else { return false }
|
|
6471
6704
|
return other.blocksBackground && !other.isHidden && other.alpha > 0.01
|
|
6705
|
+
&& other.craftPointerEvents != CraftNativePointerEvents.none && other.craftPointerEvents != .boxNone
|
|
6472
6706
|
}
|
|
6473
6707
|
}
|
|
6474
6708
|
|
|
@@ -7395,6 +7629,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7395
7629
|
appearance.configureWithOpaqueBackground()
|
|
7396
7630
|
appearance.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
7397
7631
|
appearance.shadowColor = .clear
|
|
7632
|
+
CraftChromeFonts.apply(to: appearance)
|
|
7398
7633
|
navigationItem.standardAppearance = appearance
|
|
7399
7634
|
navigationItem.scrollEdgeAppearance = appearance
|
|
7400
7635
|
navigationItem.compactAppearance = appearance
|
|
@@ -7674,6 +7909,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7674
7909
|
#if compiler(>=6.2)
|
|
7675
7910
|
if #available(iOS 26.0, *) {
|
|
7676
7911
|
trackedScrollView?.topEdgeEffect.isHidden = false
|
|
7912
|
+
trackedScrollView?.topEdgeEffect.style = .automatic
|
|
7677
7913
|
trackedScrollView?.bottomEdgeEffect.isHidden = false
|
|
7678
7914
|
}
|
|
7679
7915
|
#endif
|
|
@@ -7684,10 +7920,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7684
7920
|
}
|
|
7685
7921
|
#if compiler(>=6.2)
|
|
7686
7922
|
if #available(iOS 26.0, *) {
|
|
7687
|
-
//
|
|
7688
|
-
//
|
|
7689
|
-
//
|
|
7690
|
-
|
|
7923
|
+
// The clock and navigation title need separation from live
|
|
7924
|
+
// content. Keep UIKit's soft top scrim rather than removing
|
|
7925
|
+
// it or adding an opaque bar. The floating tab pill already
|
|
7926
|
+
// carries its own material, so its full-width wash stays off.
|
|
7927
|
+
scroll?.topEdgeEffect.isHidden = false
|
|
7928
|
+
scroll?.topEdgeEffect.style = topEdgeToEdge && !navigationBarSolid ? .soft : .automatic
|
|
7691
7929
|
scroll?.bottomEdgeEffect.isHidden = tabbedScroll
|
|
7692
7930
|
}
|
|
7693
7931
|
#endif
|
|
@@ -8332,7 +8570,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8332
8570
|
// Internal so simulator-hosted XCTest can assert actual UIView identity.
|
|
8333
8571
|
func render(_ document: [String: Any]) {
|
|
8334
8572
|
mutationDocument.replace(with: document)
|
|
8335
|
-
|
|
8573
|
+
// Import assigns protocol IDs to legacy descendants. Draw that same
|
|
8574
|
+
// committed tree from the first frame, so later patches cannot turn
|
|
8575
|
+
// selector/positional identities into different native controls.
|
|
8576
|
+
guard let committed = mutationDocument.document else { return }
|
|
8577
|
+
renderCommitted(committed)
|
|
8336
8578
|
if !hybridRendered {
|
|
8337
8579
|
hybridRendered = true
|
|
8338
8580
|
// The app script's drawing is never on screen: not a first frame.
|
|
@@ -8973,6 +9215,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8973
9215
|
if let current = image.image { image.image = imageForDisplay(current, view: image) }
|
|
8974
9216
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
8975
9217
|
image.contentMode = imageContentMode(requestedResizeMode)
|
|
9218
|
+
(image as? CraftNativePositionedImageView)?.configureObjectGeometry(props: props, style: style)
|
|
8976
9219
|
applyImageClipping(image, contentMode: image.contentMode, style: style)
|
|
8977
9220
|
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: image)
|
|
8978
9221
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: image)
|
|
@@ -8992,6 +9235,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8992
9235
|
if let current = background.image { background.image = imageForDisplay(current, view: background) }
|
|
8993
9236
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
8994
9237
|
background.contentMode = imageContentMode(requestedResizeMode)
|
|
9238
|
+
background.configureObjectGeometry(props: props, style: style)
|
|
8995
9239
|
applyImageClipping(background, contentMode: background.contentMode, style: style)
|
|
8996
9240
|
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: background)
|
|
8997
9241
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: background)
|
|
@@ -9133,6 +9377,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9133
9377
|
)
|
|
9134
9378
|
if disabled { result.isUserInteractionEnabled = false }
|
|
9135
9379
|
}
|
|
9380
|
+
result.craftPointerEvents = CraftNativePointerEvents.resolve(props: props, style: style)
|
|
9136
9381
|
applyAccessibility(props, type: type, to: result)
|
|
9137
9382
|
(result as? CraftNativeFlowView)?.configureCelebration(props["celebration"])
|
|
9138
9383
|
return current
|
|
@@ -9179,12 +9424,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9179
9424
|
control.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
|
|
9180
9425
|
return control
|
|
9181
9426
|
case "Picker":
|
|
9182
|
-
let picker =
|
|
9427
|
+
let picker = CraftNativePickerView()
|
|
9183
9428
|
picker.dataSource = self
|
|
9184
9429
|
picker.delegate = self
|
|
9185
9430
|
return picker
|
|
9186
9431
|
case "DatePicker":
|
|
9187
|
-
let picker =
|
|
9432
|
+
let picker = CraftNativeDatePickerView()
|
|
9188
9433
|
picker.preferredDatePickerStyle = .compact
|
|
9189
9434
|
picker.addTarget(self, action: #selector(datePicked(_:)), for: .valueChanged)
|
|
9190
9435
|
return picker
|
|
@@ -9196,11 +9441,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9196
9441
|
toggle.addTarget(self, action: #selector(solidSwitchChanged(_:)), for: .valueChanged)
|
|
9197
9442
|
return toggle
|
|
9198
9443
|
}
|
|
9199
|
-
let toggle =
|
|
9444
|
+
let toggle = CraftNativeSwitch()
|
|
9200
9445
|
toggle.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
|
|
9201
9446
|
return toggle
|
|
9202
9447
|
case "Slider":
|
|
9203
|
-
let slider =
|
|
9448
|
+
let slider = CraftNativeSlider()
|
|
9204
9449
|
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
|
|
9205
9450
|
slider.addTarget(self, action: #selector(sliderFinished(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
|
|
9206
9451
|
return slider
|
|
@@ -9209,7 +9454,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9209
9454
|
case "StatusBar", "RefreshControl":
|
|
9210
9455
|
return CraftNativeFlowView()
|
|
9211
9456
|
case "Image":
|
|
9212
|
-
return
|
|
9457
|
+
return CraftNativeImageView()
|
|
9213
9458
|
case "ImageBackground":
|
|
9214
9459
|
return CraftNativeImageBackgroundView(frame: .zero)
|
|
9215
9460
|
case "KeyboardAvoidingView":
|
|
@@ -10462,7 +10707,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10462
10707
|
if let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options) {
|
|
10463
10708
|
// At the screen's scale, so its size in points is the
|
|
10464
10709
|
// size it is drawn at.
|
|
10465
|
-
|
|
10710
|
+
let decoded = UIImage(cgImage: image, scale: target.scale, orientation: .up)
|
|
10711
|
+
decoded.craftIntrinsicSize = CGSize(width: width, height: height)
|
|
10712
|
+
return decoded
|
|
10466
10713
|
}
|
|
10467
10714
|
}
|
|
10468
10715
|
}
|
|
@@ -10499,7 +10746,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10499
10746
|
let authored = (style["overflow"] as? String) == "hidden"
|
|
10500
10747
|
|| (view.craftRequestedCornerRadius ?? 0) > 0
|
|
10501
10748
|
|| view.craftRequestedCornerRadii != nil
|
|
10502
|
-
let clips = authored || contentMode == .scaleAspectFill
|
|
10749
|
+
let clips = authored || contentMode == .scaleAspectFill || (view as? CraftNativePositionedImageView)?.usesObjectGeometry == true
|
|
10503
10750
|
view.clipsToBounds = clips
|
|
10504
10751
|
view.layer.masksToBounds = clips
|
|
10505
10752
|
}
|
|
@@ -10546,7 +10793,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10546
10793
|
if let url = imageURL(for: uri) {
|
|
10547
10794
|
// Decoded at the size it is drawn, when the style fixes that size.
|
|
10548
10795
|
let pixels = Self.displayPixels(of: view)
|
|
10549
|
-
let fill = view.contentMode == .scaleAspectFill
|
|
10796
|
+
let fill = (view as? CraftNativePositionedImageView)?.objectCovers == true || view.contentMode == .scaleAspectFill
|
|
10550
10797
|
// media.keep owns private/offline copies keyed by the authored URL.
|
|
10551
10798
|
// Read before the decoded/network cache, without retaining private
|
|
10552
10799
|
// pixels there after the app clears its media on sign-out.
|
|
@@ -10654,7 +10901,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10654
10901
|
}
|
|
10655
10902
|
|
|
10656
10903
|
private func imageForDisplay(_ image: UIImage, view: UIImageView) -> UIImage {
|
|
10657
|
-
image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
10904
|
+
let displayed = image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
10905
|
+
displayed.craftIntrinsicSize = image.craftIntrinsicSize
|
|
10906
|
+
return displayed
|
|
10658
10907
|
}
|
|
10659
10908
|
|
|
10660
10909
|
private func emitImageEvent(
|