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.
Files changed (176) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,340 @@
1
+ import UIKit
2
+
3
+ /// Applies the visual props every element type shares.
4
+ ///
5
+ /// `applyCommon` is the full container set (background, overflow,
6
+ /// display, opacity, z-index, pointer events, borders, shadows,
7
+ /// transform, accessibility). `applyDecoration` is the subset leaf
8
+ /// controls use (borders, shadow, transform, accessibility, opacity),
9
+ /// mirroring the per-handler behavior of the original iOS backend.
10
+ public enum PNViewStyler {
11
+ static let cornerRadiusKeys = [
12
+ "border_top_left_radius", "border_top_right_radius",
13
+ "border_bottom_left_radius", "border_bottom_right_radius",
14
+ ]
15
+ static let sideWidthKeys = ["border_left_width", "border_top_width", "border_right_width", "border_bottom_width"]
16
+ static let sideColorKeys = ["border_left_color", "border_top_color", "border_right_color", "border_bottom_color"]
17
+ static let shadowKeys = ["shadow_color", "shadow_offset", "shadow_opacity", "shadow_radius", "elevation"]
18
+ static let maskedCornerBits: [CACornerMask] = [
19
+ .layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
20
+ ]
21
+ static let allCorners: CACornerMask = [
22
+ .layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
23
+ ]
24
+
25
+ // MARK: - Entry points
26
+
27
+ /// Apply every shared visual prop present in `props`.
28
+ public static func applyCommon(_ view: UIView, _ props: [String: Any]) {
29
+ if let color = PNColor.parse(PNProps.value(props, "background_color")) {
30
+ view.backgroundColor = color
31
+ }
32
+ if PNProps.has(props, "overflow") {
33
+ view.clipsToBounds = PNProps.string(PNProps.value(props, "overflow")) == "hidden"
34
+ }
35
+ if PNProps.has(props, "display") {
36
+ view.isHidden = PNProps.string(PNProps.value(props, "display")) == "none"
37
+ }
38
+ if let opacity = PNProps.double(PNProps.value(props, "opacity")) {
39
+ view.alpha = CGFloat(opacity)
40
+ }
41
+ if PNProps.has(props, "z_index") {
42
+ view.layer.zPosition = CGFloat(PNProps.double(PNProps.value(props, "z_index")) ?? 0)
43
+ }
44
+ if PNProps.has(props, "pointer_events") || PNProps.has(props, "hit_slop") {
45
+ applyInteraction(view, props)
46
+ }
47
+ applyBorder(view, props)
48
+ applySideBorders(view, props)
49
+ let merged = PNViewState.existing(for: view)?.props ?? props
50
+ let cornerKeysActive = cornerRadiusKeys.contains { PNProps.value(merged, $0) != nil }
51
+ if cornerRadiusKeys.contains(where: { PNProps.has(props, $0) }) || (PNProps.has(props, "border_radius") && cornerKeysActive) {
52
+ applyCornerRadii(view, merged)
53
+ }
54
+ applyShadow(view, props)
55
+ if PNProps.has(props, "transform") {
56
+ PNTransform.apply(view, spec: PNProps.value(props, "transform"))
57
+ }
58
+ applyAccessibility(view, props)
59
+ }
60
+
61
+ /// The leaf-control subset: border, shadow, transform, accessibility, opacity.
62
+ public static func applyDecoration(_ view: UIView, _ props: [String: Any]) {
63
+ applyBorder(view, props)
64
+ applyShadow(view, props)
65
+ if PNProps.has(props, "transform") {
66
+ PNTransform.apply(view, spec: PNProps.value(props, "transform"))
67
+ }
68
+ applyAccessibility(view, props)
69
+ if let opacity = PNProps.double(PNProps.value(props, "opacity")) {
70
+ view.alpha = CGFloat(opacity)
71
+ }
72
+ }
73
+
74
+ /// Re-run the bounds-dependent parts of styling after a frame change.
75
+ public static func syncFrameDependentStyles(_ view: UIView, size: CGSize) {
76
+ guard let state = PNViewState.existing(for: view) else { return }
77
+ let maxRadius = max(0, min(size.width, size.height) / 2)
78
+ if let requested = state.requestedCornerRadius, maxRadius > 0 {
79
+ view.layer.cornerRadius = min(requested, maxRadius)
80
+ } else if state.requestedCornerRadius == nil, view.layer.cornerRadius > 0, maxRadius > 0 {
81
+ view.layer.cornerRadius = min(view.layer.cornerRadius, maxRadius)
82
+ }
83
+ updateCornerMask(view, state: state, size: size)
84
+ updateSideBorderLayers(view, state: state, size: size)
85
+ }
86
+
87
+ // MARK: - Borders
88
+
89
+ static func applyBorder(_ view: UIView, _ props: [String: Any]) {
90
+ let layer = view.layer
91
+ if let radius = PNProps.double(PNProps.value(props, "border_radius")) {
92
+ let requested = CGFloat(max(0, radius))
93
+ PNViewState.existing(for: view)?.requestedCornerRadius = requested
94
+ let size = view.bounds.size
95
+ var value = requested
96
+ if size.width > 0, size.height > 0 {
97
+ value = min(requested, min(size.width, size.height) / 2)
98
+ }
99
+ layer.cornerRadius = value
100
+ // Rounded corners clip implicitly (the React Native default).
101
+ layer.masksToBounds = true
102
+ } else if PNProps.has(props, "border_radius") {
103
+ PNViewState.existing(for: view)?.requestedCornerRadius = nil
104
+ layer.cornerRadius = 0
105
+ }
106
+ if let width = PNProps.double(PNProps.value(props, "border_width")) {
107
+ layer.borderWidth = CGFloat(width)
108
+ } else if PNProps.has(props, "border_width") {
109
+ layer.borderWidth = 0
110
+ }
111
+ if let color = PNColor.parse(PNProps.value(props, "border_color")) {
112
+ layer.borderColor = color.cgColor
113
+ }
114
+ }
115
+
116
+ static func applySideBorders(_ view: UIView, _ props: [String: Any]) {
117
+ guard sideWidthKeys.contains(where: { PNProps.value(props, $0) != nil }),
118
+ let state = PNViewState.existing(for: view)
119
+ else { return }
120
+ let merged = state.props
121
+ let baseWidth = CGFloat(PNProps.double(PNProps.value(merged, "border_width")) ?? 0)
122
+ let baseColor = PNColor.parse(PNProps.value(merged, "border_color")) ?? .black
123
+ state.sideBorderWidths = sideWidthKeys.map { key in
124
+ CGFloat(PNProps.double(PNProps.value(merged, key)) ?? Double(baseWidth))
125
+ }
126
+ state.sideBorderColors = sideColorKeys.map { key in
127
+ PNColor.parse(PNProps.value(merged, key)) ?? baseColor
128
+ }
129
+ view.layer.borderWidth = 0
130
+ let size = view.bounds.size
131
+ if size.width > 0, size.height > 0 {
132
+ updateSideBorderLayers(view, state: state, size: size)
133
+ }
134
+ }
135
+
136
+ static func updateSideBorderLayers(_ view: UIView, state: PNViewState, size: CGSize) {
137
+ guard let widths = state.sideBorderWidths, let colors = state.sideBorderColors else { return }
138
+ let frames = [
139
+ CGRect(x: 0, y: 0, width: widths[0], height: size.height),
140
+ CGRect(x: 0, y: 0, width: size.width, height: widths[1]),
141
+ CGRect(x: size.width - widths[2], y: 0, width: widths[2], height: size.height),
142
+ CGRect(x: 0, y: size.height - widths[3], width: size.width, height: widths[3]),
143
+ ]
144
+ for i in 0..<4 {
145
+ if widths[i] <= 0 {
146
+ state.sideBorderLayers[i]?.removeFromSuperlayer()
147
+ state.sideBorderLayers[i] = nil
148
+ continue
149
+ }
150
+ let layer = state.sideBorderLayers[i] ?? {
151
+ let created = CALayer()
152
+ view.layer.addSublayer(created)
153
+ state.sideBorderLayers[i] = created
154
+ return created
155
+ }()
156
+ layer.backgroundColor = colors[i].cgColor
157
+ layer.frame = frames[i]
158
+ }
159
+ }
160
+
161
+ // MARK: - Per-corner radii
162
+
163
+ static func applyCornerRadii(_ view: UIView, _ merged: [String: Any]) {
164
+ guard let state = PNViewState.existing(for: view) else { return }
165
+ let anyCorner = cornerRadiusKeys.contains { PNProps.value(merged, $0) != nil }
166
+ if !anyCorner {
167
+ if state.cornerRadii != nil {
168
+ state.cornerRadii = nil
169
+ view.layer.mask = nil
170
+ view.layer.maskedCorners = allCorners
171
+ applyBorder(view, merged)
172
+ }
173
+ return
174
+ }
175
+ let base = CGFloat(max(0, PNProps.double(PNProps.value(merged, "border_radius")) ?? 0))
176
+ let radii: [CGFloat] = cornerRadiusKeys.map { key in
177
+ guard let value = PNProps.double(PNProps.value(merged, key)), value.isFinite else { return base }
178
+ return CGFloat(max(0, value))
179
+ }
180
+ let distinct = Set(radii.filter { $0 > 0 })
181
+ if distinct.count <= 1 {
182
+ state.cornerRadii = nil
183
+ let value = distinct.first ?? 0
184
+ var mask: CACornerMask = []
185
+ for (bit, r) in zip(maskedCornerBits, radii) where r > 0 {
186
+ mask.insert(bit)
187
+ }
188
+ view.layer.mask = nil
189
+ view.layer.cornerRadius = value
190
+ view.layer.maskedCorners = mask.isEmpty ? allCorners : mask
191
+ view.layer.masksToBounds = true
192
+ state.requestedCornerRadius = value
193
+ return
194
+ }
195
+ state.requestedCornerRadius = nil
196
+ state.cornerRadii = radii
197
+ view.layer.cornerRadius = 0
198
+ view.layer.maskedCorners = allCorners
199
+ let size = view.bounds.size
200
+ if size.width > 0, size.height > 0 {
201
+ updateCornerMask(view, state: state, size: size)
202
+ }
203
+ }
204
+
205
+ static func updateCornerMask(_ view: UIView, state: PNViewState, size: CGSize) {
206
+ guard let radii = state.cornerRadii, size.width > 0, size.height > 0 else { return }
207
+ var scale: CGFloat = 1
208
+ let (tl0, tr0, bl0, br0) = (radii[0], radii[1], radii[2], radii[3])
209
+ for (sum, extent) in [(tl0 + tr0, size.width), (bl0 + br0, size.width), (tl0 + bl0, size.height), (tr0 + br0, size.height)]
210
+ where sum > extent && extent > 0 {
211
+ scale = min(scale, extent / sum)
212
+ }
213
+ let (tl, tr, bl, br) = (tl0 * scale, tr0 * scale, bl0 * scale, br0 * scale)
214
+ let w = size.width, h = size.height
215
+ let path = UIBezierPath()
216
+ let halfPi = CGFloat.pi / 2
217
+ path.move(to: CGPoint(x: tl, y: 0))
218
+ path.addLine(to: CGPoint(x: w - tr, y: 0))
219
+ if tr > 0 { path.addArc(withCenter: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: -halfPi, endAngle: 0, clockwise: true) }
220
+ path.addLine(to: CGPoint(x: w, y: h - br))
221
+ if br > 0 { path.addArc(withCenter: CGPoint(x: w - br, y: h - br), radius: br, startAngle: 0, endAngle: halfPi, clockwise: true) }
222
+ path.addLine(to: CGPoint(x: bl, y: h))
223
+ if bl > 0 { path.addArc(withCenter: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: halfPi, endAngle: .pi, clockwise: true) }
224
+ path.addLine(to: CGPoint(x: 0, y: tl))
225
+ if tl > 0 { path.addArc(withCenter: CGPoint(x: tl, y: tl), radius: tl, startAngle: .pi, endAngle: .pi + halfPi, clockwise: true) }
226
+ path.close()
227
+ let shape = CAShapeLayer()
228
+ shape.frame = CGRect(origin: .zero, size: size)
229
+ shape.path = path.cgPath
230
+ view.layer.mask = shape
231
+ }
232
+
233
+ // MARK: - Shadow
234
+
235
+ static func applyShadow(_ view: UIView, _ props: [String: Any]) {
236
+ let layer = view.layer
237
+ let hasShadow = shadowKeys.contains { PNProps.value(props, $0) != nil }
238
+ if hasShadow, PNProps.string(PNProps.value(props, "overflow")) != "hidden" {
239
+ layer.masksToBounds = false
240
+ view.clipsToBounds = false
241
+ }
242
+ if let color = PNColor.parse(PNProps.value(props, "shadow_color")) {
243
+ layer.shadowColor = color.cgColor
244
+ }
245
+ if let opacity = PNProps.double(PNProps.value(props, "shadow_opacity")) {
246
+ layer.shadowOpacity = Float(opacity)
247
+ }
248
+ if let radius = PNProps.double(PNProps.value(props, "shadow_radius")) {
249
+ layer.shadowRadius = CGFloat(radius)
250
+ }
251
+ if let offset = PNProps.value(props, "shadow_offset") {
252
+ let (dx, dy) = shadowOffset(offset)
253
+ layer.shadowOffset = CGSize(width: dx, height: dy)
254
+ }
255
+ }
256
+
257
+ /// Coerce a `shadow_offset` / `text_shadow_offset` value to `(dx, dy)`.
258
+ public static func shadowOffset(_ value: Any?) -> (CGFloat, CGFloat) {
259
+ if let dict = value as? [String: Any] {
260
+ return (CGFloat(PNProps.double(dict["width"]) ?? 0), CGFloat(PNProps.double(dict["height"]) ?? 0))
261
+ }
262
+ if let list = value as? [Any], list.count >= 2 {
263
+ return (CGFloat(PNProps.double(list[0]) ?? 0), CGFloat(PNProps.double(list[1]) ?? 0))
264
+ }
265
+ return (0, 0)
266
+ }
267
+
268
+ // MARK: - Interaction
269
+
270
+ static func applyInteraction(_ view: UIView, _ props: [String: Any]) {
271
+ guard let container = view as? PNContainerView else {
272
+ PNLog.once(PNLog.components, key: "interaction:\(PNViewState.existing(for: view)?.tag ?? 0)",
273
+ "pointer_events / hit_slop only work on container elements; ignored")
274
+ return
275
+ }
276
+ if PNProps.has(props, "pointer_events") {
277
+ let mode = PNProps.string(PNProps.value(props, "pointer_events"))
278
+ container.pointerEvents = ["none", "box_none", "box_only"].contains(mode ?? "") ? mode : nil
279
+ }
280
+ if PNProps.has(props, "hit_slop") {
281
+ container.hitSlop = hitSlop(PNProps.value(props, "hit_slop"))
282
+ }
283
+ }
284
+
285
+ /// Normalize a `hit_slop` prop into edge insets.
286
+ public static func hitSlop(_ value: Any?) -> UIEdgeInsets {
287
+ if let dict = value as? [String: Any] {
288
+ return UIEdgeInsets(
289
+ top: CGFloat(PNProps.double(dict["top"]) ?? 0), left: CGFloat(PNProps.double(dict["left"]) ?? 0),
290
+ bottom: CGFloat(PNProps.double(dict["bottom"]) ?? 0), right: CGFloat(PNProps.double(dict["right"]) ?? 0)
291
+ )
292
+ }
293
+ if let uniform = PNProps.double(value) {
294
+ let v = CGFloat(uniform)
295
+ return UIEdgeInsets(top: v, left: v, bottom: v, right: v)
296
+ }
297
+ return .zero
298
+ }
299
+
300
+ // MARK: - Accessibility
301
+
302
+ static let traitByRole: [String: UIAccessibilityTraits] = [
303
+ "button": .button, "link": .link, "image": .image, "search": .searchField,
304
+ "keyboard_key": .keyboardKey, "static_text": .staticText, "summary_element": .summaryElement,
305
+ "adjustable": .adjustable, "header": .header, "selected": .selected, "checkbox": .button, "none": [],
306
+ ]
307
+
308
+ static func applyAccessibility(_ view: UIView, _ props: [String: Any]) {
309
+ if PNProps.has(props, "accessible") {
310
+ view.isAccessibilityElement = PNProps.bool(PNProps.value(props, "accessible")) ?? false
311
+ }
312
+ if PNProps.has(props, "accessibility_label") {
313
+ view.accessibilityLabel = PNProps.string(PNProps.value(props, "accessibility_label")) ?? ""
314
+ }
315
+ if PNProps.has(props, "accessibility_hint") {
316
+ view.accessibilityHint = PNProps.string(PNProps.value(props, "accessibility_hint")) ?? ""
317
+ }
318
+ if PNProps.has(props, "test_id") {
319
+ view.accessibilityIdentifier = PNProps.string(PNProps.value(props, "test_id"))
320
+ }
321
+ let role = PNProps.string(PNProps.value(props, "accessibility_role"))
322
+ let state = PNProps.dict(PNProps.value(props, "accessibility_state"))
323
+ if role == nil, state == nil { return }
324
+ var traits: UIAccessibilityTraits = []
325
+ if let role = role {
326
+ traits.formUnion(traitByRole[role.lowercased()] ?? [])
327
+ }
328
+ if let state = state {
329
+ if PNProps.bool(state["selected"]) == true || PNProps.bool(state["checked"]) == true {
330
+ traits.insert(.selected)
331
+ }
332
+ if PNProps.bool(state["disabled"]) == true { traits.insert(.notEnabled) }
333
+ if PNProps.bool(state["busy"]) == true { traits.insert(.updatesFrequently) }
334
+ if let expanded = PNProps.bool(state["expanded"]) {
335
+ view.accessibilityValue = expanded ? "expanded" : "collapsed"
336
+ }
337
+ }
338
+ view.accessibilityTraits = traits
339
+ }
340
+ }
@@ -0,0 +1,257 @@
1
+ import UIKit
2
+
3
+ /// `VirtualList`: a `UITableView` whose rows host PythonNative subtrees.
4
+ ///
5
+ /// Rows are requested synchronously from Python: `on_bind_row` with
6
+ /// `[{"container": key, "index": i, "width": w, "height": h}]` returns
7
+ /// `{"root": tag}`, and the view with that tag is added to the cell's
8
+ /// content view. Recycled containers emit `on_unbind_row` with
9
+ /// `[{"container": key}]`.
10
+ public final class PNVirtualListManager: PNComponentManager {
11
+ public override func makeView(props: [String: Any]) -> UIView {
12
+ let table = UITableView(frame: .zero, style: .plain)
13
+ table.separatorStyle = .none
14
+ table.contentInsetAdjustmentBehavior = .never
15
+ table.register(PNVirtualListCell.self, forCellReuseIdentifier: PNVirtualListCell.reuseIdentifier)
16
+ table.rowHeight = 44
17
+ table.estimatedRowHeight = 0
18
+ table.estimatedSectionHeaderHeight = 0
19
+ table.estimatedSectionFooterHeight = 0
20
+ if #available(iOS 15.0, *) {
21
+ table.sectionHeaderTopPadding = 0
22
+ }
23
+ return table
24
+ }
25
+
26
+ public override func createView(tag: Int64, props: [String: Any]) -> UIView {
27
+ let view = super.createView(tag: tag, props: props)
28
+ if let table = view as? UITableView {
29
+ let source = PNVirtualListSource(table: table)
30
+ table.dataSource = source
31
+ table.delegate = source
32
+ PNViewState.existing(for: table)?.retained.append(source)
33
+ }
34
+ return view
35
+ }
36
+
37
+ public override func teardown(view: UIView) {
38
+ guard let table = view as? UITableView else { return }
39
+ // The list's `d` op tells Python to unmount every subtree, so the
40
+ // cells only need to drop their hosted views.
41
+ for cell in table.visibleCells {
42
+ (cell as? PNVirtualListCell)?.unhost()
43
+ }
44
+ table.dataSource = nil
45
+ table.delegate = nil
46
+ }
47
+
48
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
49
+ guard let table = view as? UITableView, let state = PNViewState.existing(for: table) else { return }
50
+ PNViewStyler.applyCommon(table, props)
51
+ var needsReload = false
52
+ if PNProps.has(props, "count") || PNProps.has(props, "row_height") || PNProps.has(props, "row_heights") {
53
+ needsReload = true
54
+ }
55
+ if PNProps.has(props, "row_height") {
56
+ table.rowHeight = CGFloat(PNProps.double(PNProps.value(props, "row_height")) ?? 44)
57
+ }
58
+ if PNProps.has(props, "shows_scroll_indicator") {
59
+ table.showsVerticalScrollIndicator = PNProps.bool(PNProps.value(props, "shows_scroll_indicator")) ?? true
60
+ }
61
+ if PNProps.has(props, "scroll_enabled") {
62
+ table.isScrollEnabled = PNProps.bool(PNProps.value(props, "scroll_enabled")) ?? true
63
+ }
64
+ if PNProps.has(props, "bounces") {
65
+ table.bounces = PNProps.bool(PNProps.value(props, "bounces")) ?? true
66
+ }
67
+ if PNProps.has(props, "separator") {
68
+ table.separatorStyle = PNProps.bool(PNProps.value(props, "separator")) == true ? .singleLine : .none
69
+ }
70
+ if PNProps.has(props, "content_inset") {
71
+ let inset = PNProps.dict(PNProps.value(props, "content_inset")) ?? [:]
72
+ table.contentInset = UIEdgeInsets(
73
+ top: CGFloat(PNProps.double(inset["top"]) ?? 0), left: CGFloat(PNProps.double(inset["left"]) ?? 0),
74
+ bottom: CGFloat(PNProps.double(inset["bottom"]) ?? 0), right: CGFloat(PNProps.double(inset["right"]) ?? 0)
75
+ )
76
+ }
77
+ if let mode = PNProps.string(PNProps.value(props, "keyboard_dismiss_mode")) {
78
+ switch mode {
79
+ case "on_drag": table.keyboardDismissMode = .onDrag
80
+ case "interactive": table.keyboardDismissMode = .interactive
81
+ default: table.keyboardDismissMode = .none
82
+ }
83
+ }
84
+ if PNProps.has(props, "generation") || PNProps.has(props, "data_version") {
85
+ needsReload = true
86
+ }
87
+ if needsReload, !initial {
88
+ state.extras["reload_pending"] = true
89
+ table.reloadData()
90
+ state.extras["reload_pending"] = false
91
+ }
92
+ }
93
+
94
+ public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {
95
+ let before = view.bounds.size
96
+ super.setFrame(view: view, x: x, y: y, w: w, h: h)
97
+ if let table = view as? UITableView, before.width != table.bounds.width, before.width > 0 {
98
+ table.reloadData()
99
+ }
100
+ }
101
+
102
+ public override func insertChild(parent: UIView, child: UIView, index: Int) {
103
+ // Row subtrees are attached by the data source, never by the
104
+ // reconciler; static children are not supported.
105
+ PNLog.once(PNLog.components, key: "virtuallist-child", "VirtualList ignores direct children; rows come from on_bind_row")
106
+ }
107
+
108
+ public override func command(view: UIView, name: String, args: [String: Any]) -> Any? {
109
+ guard let table = view as? UITableView else { return nil }
110
+ let animated = PNProps.bool(args["animated"]) ?? true
111
+ switch name {
112
+ case "scroll_to_offset":
113
+ let y = CGFloat(PNProps.finite(args["y"] ?? args["offset"]))
114
+ let x = CGFloat(PNProps.finite(args["x"]))
115
+ table.setContentOffset(CGPoint(x: x, y: y), animated: animated)
116
+ case "scroll_to_index":
117
+ let count = table.numberOfRows(inSection: 0)
118
+ guard count > 0, let index = PNProps.int(args["index"]) else { return nil }
119
+ let row = max(0, min(index, count - 1))
120
+ let position: UITableView.ScrollPosition
121
+ switch PNProps.string(args["position"]) {
122
+ case "middle", "center": position = .middle
123
+ case "bottom", "end": position = .bottom
124
+ case "top", "start": position = .top
125
+ default: position = .none
126
+ }
127
+ table.scrollToRow(at: IndexPath(row: row, section: 0), at: position, animated: animated)
128
+ case "scroll_to_end":
129
+ let count = table.numberOfRows(inSection: 0)
130
+ if count > 0 {
131
+ table.scrollToRow(at: IndexPath(row: count - 1, section: 0), at: .bottom, animated: animated)
132
+ }
133
+ case "get_scroll_offset":
134
+ return ["x": Double(table.contentOffset.x), "y": Double(table.contentOffset.y)]
135
+ case "reload":
136
+ table.reloadData()
137
+ case "flash_scroll_indicators":
138
+ table.flashScrollIndicators()
139
+ default:
140
+ break
141
+ }
142
+ return nil
143
+ }
144
+ }
145
+
146
+ /// A recycled row container. `containerKey` is stable for the cell's lifetime.
147
+ final class PNVirtualListCell: UITableViewCell {
148
+ static let reuseIdentifier = "PNVirtualListCell"
149
+ private static var nextKey: Int64 = 1
150
+
151
+ let containerKey: Int64
152
+ private(set) var hosted: UIView?
153
+
154
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
155
+ containerKey = PNVirtualListCell.nextKey
156
+ PNVirtualListCell.nextKey += 1
157
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
158
+ selectionStyle = .none
159
+ backgroundColor = .clear
160
+ contentView.clipsToBounds = true
161
+ }
162
+
163
+ required init?(coder: NSCoder) {
164
+ containerKey = PNVirtualListCell.nextKey
165
+ PNVirtualListCell.nextKey += 1
166
+ super.init(coder: coder)
167
+ }
168
+
169
+ func host(_ view: UIView) {
170
+ if hosted === view { return }
171
+ hosted?.removeFromSuperview()
172
+ hosted = view
173
+ view.translatesAutoresizingMaskIntoConstraints = true
174
+ contentView.addSubview(view)
175
+ }
176
+
177
+ func unhost() {
178
+ hosted?.removeFromSuperview()
179
+ hosted = nil
180
+ }
181
+ }
182
+
183
+ /// Data source and delegate for one list.
184
+ final class PNVirtualListSource: NSObject, UITableViewDataSource, UITableViewDelegate {
185
+ private weak var table: UITableView?
186
+ private var bound: Set<Int64> = []
187
+
188
+ init(table: UITableView) {
189
+ self.table = table
190
+ super.init()
191
+ }
192
+
193
+ private var props: [String: Any] {
194
+ table.flatMap { PNViewState.existing(for: $0)?.props } ?? [:]
195
+ }
196
+
197
+ // MARK: UITableViewDataSource
198
+
199
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
200
+ max(0, PNProps.int(PNProps.value(props, "count")) ?? 0)
201
+ }
202
+
203
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
204
+ let props = self.props
205
+ if let heights = PNProps.value(props, "row_heights") as? [Any], indexPath.row < heights.count,
206
+ let height = PNProps.double(heights[indexPath.row])
207
+ {
208
+ return CGFloat(height)
209
+ }
210
+ return CGFloat(PNProps.double(PNProps.value(props, "row_height")) ?? Double(tableView.rowHeight))
211
+ }
212
+
213
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
214
+ let dequeued = tableView.dequeueReusableCell(withIdentifier: PNVirtualListCell.reuseIdentifier, for: indexPath)
215
+ guard let cell = dequeued as? PNVirtualListCell else { return dequeued }
216
+ let width = tableView.bounds.width
217
+ let height = self.tableView(tableView, heightForRowAt: indexPath)
218
+ bind(cell, index: indexPath.row, width: width, height: height)
219
+ return cell
220
+ }
221
+
222
+ private func bind(_ cell: PNVirtualListCell, index: Int, width: CGFloat, height: CGFloat) {
223
+ guard let table = table else { return }
224
+ let payload: [String: Any] = [
225
+ "container": cell.containerKey, "index": index, "width": Double(width), "height": Double(height),
226
+ ]
227
+ bound.insert(cell.containerKey)
228
+ guard let reply = PNEvents.emit(table, "on_bind_row", [payload]) else {
229
+ cell.unhost()
230
+ return
231
+ }
232
+ let root = PNProps.dict(PNJSON.decode(reply))?["root"]
233
+ guard let tag = PNProps.int(root), let view = PNViewRegistry.shared.view(for: Int64(tag)) else {
234
+ cell.unhost()
235
+ return
236
+ }
237
+ cell.host(view)
238
+ }
239
+
240
+ func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
241
+ guard let cell = cell as? PNVirtualListCell, let table = table else { return }
242
+ cell.unhost()
243
+ if bound.remove(cell.containerKey) != nil {
244
+ PNEvents.emit(table, "on_unbind_row", [["container": cell.containerKey]])
245
+ }
246
+ }
247
+
248
+ // MARK: UIScrollViewDelegate
249
+
250
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
251
+ PNEvents.emitIfWired(scrollView, "on_scroll", [PNScrollPayload.make(scrollView)])
252
+ }
253
+
254
+ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
255
+ PNEvents.emitIfWired(scrollView, "on_momentum_scroll_end", [PNScrollPayload.make(scrollView)])
256
+ }
257
+ }