@thelacanians/vue-native-cli 0.4.2 → 0.4.4
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/cli.js +43 -23
- package/native/android/README.md +205 -0
- package/native/android/VueNativeCore/build.gradle.kts +100 -0
- package/native/android/VueNativeCore/consumer-rules.pro +12 -0
- package/native/android/VueNativeCore/proguard-rules.pro +33 -0
- package/native/android/VueNativeCore/src/main/AndroidManifest.xml +17 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/ErrorOverlayView.kt +94 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/HotReloadManager.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSPolyfills.kt +652 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSRuntime.kt +207 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +417 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/ComponentRegistry.kt +76 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActionSheetFactory.kt +78 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActivityIndicatorFactory.kt +46 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VAlertDialogFactory.kt +84 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VButtonFactory.kt +73 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VCheckboxFactory.kt +93 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VDropdownFactory.kt +125 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VImageFactory.kt +75 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VInputFactory.kt +210 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VKeyboardAvoidingFactory.kt +31 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +183 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VModalFactory.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPickerFactory.kt +57 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPressableFactory.kt +109 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VProgressBarFactory.kt +43 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRadioFactory.kt +103 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRefreshControlFactory.kt +73 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRootFactory.kt +39 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSafeAreaFactory.kt +48 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSectionListFactory.kt +144 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSegmentedControlFactory.kt +77 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +74 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VStatusBarFactory.kt +52 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSwitchFactory.kt +62 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VTextFactory.kt +53 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VVideoFactory.kt +191 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +48 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VWebViewFactory.kt +90 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/NativeComponentFactory.kt +40 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/VTextNodeView.kt +23 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/GestureHelper.kt +16 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/TouchableView.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AnimationModule.kt +292 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AppStateModule.kt +41 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AsyncStorageModule.kt +59 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AudioModule.kt +331 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BackgroundTaskModule.kt +166 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BiometryModule.kt +56 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BluetoothModule.kt +302 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CalendarModule.kt +198 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CameraModule.kt +64 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ClipboardModule.kt +36 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ContactsModule.kt +288 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DatabaseModule.kt +229 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DeviceInfoModule.kt +39 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/FileSystemModule.kt +193 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeolocationModule.kt +68 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HapticsModule.kt +61 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HttpModule.kt +111 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/IAPModule.kt +302 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/KeyboardModule.kt +26 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/LinkingModule.kt +43 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModule.kt +27 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +92 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NetworkModule.kt +75 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NotificationsModule.kt +181 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/OTAModule.kt +255 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PerformanceModule.kt +147 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PermissionsModule.kt +126 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SecureStorageModule.kt +51 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SensorsModule.kt +134 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ShareModule.kt +36 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SocialAuthModule.kt +160 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/WebSocketModule.kt +155 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Styling/StyleEngine.kt +802 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Tags.kt +43 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/VueNativeActivity.kt +169 -0
- package/native/android/VueNativeCore/src/main/res/values/ids.xml +8 -0
- package/native/android/app/build.gradle.kts +45 -0
- package/native/android/app/proguard-rules.pro +5 -0
- package/native/android/app/src/main/AndroidManifest.xml +25 -0
- package/native/android/app/src/main/assets/.gitkeep +0 -0
- package/native/android/app/src/main/kotlin/com/vuenative/example/counter/MainActivity.kt +14 -0
- package/native/android/app/src/main/res/layout/activity_main.xml +6 -0
- package/native/android/app/src/main/res/values/strings.xml +3 -0
- package/native/android/app/src/main/res/values/themes.xml +9 -0
- package/native/android/app/src/main/res/xml/network_security_config.xml +8 -0
- package/native/android/build.gradle.kts +6 -0
- package/native/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/native/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/native/android/gradle.properties +4 -0
- package/native/android/gradlew +87 -0
- package/native/android/gradlew.bat +48 -0
- package/native/android/settings.gradle.kts +20 -0
- package/native/ios/VueNativeCore/Package.resolved +23 -0
- package/native/ios/VueNativeCore/Package.swift +32 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/CertificatePinning.swift +132 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/ErrorOverlayView.swift +92 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/HotReloadManager.swift +147 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSPolyfills.swift +711 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSRuntime.swift +421 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +891 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/VueNativeViewController.swift +88 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/ComponentRegistry.swift +193 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActionSheetFactory.swift +91 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActivityIndicatorFactory.swift +74 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VAlertDialogFactory.swift +150 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VButtonFactory.swift +93 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VCheckboxFactory.swift +114 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VDropdownFactory.swift +112 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VImageFactory.swift +172 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VInputFactory.swift +357 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VKeyboardAvoidingFactory.swift +99 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +250 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VModalFactory.swift +112 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VPickerFactory.swift +96 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VPressableFactory.swift +168 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VProgressBarFactory.swift +39 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VRadioFactory.swift +167 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VRefreshControlFactory.swift +153 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSafeAreaFactory.swift +56 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +240 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSectionListFactory.swift +248 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSegmentedControlFactory.swift +73 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +63 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VStatusBarFactory.swift +50 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSwitchFactory.swift +108 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +290 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VVideoFactory.swift +246 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +157 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VWebViewFactory.swift +172 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/NativeComponentFactory.swift +53 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +107 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/TouchableView.swift +136 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/UIColor+Hex.swift +80 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AnimationModule.swift +291 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AppStateModule.swift +65 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AsyncStorageModule.swift +68 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AudioModule.swift +366 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BackgroundTaskModule.swift +135 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BiometryModule.swift +61 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BluetoothModule.swift +387 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CalendarModule.swift +161 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CameraModule.swift +318 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ClipboardModule.swift +33 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ContactsModule.swift +173 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/DatabaseModule.swift +259 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/DeviceInfoModule.swift +34 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/FileSystemModule.swift +233 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeolocationModule.swift +147 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/HapticsModule.swift +50 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/IAPModule.swift +194 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/KeyboardModule.swift +31 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/LinkingModule.swift +42 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModule.swift +28 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +78 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NetworkModule.swift +62 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NotificationsModule.swift +215 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/OTAModule.swift +281 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PerformanceModule.swift +138 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PermissionsModule.swift +190 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SecureStorageModule.swift +118 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SensorsModule.swift +103 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ShareModule.swift +49 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SocialAuthModule.swift +240 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/WebSocketModule.swift +213 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Resources/vue-native-placeholder.js +8 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Styling/StyleEngine.swift +885 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSRuntimeTests.swift +362 -0
- package/package.json +3 -2
package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VInputFactory.swift
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import ObjectiveC
|
|
4
|
+
import FlexLayout
|
|
5
|
+
|
|
6
|
+
/// Factory for VInput — the text input component.
|
|
7
|
+
/// Maps to a UITextField with FlexLayout enabled.
|
|
8
|
+
/// Supports v-model via text prop and changetext event.
|
|
9
|
+
final class VInputFactory: NativeComponentFactory {
|
|
10
|
+
|
|
11
|
+
// MARK: - Associated object keys
|
|
12
|
+
|
|
13
|
+
private static var delegateKey: UInt8 = 0
|
|
14
|
+
private static var changeTextHandlerKey: UInt8 = 0
|
|
15
|
+
private static var focusHandlerKey: UInt8 = 0
|
|
16
|
+
private static var blurHandlerKey: UInt8 = 0
|
|
17
|
+
private static var submitHandlerKey: UInt8 = 0
|
|
18
|
+
|
|
19
|
+
// MARK: - Keyboard type mapping
|
|
20
|
+
|
|
21
|
+
static let keyboardTypeMap: [String: UIKeyboardType] = [
|
|
22
|
+
"default": .default,
|
|
23
|
+
"numeric": .numberPad,
|
|
24
|
+
"number-pad": .numberPad,
|
|
25
|
+
"decimal-pad": .decimalPad,
|
|
26
|
+
"email": .emailAddress,
|
|
27
|
+
"email-address": .emailAddress,
|
|
28
|
+
"phone": .phonePad,
|
|
29
|
+
"phone-pad": .phonePad,
|
|
30
|
+
"url": .URL,
|
|
31
|
+
"web-search": .webSearch,
|
|
32
|
+
"ascii": .asciiCapable,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
// MARK: - Return key mapping
|
|
36
|
+
|
|
37
|
+
static let returnKeyMap: [String: UIReturnKeyType] = [
|
|
38
|
+
"default": .default,
|
|
39
|
+
"done": .done,
|
|
40
|
+
"go": .go,
|
|
41
|
+
"next": .next,
|
|
42
|
+
"search": .search,
|
|
43
|
+
"send": .send,
|
|
44
|
+
"join": .join,
|
|
45
|
+
"route": .route,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
// MARK: - Auto-capitalize mapping
|
|
49
|
+
|
|
50
|
+
static let autoCapitalizeMap: [String: UITextAutocapitalizationType] = [
|
|
51
|
+
"none": .none,
|
|
52
|
+
"words": .words,
|
|
53
|
+
"sentences": .sentences,
|
|
54
|
+
"characters": .allCharacters,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
// MARK: - NativeComponentFactory
|
|
58
|
+
|
|
59
|
+
func createView() -> UIView {
|
|
60
|
+
let textField = UITextField()
|
|
61
|
+
textField.borderStyle = .none
|
|
62
|
+
// Accessing .flex automatically enables Yoga layout.
|
|
63
|
+
// Set a sensible default height so the text field is not collapsed.
|
|
64
|
+
textField.flex.height(44)
|
|
65
|
+
return textField
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func updateProp(view: UIView, key: String, value: Any?) {
|
|
69
|
+
guard let textField = view as? UITextField else {
|
|
70
|
+
StyleEngine.apply(key: key, value: value, to: view)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
switch key {
|
|
75
|
+
case "text", "value":
|
|
76
|
+
if let text = value as? String {
|
|
77
|
+
// Only update if different to avoid cursor jump
|
|
78
|
+
if textField.text != text {
|
|
79
|
+
textField.text = text
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
textField.text = nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "placeholder":
|
|
86
|
+
if let placeholder = value as? String {
|
|
87
|
+
textField.placeholder = placeholder
|
|
88
|
+
} else {
|
|
89
|
+
textField.placeholder = nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "placeholderColor", "placeholderTextColor":
|
|
93
|
+
if let colorStr = value as? String, let placeholder = textField.placeholder {
|
|
94
|
+
textField.attributedPlaceholder = NSAttributedString(
|
|
95
|
+
string: placeholder,
|
|
96
|
+
attributes: [.foregroundColor: UIColor.fromHex(colorStr)]
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "secureTextEntry":
|
|
101
|
+
if let secure = value as? Bool {
|
|
102
|
+
textField.isSecureTextEntry = secure
|
|
103
|
+
} else if let secure = value as? Int {
|
|
104
|
+
textField.isSecureTextEntry = secure != 0
|
|
105
|
+
} else {
|
|
106
|
+
textField.isSecureTextEntry = false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "keyboardType":
|
|
110
|
+
if let typeStr = value as? String {
|
|
111
|
+
textField.keyboardType = VInputFactory.keyboardTypeMap[typeStr] ?? .default
|
|
112
|
+
} else {
|
|
113
|
+
textField.keyboardType = .default
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "returnKeyType":
|
|
117
|
+
if let typeStr = value as? String {
|
|
118
|
+
textField.returnKeyType = VInputFactory.returnKeyMap[typeStr] ?? .default
|
|
119
|
+
} else {
|
|
120
|
+
textField.returnKeyType = .default
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "autoCapitalize", "autocapitalize":
|
|
124
|
+
if let capStr = value as? String {
|
|
125
|
+
textField.autocapitalizationType = VInputFactory.autoCapitalizeMap[capStr] ?? .sentences
|
|
126
|
+
} else {
|
|
127
|
+
textField.autocapitalizationType = .sentences
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "autoCorrect", "autocorrect":
|
|
131
|
+
if let correct = value as? Bool {
|
|
132
|
+
textField.autocorrectionType = correct ? .yes : .no
|
|
133
|
+
} else {
|
|
134
|
+
textField.autocorrectionType = .default
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "editable":
|
|
138
|
+
if let editable = value as? Bool {
|
|
139
|
+
textField.isEnabled = editable
|
|
140
|
+
} else {
|
|
141
|
+
textField.isEnabled = true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "maxLength":
|
|
145
|
+
// Store max length for use in delegate
|
|
146
|
+
if let maxLen = value as? Int {
|
|
147
|
+
storeMaxLength(maxLen, on: textField)
|
|
148
|
+
} else if let maxLen = value as? Double {
|
|
149
|
+
storeMaxLength(Int(maxLen), on: textField)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "color":
|
|
153
|
+
if let colorStr = value as? String {
|
|
154
|
+
textField.textColor = UIColor.fromHex(colorStr)
|
|
155
|
+
} else {
|
|
156
|
+
textField.textColor = .label
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "fontSize":
|
|
160
|
+
if let size = value as? Double {
|
|
161
|
+
textField.font = UIFont.systemFont(ofSize: CGFloat(size))
|
|
162
|
+
} else if let size = value as? Int {
|
|
163
|
+
textField.font = UIFont.systemFont(ofSize: CGFloat(size))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "textAlign":
|
|
167
|
+
if let alignStr = value as? String {
|
|
168
|
+
switch alignStr {
|
|
169
|
+
case "left": textField.textAlignment = .left
|
|
170
|
+
case "center": textField.textAlignment = .center
|
|
171
|
+
case "right": textField.textAlignment = .right
|
|
172
|
+
default: textField.textAlignment = .natural
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
StyleEngine.apply(key: key, value: value, to: view)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func addEventListener(view: UIView, event: String, handler: @escaping (Any?) -> Void) {
|
|
182
|
+
guard let textField = view as? UITextField else { return }
|
|
183
|
+
|
|
184
|
+
// Ensure we have a delegate proxy set up
|
|
185
|
+
let delegate = ensureDelegate(for: textField)
|
|
186
|
+
|
|
187
|
+
switch event {
|
|
188
|
+
case "changetext":
|
|
189
|
+
delegate.onChangeText = handler
|
|
190
|
+
// Add editingChanged target
|
|
191
|
+
textField.addTarget(
|
|
192
|
+
delegate,
|
|
193
|
+
action: #selector(InputDelegateProxy.textFieldEditingChanged(_:)),
|
|
194
|
+
for: .editingChanged
|
|
195
|
+
)
|
|
196
|
+
objc_setAssociatedObject(
|
|
197
|
+
view,
|
|
198
|
+
&VInputFactory.changeTextHandlerKey,
|
|
199
|
+
handler as AnyObject,
|
|
200
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
case "focus":
|
|
204
|
+
delegate.onFocus = handler
|
|
205
|
+
textField.addTarget(
|
|
206
|
+
delegate,
|
|
207
|
+
action: #selector(InputDelegateProxy.textFieldDidBeginEditing(_:)),
|
|
208
|
+
for: .editingDidBegin
|
|
209
|
+
)
|
|
210
|
+
objc_setAssociatedObject(
|
|
211
|
+
view,
|
|
212
|
+
&VInputFactory.focusHandlerKey,
|
|
213
|
+
handler as AnyObject,
|
|
214
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
case "blur":
|
|
218
|
+
delegate.onBlur = handler
|
|
219
|
+
textField.addTarget(
|
|
220
|
+
delegate,
|
|
221
|
+
action: #selector(InputDelegateProxy.textFieldDidEndEditing(_:)),
|
|
222
|
+
for: .editingDidEnd
|
|
223
|
+
)
|
|
224
|
+
objc_setAssociatedObject(
|
|
225
|
+
view,
|
|
226
|
+
&VInputFactory.blurHandlerKey,
|
|
227
|
+
handler as AnyObject,
|
|
228
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
case "submit":
|
|
232
|
+
delegate.onSubmit = handler
|
|
233
|
+
textField.addTarget(
|
|
234
|
+
delegate,
|
|
235
|
+
action: #selector(InputDelegateProxy.textFieldDidReturn(_:)),
|
|
236
|
+
for: .editingDidEndOnExit
|
|
237
|
+
)
|
|
238
|
+
objc_setAssociatedObject(
|
|
239
|
+
view,
|
|
240
|
+
&VInputFactory.submitHandlerKey,
|
|
241
|
+
handler as AnyObject,
|
|
242
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
default:
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func removeEventListener(view: UIView, event: String) {
|
|
251
|
+
guard let textField = view as? UITextField else { return }
|
|
252
|
+
guard let delegate = objc_getAssociatedObject(textField, &VInputFactory.delegateKey) as? InputDelegateProxy else { return }
|
|
253
|
+
|
|
254
|
+
switch event {
|
|
255
|
+
case "changetext":
|
|
256
|
+
delegate.onChangeText = nil
|
|
257
|
+
textField.removeTarget(delegate, action: #selector(InputDelegateProxy.textFieldEditingChanged(_:)), for: .editingChanged)
|
|
258
|
+
case "focus":
|
|
259
|
+
delegate.onFocus = nil
|
|
260
|
+
textField.removeTarget(delegate, action: #selector(InputDelegateProxy.textFieldDidBeginEditing(_:)), for: .editingDidBegin)
|
|
261
|
+
case "blur":
|
|
262
|
+
delegate.onBlur = nil
|
|
263
|
+
textField.removeTarget(delegate, action: #selector(InputDelegateProxy.textFieldDidEndEditing(_:)), for: .editingDidEnd)
|
|
264
|
+
case "submit":
|
|
265
|
+
delegate.onSubmit = nil
|
|
266
|
+
textField.removeTarget(delegate, action: #selector(InputDelegateProxy.textFieldDidReturn(_:)), for: .editingDidEndOnExit)
|
|
267
|
+
default:
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// MARK: - Private helpers
|
|
273
|
+
|
|
274
|
+
private func ensureDelegate(for textField: UITextField) -> InputDelegateProxy {
|
|
275
|
+
if let existing = objc_getAssociatedObject(textField, &VInputFactory.delegateKey) as? InputDelegateProxy {
|
|
276
|
+
return existing
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let delegate = InputDelegateProxy()
|
|
280
|
+
delegate.maxLengthProvider = { [weak textField] in
|
|
281
|
+
guard let tf = textField else { return nil }
|
|
282
|
+
return self.storedMaxLength(on: tf)
|
|
283
|
+
}
|
|
284
|
+
textField.delegate = delegate
|
|
285
|
+
objc_setAssociatedObject(
|
|
286
|
+
textField,
|
|
287
|
+
&VInputFactory.delegateKey,
|
|
288
|
+
delegate,
|
|
289
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
290
|
+
)
|
|
291
|
+
return delegate
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// MARK: - Max length storage
|
|
295
|
+
|
|
296
|
+
private static var maxLengthKey: UInt8 = 0
|
|
297
|
+
|
|
298
|
+
private func storeMaxLength(_ length: Int, on view: UIView) {
|
|
299
|
+
objc_setAssociatedObject(view, &VInputFactory.maxLengthKey, length, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func storedMaxLength(on view: UIView) -> Int? {
|
|
303
|
+
return objc_getAssociatedObject(view, &VInputFactory.maxLengthKey) as? Int
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// MARK: - InputDelegateProxy
|
|
308
|
+
|
|
309
|
+
/// UITextFieldDelegate proxy that routes text field events to closure-based handlers.
|
|
310
|
+
/// Stored as an associated object on the UITextField.
|
|
311
|
+
final class InputDelegateProxy: NSObject, UITextFieldDelegate {
|
|
312
|
+
|
|
313
|
+
var onChangeText: ((Any?) -> Void)?
|
|
314
|
+
var onFocus: ((Any?) -> Void)?
|
|
315
|
+
var onBlur: ((Any?) -> Void)?
|
|
316
|
+
var onSubmit: ((Any?) -> Void)?
|
|
317
|
+
var maxLengthProvider: (() -> Int?)?
|
|
318
|
+
|
|
319
|
+
@objc func textFieldEditingChanged(_ textField: UITextField) {
|
|
320
|
+
onChangeText?(textField.text ?? "")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@objc func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
324
|
+
onFocus?(nil)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@objc func textFieldDidEndEditing(_ textField: UITextField) {
|
|
328
|
+
onBlur?(nil)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@objc func textFieldDidReturn(_ textField: UITextField) {
|
|
332
|
+
onSubmit?(textField.text ?? "")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// MARK: - UITextFieldDelegate
|
|
336
|
+
|
|
337
|
+
func textField(
|
|
338
|
+
_ textField: UITextField,
|
|
339
|
+
shouldChangeCharactersIn range: NSRange,
|
|
340
|
+
replacementString string: String
|
|
341
|
+
) -> Bool {
|
|
342
|
+
// Enforce max length if set
|
|
343
|
+
if let maxLength = maxLengthProvider?() {
|
|
344
|
+
let currentText = textField.text ?? ""
|
|
345
|
+
let newLength = currentText.count + string.count - range.length
|
|
346
|
+
return newLength <= maxLength
|
|
347
|
+
}
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
352
|
+
// Allow return key to trigger editingDidEndOnExit
|
|
353
|
+
textField.resignFirstResponder()
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
#endif
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import FlexLayout
|
|
4
|
+
|
|
5
|
+
/// Factory for VKeyboardAvoiding — a container that adjusts its bottom padding
|
|
6
|
+
/// to avoid the system keyboard.
|
|
7
|
+
///
|
|
8
|
+
/// Listens to UIResponder.keyboardWillShowNotification / keyboardWillHideNotification
|
|
9
|
+
/// and updates FlexLayout bottom padding accordingly.
|
|
10
|
+
final class VKeyboardAvoidingFactory: NativeComponentFactory {
|
|
11
|
+
|
|
12
|
+
// MARK: - Associated object keys
|
|
13
|
+
|
|
14
|
+
private static var observerKey: UInt8 = 0
|
|
15
|
+
|
|
16
|
+
// MARK: - NativeComponentFactory
|
|
17
|
+
|
|
18
|
+
func createView() -> UIView {
|
|
19
|
+
let view = KeyboardAvoidingView()
|
|
20
|
+
_ = view.flex
|
|
21
|
+
return view
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func updateProp(view: UIView, key: String, value: Any?) {
|
|
25
|
+
StyleEngine.apply(key: key, value: value, to: view)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func addEventListener(view: UIView, event: String, handler: @escaping (Any?) -> Void) {
|
|
29
|
+
// No events exposed for keyboard avoiding view
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// MARK: - KeyboardAvoidingView
|
|
34
|
+
|
|
35
|
+
/// UIView subclass that automatically adjusts its Yoga bottom padding
|
|
36
|
+
/// based on keyboard visibility.
|
|
37
|
+
private final class KeyboardAvoidingView: UIView {
|
|
38
|
+
|
|
39
|
+
private var showObserver: NSObjectProtocol?
|
|
40
|
+
private var hideObserver: NSObjectProtocol?
|
|
41
|
+
|
|
42
|
+
override init(frame: CGRect) {
|
|
43
|
+
super.init(frame: frame)
|
|
44
|
+
setupKeyboardObservers()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
required init?(coder: NSCoder) {
|
|
48
|
+
super.init(coder: coder)
|
|
49
|
+
setupKeyboardObservers()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
deinit {
|
|
53
|
+
if let obs = showObserver { NotificationCenter.default.removeObserver(obs) }
|
|
54
|
+
if let obs = hideObserver { NotificationCenter.default.removeObserver(obs) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func setupKeyboardObservers() {
|
|
58
|
+
showObserver = NotificationCenter.default.addObserver(
|
|
59
|
+
forName: UIResponder.keyboardWillShowNotification,
|
|
60
|
+
object: nil,
|
|
61
|
+
queue: .main
|
|
62
|
+
) { [weak self] notification in
|
|
63
|
+
self?.handleKeyboardShow(notification)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
hideObserver = NotificationCenter.default.addObserver(
|
|
67
|
+
forName: UIResponder.keyboardWillHideNotification,
|
|
68
|
+
object: nil,
|
|
69
|
+
queue: .main
|
|
70
|
+
) { [weak self] _ in
|
|
71
|
+
self?.handleKeyboardHide()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func handleKeyboardShow(_ notification: Notification) {
|
|
76
|
+
guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
let keyboardHeight = keyboardFrame.height
|
|
80
|
+
guard keyboardHeight > 0 else { return }
|
|
81
|
+
flex.paddingBottom(keyboardHeight)
|
|
82
|
+
triggerLayout()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private func handleKeyboardHide() {
|
|
86
|
+
flex.paddingBottom(0)
|
|
87
|
+
triggerLayout()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func triggerLayout() {
|
|
91
|
+
// Walk up to find the root flex view and trigger layout
|
|
92
|
+
var view: UIView? = self
|
|
93
|
+
while let v = view?.superview {
|
|
94
|
+
view = v
|
|
95
|
+
}
|
|
96
|
+
view?.flex.layout()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
#endif
|
package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import FlexLayout
|
|
4
|
+
|
|
5
|
+
// MARK: - VListFactory
|
|
6
|
+
|
|
7
|
+
/// Factory for VList — a virtualized scrollable list backed by UITableView.
|
|
8
|
+
/// Children inserted via the bridge are stored as table cells, not regular subviews.
|
|
9
|
+
/// Supports scroll and endReached events.
|
|
10
|
+
final class VListFactory: NativeComponentFactory {
|
|
11
|
+
|
|
12
|
+
func createView() -> UIView {
|
|
13
|
+
let container = VListContainerView()
|
|
14
|
+
_ = container.flex
|
|
15
|
+
return container
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func updateProp(view: UIView, key: String, value: Any?) {
|
|
19
|
+
guard let container = view as? VListContainerView else {
|
|
20
|
+
StyleEngine.apply(key: key, value: value, to: view)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
switch key {
|
|
24
|
+
case "estimatedItemHeight":
|
|
25
|
+
container.estimatedItemHeight = CGFloat(value as? Double ?? 44)
|
|
26
|
+
case "showsScrollIndicator":
|
|
27
|
+
container.tableView.showsVerticalScrollIndicator = value as? Bool ?? true
|
|
28
|
+
case "bounces":
|
|
29
|
+
container.tableView.bounces = value as? Bool ?? true
|
|
30
|
+
default:
|
|
31
|
+
StyleEngine.apply(key: key, value: value, to: view)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func addEventListener(view: UIView, event: String, handler: @escaping (Any?) -> Void) {
|
|
36
|
+
guard let container = view as? VListContainerView else { return }
|
|
37
|
+
switch event {
|
|
38
|
+
case "scroll":
|
|
39
|
+
container.onScroll = handler
|
|
40
|
+
case "endReached":
|
|
41
|
+
container.onEndReached = handler
|
|
42
|
+
default:
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func removeEventListener(view: UIView, event: String) {
|
|
48
|
+
guard let container = view as? VListContainerView else { return }
|
|
49
|
+
switch event {
|
|
50
|
+
case "scroll": container.onScroll = nil
|
|
51
|
+
case "endReached": container.onEndReached = nil
|
|
52
|
+
default: break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - Custom child management
|
|
57
|
+
|
|
58
|
+
func insertChild(_ child: UIView, into parent: UIView, before anchor: UIView?) {
|
|
59
|
+
guard let container = parent as? VListContainerView else {
|
|
60
|
+
// Fallback for non-VList parents (shouldn't happen)
|
|
61
|
+
if let anchor = anchor, let idx = parent.subviews.firstIndex(of: anchor) {
|
|
62
|
+
parent.insertSubview(child, at: idx)
|
|
63
|
+
} else {
|
|
64
|
+
parent.addSubview(child)
|
|
65
|
+
}
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
let insertIdx: Int
|
|
69
|
+
if let anchor = anchor, let idx = container.itemViews.firstIndex(where: { $0 === anchor }) {
|
|
70
|
+
container.itemViews.insert(child, at: idx)
|
|
71
|
+
insertIdx = idx
|
|
72
|
+
} else {
|
|
73
|
+
insertIdx = container.itemViews.count
|
|
74
|
+
container.itemViews.append(child)
|
|
75
|
+
}
|
|
76
|
+
// Trigger layout so Yoga can calculate item height before the cell is displayed
|
|
77
|
+
container.setNeedsLayout()
|
|
78
|
+
// Use targeted insert rather than reloadData to avoid triggering layoutSubviews recursively
|
|
79
|
+
container.tableView.insertRows(at: [IndexPath(row: insertIdx, section: 0)], with: .none)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func removeChild(_ child: UIView, from parent: UIView) {
|
|
83
|
+
guard let container = parent as? VListContainerView else {
|
|
84
|
+
child.removeFromSuperview()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
guard let idx = container.itemViews.firstIndex(where: { $0 === child }) else {
|
|
88
|
+
child.removeFromSuperview()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
container.itemViews.remove(at: idx)
|
|
92
|
+
// Remove from any cell it's currently displayed in
|
|
93
|
+
child.removeFromSuperview()
|
|
94
|
+
// Use targeted delete rather than reloadData
|
|
95
|
+
container.tableView.deleteRows(at: [IndexPath(row: idx, section: 0)], with: .none)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// MARK: - VListContainerView
|
|
100
|
+
|
|
101
|
+
/// Container view that hosts a UITableView filling its bounds.
|
|
102
|
+
/// Item views are managed in itemViews array (not as regular subviews).
|
|
103
|
+
final class VListContainerView: UIView {
|
|
104
|
+
|
|
105
|
+
let tableView: UITableView
|
|
106
|
+
var itemViews: [UIView] = []
|
|
107
|
+
var estimatedItemHeight: CGFloat = 44
|
|
108
|
+
var onScroll: ((Any?) -> Void)?
|
|
109
|
+
var onEndReached: ((Any?) -> Void)?
|
|
110
|
+
fileprivate var firedEndReached = false
|
|
111
|
+
private lazy var internalDelegate = VListInternalDelegate(container: self)
|
|
112
|
+
|
|
113
|
+
init() {
|
|
114
|
+
tableView = UITableView(frame: .zero, style: .plain)
|
|
115
|
+
super.init(frame: .zero)
|
|
116
|
+
tableView.separatorStyle = .none
|
|
117
|
+
tableView.tableFooterView = UIView()
|
|
118
|
+
tableView.dataSource = internalDelegate
|
|
119
|
+
tableView.delegate = internalDelegate
|
|
120
|
+
tableView.register(VListCell.self, forCellReuseIdentifier: "VListCell")
|
|
121
|
+
// Add tableView as a real subview of self
|
|
122
|
+
super.addSubview(tableView)
|
|
123
|
+
|
|
124
|
+
// Accessibility: let VoiceOver navigate to individual children within the list
|
|
125
|
+
isAccessibilityElement = false
|
|
126
|
+
shouldGroupAccessibilityChildren = true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
130
|
+
|
|
131
|
+
override func layoutSubviews() {
|
|
132
|
+
super.layoutSubviews()
|
|
133
|
+
tableView.frame = bounds
|
|
134
|
+
|
|
135
|
+
// Run Yoga layout on each item view to compute its height
|
|
136
|
+
let width = bounds.width
|
|
137
|
+
guard width > 0 else { return }
|
|
138
|
+
|
|
139
|
+
var changedIndexPaths: [IndexPath] = []
|
|
140
|
+
for (idx, itemView) in itemViews.enumerated() {
|
|
141
|
+
// Only recompute if width changed (avoids re-entrant layout loops)
|
|
142
|
+
if abs(itemView.frame.size.width - width) > 0.5 {
|
|
143
|
+
itemView.frame.size.width = width
|
|
144
|
+
itemView.flex.layout(mode: .adjustHeight)
|
|
145
|
+
changedIndexPaths.append(IndexPath(row: idx, section: 0))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if !changedIndexPaths.isEmpty {
|
|
149
|
+
// Reload only the rows whose heights changed, not the entire table
|
|
150
|
+
tableView.reloadRows(at: changedIndexPaths, with: .none)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MARK: - VListInternalDelegate
|
|
156
|
+
|
|
157
|
+
private final class VListInternalDelegate: NSObject,
|
|
158
|
+
UITableViewDataSource, UITableViewDelegate
|
|
159
|
+
{
|
|
160
|
+
private weak var container: VListContainerView?
|
|
161
|
+
|
|
162
|
+
init(container: VListContainerView) {
|
|
163
|
+
self.container = container
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
167
|
+
container?.itemViews.count ?? 0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func tableView(_ tableView: UITableView,
|
|
171
|
+
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
172
|
+
let cell = tableView.dequeueReusableCell(
|
|
173
|
+
withIdentifier: "VListCell", for: indexPath) as! VListCell
|
|
174
|
+
guard let container = container,
|
|
175
|
+
indexPath.row < container.itemViews.count else { return cell }
|
|
176
|
+
cell.setItemView(container.itemViews[indexPath.row])
|
|
177
|
+
return cell
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func tableView(_ tableView: UITableView,
|
|
181
|
+
estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
182
|
+
container?.estimatedItemHeight ?? 44
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func tableView(_ tableView: UITableView,
|
|
186
|
+
heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
187
|
+
guard let container = container,
|
|
188
|
+
indexPath.row < container.itemViews.count else {
|
|
189
|
+
return container?.estimatedItemHeight ?? 44
|
|
190
|
+
}
|
|
191
|
+
let h = container.itemViews[indexPath.row].frame.size.height
|
|
192
|
+
return h > 1 ? h : (container.estimatedItemHeight)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
196
|
+
guard let container = container else { return }
|
|
197
|
+
let offset = scrollView.contentOffset
|
|
198
|
+
container.onScroll?(["x": Double(offset.x), "y": Double(offset.y)])
|
|
199
|
+
|
|
200
|
+
// endReached detection (threshold = 20% from bottom)
|
|
201
|
+
let contentH = scrollView.contentSize.height
|
|
202
|
+
let frameH = scrollView.frame.size.height
|
|
203
|
+
guard contentH > frameH else { return }
|
|
204
|
+
let distanceFromBottom = contentH - frameH - offset.y
|
|
205
|
+
let threshold = frameH * 0.2
|
|
206
|
+
|
|
207
|
+
if distanceFromBottom < threshold && !container.firedEndReached {
|
|
208
|
+
container.firedEndReached = true
|
|
209
|
+
container.onEndReached?(nil)
|
|
210
|
+
} else if distanceFromBottom >= threshold {
|
|
211
|
+
container.firedEndReached = false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// MARK: - VListCell
|
|
217
|
+
|
|
218
|
+
private final class VListCell: UITableViewCell {
|
|
219
|
+
|
|
220
|
+
private var currentView: UIView?
|
|
221
|
+
|
|
222
|
+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
223
|
+
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
224
|
+
// Accessibility: cells are containers — VoiceOver should navigate to children inside
|
|
225
|
+
isAccessibilityElement = false
|
|
226
|
+
accessibilityTraits = .none
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
230
|
+
|
|
231
|
+
func setItemView(_ view: UIView) {
|
|
232
|
+
guard currentView !== view else { return }
|
|
233
|
+
currentView?.removeFromSuperview()
|
|
234
|
+
currentView = view
|
|
235
|
+
contentView.addSubview(view)
|
|
236
|
+
view.frame = contentView.bounds
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override func layoutSubviews() {
|
|
240
|
+
super.layoutSubviews()
|
|
241
|
+
currentView?.frame = contentView.bounds
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
override func prepareForReuse() {
|
|
245
|
+
super.prepareForReuse()
|
|
246
|
+
currentView?.removeFromSuperview()
|
|
247
|
+
currentView = nil
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
#endif
|