craft-native 0.0.486 → 0.0.490
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 +28 -0
- package/dist/android/src/index.js +15 -0
- package/dist/android/templates/CraftNativeServices.kt.template +10 -1
- package/dist/android/templates/CraftNativeShell.kt.template +96 -8
- package/dist/android/templates/MainActivityNative.kt.template +145 -30
- 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 +42 -1
- package/dist/ios/src/index.js +15 -0
- package/dist/ios/templates/CraftApp.swift +30 -0
- package/dist/ios/templates/CraftHybrid.swift +49 -0
- package/dist/ios/templates/CraftNativeActions.swift +9 -1
- package/dist/ios/templates/CraftNativeScreen.swift +353 -95
- package/dist/mobile.js +2 -2
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import OSLog
|
|
|
12
12
|
import Security
|
|
13
13
|
import SwiftUI
|
|
14
14
|
import UIKit
|
|
15
|
+
import UIKit.UIGestureRecognizerSubclass
|
|
15
16
|
|
|
16
17
|
/// A menu belongs to its native view, not to coordinates in a web page.
|
|
17
18
|
final class CraftNativeContextMenu: NSObject, UIContextMenuInteractionDelegate {
|
|
@@ -168,8 +169,8 @@ private func craftNativeFontWeight(_ value: Any?) -> String? {
|
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
171
|
-
if let value = value as? String
|
|
172
|
-
if let value = craftNativeNumber(value), value.doubleValue.isFinite { return
|
|
172
|
+
if let value = value as? String { return value }
|
|
173
|
+
if let value = craftNativeNumber(value), value.doubleValue.isFinite { return value.stringValue }
|
|
173
174
|
return nil
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -5195,6 +5196,23 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
5195
5196
|
/// Native inspection of an SVG plot. Coordinates are fractions of the plot's
|
|
5196
5197
|
/// bounds; labels and values are already localized by the application. No JS
|
|
5197
5198
|
/// callback or bridge round trip is needed while a finger moves.
|
|
5199
|
+
/// Reports where each touch starts and then fails, so it observes a window
|
|
5200
|
+
/// without ever taking a touch from the view underneath.
|
|
5201
|
+
final class CraftTouchBeganRecognizer: UIGestureRecognizer {
|
|
5202
|
+
private let began: (CGPoint) -> Void
|
|
5203
|
+
init(began: @escaping (CGPoint) -> Void) {
|
|
5204
|
+
self.began = began
|
|
5205
|
+
super.init(target: nil, action: nil)
|
|
5206
|
+
cancelsTouchesInView = false
|
|
5207
|
+
delaysTouchesBegan = false
|
|
5208
|
+
delaysTouchesEnded = false
|
|
5209
|
+
}
|
|
5210
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
5211
|
+
state = .failed
|
|
5212
|
+
if let touch = touches.first { began(touch.location(in: view)) }
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5198
5216
|
final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
5199
5217
|
struct Reading {
|
|
5200
5218
|
let key: String
|
|
@@ -5214,6 +5232,7 @@ final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
|
5214
5232
|
private lazy var tap = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
|
|
5215
5233
|
private lazy var pan = UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
|
|
5216
5234
|
private lazy var hold = UILongPressGestureRecognizer(target: self, action: #selector(held(_:)))
|
|
5235
|
+
private var outside: CraftTouchBeganRecognizer?
|
|
5217
5236
|
|
|
5218
5237
|
init(view: UIView) {
|
|
5219
5238
|
self.view = view
|
|
@@ -5241,6 +5260,7 @@ final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
|
5241
5260
|
color: (point["color"] as? String).flatMap { UIColor(css: $0) } ?? .systemBlue, label: label, rows: rows)
|
|
5242
5261
|
}
|
|
5243
5262
|
selectedIndex = -1
|
|
5263
|
+
unwatchOutside()
|
|
5244
5264
|
view?.isUserInteractionEnabled = !readings.isEmpty
|
|
5245
5265
|
if readings.isEmpty { view?.accessibilityTraits.remove(.adjustable); view?.accessibilityValue = nil }
|
|
5246
5266
|
[tap, pan, hold].forEach { $0.isEnabled = !readings.isEmpty }
|
|
@@ -5270,6 +5290,7 @@ final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
|
5270
5290
|
}
|
|
5271
5291
|
|
|
5272
5292
|
@discardableResult func dismiss() -> Bool {
|
|
5293
|
+
unwatchOutside()
|
|
5273
5294
|
guard selectedIndex >= 0 else { return false }
|
|
5274
5295
|
selectedIndex = -1
|
|
5275
5296
|
configureAccessibility()
|
|
@@ -5277,20 +5298,47 @@ final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
|
5277
5298
|
return true
|
|
5278
5299
|
}
|
|
5279
5300
|
|
|
5280
|
-
private func
|
|
5281
|
-
guard let view, view.bounds.width > 0 else { return }
|
|
5301
|
+
private func nearest(at point: CGPoint) -> Int? {
|
|
5302
|
+
guard let view, view.bounds.width > 0 else { return nil }
|
|
5282
5303
|
let fraction = max(0, min(1, point.x / view.bounds.width))
|
|
5283
|
-
|
|
5304
|
+
return readings.indices.min(by: { abs(readings[$0].x - fraction) < abs(readings[$1].x - fraction) })
|
|
5305
|
+
}
|
|
5306
|
+
private func select(at point: CGPoint) {
|
|
5307
|
+
if let index = nearest(at: point) { select(index); watchOutside() }
|
|
5308
|
+
}
|
|
5309
|
+
/// A tap pins the reading under it. Tapping the pinned reading again puts
|
|
5310
|
+
/// it away, so a reading can always be closed where it was opened.
|
|
5311
|
+
func tap(at point: CGPoint) {
|
|
5312
|
+
guard let index = nearest(at: point) else { return }
|
|
5313
|
+
if index == selectedIndex { dismiss() } else { select(at: point) }
|
|
5314
|
+
}
|
|
5315
|
+
/// A drag or a hold reads the chart only while the finger is down.
|
|
5316
|
+
func scrub(_ state: UIGestureRecognizer.State, at point: CGPoint) {
|
|
5317
|
+
switch state {
|
|
5318
|
+
case .began, .changed: select(at: point)
|
|
5319
|
+
case .ended, .cancelled, .failed: dismiss()
|
|
5320
|
+
default: break
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
/// A touch that starts anywhere off the chart closes a pinned reading. The
|
|
5324
|
+
/// watcher never recognises, so the touch still reaches what it landed on.
|
|
5325
|
+
func touchBegan(inWindow point: CGPoint) {
|
|
5326
|
+
guard let view, let window = view.window else { return }
|
|
5327
|
+
if !view.bounds.contains(view.convert(point, from: window)) { dismiss() }
|
|
5284
5328
|
}
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5329
|
+
private func watchOutside() {
|
|
5330
|
+
guard outside == nil, let window = view?.window else { return }
|
|
5331
|
+
let watcher = CraftTouchBeganRecognizer { [weak self] point in self?.touchBegan(inWindow: point) }
|
|
5332
|
+
window.addGestureRecognizer(watcher)
|
|
5333
|
+
outside = watcher
|
|
5289
5334
|
}
|
|
5290
|
-
|
|
5291
|
-
if
|
|
5292
|
-
|
|
5335
|
+
private func unwatchOutside() {
|
|
5336
|
+
if let outside { outside.view?.removeGestureRecognizer(outside) }
|
|
5337
|
+
outside = nil
|
|
5293
5338
|
}
|
|
5339
|
+
@objc private func tapped(_ gesture: UITapGestureRecognizer) { tap(at: gesture.location(in: view)) }
|
|
5340
|
+
@objc private func panned(_ gesture: UIPanGestureRecognizer) { scrub(gesture.state, at: gesture.location(in: view)) }
|
|
5341
|
+
@objc private func held(_ gesture: UILongPressGestureRecognizer) { scrub(gesture.state, at: gesture.location(in: view)) }
|
|
5294
5342
|
func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
|
|
5295
5343
|
guard gesture === pan else { return true }
|
|
5296
5344
|
let velocity = pan.velocity(in: view)
|
|
@@ -6002,9 +6050,23 @@ final class CraftNativeSolidSwitch: UIControl {
|
|
|
6002
6050
|
return CGRect(x: min(max(x, track.minX), track.maxX - rect.width - stretch),
|
|
6003
6051
|
y: rect.minY, width: rect.width + stretch, height: rect.height)
|
|
6004
6052
|
}
|
|
6053
|
+
/// How much wider than tall the thumb is drawn.
|
|
6054
|
+
///
|
|
6055
|
+
/// iOS 26 draws a wide pill where every system before it drew a circle,
|
|
6056
|
+
/// so a square thumb reads as a control from an older OS sitting among
|
|
6057
|
+
/// native ones. Measured on iOS 26.5 (iPhone 17 Pro simulator) by walking
|
|
6058
|
+
/// a real `UISwitch`: the control is 63x28 and its thumb 37x24, inset 2 on
|
|
6059
|
+
/// every side, travelling from x=2 to x=24.
|
|
6060
|
+
///
|
|
6061
|
+
/// Expressed against the thumb's own height rather than as a fixed size,
|
|
6062
|
+
/// so it keeps following whatever the real `UISwitch` reports — which is
|
|
6063
|
+
/// where `naturalSize` already comes from. At the system's own 24-point
|
|
6064
|
+
/// thumb height this is exactly 37.
|
|
6065
|
+
private static let thumbAspect: CGFloat = 37.0 / 24.0
|
|
6005
6066
|
private func thumbRect(at position: CGFloat) -> CGRect {
|
|
6006
6067
|
let track = trackRect.insetBy(dx: 2, dy: 2)
|
|
6007
|
-
|
|
6068
|
+
let width = min(track.width, track.height * Self.thumbAspect)
|
|
6069
|
+
return CGRect(x: track.minX + (track.width - width) * position, y: track.minY, width: width, height: track.height)
|
|
6008
6070
|
}
|
|
6009
6071
|
override func draw(_ rect: CGRect) {
|
|
6010
6072
|
let track = trackRect
|
|
@@ -6341,12 +6403,37 @@ private final class CraftNativeTextView: UITextView {
|
|
|
6341
6403
|
if !isScrollEnabled, contentOffset != .zero { contentOffset = .zero }
|
|
6342
6404
|
let inset = textContainerInset
|
|
6343
6405
|
let padding = textContainer.lineFragmentPadding
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6406
|
+
let measured = placeholderLabel
|
|
6407
|
+
.sizeThatFits(CGSize(width: Self.placeholderWidth(bounds: bounds, inset: inset, padding: padding),
|
|
6408
|
+
height: .greatestFiniteMagnitude))
|
|
6409
|
+
placeholderLabel.frame = Self.placeholderFrame(bounds: bounds, inset: inset, padding: padding,
|
|
6410
|
+
fittedHeight: measured.height)
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
static func placeholderWidth(bounds: CGRect, inset: UIEdgeInsets, padding: CGFloat) -> CGFloat {
|
|
6414
|
+
max(0, bounds.width - inset.left - inset.right - padding * 2)
|
|
6415
|
+
}
|
|
6416
|
+
|
|
6417
|
+
/// Where the placeholder sits inside the box.
|
|
6418
|
+
///
|
|
6419
|
+
/// Anchored at the top, and no taller than the text it holds. `UILabel`
|
|
6420
|
+
/// centres its text block vertically within its frame, while
|
|
6421
|
+
/// `UITextView` lays its glyphs out from the top, so a label handed the
|
|
6422
|
+
/// box's whole remaining height drew the placeholder of a four-row notes
|
|
6423
|
+
/// field halfway down it — nowhere near the caret that replaced it the
|
|
6424
|
+
/// moment the field was tapped (#501). The two rules only agreed when
|
|
6425
|
+
/// the box was exactly one line tall, which is why single-line fields
|
|
6426
|
+
/// looked right and every `textarea` did not. Given only the height its
|
|
6427
|
+
/// text needs, the label has no spare height to centre within.
|
|
6428
|
+
///
|
|
6429
|
+
/// Clamped to what is left of the box so that a placeholder longer than
|
|
6430
|
+
/// the field stays inside it rather than drawing past the bottom.
|
|
6431
|
+
static func placeholderFrame(bounds: CGRect, inset: UIEdgeInsets, padding: CGFloat, fittedHeight: CGFloat) -> CGRect {
|
|
6432
|
+
let available = max(0, bounds.height - inset.top - inset.bottom)
|
|
6433
|
+
return CGRect(x: inset.left + padding,
|
|
6434
|
+
y: inset.top,
|
|
6435
|
+
width: placeholderWidth(bounds: bounds, inset: inset, padding: padding),
|
|
6436
|
+
height: min(max(0, fittedHeight), available))
|
|
6350
6437
|
}
|
|
6351
6438
|
|
|
6352
6439
|
func refreshPlaceholder() {
|
|
@@ -6595,11 +6682,108 @@ enum CraftNativeBundle {
|
|
|
6595
6682
|
}
|
|
6596
6683
|
}
|
|
6597
6684
|
|
|
6598
|
-
|
|
6685
|
+
/// One shared single-choice control, compact like a form field. UIKit owns
|
|
6686
|
+
/// presentation, dismissal, checkmarks, keyboard navigation and VoiceOver.
|
|
6687
|
+
final class CraftNativePickerView: UIButton {
|
|
6688
|
+
struct Option: Equatable {
|
|
6689
|
+
let value: String
|
|
6690
|
+
let label: String
|
|
6691
|
+
let disabled: Bool
|
|
6692
|
+
}
|
|
6693
|
+
private var options: [Option] = []
|
|
6694
|
+
private var selectedValue: String?
|
|
6695
|
+
private var handlerIdentity: String?
|
|
6696
|
+
private var dispatch: ((String, Int) -> Void)?
|
|
6697
|
+
private var revision = 0
|
|
6698
|
+
private(set) var selectedLabel = ""
|
|
6699
|
+
|
|
6700
|
+
override init(frame: CGRect) {
|
|
6701
|
+
super.init(frame: frame)
|
|
6702
|
+
showsMenuAsPrimaryAction = true
|
|
6703
|
+
contentHorizontalAlignment = .fill
|
|
6704
|
+
titleLabel?.adjustsFontForContentSizeCategory = true
|
|
6705
|
+
layer.cornerRadius = 10
|
|
6706
|
+
}
|
|
6707
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
6708
|
+
|
|
6709
|
+
func update(options: [Option], selected: String?, enabled: Bool, handler: String?,
|
|
6710
|
+
font: UIFont, foreground: UIColor, dispatch: @escaping (String, Int) -> Void) {
|
|
6711
|
+
let candidate = selected ?? self.selectedValue
|
|
6712
|
+
let resolved = options.contains(where: { $0.value == candidate }) ? candidate : options.first(where: { !$0.disabled })?.value
|
|
6713
|
+
if self.options != options || self.selectedValue != resolved || self.isEnabled != enabled || self.handlerIdentity != handler {
|
|
6714
|
+
revision += 1
|
|
6715
|
+
interactions.compactMap { $0 as? UIContextMenuInteraction }.forEach { $0.dismissMenu() }
|
|
6716
|
+
}
|
|
6717
|
+
self.options = options
|
|
6718
|
+
self.selectedValue = resolved
|
|
6719
|
+
self.handlerIdentity = handler
|
|
6720
|
+
self.dispatch = dispatch
|
|
6721
|
+
isEnabled = enabled && options.contains(where: { !$0.disabled })
|
|
6722
|
+
selectedLabel = options.first(where: { $0.value == resolved })?.label ?? ""
|
|
6723
|
+
var configuration = UIButton.Configuration.plain()
|
|
6724
|
+
configuration.title = selectedLabel
|
|
6725
|
+
configuration.image = UIImage(systemName: "chevron.down")
|
|
6726
|
+
configuration.imagePlacement = .trailing
|
|
6727
|
+
configuration.imagePadding = 12
|
|
6728
|
+
configuration.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12)
|
|
6729
|
+
configuration.titleAlignment = .leading
|
|
6730
|
+
configuration.baseForegroundColor = foreground
|
|
6731
|
+
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
6732
|
+
var result = attributes
|
|
6733
|
+
result.font = font
|
|
6734
|
+
return result
|
|
6735
|
+
}
|
|
6736
|
+
configuration.imageColorTransformer = UIConfigurationColorTransformer { _ in .secondaryLabel }
|
|
6737
|
+
self.configuration = configuration
|
|
6738
|
+
refreshMenu()
|
|
6739
|
+
invalidateIntrinsicContentSize()
|
|
6740
|
+
}
|
|
6741
|
+
|
|
6742
|
+
private func refreshMenu() {
|
|
6743
|
+
menu = UIMenu(children: options.enumerated().map { index, option in
|
|
6744
|
+
let choose = selection(index)
|
|
6745
|
+
return UIAction(title: option.label, attributes: option.disabled ? [.disabled] : [],
|
|
6746
|
+
state: option.value == selectedValue ? .on : .off) { _ in choose() }
|
|
6747
|
+
})
|
|
6748
|
+
}
|
|
6749
|
+
|
|
6750
|
+
/// An open menu must not apply an old option to a rebound or disabled field.
|
|
6751
|
+
func selection(_ index: Int) -> () -> Void {
|
|
6752
|
+
let expected = revision
|
|
6753
|
+
return { [weak self] in
|
|
6754
|
+
guard let self, self.revision == expected, self.isEnabled,
|
|
6755
|
+
self.options.indices.contains(index), !self.options[index].disabled else { return }
|
|
6756
|
+
let value = self.options[index].value
|
|
6757
|
+
guard value != self.selectedValue else { return }
|
|
6758
|
+
self.selectedValue = value
|
|
6759
|
+
self.selectedLabel = self.options[index].label
|
|
6760
|
+
self.configuration?.title = self.selectedLabel
|
|
6761
|
+
self.accessibilityValue = self.selectedLabel
|
|
6762
|
+
self.revision += 1
|
|
6763
|
+
self.refreshMenu()
|
|
6764
|
+
self.invalidateIntrinsicContentSize()
|
|
6765
|
+
self.dispatch?(value, index)
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
func detach() {
|
|
6770
|
+
revision += 1
|
|
6771
|
+
dispatch = nil
|
|
6772
|
+
interactions.compactMap { $0 as? UIContextMenuInteraction }.forEach { $0.dismissMenu() }
|
|
6773
|
+
menu = nil
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6776
|
+
override var intrinsicContentSize: CGSize {
|
|
6777
|
+
let size = super.intrinsicContentSize
|
|
6778
|
+
return CGSize(width: max(44, size.width), height: max(44, size.height))
|
|
6779
|
+
}
|
|
6780
|
+
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
|
6781
|
+
let fitted = super.sizeThatFits(size)
|
|
6782
|
+
return CGSize(width: fitted.width, height: max(44, fitted.height))
|
|
6783
|
+
}
|
|
6599
6784
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6600
6785
|
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6601
6786
|
}
|
|
6602
|
-
|
|
6603
6787
|
}
|
|
6604
6788
|
|
|
6605
6789
|
private final class CraftNativeDatePickerView: UIDatePicker {
|
|
@@ -6650,7 +6834,7 @@ private final class CraftNativeScreenRootView: UIView {
|
|
|
6650
6834
|
}
|
|
6651
6835
|
}
|
|
6652
6836
|
|
|
6653
|
-
final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, UITextFieldDelegate, UITextViewDelegate
|
|
6837
|
+
final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, UITextFieldDelegate, UITextViewDelegate {
|
|
6654
6838
|
private final class RenderedNode {
|
|
6655
6839
|
/// A recycled list row's root takes the identity of the row it shows next.
|
|
6656
6840
|
var identity: String
|
|
@@ -6714,7 +6898,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6714
6898
|
/// the user's tap. Remember that native value briefly so the echo does not
|
|
6715
6899
|
/// interrupt the animation and leave the thumb painted at its old edge.
|
|
6716
6900
|
private var switchUserValues: [ObjectIdentifier: Bool] = [:]
|
|
6717
|
-
private var pickerOptions: [ObjectIdentifier: [(value: String, label: String)]] = [:]
|
|
6718
6901
|
private var modalVisibility: [ObjectIdentifier: Bool] = [:]
|
|
6719
6902
|
private var modalFocusRestore: [ObjectIdentifier: UIView] = [:]
|
|
6720
6903
|
private var layoutHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -6941,6 +7124,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6941
7124
|
|
|
6942
7125
|
override func viewWillDisappear(_ animated: Bool) {
|
|
6943
7126
|
super.viewWillDisappear(animated)
|
|
7127
|
+
// A screen does not take the keyboard with it. Its field is still
|
|
7128
|
+
// first responder at this point, and once the view leaves the window
|
|
7129
|
+
// the next screen's `window?.endEditing` no longer reaches it — the
|
|
7130
|
+
// keyboard then stays up with nothing able to dismiss it until the
|
|
7131
|
+
// app is backgrounded (#497). This is the one moment both facts hold:
|
|
7132
|
+
// the screen is going, and its view still owns the responder.
|
|
7133
|
+
//
|
|
7134
|
+
// `lastFocusedInput` is deliberately left alone. A screen merely
|
|
7135
|
+
// covered by a push comes back, and restoreFocus putting the person
|
|
7136
|
+
// back where they were is the behaviour that was already there.
|
|
7137
|
+
view.endEditing(true)
|
|
6944
7138
|
screenVisibilityChanged(false)
|
|
6945
7139
|
stopCelebrations(in: view)
|
|
6946
7140
|
// Release only this screen's accessory. A retained or prepared screen
|
|
@@ -6949,7 +7143,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6949
7143
|
CraftChrome.shared.releaseTabAccessory(ownedBy: self)
|
|
6950
7144
|
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
6951
7145
|
// back for whatever shows next, a web page included.
|
|
6952
|
-
if
|
|
7146
|
+
if tabBarHiddenApplied, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
6953
7147
|
CraftChrome.shared.setHiddenByScreen(false)
|
|
6954
7148
|
}
|
|
6955
7149
|
}
|
|
@@ -6991,7 +7185,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6991
7185
|
|
|
6992
7186
|
private func restoreShellChrome() {
|
|
6993
7187
|
guard !isAppScript else { return }
|
|
6994
|
-
|
|
7188
|
+
applyTabBarHidden()
|
|
6995
7189
|
// A screen not yet asked about the accessory (one shown for the first
|
|
6996
7190
|
// time, before its script has run) leaves the bar as it is; one that
|
|
6997
7191
|
// has drawn without asking for it takes off what nobody claims.
|
|
@@ -7087,6 +7281,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7087
7281
|
private func disposeRuntime() {
|
|
7088
7282
|
guard !runtimeDisposed else { return }
|
|
7089
7283
|
runtimeDisposed = true
|
|
7284
|
+
// Again here, and forgetting the input this time: a screen can be torn
|
|
7285
|
+
// down without ever appearing (one prepared and then replaced), so
|
|
7286
|
+
// viewWillDisappear is not guaranteed to have run. Nothing comes back
|
|
7287
|
+
// from here, so there is no focus worth restoring.
|
|
7288
|
+
//
|
|
7289
|
+
// `viewIfLoaded`, never `view`: this also runs from `deinit`, where
|
|
7290
|
+
// `view` is a `UIView!` that UIKit has already cleared — reading it
|
|
7291
|
+
// traps on the implicit unwrap, and a screen that never appeared
|
|
7292
|
+
// would instead have `loadView` resurrect `self` while it is being
|
|
7293
|
+
// deallocated. A screen with no loaded view has nothing focused.
|
|
7294
|
+
forgetFocusedInput()
|
|
7295
|
+
viewIfLoaded?.endEditing(true)
|
|
7090
7296
|
frameDelivery.close()
|
|
7091
7297
|
storageLifetime.close()
|
|
7092
7298
|
pendingCapabilityDeadlines.values.forEach { $0.cancel() }
|
|
@@ -7562,7 +7768,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7562
7768
|
/// - `craft.secureStorage.getSync(key)`: the Keychain item the web page's
|
|
7563
7769
|
/// `craft.secureStorage.set` wrote.
|
|
7564
7770
|
/// - `craft.navigation.setOptions({ title, titleColor, largeTitle, hidden,
|
|
7565
|
-
/// backTitle, tabBarHidden, tabItem, swipeBack,
|
|
7771
|
+
/// backTitle, tabBarHidden, tabBarHiddenWhenPushed, tabItem, swipeBack,
|
|
7772
|
+
/// contentSwipeBack,
|
|
7566
7773
|
/// tabSwipeEnabled, backgroundColor, navigationBarSolid, leftButtons,
|
|
7567
7774
|
/// rightButtons, tabAccessory })`; a button's tap is the event
|
|
7568
7775
|
/// `navButton`. `craft.d.ts` types no navigation surface, so this list
|
|
@@ -8029,6 +8236,32 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8029
8236
|
private var statusBarHiddenOverride: Bool?
|
|
8030
8237
|
/// The app's tab bar away while this screen shows (`tabBarHidden`).
|
|
8031
8238
|
private var tabBarHidden = false
|
|
8239
|
+
/// The bar away only once this screen is pushed onto something
|
|
8240
|
+
/// (`tabBarHiddenWhenPushed`): UIKit's `hidesBottomBarWhenPushed` idiom,
|
|
8241
|
+
/// for a screen that keeps the bar as its stack's root and drops it when
|
|
8242
|
+
/// it is a detail. Craft's tab bar is drawn by CraftChrome rather than a
|
|
8243
|
+
/// UITabBarController, so the depth is read here and the bar is asked
|
|
8244
|
+
/// directly; the Android shell has honoured the same key all along.
|
|
8245
|
+
private var tabBarHiddenWhenPushed = false
|
|
8246
|
+
/// What this screen last told the chrome. Teardown has to put back a bar
|
|
8247
|
+
/// this screen took away, and recomputing the answer there is no good:
|
|
8248
|
+
/// UIKit has already taken a popped screen out of `viewControllers` by
|
|
8249
|
+
/// `viewWillDisappear`, so a bar hidden for depth would read as never
|
|
8250
|
+
/// hidden at all and stay off for whatever shows next.
|
|
8251
|
+
private var tabBarHiddenApplied = false
|
|
8252
|
+
|
|
8253
|
+
/// Whether the bar should be away for this screen as it stands now.
|
|
8254
|
+
private var wantsTabBarHidden: Bool {
|
|
8255
|
+
if tabBarHidden { return true }
|
|
8256
|
+
guard tabBarHiddenWhenPushed, let stack = navigationController?.viewControllers else { return false }
|
|
8257
|
+
guard let depth = stack.firstIndex(where: { $0 === self }) else { return false }
|
|
8258
|
+
return depth > 0
|
|
8259
|
+
}
|
|
8260
|
+
|
|
8261
|
+
private func applyTabBarHidden() {
|
|
8262
|
+
tabBarHiddenApplied = wantsTabBarHidden
|
|
8263
|
+
CraftChrome.shared.setHiddenByScreen(tabBarHiddenApplied)
|
|
8264
|
+
}
|
|
8032
8265
|
private var tabAccessory: [String: Any]?
|
|
8033
8266
|
/// Whether the screen has said anything about the accessory yet
|
|
8034
8267
|
/// (`tabAccessory`, null included): until it has, the bar keeps showing
|
|
@@ -8114,8 +8347,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8114
8347
|
}
|
|
8115
8348
|
if let hidden = options["tabBarHidden"] as? Bool {
|
|
8116
8349
|
tabBarHidden = hidden
|
|
8350
|
+
}
|
|
8351
|
+
if let hiddenWhenPushed = options["tabBarHiddenWhenPushed"] as? Bool {
|
|
8352
|
+
tabBarHiddenWhenPushed = hiddenWhenPushed
|
|
8353
|
+
}
|
|
8354
|
+
if options.keys.contains("tabBarHidden") || options.keys.contains("tabBarHiddenWhenPushed") {
|
|
8355
|
+
// Prepared tabs and screens under a push keep the answer and apply
|
|
8356
|
+
// it when they are shown (restoreShellChrome); only the screen on
|
|
8357
|
+
// display may move the shared bar.
|
|
8117
8358
|
if navigationController?.topViewController === self, isShownOnScreen {
|
|
8118
|
-
|
|
8359
|
+
applyTabBarHidden()
|
|
8119
8360
|
}
|
|
8120
8361
|
}
|
|
8121
8362
|
if let swipeBack = options["swipeBack"] as? Bool {
|
|
@@ -8228,6 +8469,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8228
8469
|
|
|
8229
8470
|
/// A bar button from `{ id, symbol | title }`. Its tap is the event
|
|
8230
8471
|
/// `navButton` with `{ id }`.
|
|
8472
|
+
/// A bar button's choices, or nil when it has none worth a menu.
|
|
8473
|
+
static func navigationMenu(_ raw: Any?, chosen: @escaping (String) -> Void) -> UIMenu? {
|
|
8474
|
+
let actions: [UIAction] = ((raw as? [[String: Any]]) ?? []).prefix(12).compactMap { choice in
|
|
8475
|
+
guard let id = choice["id"] as? String, !id.isEmpty,
|
|
8476
|
+
let title = choice["title"] as? String, !title.isEmpty else { return nil }
|
|
8477
|
+
var attributes: UIMenuElement.Attributes = []
|
|
8478
|
+
if choice["disabled"] as? Bool == true { attributes.insert(.disabled) }
|
|
8479
|
+
if choice["destructive"] as? Bool == true { attributes.insert(.destructive) }
|
|
8480
|
+
let action = UIAction(title: title, image: (choice["symbol"] as? String).flatMap { UIImage(systemName: $0) },
|
|
8481
|
+
attributes: attributes) { _ in chosen(id) }
|
|
8482
|
+
action.accessibilityIdentifier = "nav-menu-\(id)"
|
|
8483
|
+
return action
|
|
8484
|
+
}
|
|
8485
|
+
return actions.isEmpty ? nil : UIMenu(children: actions)
|
|
8486
|
+
}
|
|
8487
|
+
|
|
8231
8488
|
private func navigationButton(_ spec: [String: Any]) -> UIBarButtonItem? {
|
|
8232
8489
|
guard let id = spec["id"] as? String, !id.isEmpty else { return nil }
|
|
8233
8490
|
let action = UIAction { [weak self] _ in
|
|
@@ -8236,11 +8493,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8236
8493
|
if spec["variant"] as? String == "accent" || spec["variant"] as? String == "account" {
|
|
8237
8494
|
return accountNavigationButton(spec, action: action)
|
|
8238
8495
|
}
|
|
8496
|
+
// `menu`: the button opens a native menu instead of sending its own
|
|
8497
|
+
// id, and each choice is reported as a press of that choice's id.
|
|
8498
|
+
let menu = Self.navigationMenu(spec["menu"]) { [weak self] choice in
|
|
8499
|
+
self?.send(type: "EVENT", payload: ["handlerName": "navButton", "nativeEvent": ["id": choice]])
|
|
8500
|
+
}
|
|
8239
8501
|
let item: UIBarButtonItem
|
|
8240
8502
|
if let symbol = spec["symbol"] as? String, let image = UIImage(systemName: symbol) {
|
|
8241
|
-
item = UIBarButtonItem(image: image, primaryAction: action)
|
|
8503
|
+
item = menu.map { UIBarButtonItem(image: image, menu: $0) } ?? UIBarButtonItem(image: image, primaryAction: action)
|
|
8242
8504
|
} else if let title = spec["title"] as? String {
|
|
8243
|
-
item = UIBarButtonItem(title: title, primaryAction: action)
|
|
8505
|
+
item = menu.map { UIBarButtonItem(title: title, menu: $0) } ?? UIBarButtonItem(title: title, primaryAction: action)
|
|
8244
8506
|
} else {
|
|
8245
8507
|
return nil
|
|
8246
8508
|
}
|
|
@@ -8831,14 +9093,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8831
9093
|
])
|
|
8832
9094
|
}
|
|
8833
9095
|
case "NAVIGATE", "NAVIGATE_REPLACE":
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
9096
|
+
guard let screen = payload["screen"] as? String, !screen.isEmpty else { return }
|
|
9097
|
+
// Replacing a path needs no stack of its own: it asks the shell to
|
|
9098
|
+
// show that path, which is why it delegates rather than pushing.
|
|
9099
|
+
// The app script has no stack, so this was refused to it along
|
|
9100
|
+
// with everything else, and its only way to redirect was to push
|
|
9101
|
+
// sign-in on top of the screen it meant to prevent (#499). The
|
|
9102
|
+
// gate in CraftHybrid.configure is the fix for launch; this is
|
|
9103
|
+
// what lets a session that ends mid-run replace rather than stack.
|
|
8838
9104
|
if type == "NAVIGATE_REPLACE", screen.hasPrefix("/") {
|
|
9105
|
+
guard isAppScript || navigationController?.topViewController === self else { return }
|
|
8839
9106
|
hybridEvents?.nativeScreen(self, replace: screen)
|
|
8840
9107
|
return
|
|
8841
9108
|
}
|
|
9109
|
+
// A screen's own stack; the app script has none (it opens paths).
|
|
9110
|
+
guard !isAppScript, navigationController?.topViewController === self,
|
|
9111
|
+
let navigation = navigationController else { return }
|
|
8842
9112
|
let params = payload["params"] as? [String: Any] ?? [:]
|
|
8843
9113
|
let next = CraftNativeScreenController(config: config, routeName: screen, routeParams: params)
|
|
8844
9114
|
next.hybridEvents = hybridEvents
|
|
@@ -9280,7 +9550,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9280
9550
|
previousNodesByProtocolID.removeAll(keepingCapacity: true)
|
|
9281
9551
|
adoptedPreviousProtocolIDs.removeAll(keepingCapacity: true)
|
|
9282
9552
|
indexPreviousNodes(previous)
|
|
9283
|
-
|
|
9553
|
+
// The app's configured body face seeds the inheritance chain, so a
|
|
9554
|
+
// screen's text is branded for the same reason its chrome is. Without
|
|
9555
|
+
// this the chain starts empty and `fontFamily` reached the navigation
|
|
9556
|
+
// bar, the tab labels and the bar buttons — all UIAppearance proxies —
|
|
9557
|
+
// while every label, button and field inside a screen stayed in the
|
|
9558
|
+
// system face (#500).
|
|
9559
|
+
//
|
|
9560
|
+
// A node that sets its own family still wins: the fill below only
|
|
9561
|
+
// applies where a node set none or `inherit`. Nil unless the app
|
|
9562
|
+
// configured one, which the same guard makes free.
|
|
9563
|
+
let next = reconcile(document, identity: "root", path: "root", previous: previous,
|
|
9564
|
+
inheritedFontFamily: CraftChromeFonts.body)
|
|
9284
9565
|
previousNodesByProtocolID.removeAll(keepingCapacity: true)
|
|
9285
9566
|
adoptedPreviousProtocolIDs.removeAll(keepingCapacity: true)
|
|
9286
9567
|
if previous?.view !== next.view {
|
|
@@ -9594,31 +9875,33 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9594
9875
|
control.configureAccessibility(items: items)
|
|
9595
9876
|
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: control)
|
|
9596
9877
|
case "Picker":
|
|
9597
|
-
let picker = result as!
|
|
9598
|
-
let
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
guard let node = child as? [String: Any], node["type"] as? String == "Text" else { return nil }
|
|
9605
|
-
let optionProps = node["props"] as? [String: Any] ?? [:]
|
|
9606
|
-
let label = (node["children"] as? [Any] ?? []).compactMap { $0 as? String }.joined()
|
|
9607
|
-
guard !label.isEmpty else { return nil }
|
|
9608
|
-
return (value: craftNativePickerValue(optionProps["value"]) ?? label, label: label)
|
|
9878
|
+
let picker = result as! CraftNativePickerView
|
|
9879
|
+
let rawItems = props["items"] as? [[String: Any]]
|
|
9880
|
+
let options: [CraftNativePickerView.Option] = rawItems.map { items in
|
|
9881
|
+
items.compactMap { item in
|
|
9882
|
+
guard let label = item["label"] as? String, !label.isEmpty else { return nil }
|
|
9883
|
+
return CraftNativePickerView.Option(value: craftNativePickerValue(item["value"]) ?? label,
|
|
9884
|
+
label: label, disabled: item["disabled"] as? Bool == true)
|
|
9609
9885
|
}
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
}
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
picker.
|
|
9621
|
-
|
|
9886
|
+
} ?? children.compactMap { child in
|
|
9887
|
+
guard let node = child as? [String: Any], node["type"] as? String == "Text" else { return nil }
|
|
9888
|
+
let optionProps = node["props"] as? [String: Any] ?? [:]
|
|
9889
|
+
let label = (node["children"] as? [Any] ?? []).compactMap { $0 as? String }.joined()
|
|
9890
|
+
guard !label.isEmpty else { return nil }
|
|
9891
|
+
return CraftNativePickerView.Option(value: craftNativePickerValue(optionProps["value"]) ?? label,
|
|
9892
|
+
label: label, disabled: optionProps["disabled"] as? Bool == true)
|
|
9893
|
+
}
|
|
9894
|
+
let handler = nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"])
|
|
9895
|
+
updateHandler(handler, for: picker)
|
|
9896
|
+
picker.update(options: options,
|
|
9897
|
+
selected: craftNativePickerValue(props["selectedValue"]) ?? craftNativePickerValue(props["value"]),
|
|
9898
|
+
enabled: props["disabled"] as? Bool != true,
|
|
9899
|
+
handler: handler, font: textFont(style, default: .preferredFont(forTextStyle: .body)),
|
|
9900
|
+
foreground: color(style["color"]) ?? .label) { [weak self, weak picker] value, index in
|
|
9901
|
+
guard let self, let picker, let handler = self.handlers[ObjectIdentifier(picker)] else { return }
|
|
9902
|
+
self.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": value, "index": index]])
|
|
9903
|
+
}
|
|
9904
|
+
if style["backgroundColor"] == nil { picker.backgroundColor = .tertiarySystemFill }
|
|
9622
9905
|
case "DatePicker":
|
|
9623
9906
|
let picker = result as! UIDatePicker
|
|
9624
9907
|
let mode = Self.datePickerMode(props["mode"])
|
|
@@ -9994,6 +10277,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9994
10277
|
result.craftPointerEvents = CraftNativePointerEvents.resolve(props: props, style: style)
|
|
9995
10278
|
applyAccessibility(props, type: type, to: result)
|
|
9996
10279
|
(result as? CraftNativeSvgView)?.inspection.configureAccessibility()
|
|
10280
|
+
if let picker = result as? CraftNativePickerView, props["accessibilityValue"] == nil {
|
|
10281
|
+
picker.accessibilityValue = picker.selectedLabel
|
|
10282
|
+
}
|
|
9997
10283
|
// After applyAccessibility: it settles whether the view is an element
|
|
9998
10284
|
// at all, and an action on a non-element is unreachable.
|
|
9999
10285
|
updateAccessibilityActions(props["accessibilityActions"], handler: menuDisabled ? nil : events["onAccessibilityAction"], for: result)
|
|
@@ -10053,10 +10339,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10053
10339
|
control.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
|
|
10054
10340
|
return control
|
|
10055
10341
|
case "Picker":
|
|
10056
|
-
|
|
10057
|
-
picker.dataSource = self
|
|
10058
|
-
picker.delegate = self
|
|
10059
|
-
return picker
|
|
10342
|
+
return CraftNativePickerView(frame: .zero)
|
|
10060
10343
|
case "DatePicker":
|
|
10061
10344
|
let picker = CraftNativeDatePickerView()
|
|
10062
10345
|
picker.preferredDatePickerStyle = .compact
|
|
@@ -10444,10 +10727,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10444
10727
|
// its subtree is gone.
|
|
10445
10728
|
if !preservingInputDrafts {
|
|
10446
10729
|
if let textView = node.view as? UITextView { textView.delegate = nil }
|
|
10447
|
-
|
|
10448
|
-
picker.delegate = nil
|
|
10449
|
-
picker.dataSource = nil
|
|
10450
|
-
}
|
|
10730
|
+
(node.view as? CraftNativePickerView)?.detach()
|
|
10451
10731
|
if let scroll = node.view as? UIScrollView { scroll.delegate = nil }
|
|
10452
10732
|
(node.view as? UIControl)?.removeTarget(self, action: nil, for: .allEvents)
|
|
10453
10733
|
}
|
|
@@ -10502,7 +10782,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10502
10782
|
scrollMomentumEndHandlers.removeValue(forKey: id)
|
|
10503
10783
|
sliderCompleteHandlers.removeValue(forKey: id)
|
|
10504
10784
|
sliderSteps.removeValue(forKey: id)
|
|
10505
|
-
pickerOptions.removeValue(forKey: id)
|
|
10506
10785
|
modalVisibility.removeValue(forKey: id)
|
|
10507
10786
|
if let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
10508
10787
|
node.view.removeGestureRecognizer(recognizer)
|
|
@@ -10636,12 +10915,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10636
10915
|
/// Views that take their own touches whatever handlers the screen gives
|
|
10637
10916
|
/// them: native controls, a scroll view, a modal, and a video's controls (with
|
|
10638
10917
|
/// interaction off, its play, scrubber and full screen never answered).
|
|
10639
|
-
///
|
|
10640
|
-
///
|
|
10641
|
-
/// not be panned, zoomed, or have a pin tapped; a picker could not be spun.
|
|
10918
|
+
/// Maps too: a map without a long-press handler must still pan, zoom,
|
|
10919
|
+
/// and take taps on its pins.
|
|
10642
10920
|
private func ownsTouches(_ view: UIView) -> Bool {
|
|
10643
10921
|
view is UIControl || view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
10644
|
-
|| view is MKMapView ||
|
|
10922
|
+
|| view is MKMapView || ((view as? CraftNativeSvgView)?.inspection.readings.isEmpty == false)
|
|
10645
10923
|
}
|
|
10646
10924
|
|
|
10647
10925
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|
|
@@ -11630,9 +11908,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11630
11908
|
// A video with its system controls is a group too: VoiceOver reaches
|
|
11631
11909
|
// play, the scrubber, skipping and full screen inside it.
|
|
11632
11910
|
let controlledVideo = type == "Video" && (props["controls"] as? Bool) == true
|
|
11633
|
-
//
|
|
11634
|
-
//
|
|
11635
|
-
let container = ["ScrollView", "FlatList", "SectionList", "
|
|
11911
|
+
// Compact single-choice pickers are accessible menu buttons. Controls
|
|
11912
|
+
// containing several independent elements remain semantic groups.
|
|
11913
|
+
let container = ["ScrollView", "FlatList", "SectionList", "SegmentedControl", "Modal", "Drawer"].contains(type)
|
|
11636
11914
|
|| ["tablist", "radiogroup", "list", "menu", "menubar", "toolbar"].contains(role ?? "")
|
|
11637
11915
|
|| controlledVideo
|
|
11638
11916
|
view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
|
|
@@ -11644,6 +11922,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11644
11922
|
var traits: UIAccessibilityTraits = []
|
|
11645
11923
|
switch type {
|
|
11646
11924
|
case "Slider", "DatePicker": traits.insert(.adjustable)
|
|
11925
|
+
case "Picker": traits.insert(.button)
|
|
11647
11926
|
default: break
|
|
11648
11927
|
}
|
|
11649
11928
|
switch role ?? type.lowercased() {
|
|
@@ -12056,27 +12335,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
12056
12335
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": Self.sliderEventValue(snappedSliderValue(sender.value, for: sender), step: sliderSteps[ObjectIdentifier(sender)])]])
|
|
12057
12336
|
}
|
|
12058
12337
|
|
|
12059
|
-
func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
|
12060
|
-
|
|
12061
|
-
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
|
12062
|
-
pickerOptions[ObjectIdentifier(pickerView)]?.count ?? 0
|
|
12063
|
-
}
|
|
12064
|
-
|
|
12065
|
-
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
|
12066
|
-
guard let options = pickerOptions[ObjectIdentifier(pickerView)], options.indices.contains(row) else { return nil }
|
|
12067
|
-
return options[row].label
|
|
12068
|
-
}
|
|
12069
|
-
|
|
12070
|
-
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
|
12071
|
-
guard let options = pickerOptions[ObjectIdentifier(pickerView)], options.indices.contains(row),
|
|
12072
|
-
let handler = handlers[ObjectIdentifier(pickerView)] else { return }
|
|
12073
|
-
let option = options[row]
|
|
12074
|
-
send(type: "EVENT", payload: [
|
|
12075
|
-
"handlerName": handler,
|
|
12076
|
-
"nativeEvent": ["value": option.value, "index": row],
|
|
12077
|
-
])
|
|
12078
|
-
}
|
|
12079
|
-
|
|
12080
12338
|
@objc private func textFocused(_ sender: UITextField) {
|
|
12081
12339
|
lastFocusedInput = sender
|
|
12082
12340
|
// Moving between fields does not move the keyboard, so nothing else
|