@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.1
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/LICENSE +21 -0
- package/Package.swift +23 -0
- package/README.md +53 -2
- package/RdlaboCapacitorNativeUi.podspec +17 -0
- package/android/build.gradle +69 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +3 -0
- package/dist/native/activation.d.ts +14 -0
- package/dist/native/activation.d.ts.map +1 -0
- package/dist/native/activation.js +305 -0
- package/dist/native/activation.js.map +1 -0
- package/dist/native/components/index.d.ts +19 -0
- package/dist/native/components/index.d.ts.map +1 -0
- package/dist/native/components/index.js +38 -0
- package/dist/native/components/index.js.map +1 -0
- package/dist/native/components/ion-back-button.d.ts +10 -0
- package/dist/native/components/ion-back-button.d.ts.map +1 -0
- package/dist/native/components/ion-back-button.js +21 -0
- package/dist/native/components/ion-back-button.js.map +1 -0
- package/dist/native/components/ion-button.d.ts +10 -0
- package/dist/native/components/ion-button.d.ts.map +1 -0
- package/dist/native/components/ion-button.js +25 -0
- package/dist/native/components/ion-button.js.map +1 -0
- package/dist/native/components/ion-buttons.d.ts +11 -0
- package/dist/native/components/ion-buttons.d.ts.map +1 -0
- package/dist/native/components/ion-buttons.js +30 -0
- package/dist/native/components/ion-buttons.js.map +1 -0
- package/dist/native/components/ion-fab.d.ts +10 -0
- package/dist/native/components/ion-fab.d.ts.map +1 -0
- package/dist/native/components/ion-fab.js +132 -0
- package/dist/native/components/ion-fab.js.map +1 -0
- package/dist/native/components/ion-menu-button.d.ts +13 -0
- package/dist/native/components/ion-menu-button.d.ts.map +1 -0
- package/dist/native/components/ion-menu-button.js +44 -0
- package/dist/native/components/ion-menu-button.js.map +1 -0
- package/dist/native/components/ion-segment.d.ts +11 -0
- package/dist/native/components/ion-segment.d.ts.map +1 -0
- package/dist/native/components/ion-segment.js +30 -0
- package/dist/native/components/ion-segment.js.map +1 -0
- package/dist/native/components/ion-tab-bar.d.ts +11 -0
- package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
- package/dist/native/components/ion-tab-bar.js +50 -0
- package/dist/native/components/ion-tab-bar.js.map +1 -0
- package/dist/native/components/searchable-tabs.d.ts +20 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -0
- package/dist/native/components/searchable-tabs.js +413 -0
- package/dist/native/components/searchable-tabs.js.map +1 -0
- package/dist/native/definitions.d.ts +255 -0
- package/dist/native/definitions.d.ts.map +1 -0
- package/dist/native/definitions.js +0 -0
- package/dist/native/extensions.d.ts +31 -0
- package/dist/native/extensions.d.ts.map +1 -0
- package/dist/native/extensions.js +122 -0
- package/dist/native/extensions.js.map +1 -0
- package/dist/native/foldable/fold-state.d.ts +5 -0
- package/dist/native/foldable/fold-state.d.ts.map +1 -0
- package/dist/native/foldable/fold-state.js +15 -0
- package/dist/native/foldable/fold-state.js.map +1 -0
- package/dist/native/foldable/types.d.ts +12 -0
- package/dist/native/foldable/types.d.ts.map +1 -0
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/html.d.ts +9 -0
- package/dist/native/html.d.ts.map +1 -0
- package/dist/native/html.js +188 -0
- package/dist/native/html.js.map +1 -0
- package/dist/native/index.d.ts +10 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +28 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/legacy-layout.d.ts +5 -0
- package/dist/native/legacy-layout.d.ts.map +1 -0
- package/dist/native/legacy-layout.js +27 -0
- package/dist/native/legacy-layout.js.map +1 -0
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +285 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +9 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +53 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +12 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +110 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +11 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +167 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts +8 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +238 -0
- package/dist/native/prehide.js.map +1 -0
- package/dist/native/runtime.d.ts +7 -0
- package/dist/native/runtime.d.ts.map +1 -0
- package/dist/native/runtime.js +788 -0
- package/dist/native/runtime.js.map +1 -0
- package/dist/native/shared/bridge.d.ts +6 -0
- package/dist/native/shared/bridge.d.ts.map +1 -0
- package/dist/native/shared/bridge.js +18 -0
- package/dist/native/shared/bridge.js.map +1 -0
- package/dist/native/shared/candidate.d.ts +25 -0
- package/dist/native/shared/candidate.d.ts.map +1 -0
- package/dist/native/shared/candidate.js +106 -0
- package/dist/native/shared/candidate.js.map +1 -0
- package/dist/native/shared/clipping.d.ts +5 -0
- package/dist/native/shared/clipping.d.ts.map +1 -0
- package/dist/native/shared/clipping.js +26 -0
- package/dist/native/shared/clipping.js.map +1 -0
- package/dist/native/shared/crossfade.d.ts +12 -0
- package/dist/native/shared/crossfade.d.ts.map +1 -0
- package/dist/native/shared/crossfade.js +67 -0
- package/dist/native/shared/crossfade.js.map +1 -0
- package/dist/native/shared/diagnostics.d.ts +4 -0
- package/dist/native/shared/diagnostics.d.ts.map +1 -0
- package/dist/native/shared/diagnostics.js +18 -0
- package/dist/native/shared/diagnostics.js.map +1 -0
- package/dist/native/shared/dom.d.ts +46 -0
- package/dist/native/shared/dom.d.ts.map +1 -0
- package/dist/native/shared/dom.js +180 -0
- package/dist/native/shared/dom.js.map +1 -0
- package/dist/native/shared/events.d.ts +15 -0
- package/dist/native/shared/events.d.ts.map +1 -0
- package/dist/native/shared/events.js +17 -0
- package/dist/native/shared/events.js.map +1 -0
- package/dist/native/shared/icons.d.ts +8 -0
- package/dist/native/shared/icons.d.ts.map +1 -0
- package/dist/native/shared/icons.js +61 -0
- package/dist/native/shared/icons.js.map +1 -0
- package/dist/native/shared/modal.d.ts +10 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +160 -0
- package/dist/native/shared/modal.js.map +1 -0
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/shared/toolbar-options.d.ts +9 -0
- package/dist/native/shared/toolbar-options.d.ts.map +1 -0
- package/dist/native/shared/toolbar-options.js +36 -0
- package/dist/native/shared/toolbar-options.js.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.js +85 -0
- package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
- package/dist/native/startup.d.ts +8 -0
- package/dist/native/startup.d.ts.map +1 -0
- package/dist/native/startup.js +86 -0
- package/dist/native/startup.js.map +1 -0
- package/dist/native/vertical-bars-web-search.d.ts +10 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +90 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts +5 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +421 -0
- package/dist/native/vertical-bars-web.js.map +1 -0
- package/dist/native-integration/index.d.ts +25 -0
- package/dist/native-integration/index.d.ts.map +1 -0
- package/dist/native-integration/index.js +33 -0
- package/dist/native-integration/index.js.map +1 -0
- package/dist/vertical-bars.d.ts +6 -0
- package/dist/vertical-bars.js +5 -0
- package/docs/configuration.md +187 -0
- package/docs/framework-independent.md +82 -0
- package/docs/migration.md +206 -0
- package/docs/native-mini-player.md +47 -0
- package/docs/native-ui-extensions.md +365 -0
- package/docs/native-ui.md +409 -0
- package/docs/setup.md +205 -0
- package/docs/vertical-bars.md +379 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
- package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
- package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
- package/package.json +89 -4
- package/src/core.ts +15 -0
- package/src/native/activation.ts +394 -0
- package/src/native/components/index.ts +45 -0
- package/src/native/components/ion-back-button.ts +22 -0
- package/src/native/components/ion-button.ts +25 -0
- package/src/native/components/ion-buttons.ts +37 -0
- package/src/native/components/ion-fab.ts +151 -0
- package/src/native/components/ion-menu-button.ts +37 -0
- package/src/native/components/ion-segment.ts +27 -0
- package/src/native/components/ion-tab-bar.ts +50 -0
- package/src/native/components/searchable-tabs.ts +499 -0
- package/src/native/definitions.ts +324 -0
- package/src/native/extensions.ts +176 -0
- package/src/native/foldable/fold-state.ts +10 -0
- package/src/native/foldable/types.ts +4 -0
- package/src/native/html.ts +193 -0
- package/src/native/index.ts +41 -0
- package/src/native/legacy-layout.ts +21 -0
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +349 -0
- package/src/native/overlays/dialog-animation.ts +53 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +120 -0
- package/src/native/overlays/styles.ts +181 -0
- package/src/native/prehide.ts +285 -0
- package/src/native/runtime.ts +988 -0
- package/src/native/shared/bridge.ts +18 -0
- package/src/native/shared/candidate.ts +162 -0
- package/src/native/shared/clipping.ts +24 -0
- package/src/native/shared/crossfade.ts +67 -0
- package/src/native/shared/diagnostics.ts +13 -0
- package/src/native/shared/dom.ts +304 -0
- package/src/native/shared/events.ts +14 -0
- package/src/native/shared/icons.ts +57 -0
- package/src/native/shared/modal.ts +157 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/shared/toolbar-options.ts +53 -0
- package/src/native/shared/vertical-bars-geometry.ts +80 -0
- package/src/native/startup.ts +84 -0
- package/src/native/vertical-bars-web-search.ts +86 -0
- package/src/native/vertical-bars-web.ts +498 -0
- package/src/native-integration/index.ts +49 -0
- package/src/vertical-bars.ts +11 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
struct NativeUISearch: Decodable, Equatable {
|
|
4
|
+
let id: String
|
|
5
|
+
let field: NativeUIItemContent
|
|
6
|
+
let trigger: NativeUIItem
|
|
7
|
+
let closeId: String
|
|
8
|
+
let active: Bool
|
|
9
|
+
let available: Bool
|
|
10
|
+
let focused: Bool
|
|
11
|
+
let value: String
|
|
12
|
+
let placeholder: String
|
|
13
|
+
let disabled: Bool
|
|
14
|
+
let editSequence: Int
|
|
15
|
+
let valueVersion: Int
|
|
16
|
+
|
|
17
|
+
var isValid: Bool {
|
|
18
|
+
!id.isEmpty && !closeId.isEmpty && field.isValid && trigger.isValid && editSequence >= 0 && valueVersion >= 0
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
enum NativeUISearchPhase: String { case input, focus, blur, clear, commit }
|
|
23
|
+
|
|
24
|
+
// A controller's empty content must not intercept the existing WebView.
|
|
25
|
+
final class NativeUISearchHost: UIView {
|
|
26
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
27
|
+
guard let hit = super.hitTest(point, with: event) else { return nil }
|
|
28
|
+
var ancestor: UIView? = hit
|
|
29
|
+
while let current = ancestor, current !== self {
|
|
30
|
+
if current is UIControl || current is UISearchBar || current is UITabBar { return hit }
|
|
31
|
+
ancestor = current.superview
|
|
32
|
+
}
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Preserve UISearchBar's delegate while returning its clear action to Ionic.
|
|
38
|
+
final class NativeUISearchInputDelegate: NSObject, UITextFieldDelegate {
|
|
39
|
+
weak var original: UITextFieldDelegate?
|
|
40
|
+
var clear: (() -> Void)?
|
|
41
|
+
override func responds(to selector: Selector!) -> Bool {
|
|
42
|
+
super.responds(to: selector) || original?.responds(to: selector) == true
|
|
43
|
+
}
|
|
44
|
+
override func forwardingTarget(for selector: Selector!) -> Any? { original }
|
|
45
|
+
func textFieldShouldClear(_ textField: UITextField) -> Bool { clear?(); return false }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Lets the plugin registry retain iOS 26-gated search controllers without
|
|
49
|
+
// availability-gated stored properties, mirroring NativeUIVerticalBarsControlling.
|
|
50
|
+
protocol NativeUISearchControlling: AnyObject {
|
|
51
|
+
var surface: NativeUISearchHost { get }
|
|
52
|
+
var ownsKeyboard: Bool { get }
|
|
53
|
+
var ownsKeyboardChrome: Bool { get }
|
|
54
|
+
var transitionCoordinator: UIViewControllerTransitionCoordinator? { get }
|
|
55
|
+
var activate: ((String) -> Void)? { get set }
|
|
56
|
+
var changed: ((String, NativeUISearchPhase, String, Bool, Int) -> Int)? { get set }
|
|
57
|
+
func attach(to parent: UIViewController, in container: UIView)
|
|
58
|
+
func detach()
|
|
59
|
+
func apply(_ snapshot: NativeUIControl, webFrame: CGRect, barFrame: CGRect, triggerFrame: CGRect, rendering: NativeUIControlRendering) -> Bool
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@available(iOS 26.0, *)
|
|
63
|
+
final class NativeUISearchController: UITabBarController, UITabBarControllerDelegate, UISearchBarDelegate, NativeUISearchControlling {
|
|
64
|
+
// Wire stays active+focused; local session drives chrome (idle / presented / focused).
|
|
65
|
+
private enum Session: Equatable { case idle, presented, focused }
|
|
66
|
+
|
|
67
|
+
let surface = NativeUISearchHost()
|
|
68
|
+
private var restingBarFrame: CGRect?
|
|
69
|
+
private var restingBarAnchor: NativeUITabBar.Anchor?
|
|
70
|
+
private var fittingTabBar = false
|
|
71
|
+
private let search = UISearchController(searchResultsController: nil)
|
|
72
|
+
private let inputDelegate = NativeUISearchInputDelegate()
|
|
73
|
+
private var searchTab: UISearchTab!
|
|
74
|
+
private var ordinary: [String: UITab] = [:]
|
|
75
|
+
private var configuration: NativeUISearch?
|
|
76
|
+
private var selectedID = ""
|
|
77
|
+
private var closing = false
|
|
78
|
+
private var editingSequence = 0
|
|
79
|
+
private var valueVersion = -1
|
|
80
|
+
private var lockedWebFrame: CGRect? // frozen while search is active (width changes re-lock)
|
|
81
|
+
private var session: Session = .idle
|
|
82
|
+
private var focusWork: DispatchWorkItem?
|
|
83
|
+
private var pendingSelection: NativeUITabBar.PendingSelection? // optimistic ordinary tab
|
|
84
|
+
private var pendingExpiryWork: DispatchWorkItem?
|
|
85
|
+
var ownsKeyboard: Bool { search.searchBar.searchTextField.isFirstResponder }
|
|
86
|
+
var ownsKeyboardChrome: Bool { session == .focused || ownsKeyboard }
|
|
87
|
+
var activate: ((String) -> Void)?
|
|
88
|
+
var changed: ((String, NativeUISearchPhase, String, Bool, Int) -> Int)?
|
|
89
|
+
|
|
90
|
+
override func viewDidLoad() {
|
|
91
|
+
super.viewDidLoad()
|
|
92
|
+
view.backgroundColor = .clear
|
|
93
|
+
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
94
|
+
surface.backgroundColor = .clear
|
|
95
|
+
surface.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
96
|
+
delegate = self
|
|
97
|
+
mode = .tabBar
|
|
98
|
+
NativeUITabBar.configureLayout(tabBar)
|
|
99
|
+
search.obscuresBackgroundDuringPresentation = false
|
|
100
|
+
search.hidesNavigationBarDuringPresentation = false
|
|
101
|
+
search.searchBar.delegate = self
|
|
102
|
+
search.searchBar.autocapitalizationType = .none
|
|
103
|
+
search.searchBar.autocorrectionType = .no
|
|
104
|
+
search.searchBar.spellCheckingType = .no
|
|
105
|
+
search.searchBar.searchTextField.clearButtonMode = .always
|
|
106
|
+
searchTab = UISearchTab { [weak self] _ in
|
|
107
|
+
let child = UIViewController()
|
|
108
|
+
child.view.backgroundColor = .clear
|
|
109
|
+
child.definesPresentationContext = true
|
|
110
|
+
child.navigationItem.searchController = self?.search
|
|
111
|
+
child.navigationItem.hidesSearchBarWhenScrolling = false
|
|
112
|
+
child.navigationItem.preferredSearchBarPlacement = .integrated
|
|
113
|
+
let navigation = UINavigationController(rootViewController: child)
|
|
114
|
+
navigation.view.backgroundColor = .clear
|
|
115
|
+
return navigation
|
|
116
|
+
}
|
|
117
|
+
searchTab.automaticallyActivatesSearch = false
|
|
118
|
+
inputDelegate.clear = { [weak self] in self?.emit(.clear) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
deinit {
|
|
122
|
+
focusWork?.cancel()
|
|
123
|
+
pendingExpiryWork?.cancel()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func wantedSession(active: Bool, focused: Bool) -> Session {
|
|
127
|
+
if !active { return .idle }
|
|
128
|
+
return focused ? .focused : .presented
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func endEditing() {
|
|
132
|
+
focusWork?.cancel()
|
|
133
|
+
focusWork = nil
|
|
134
|
+
if search.searchBar.searchTextField.isFirstResponder {
|
|
135
|
+
search.searchBar.searchTextField.resignFirstResponder()
|
|
136
|
+
}
|
|
137
|
+
if search.isActive { search.isActive = false }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func applySession(_ wanted: Session, selectingSearchTab: Bool) {
|
|
141
|
+
if wanted == session && !selectingSearchTab { return }
|
|
142
|
+
if wanted == .idle {
|
|
143
|
+
endEditing()
|
|
144
|
+
session = .idle
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if selectingSearchTab || selectedTab !== searchTab { selectedTab = searchTab }
|
|
148
|
+
if wanted == .presented {
|
|
149
|
+
if session == .focused { endEditing() }
|
|
150
|
+
session = .presented
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
session = .focused
|
|
154
|
+
if !search.isActive { search.isActive = true }
|
|
155
|
+
guard !search.searchBar.searchTextField.isFirstResponder, focusWork == nil else { return }
|
|
156
|
+
let work = DispatchWorkItem { [weak self] in
|
|
157
|
+
guard let self else { return }
|
|
158
|
+
self.focusWork = nil
|
|
159
|
+
guard self.session == .focused, !self.search.searchBar.searchTextField.isFirstResponder else { return }
|
|
160
|
+
if !self.search.isActive { self.search.isActive = true }
|
|
161
|
+
_ = self.search.searchBar.searchTextField.becomeFirstResponder()
|
|
162
|
+
}
|
|
163
|
+
focusWork = work
|
|
164
|
+
DispatchQueue.main.async(execute: work)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func armPendingSelection(_ id: String) {
|
|
168
|
+
pendingSelection = .start(id)
|
|
169
|
+
schedulePendingExpiry()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func clearPendingSelection() {
|
|
173
|
+
pendingExpiryWork?.cancel()
|
|
174
|
+
pendingExpiryWork = nil
|
|
175
|
+
pendingSelection = nil
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func schedulePendingExpiry() {
|
|
179
|
+
pendingExpiryWork?.cancel()
|
|
180
|
+
pendingExpiryWork = nil
|
|
181
|
+
guard let pending = pendingSelection else { return }
|
|
182
|
+
let delay = max(0, pending.until - CFAbsoluteTimeGetCurrent()) + 0.02
|
|
183
|
+
let work = DispatchWorkItem { [weak self] in
|
|
184
|
+
guard let self else { return }
|
|
185
|
+
self.pendingExpiryWork = nil
|
|
186
|
+
guard let pending = self.pendingSelection, CFAbsoluteTimeGetCurrent() >= pending.until else { return }
|
|
187
|
+
self.pendingSelection = nil
|
|
188
|
+
// Revert to the last DOM selection if Ionic did not accept the action.
|
|
189
|
+
if let selected = self.ordinary[self.selectedID], self.selectedTab !== selected {
|
|
190
|
+
self.selectedTab = selected
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
pendingExpiryWork = work
|
|
194
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func resolveOrdinarySelection(_ items: [NativeUIItem], fallback: UITab?) {
|
|
198
|
+
let domSelected = ordinary[selectedID] ?? fallback
|
|
199
|
+
guard let pending = pendingSelection else {
|
|
200
|
+
pendingExpiryWork?.cancel()
|
|
201
|
+
pendingExpiryWork = nil
|
|
202
|
+
if selectedTab !== domSelected { selectedTab = domSelected }
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
let pendingTab = ordinary[pending.id]
|
|
206
|
+
let expired = CFAbsoluteTimeGetCurrent() >= pending.until
|
|
207
|
+
let unavailable = pendingTab == nil || (items.first { $0.id == pending.id }?.content.disabled ?? true)
|
|
208
|
+
if unavailable || expired {
|
|
209
|
+
clearPendingSelection()
|
|
210
|
+
selectedTab = domSelected
|
|
211
|
+
} else if selectedID == pending.id {
|
|
212
|
+
clearPendingSelection()
|
|
213
|
+
if selectedTab !== domSelected { selectedTab = domSelected }
|
|
214
|
+
} else if selectedTab !== pendingTab {
|
|
215
|
+
selectedTab = pendingTab
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func attach(to parent: UIViewController, in container: UIView) {
|
|
220
|
+
parent.addChild(self)
|
|
221
|
+
container.addSubview(surface)
|
|
222
|
+
surface.addSubview(view)
|
|
223
|
+
didMove(toParent: parent)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func detach() {
|
|
227
|
+
closing = true
|
|
228
|
+
lockedWebFrame = nil
|
|
229
|
+
clearPendingSelection()
|
|
230
|
+
applySession(.idle, selectingSearchTab: false)
|
|
231
|
+
willMove(toParent: nil)
|
|
232
|
+
restingBarFrame = nil
|
|
233
|
+
surface.removeFromSuperview()
|
|
234
|
+
view.removeFromSuperview()
|
|
235
|
+
removeFromParent()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func apply(_ snapshot: NativeUIControl, webFrame: CGRect, barFrame: CGRect, triggerFrame: CGRect,
|
|
239
|
+
rendering: NativeUIControlRendering) -> Bool {
|
|
240
|
+
loadViewIfNeeded()
|
|
241
|
+
guard let configuration = snapshot.search else { return false }
|
|
242
|
+
if self.configuration?.id != configuration.id {
|
|
243
|
+
editingSequence = 0
|
|
244
|
+
valueVersion = -1
|
|
245
|
+
}
|
|
246
|
+
let wasActive = self.configuration.map { $0.available && $0.active } ?? false
|
|
247
|
+
self.configuration = configuration
|
|
248
|
+
let available = configuration.available
|
|
249
|
+
let active = available && configuration.active
|
|
250
|
+
let wanted = wantedSession(active: active, focused: configuration.focused)
|
|
251
|
+
let items = snapshot.items
|
|
252
|
+
let ids = items.map(\.id)
|
|
253
|
+
for id in Array(ordinary.keys) where !ids.contains(id) { ordinary.removeValue(forKey: id) }
|
|
254
|
+
for item in items {
|
|
255
|
+
let id = item.id
|
|
256
|
+
let tab = ordinary[id] ?? UITab(title: "", image: nil, identifier: id) { _ in
|
|
257
|
+
let child = UIViewController()
|
|
258
|
+
child.view.backgroundColor = .clear
|
|
259
|
+
return child
|
|
260
|
+
}
|
|
261
|
+
tab.accessibilityIdentifier = id
|
|
262
|
+
tab.title = item.content.label
|
|
263
|
+
tab.image = rendering.image(item.content)
|
|
264
|
+
tab.badgeValue = item.content.badge?.value
|
|
265
|
+
tab.isEnabled = !item.content.disabled
|
|
266
|
+
ordinary[id] = tab
|
|
267
|
+
if item.content.selected { selectedID = id }
|
|
268
|
+
}
|
|
269
|
+
let trigger = configuration.trigger
|
|
270
|
+
searchTab.accessibilityIdentifier = trigger.id
|
|
271
|
+
searchTab.image = rendering.image(trigger.content)
|
|
272
|
+
searchTab.isEnabled = !configuration.disabled
|
|
273
|
+
let requested = ids.compactMap { ordinary[$0] } + (available ? [searchTab!] : [])
|
|
274
|
+
if !active, tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
|
|
275
|
+
tabs = requested
|
|
276
|
+
}
|
|
277
|
+
for item in items {
|
|
278
|
+
if let nativeItem = ordinary[item.id]?.viewController?.tabBarItem {
|
|
279
|
+
nativeItem.accessibilityLabel = item.content.accessibilityLabel
|
|
280
|
+
NativeUITabBar.applyTypography(item.content, to: nativeItem)
|
|
281
|
+
NativeUITabBar.applyBadge(item.content.badge, to: nativeItem, rendering: rendering)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
searchTab.viewController?.tabBarItem.accessibilityLabel = trigger.content.accessibilityLabel
|
|
285
|
+
let prominent = NSSelectorFromString("setProminentTabIdentifier:")
|
|
286
|
+
if responds(to: prominent) { setValue(available ? searchTab.identifier : nil, forKey: "prominentTabIdentifier") }
|
|
287
|
+
let field = configuration.field
|
|
288
|
+
search.searchBar.searchTextField.accessibilityIdentifier = configuration.id
|
|
289
|
+
search.searchBar.searchTextField.accessibilityLabel = field.accessibilityLabel
|
|
290
|
+
search.searchBar.setImage(rendering.image(field), for: .search, state: .normal)
|
|
291
|
+
search.searchBar.placeholder = configuration.placeholder
|
|
292
|
+
search.searchBar.searchTextField.isEnabled = !configuration.disabled
|
|
293
|
+
let nextValueVersion = configuration.valueVersion
|
|
294
|
+
if nextValueVersion != valueVersion || configuration.editSequence >= editingSequence {
|
|
295
|
+
valueVersion = nextValueVersion
|
|
296
|
+
let value = configuration.value
|
|
297
|
+
if search.searchBar.text != value { search.searchBar.text = value }
|
|
298
|
+
}
|
|
299
|
+
if search.searchBar.searchTextField.delegate !== inputDelegate {
|
|
300
|
+
inputDelegate.original = search.searchBar.searchTextField.delegate
|
|
301
|
+
search.searchBar.searchTextField.delegate = inputDelegate
|
|
302
|
+
}
|
|
303
|
+
// Resting keeps closing=true so hidden UISearchBar noise cannot emit; clear it while active.
|
|
304
|
+
closing = !active
|
|
305
|
+
surface.isHidden = false
|
|
306
|
+
surface.overrideUserInterfaceStyle = snapshot.dark ? .dark : .light
|
|
307
|
+
if active {
|
|
308
|
+
// UISearchTab session owns the bar + keyboard; freeze Web layout for the session.
|
|
309
|
+
if tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
|
|
310
|
+
tabs = requested
|
|
311
|
+
}
|
|
312
|
+
restingBarFrame = nil
|
|
313
|
+
view.frame = surface.bounds
|
|
314
|
+
let nextLock = surface.bounds.isEmpty ? webFrame : surface.frame
|
|
315
|
+
if let locked = lockedWebFrame, abs(locked.width - webFrame.width) > 0.5 {
|
|
316
|
+
// Rotation / size-class change: adopt the new width while still ignoring keyboard shrink.
|
|
317
|
+
lockedWebFrame = webFrame
|
|
318
|
+
} else if lockedWebFrame == nil {
|
|
319
|
+
lockedWebFrame = nextLock
|
|
320
|
+
}
|
|
321
|
+
if let lockedWebFrame, surface.frame != lockedWebFrame { surface.frame = lockedWebFrame }
|
|
322
|
+
clearPendingSelection()
|
|
323
|
+
applySession(wanted, selectingSearchTab: !wasActive)
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
surface.frame = webFrame
|
|
328
|
+
view.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
329
|
+
tabBar.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
330
|
+
tabBar.accessibilityIdentifier = snapshot.id
|
|
331
|
+
if lockedWebFrame != nil {
|
|
332
|
+
lockedWebFrame = nil
|
|
333
|
+
}
|
|
334
|
+
applySession(.idle, selectingSearchTab: false)
|
|
335
|
+
|
|
336
|
+
view.frame = surface.bounds
|
|
337
|
+
resolveOrdinarySelection(items, fallback: requested.first)
|
|
338
|
+
restingBarFrame = surface.convert(barFrame, from: surface.superview)
|
|
339
|
+
restingBarAnchor = snapshot.tabBarAnchor
|
|
340
|
+
view.setNeedsLayout()
|
|
341
|
+
view.layoutIfNeeded()
|
|
342
|
+
return fitRestingTabBar()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
override func viewDidLayoutSubviews() {
|
|
346
|
+
super.viewDidLayoutSubviews()
|
|
347
|
+
if session == .idle { _ = fitRestingTabBar() }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Match the ordinary tab platter to the source ion-tab-bar by resizing
|
|
351
|
+
/// the controller's view, so UIKit can keep managing its tab bar through
|
|
352
|
+
/// search expansion and dismissal.
|
|
353
|
+
private func fitRestingTabBar() -> Bool {
|
|
354
|
+
guard let bounds = restingBarFrame, !fittingTabBar else { return true }
|
|
355
|
+
fittingTabBar = true
|
|
356
|
+
defer { fittingTabBar = false }
|
|
357
|
+
tabBar.layoutIfNeeded()
|
|
358
|
+
for _ in 0..<2 {
|
|
359
|
+
let regions = NativeUITabBar.contentViews(tabBar)
|
|
360
|
+
func controlCount(_ view: UIView) -> Int {
|
|
361
|
+
(view is UIControl ? 1 : 0) + view.subviews.reduce(0) { $0 + controlCount($1) }
|
|
362
|
+
}
|
|
363
|
+
// Ordinary tabs contain their item controls; Search contains one.
|
|
364
|
+
// For a single tab, UIKit places ordinary tabs at the leading edge.
|
|
365
|
+
let rtl = tabBar.effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
366
|
+
guard let region = regions.max(by: {
|
|
367
|
+
let lhs = controlCount($0), rhs = controlCount($1)
|
|
368
|
+
if lhs != rhs { return lhs < rhs }
|
|
369
|
+
return rtl ? $0.frame.midX < $1.frame.midX : $0.frame.midX > $1.frame.midX
|
|
370
|
+
}) else { return false }
|
|
371
|
+
let content = region.frame
|
|
372
|
+
let contents = regions.map { $0.frame }
|
|
373
|
+
let measured = tabBar.convert(content, to: surface)
|
|
374
|
+
let width = bounds.width - measured.width
|
|
375
|
+
let x = bounds.minX + (bounds.width - measured.width) * (restingBarAnchor?.x ?? 0) - measured.minX
|
|
376
|
+
let y = bounds.minY + (bounds.height - measured.height) * (restingBarAnchor?.y ?? 0) - measured.minY
|
|
377
|
+
// UIKit rounds the platter frame; a subpoint difference must not
|
|
378
|
+
// keep invalidating its layout during search transitions.
|
|
379
|
+
if abs(width) <= 0.5 && abs(x) <= 0.5 && abs(y) <= 0.5 { break }
|
|
380
|
+
var frame = view.frame
|
|
381
|
+
// The controller also contains the separate search button. Include
|
|
382
|
+
// that space in its width limit without enlarging capped tab groups.
|
|
383
|
+
let searchWidth = contents.filter { $0 != content }.reduce(CGFloat(0)) { $0 + $1.width }
|
|
384
|
+
frame.size.width = min(frame.width + width, surface.bounds.width + searchWidth)
|
|
385
|
+
frame.origin.x += x
|
|
386
|
+
frame.origin.y += y
|
|
387
|
+
if abs(frame.width - view.frame.width) <= 0.5 && abs(x) <= 0.5 && abs(y) <= 0.5 { break }
|
|
388
|
+
view.frame = frame
|
|
389
|
+
view.setNeedsLayout()
|
|
390
|
+
view.layoutIfNeeded()
|
|
391
|
+
}
|
|
392
|
+
// Fitting the ordinary platter can move UIKit's separate Search control
|
|
393
|
+
// outside the WebView, especially for centered or trailing source bars.
|
|
394
|
+
// Reject that layout so Search keeps its usable Web fallback.
|
|
395
|
+
let visibleBounds = surface.bounds.insetBy(dx: -0.5, dy: -0.5)
|
|
396
|
+
let regions = NativeUITabBar.contentViews(tabBar)
|
|
397
|
+
return !regions.isEmpty && regions.allSatisfy {
|
|
398
|
+
visibleBounds.contains(tabBar.convert($0.frame, to: surface))
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
|
403
|
+
guard let configuration else { return false }
|
|
404
|
+
if tab === searchTab {
|
|
405
|
+
restingBarFrame = nil
|
|
406
|
+
clearPendingSelection()
|
|
407
|
+
activate?(configuration.trigger.id)
|
|
408
|
+
return true
|
|
409
|
+
}
|
|
410
|
+
if configuration.active {
|
|
411
|
+
if tab.identifier == selectedID { activate?(configuration.closeId) }
|
|
412
|
+
else { activate?(tab.identifier) }
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
armPendingSelection(tab.identifier)
|
|
416
|
+
if selectedTab !== tab { selectedTab = tab }
|
|
417
|
+
activate?(tab.identifier)
|
|
418
|
+
return true
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private func emit(_ phase: NativeUISearchPhase) {
|
|
422
|
+
guard !closing, let configuration else { return }
|
|
423
|
+
editingSequence = changed?(configuration.id, phase, search.searchBar.text ?? "", search.searchBar.searchTextField.markedTextRange != nil, valueVersion) ?? editingSequence
|
|
424
|
+
}
|
|
425
|
+
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { emit(.input) }
|
|
426
|
+
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
|
427
|
+
if session != .idle { session = .focused }
|
|
428
|
+
emit(.focus)
|
|
429
|
+
}
|
|
430
|
+
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
|
431
|
+
if session == .focused { session = .presented }
|
|
432
|
+
emit(.blur)
|
|
433
|
+
}
|
|
434
|
+
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { emit(.commit) }
|
|
435
|
+
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
|
436
|
+
closing = true
|
|
437
|
+
if let configuration { activate?(configuration.closeId) }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
private final class NativeUISegmentElement: UIAccessibilityElement {
|
|
4
|
+
var activate: (() -> Bool)?
|
|
5
|
+
override func accessibilityActivate() -> Bool { activate?() ?? false }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
final class NativeUISegment: UISegmentedControl {
|
|
9
|
+
static let kind = NativeUIComponent.segment
|
|
10
|
+
var labels: [String] = []
|
|
11
|
+
private var items: [NativeUIItem] = []
|
|
12
|
+
override func layoutSubviews() {
|
|
13
|
+
super.layoutSubviews()
|
|
14
|
+
let total = (0..<numberOfSegments).reduce(CGFloat.zero) { $0 + widthForSegment(at: $1) }
|
|
15
|
+
var x: CGFloat = 0
|
|
16
|
+
accessibilityElements = labels.prefix(numberOfSegments).enumerated().map { index, label in
|
|
17
|
+
let width = total > 0 ? bounds.width * widthForSegment(at: index) / total : bounds.width / CGFloat(numberOfSegments)
|
|
18
|
+
let element = NativeUISegmentElement(accessibilityContainer: self)
|
|
19
|
+
element.accessibilityLabel = label
|
|
20
|
+
element.accessibilityTraits = [.button]
|
|
21
|
+
if index == selectedSegmentIndex { element.accessibilityTraits.insert(.selected) }
|
|
22
|
+
if !isEnabledForSegment(at: index) { element.accessibilityTraits.insert(.notEnabled) }
|
|
23
|
+
let origin = effectiveUserInterfaceLayoutDirection == .rightToLeft ? bounds.width - x - width : x
|
|
24
|
+
element.accessibilityFrameInContainerSpace = CGRect(x: origin, y: 0, width: width, height: bounds.height)
|
|
25
|
+
element.activate = { [weak self] in
|
|
26
|
+
guard let self, self.isEnabledForSegment(at: index) else { return false }
|
|
27
|
+
self.selectedSegmentIndex = index
|
|
28
|
+
self.sendActions(for: .valueChanged)
|
|
29
|
+
self.setNeedsLayout()
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
x += width
|
|
33
|
+
return element
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@available(iOS 26.0, *)
|
|
38
|
+
static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UISegmentedControl? {
|
|
39
|
+
guard !node.items.isEmpty else { return nil }
|
|
40
|
+
let control = NativeUISegment(items: [])
|
|
41
|
+
control.isAccessibilityElement = false
|
|
42
|
+
control.apportionsSegmentWidthsByContent = false
|
|
43
|
+
control.update(node, scale: scale, rendering: rendering)
|
|
44
|
+
control.addAction(UIAction { [weak control] _ in
|
|
45
|
+
guard let control, control.items.indices.contains(control.selectedSegmentIndex) else { return }
|
|
46
|
+
activate(control.items[control.selectedSegmentIndex].id)
|
|
47
|
+
}, for: .valueChanged)
|
|
48
|
+
return control
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@available(iOS 26.0, *)
|
|
52
|
+
func update(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering) {
|
|
53
|
+
let previous = items
|
|
54
|
+
let rebuilt = previous.map(\.id) != node.items.map(\.id)
|
|
55
|
+
if rebuilt {
|
|
56
|
+
removeAllSegments()
|
|
57
|
+
for index in node.items.indices { insertSegment(withTitle: "", at: index, animated: false) }
|
|
58
|
+
}
|
|
59
|
+
if let first = node.items.first, previous.first?.content.fontSize != first.content.fontSize {
|
|
60
|
+
setTitleTextAttributes([.font: UIFont.systemFont(ofSize: first.content.fontSize, weight: .medium)], for: .normal)
|
|
61
|
+
}
|
|
62
|
+
let direction: UISemanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
63
|
+
if semanticContentAttribute != direction { semanticContentAttribute = direction }
|
|
64
|
+
// Ionic button frames exclude their margins. Distribute the full track
|
|
65
|
+
// width so UIKit's fixed segment widths do not shrink the projected track.
|
|
66
|
+
let totalItemWidth = node.items.reduce(0.0) { $0 + $1.frame.width }
|
|
67
|
+
let widthScale = totalItemWidth > 0 ? node.frame.width / totalItemWidth * scale : scale
|
|
68
|
+
for (index, item) in node.items.enumerated() {
|
|
69
|
+
let old = rebuilt ? nil : previous[index].content
|
|
70
|
+
let content = item.content
|
|
71
|
+
if old?.label != content.label || old?.icon != content.icon ||
|
|
72
|
+
old?.iconWidth != content.iconWidth || old?.iconHeight != content.iconHeight ||
|
|
73
|
+
old?.iconTemplate != content.iconTemplate || old?.color != content.color {
|
|
74
|
+
let image = rendering.image(content)
|
|
75
|
+
setImage(image, forSegmentAt: index)
|
|
76
|
+
setTitle(image == nil ? content.label : nil, forSegmentAt: index)
|
|
77
|
+
}
|
|
78
|
+
if isEnabledForSegment(at: index) == content.disabled { setEnabled(!content.disabled, forSegmentAt: index) }
|
|
79
|
+
let width = item.frame.width * widthScale
|
|
80
|
+
if widthForSegment(at: index) != width { setWidth(width, forSegmentAt: index) }
|
|
81
|
+
}
|
|
82
|
+
items = node.items
|
|
83
|
+
labels = items.map { $0.content.accessibilityLabel }
|
|
84
|
+
accessibilityIdentifier = node.id
|
|
85
|
+
let selected = items.firstIndex { $0.content.selected } ?? UISegmentedControl.noSegment
|
|
86
|
+
// A native tap has already selected this index. Reassigning it can interrupt
|
|
87
|
+
// UIKit's in-flight lens animation when the Web selection echoes back.
|
|
88
|
+
if selectedSegmentIndex != selected { selectedSegmentIndex = selected }
|
|
89
|
+
setNeedsLayout()
|
|
90
|
+
}
|
|
91
|
+
}
|