pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.whl
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.
- pythonnative/__init__.py +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/cli/pn.py +24 -6
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/mutations.py +21 -0
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// `Image`: a `UIImageView` loading from http(s) URLs (with memory and
|
|
5
|
+
/// disk caching), local file paths, bundle asset names, and base64 data URIs.
|
|
6
|
+
public final class PNImageManager: PNComponentManager {
|
|
7
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
8
|
+
let view = UIImageView(frame: .zero)
|
|
9
|
+
view.clipsToBounds = true
|
|
10
|
+
view.contentMode = .scaleAspectFit
|
|
11
|
+
return view
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
15
|
+
guard let imageView = view as? UIImageView else { return }
|
|
16
|
+
if let tint = PNColor.parse(PNProps.value(props, "tint_color") ?? PNProps.value(props, "tint")) {
|
|
17
|
+
imageView.tintColor = tint
|
|
18
|
+
if let image = imageView.image {
|
|
19
|
+
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if let placeholder = PNColor.parse(PNProps.value(props, "placeholder_color")) {
|
|
23
|
+
imageView.backgroundColor = placeholder
|
|
24
|
+
}
|
|
25
|
+
if let source = PNProps.string(PNProps.value(props, "source")), !source.isEmpty {
|
|
26
|
+
load(imageView, source: source)
|
|
27
|
+
}
|
|
28
|
+
if let mode = PNProps.string(PNProps.value(props, "scale_type") ?? PNProps.value(props, "resize_mode")) {
|
|
29
|
+
imageView.contentMode = PNImageManager.contentMode(mode)
|
|
30
|
+
}
|
|
31
|
+
PNViewStyler.applyCommon(imageView, props)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
|
|
35
|
+
guard let image = (view as? UIImageView)?.image else { return .zero }
|
|
36
|
+
var size = image.size
|
|
37
|
+
if maxW.isFinite, maxW > 0, size.width > maxW {
|
|
38
|
+
let scale = maxW / size.width
|
|
39
|
+
size = CGSize(width: maxW, height: size.height * scale)
|
|
40
|
+
}
|
|
41
|
+
return size
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static func contentMode(_ name: String) -> UIView.ContentMode {
|
|
45
|
+
switch name {
|
|
46
|
+
case "cover": return .scaleAspectFill
|
|
47
|
+
case "stretch": return .scaleToFill
|
|
48
|
+
case "center": return .center
|
|
49
|
+
case "repeat": return .scaleAspectFill
|
|
50
|
+
default: return .scaleAspectFit
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Loading
|
|
55
|
+
|
|
56
|
+
private func load(_ imageView: UIImageView, source: String) {
|
|
57
|
+
guard let state = PNViewState.existing(for: imageView) else { return }
|
|
58
|
+
state.extras["pending_source"] = source
|
|
59
|
+
if source.hasPrefix("data:") {
|
|
60
|
+
loadDataURI(imageView, source)
|
|
61
|
+
} else if source.hasPrefix("http://") || source.hasPrefix("https://") {
|
|
62
|
+
PNImageLoader.shared.fetch(source) { [weak imageView] result in
|
|
63
|
+
guard let imageView = imageView, PNViewState.existing(for: imageView)?.extras["pending_source"] as? String == source else { return }
|
|
64
|
+
switch result {
|
|
65
|
+
case .success(let data):
|
|
66
|
+
if let image = PNImageManager.decode(data, targetSize: imageView.bounds.size) {
|
|
67
|
+
self.setImage(imageView, image)
|
|
68
|
+
PNEvents.emit(imageView, "on_load", [["width": Double(image.size.width), "height": Double(image.size.height)]])
|
|
69
|
+
} else {
|
|
70
|
+
PNEvents.emit(imageView, "on_error", ["decode failed"])
|
|
71
|
+
}
|
|
72
|
+
case .failure(let error):
|
|
73
|
+
PNEvents.emit(imageView, "on_error", [error.localizedDescription])
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
var image = UIImage(named: source)
|
|
78
|
+
if image == nil {
|
|
79
|
+
let path = source.hasPrefix("file://") ? (URL(string: source)?.path ?? source) : source
|
|
80
|
+
image = UIImage(contentsOfFile: path)
|
|
81
|
+
}
|
|
82
|
+
if let image = image {
|
|
83
|
+
setImage(imageView, image)
|
|
84
|
+
PNEvents.emit(imageView, "on_load", [["width": Double(image.size.width), "height": Double(image.size.height)]])
|
|
85
|
+
} else {
|
|
86
|
+
PNEvents.emit(imageView, "on_error", ["image '\(source)' not found"])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func setImage(_ imageView: UIImageView, _ image: UIImage) {
|
|
92
|
+
let tinted = PNProps.value(mergedProps(imageView), "tint_color") ?? PNProps.value(mergedProps(imageView), "tint")
|
|
93
|
+
imageView.image = tinted != nil ? image.withRenderingMode(.alwaysTemplate) : image
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func loadDataURI(_ imageView: UIImageView, _ source: String) {
|
|
97
|
+
guard let comma = source.firstIndex(of: ","),
|
|
98
|
+
let data = Data(base64Encoded: String(source[source.index(after: comma)...]), options: [.ignoreUnknownCharacters]),
|
|
99
|
+
let image = UIImage(data: data)
|
|
100
|
+
else {
|
|
101
|
+
PNEvents.emit(imageView, "on_error", ["data URI decode failed"])
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
setImage(imageView, image)
|
|
105
|
+
PNEvents.emit(imageView, "on_load", [["width": Double(image.size.width), "height": Double(image.size.height)]])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Decode `data`, downsampling when the bitmap is more than 2x the target.
|
|
109
|
+
static func decode(_ data: Data, targetSize: CGSize) -> UIImage? {
|
|
110
|
+
guard let image = UIImage(data: data) else { return nil }
|
|
111
|
+
guard targetSize.width > 0, targetSize.height > 0 else { return image }
|
|
112
|
+
let size = image.size
|
|
113
|
+
if size.width <= targetSize.width * 2, size.height <= targetSize.height * 2 { return image }
|
|
114
|
+
let scale = min(targetSize.width * 2 / size.width, targetSize.height * 2 / size.height)
|
|
115
|
+
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
|
|
116
|
+
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
117
|
+
return renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: newSize)) }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Shared image pipeline: memory LRU + disk cache under `Caches/pn_images`,
|
|
122
|
+
/// request deduplication, delivery on the main queue.
|
|
123
|
+
public final class PNImageLoader {
|
|
124
|
+
public static let shared = PNImageLoader()
|
|
125
|
+
|
|
126
|
+
public enum LoadError: Error, LocalizedError {
|
|
127
|
+
case badURL
|
|
128
|
+
case http(Int)
|
|
129
|
+
case empty
|
|
130
|
+
|
|
131
|
+
public var errorDescription: String? {
|
|
132
|
+
switch self {
|
|
133
|
+
case .badURL: return "invalid image URL"
|
|
134
|
+
case .http(let code): return "HTTP \(code)"
|
|
135
|
+
case .empty: return "empty response"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private let memory = NSCache<NSString, NSData>()
|
|
141
|
+
private var inflight: [String: [(Result<Data, Error>) -> Void]] = [:]
|
|
142
|
+
private let session: URLSession
|
|
143
|
+
private let queue = DispatchQueue(label: "com.pythonnative.images", qos: .utility)
|
|
144
|
+
private lazy var directory: URL = {
|
|
145
|
+
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSTemporaryDirectory())
|
|
146
|
+
let dir = caches.appendingPathComponent("pn_images", isDirectory: true)
|
|
147
|
+
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
148
|
+
return dir
|
|
149
|
+
}()
|
|
150
|
+
|
|
151
|
+
private init() {
|
|
152
|
+
memory.totalCostLimit = 16 * 1024 * 1024
|
|
153
|
+
let config = URLSessionConfiguration.default
|
|
154
|
+
config.requestCachePolicy = .returnCacheDataElseLoad
|
|
155
|
+
session = URLSession(configuration: config)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Fetch `url`, delivering the raw bytes on the main queue.
|
|
159
|
+
public func fetch(_ url: String, completion: @escaping (Result<Data, Error>) -> Void) {
|
|
160
|
+
if let cached = memory.object(forKey: url as NSString) {
|
|
161
|
+
completion(.success(cached as Data))
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if inflight[url] != nil {
|
|
165
|
+
inflight[url]?.append(completion)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
inflight[url] = [completion]
|
|
169
|
+
let path = cachePath(for: url)
|
|
170
|
+
queue.async {
|
|
171
|
+
if let data = try? Data(contentsOf: path), !data.isEmpty {
|
|
172
|
+
self.deliver(url, .success(data))
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
guard let remote = URL(string: url) else {
|
|
176
|
+
self.deliver(url, .failure(LoadError.badURL))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
let task = self.session.dataTask(with: remote) { data, response, error in
|
|
180
|
+
if let error = error {
|
|
181
|
+
self.deliver(url, .failure(error))
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
|
185
|
+
self.deliver(url, .failure(LoadError.http(http.statusCode)))
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
guard let data = data, !data.isEmpty else {
|
|
189
|
+
self.deliver(url, .failure(LoadError.empty))
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
try? data.write(to: path, options: [.atomic])
|
|
193
|
+
self.deliver(url, .success(data))
|
|
194
|
+
}
|
|
195
|
+
task.resume()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Remove every cached image (memory and disk).
|
|
200
|
+
public func clear() {
|
|
201
|
+
memory.removeAllObjects()
|
|
202
|
+
try? FileManager.default.removeItem(at: directory)
|
|
203
|
+
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private func deliver(_ url: String, _ result: Result<Data, Error>) {
|
|
207
|
+
DispatchQueue.main.async {
|
|
208
|
+
if case .success(let data) = result {
|
|
209
|
+
self.memory.setObject(data as NSData, forKey: url as NSString, cost: data.count)
|
|
210
|
+
}
|
|
211
|
+
let waiters = self.inflight.removeValue(forKey: url) ?? []
|
|
212
|
+
for waiter in waiters { waiter(result) }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private func cachePath(for url: String) -> URL {
|
|
217
|
+
let digest = SHA256.hash(data: Data(url.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
218
|
+
let ext = URL(string: url)?.pathExtension ?? ""
|
|
219
|
+
return directory.appendingPathComponent(ext.isEmpty ? digest : "\(digest).\(ext)")
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// `Modal`: a hidden on-tree placeholder whose children mount into a
|
|
4
|
+
/// presented `UIViewController` while `visible` is true.
|
|
5
|
+
public final class PNModalManager: PNComponentManager {
|
|
6
|
+
final class Presentation {
|
|
7
|
+
let controller: UIViewController
|
|
8
|
+
let content: UIView
|
|
9
|
+
init(controller: UIViewController, content: UIView) {
|
|
10
|
+
self.controller = controller
|
|
11
|
+
self.content = content
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
16
|
+
let placeholder = UIView(frame: .zero)
|
|
17
|
+
placeholder.isHidden = true
|
|
18
|
+
return placeholder
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
22
|
+
guard let state = PNViewState.existing(for: view) else { return }
|
|
23
|
+
let visible = PNProps.bool(PNProps.value(state.props, "visible")) ?? false
|
|
24
|
+
let presented = state.extras["modal"] is Presentation
|
|
25
|
+
if visible, !presented {
|
|
26
|
+
present(view, state)
|
|
27
|
+
} else if !visible, presented {
|
|
28
|
+
dismiss(view, state, emit: true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public override func teardown(view: UIView) {
|
|
33
|
+
if let state = PNViewState.existing(for: view), state.extras["modal"] is Presentation {
|
|
34
|
+
dismiss(view, state, emit: false)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public override func childContainer(for view: UIView) -> UIView {
|
|
39
|
+
guard let state = PNViewState.existing(for: view), let modal = state.extras["modal"] as? Presentation else {
|
|
40
|
+
return view
|
|
41
|
+
}
|
|
42
|
+
return modal.content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public override func insertChild(parent: UIView, child: UIView, index: Int) {
|
|
46
|
+
if let state = PNViewState.existing(for: parent), !(state.extras["modal"] is Presentation) {
|
|
47
|
+
var pending = (state.extras["pending_children"] as? [UIView]) ?? []
|
|
48
|
+
pending.removeAll { $0 === child }
|
|
49
|
+
pending.insert(child, at: max(0, min(index, pending.count)))
|
|
50
|
+
state.extras["pending_children"] = pending
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
super.insertChild(parent: parent, child: child, index: index)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public override func removeChild(parent: UIView, child: UIView) {
|
|
57
|
+
super.removeChild(parent: parent, child: child)
|
|
58
|
+
if let state = PNViewState.existing(for: parent), var pending = state.extras["pending_children"] as? [UIView] {
|
|
59
|
+
pending.removeAll { $0 === child }
|
|
60
|
+
state.extras["pending_children"] = pending
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {}
|
|
65
|
+
|
|
66
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize { .zero }
|
|
67
|
+
|
|
68
|
+
private func present(_ placeholder: UIView, _ state: PNViewState) {
|
|
69
|
+
let props = state.props
|
|
70
|
+
let controller = PNModalViewController()
|
|
71
|
+
controller.onDismissed = { [weak placeholder] in
|
|
72
|
+
guard let placeholder = placeholder, let state = PNViewState.existing(for: placeholder),
|
|
73
|
+
state.extras["modal"] is Presentation
|
|
74
|
+
else { return }
|
|
75
|
+
state.extras.removeValue(forKey: "modal")
|
|
76
|
+
PNEvents.emit(placeholder, "on_dismiss")
|
|
77
|
+
PNEvents.emitIfWired(placeholder, "on_request_close")
|
|
78
|
+
}
|
|
79
|
+
let style = PNProps.string(PNProps.value(props, "presentation_style")) ?? "page_sheet"
|
|
80
|
+
let isOverlay = style == "overlay" || PNProps.bool(PNProps.value(props, "transparent")) == true
|
|
81
|
+
let content = UIView(frame: controller.view.bounds)
|
|
82
|
+
content.backgroundColor = isOverlay ? .clear : PNColor.parse(PNProps.value(props, "background_color")) ?? .systemBackground
|
|
83
|
+
content.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
84
|
+
controller.view.addSubview(content)
|
|
85
|
+
controller.view.backgroundColor = isOverlay ? UIColor.black.withAlphaComponent(0.4) : content.backgroundColor
|
|
86
|
+
switch isOverlay ? "overlay" : style {
|
|
87
|
+
case "full_screen": controller.modalPresentationStyle = .fullScreen
|
|
88
|
+
case "form_sheet": controller.modalPresentationStyle = .formSheet
|
|
89
|
+
case "overlay": controller.modalPresentationStyle = .overCurrentContext
|
|
90
|
+
default: controller.modalPresentationStyle = .pageSheet
|
|
91
|
+
}
|
|
92
|
+
switch PNProps.string(PNProps.value(props, "animation_type")) ?? PNProps.string(PNProps.value(props, "animation")) {
|
|
93
|
+
case "fade": controller.modalTransitionStyle = .crossDissolve
|
|
94
|
+
case "flip": controller.modalTransitionStyle = .flipHorizontal
|
|
95
|
+
default: controller.modalTransitionStyle = .coverVertical
|
|
96
|
+
}
|
|
97
|
+
if !isOverlay, PNProps.bool(PNProps.value(props, "dismiss_on_backdrop")) == false {
|
|
98
|
+
controller.isModalInPresentation = true
|
|
99
|
+
}
|
|
100
|
+
let presentation = Presentation(controller: controller, content: content)
|
|
101
|
+
state.extras["modal"] = presentation
|
|
102
|
+
for child in (state.extras.removeValue(forKey: "pending_children") as? [UIView]) ?? [] {
|
|
103
|
+
child.translatesAutoresizingMaskIntoConstraints = true
|
|
104
|
+
content.addSubview(child)
|
|
105
|
+
}
|
|
106
|
+
let animated = PNProps.string(PNProps.value(props, "animation_type")) != "none"
|
|
107
|
+
guard let top = PNWindow.topViewController() else {
|
|
108
|
+
state.extras.removeValue(forKey: "modal")
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
top.present(controller, animated: animated) { [weak placeholder] in
|
|
112
|
+
if let placeholder = placeholder { PNEvents.emit(placeholder, "on_show") }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func dismiss(_ placeholder: UIView, _ state: PNViewState, emit: Bool) {
|
|
117
|
+
guard let modal = state.extras.removeValue(forKey: "modal") as? Presentation else { return }
|
|
118
|
+
(modal.controller as? PNModalViewController)?.onDismissed = nil
|
|
119
|
+
let animated = PNProps.string(PNProps.value(state.props, "animation_type")) != "none"
|
|
120
|
+
modal.controller.dismiss(animated: animated)
|
|
121
|
+
if emit { PNEvents.emit(placeholder, "on_dismiss") }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Reports interactive dismissal (sheet swipe) back to the manager.
|
|
126
|
+
final class PNModalViewController: UIViewController {
|
|
127
|
+
var onDismissed: (() -> Void)?
|
|
128
|
+
|
|
129
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
130
|
+
super.viewDidDisappear(animated)
|
|
131
|
+
if isBeingDismissed || presentingViewController == nil {
|
|
132
|
+
onDismissed?()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Overlay view that only claims touches landing on one of its subviews.
|
|
138
|
+
final class PNPortalView: UIView {
|
|
139
|
+
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
140
|
+
for sub in subviews where !sub.isHidden && sub.alpha > 0.01 && sub.isUserInteractionEnabled {
|
|
141
|
+
if sub.frame.contains(point) { return true }
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// `Portal`: floats children over the screen in a top-level overlay that
|
|
148
|
+
/// mirrors the screen host's root frame.
|
|
149
|
+
public final class PNPortalManager: PNComponentManager {
|
|
150
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
151
|
+
let overlay = PNPortalView(frame: .zero)
|
|
152
|
+
overlay.backgroundColor = .clear
|
|
153
|
+
return overlay
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
157
|
+
PNViewStyler.applyCommon(view, props)
|
|
158
|
+
if !initial { ensureAttached(view) }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public override func insertChild(parent: UIView, child: UIView, index: Int) {
|
|
162
|
+
ensureAttached(parent)
|
|
163
|
+
super.insertChild(parent: parent, child: child, index: index)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {}
|
|
167
|
+
|
|
168
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize { .zero }
|
|
169
|
+
|
|
170
|
+
private func ensureAttached(_ overlay: UIView) {
|
|
171
|
+
guard let host = PNWindow.topViewController()?.view else { return }
|
|
172
|
+
if overlay.superview === host {
|
|
173
|
+
if host.subviews.last !== overlay {
|
|
174
|
+
host.bringSubviewToFront(overlay)
|
|
175
|
+
}
|
|
176
|
+
syncFrame(overlay, host: host)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
overlay.removeFromSuperview()
|
|
180
|
+
host.addSubview(overlay)
|
|
181
|
+
syncFrame(overlay, host: host)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static func rootFrame(in host: UIView) -> CGRect {
|
|
185
|
+
let bounds = host.bounds
|
|
186
|
+
let insets = host.safeAreaInsets
|
|
187
|
+
let w = max(0, bounds.width - insets.left - insets.right)
|
|
188
|
+
let h = max(0, bounds.height - insets.top)
|
|
189
|
+
return CGRect(x: insets.left, y: insets.top, width: w, height: h)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func syncFrame(_ overlay: UIView, host: UIView) {
|
|
193
|
+
overlay.frame = PNPortalManager.rootFrame(in: host)
|
|
194
|
+
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Process-wide status bar state consulted by `PNViewController`.
|
|
199
|
+
public enum PNStatusBarState {
|
|
200
|
+
public static var hidden = false
|
|
201
|
+
public static var style: UIStatusBarStyle = .default
|
|
202
|
+
public static var animation: UIStatusBarAnimation = .fade
|
|
203
|
+
|
|
204
|
+
static func refresh() {
|
|
205
|
+
var controller = PNWindow.keyWindow()?.rootViewController
|
|
206
|
+
if let nav = controller as? UINavigationController { controller = nav.topViewController }
|
|
207
|
+
controller?.setNeedsStatusBarAppearanceUpdate()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// `StatusBar`: a global side effect (hidden / style), no on-screen view.
|
|
212
|
+
public final class PNStatusBarManager: PNComponentManager {
|
|
213
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
214
|
+
let placeholder = UIView(frame: .zero)
|
|
215
|
+
placeholder.isHidden = true
|
|
216
|
+
return placeholder
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
220
|
+
if let hidden = PNProps.bool(PNProps.value(props, "hidden")) {
|
|
221
|
+
PNStatusBarState.hidden = hidden
|
|
222
|
+
}
|
|
223
|
+
if let style = PNProps.string(PNProps.value(props, "bar_style")) {
|
|
224
|
+
switch style {
|
|
225
|
+
case "light", "light_content": PNStatusBarState.style = .lightContent
|
|
226
|
+
case "dark", "dark_content": PNStatusBarState.style = .darkContent
|
|
227
|
+
default: PNStatusBarState.style = .default
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if let animation = PNProps.string(PNProps.value(props, "animation")) {
|
|
231
|
+
switch animation {
|
|
232
|
+
case "slide": PNStatusBarState.animation = .slide
|
|
233
|
+
case "none": PNStatusBarState.animation = .none
|
|
234
|
+
default: PNStatusBarState.animation = .fade
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
PNStatusBarState.refresh()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {}
|
|
241
|
+
|
|
242
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize { .zero }
|
|
243
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// `Pressable`: a touchable container with press feedback.
|
|
4
|
+
///
|
|
5
|
+
/// A zero-duration long-press recognizer tracks the raw touch for
|
|
6
|
+
/// `on_press_in` / `on_press_out` and the pressed-opacity feedback; a tap
|
|
7
|
+
/// recognizer fires `on_press` and a standard long press fires
|
|
8
|
+
/// `on_long_press`. Hit slop and pointer events come from
|
|
9
|
+
/// `PNContainerView`, so hit testing is entirely UIKit's.
|
|
10
|
+
public final class PNPressableManager: PNComponentManager {
|
|
11
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
12
|
+
let view = PNContainerView(frame: .zero)
|
|
13
|
+
view.isUserInteractionEnabled = true
|
|
14
|
+
return view
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public override func createView(tag: Int64, props: [String: Any]) -> UIView {
|
|
18
|
+
let view = super.createView(tag: tag, props: props)
|
|
19
|
+
let handler = PNPressHandler(view: view)
|
|
20
|
+
PNViewState.existing(for: view)?.retained.append(handler)
|
|
21
|
+
handler.install()
|
|
22
|
+
return view
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
26
|
+
PNViewStyler.applyCommon(view, props)
|
|
27
|
+
if PNProps.has(props, "enabled") {
|
|
28
|
+
view.isUserInteractionEnabled = PNProps.bool(PNProps.value(props, "enabled")) ?? true
|
|
29
|
+
}
|
|
30
|
+
if PNProps.has(props, "disabled") {
|
|
31
|
+
view.isUserInteractionEnabled = !(PNProps.bool(PNProps.value(props, "disabled")) ?? false)
|
|
32
|
+
}
|
|
33
|
+
if let delay = PNProps.double(PNProps.value(props, "delay_long_press")),
|
|
34
|
+
let handler = PNViewState.existing(for: view)?.retained.compactMap({ $0 as? PNPressHandler }).first
|
|
35
|
+
{
|
|
36
|
+
handler.longPress.minimumPressDuration = max(0.05, delay / 1000)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Owns the three press recognizers of one Pressable.
|
|
42
|
+
final class PNPressHandler: NSObject, UIGestureRecognizerDelegate {
|
|
43
|
+
private weak var view: UIView?
|
|
44
|
+
let tap = UITapGestureRecognizer()
|
|
45
|
+
let longPress = UILongPressGestureRecognizer()
|
|
46
|
+
let touch = UILongPressGestureRecognizer()
|
|
47
|
+
|
|
48
|
+
init(view: UIView) {
|
|
49
|
+
self.view = view
|
|
50
|
+
super.init()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func install() {
|
|
54
|
+
guard let view = view else { return }
|
|
55
|
+
touch.minimumPressDuration = 0
|
|
56
|
+
tap.addTarget(self, action: #selector(onTap(_:)))
|
|
57
|
+
longPress.addTarget(self, action: #selector(onLongPress(_:)))
|
|
58
|
+
touch.addTarget(self, action: #selector(onTouch(_:)))
|
|
59
|
+
for recognizer in [tap, longPress, touch] {
|
|
60
|
+
recognizer.cancelsTouchesInView = false
|
|
61
|
+
recognizer.delegate = self
|
|
62
|
+
view.addGestureRecognizer(recognizer)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private var merged: [String: Any] {
|
|
67
|
+
view.flatMap { PNViewState.existing(for: $0)?.props } ?? [:]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@objc private func onTap(_ recognizer: UITapGestureRecognizer) {
|
|
71
|
+
guard let view = view, recognizer.state == .ended else { return }
|
|
72
|
+
PNEvents.emit(view, "on_press")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@objc private func onLongPress(_ recognizer: UILongPressGestureRecognizer) {
|
|
76
|
+
guard let view = view, recognizer.state == .began else { return }
|
|
77
|
+
PNEvents.emit(view, "on_long_press")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@objc private func onTouch(_ recognizer: UILongPressGestureRecognizer) {
|
|
81
|
+
guard let view = view else { return }
|
|
82
|
+
switch recognizer.state {
|
|
83
|
+
case .began:
|
|
84
|
+
feedback(pressed: true)
|
|
85
|
+
PNEvents.emit(view, "on_press_in")
|
|
86
|
+
case .ended, .cancelled, .failed:
|
|
87
|
+
feedback(pressed: false)
|
|
88
|
+
PNEvents.emit(view, "on_press_out")
|
|
89
|
+
default:
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func feedback(pressed: Bool) {
|
|
95
|
+
guard let view = view else { return }
|
|
96
|
+
let props = merged
|
|
97
|
+
let target: CGFloat
|
|
98
|
+
let duration: TimeInterval
|
|
99
|
+
if pressed {
|
|
100
|
+
target = CGFloat(PNProps.double(PNProps.value(props, "pressed_opacity")) ?? 0.6)
|
|
101
|
+
duration = 0.05
|
|
102
|
+
} else {
|
|
103
|
+
target = CGFloat(PNProps.double(PNProps.value(props, "opacity")) ?? 1)
|
|
104
|
+
duration = 0.1
|
|
105
|
+
}
|
|
106
|
+
UIView.animate(withDuration: duration) { view.alpha = target }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Press recognizers recognize alongside everything, including the
|
|
110
|
+
// declarative `gestures` recognizers and a parent scroll view's pan.
|
|
111
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
|
112
|
+
true
|
|
113
|
+
}
|
|
114
|
+
}
|