pythonnative 0.36.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/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/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.36.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.36.0.dist-info/RECORD +0 -155
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import PythonNativeKit
|
|
3
|
+
|
|
4
|
+
final class PNGestureTests: XCTestCase {
|
|
5
|
+
override func tearDown() {
|
|
6
|
+
PNViewRegistry.shared.removeAll()
|
|
7
|
+
super.tearDown()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
func testSpecsBecomeRecognizersWithRelationships() {
|
|
11
|
+
let manager = PNViewManager()
|
|
12
|
+
let view = manager.createView(tag: 1, props: [
|
|
13
|
+
"gestures": [
|
|
14
|
+
["kind": "tap", "n_taps": 2, "simultaneous": [1], "wait_for": []],
|
|
15
|
+
["kind": "pan", "min_distance": 10, "simultaneous": [0], "wait_for": []],
|
|
16
|
+
["kind": "long_press", "min_duration_ms": 800, "simultaneous": [], "wait_for": [0]],
|
|
17
|
+
["kind": "swipe", "direction": "left", "simultaneous": [], "wait_for": []],
|
|
18
|
+
["kind": "pinch", "simultaneous": [], "wait_for": []],
|
|
19
|
+
["kind": "rotation", "simultaneous": [], "wait_for": []],
|
|
20
|
+
["kind": "fling", "direction": "down", "n_pointers": 2, "simultaneous": [], "wait_for": []],
|
|
21
|
+
],
|
|
22
|
+
])
|
|
23
|
+
let recognizers = PNViewState.existing(for: view)?.gestureRecognizers ?? []
|
|
24
|
+
XCTAssertEqual(recognizers.count, 7)
|
|
25
|
+
XCTAssertEqual((recognizers[0] as? UITapGestureRecognizer)?.numberOfTapsRequired, 2)
|
|
26
|
+
XCTAssertTrue(recognizers[1] is UIPanGestureRecognizer)
|
|
27
|
+
XCTAssertEqual((recognizers[2] as? UILongPressGestureRecognizer)?.minimumPressDuration ?? 0, 0.8, accuracy: 0.001)
|
|
28
|
+
XCTAssertEqual((recognizers[3] as? UISwipeGestureRecognizer)?.direction, .left)
|
|
29
|
+
XCTAssertTrue(recognizers[4] is UIPinchGestureRecognizer)
|
|
30
|
+
XCTAssertTrue(recognizers[5] is UIRotationGestureRecognizer)
|
|
31
|
+
XCTAssertEqual((recognizers[6] as? UISwipeGestureRecognizer)?.numberOfTouchesRequired, 2)
|
|
32
|
+
XCTAssertEqual((recognizers[6] as? UISwipeGestureRecognizer)?.direction, .down)
|
|
33
|
+
|
|
34
|
+
let coordinator = PNGestureCoordinator.shared
|
|
35
|
+
XCTAssertEqual(coordinator.index(of: recognizers[2]), 2)
|
|
36
|
+
XCTAssertTrue(coordinator.allowsSimultaneous(recognizers[0], recognizers[1]))
|
|
37
|
+
XCTAssertFalse(coordinator.allowsSimultaneous(recognizers[0], recognizers[2]))
|
|
38
|
+
XCTAssertFalse(coordinator.allowsSimultaneous(recognizers[3], recognizers[4]))
|
|
39
|
+
XCTAssertTrue(view.isUserInteractionEnabled)
|
|
40
|
+
XCTAssertTrue(recognizers.allSatisfy { $0.view === view && !$0.cancelsTouchesInView })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func testRewiringReplacesRecognizersAndUnknownKindsAreSkipped() {
|
|
44
|
+
let manager = PNViewManager()
|
|
45
|
+
let view = manager.createView(tag: 2, props: ["gestures": [["kind": "tap"]]])
|
|
46
|
+
XCTAssertEqual(view.gestureRecognizers?.count, 1)
|
|
47
|
+
manager.update(view: view, changed: ["gestures": [["kind": "nonsense"], ["kind": "pan"]]])
|
|
48
|
+
let recognizers = PNViewState.existing(for: view)?.gestureRecognizers ?? []
|
|
49
|
+
XCTAssertEqual(recognizers.count, 1)
|
|
50
|
+
XCTAssertTrue(recognizers[0] is UIPanGestureRecognizer)
|
|
51
|
+
XCTAssertEqual(PNGestureCoordinator.shared.index(of: recognizers[0]), 1, "index follows the spec position")
|
|
52
|
+
manager.update(view: view, changed: ["gestures": NSNull()])
|
|
53
|
+
XCTAssertEqual(view.gestureRecognizers?.count ?? 0, 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testStateAndDirectionNames() {
|
|
57
|
+
XCTAssertEqual(PNGestureCoordinator.stateName(.began), "began")
|
|
58
|
+
XCTAssertEqual(PNGestureCoordinator.stateName(.changed), "changed")
|
|
59
|
+
XCTAssertEqual(PNGestureCoordinator.stateName(.ended), "ended")
|
|
60
|
+
XCTAssertEqual(PNGestureCoordinator.stateName(.cancelled), "cancelled")
|
|
61
|
+
XCTAssertEqual(PNGestureCoordinator.swipeDirection("up"), .up)
|
|
62
|
+
XCTAssertEqual(PNGestureCoordinator.swipeDirection("any"), [.left, .right, .up, .down])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func testAnyDirectionSwipeInstallsOneRecognizerPerDirection() {
|
|
66
|
+
let manager = PNViewManager()
|
|
67
|
+
let view = manager.createView(tag: 3, props: [
|
|
68
|
+
"gestures": [
|
|
69
|
+
["kind": "tap", "simultaneous": [], "wait_for": [1]],
|
|
70
|
+
["kind": "swipe", "direction": "any", "simultaneous": [], "wait_for": []],
|
|
71
|
+
],
|
|
72
|
+
])
|
|
73
|
+
let recognizers = PNViewState.existing(for: view)?.gestureRecognizers ?? []
|
|
74
|
+
XCTAssertEqual(recognizers.count, 5, "one tap plus four single-direction swipes")
|
|
75
|
+
let coordinator = PNGestureCoordinator.shared
|
|
76
|
+
let swipes = recognizers.compactMap { $0 as? UISwipeGestureRecognizer }
|
|
77
|
+
XCTAssertEqual(swipes.map { coordinator.direction(of: $0) }, ["left", "right", "up", "down"])
|
|
78
|
+
XCTAssertEqual(swipes.map { $0.direction }, [.left, .right, .up, .down])
|
|
79
|
+
XCTAssertTrue(swipes.allSatisfy { coordinator.index(of: $0) == 1 }, "every recognizer reports the spec index")
|
|
80
|
+
XCTAssertNil(coordinator.direction(of: recognizers[0]))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func testAnimatorTimingParameters() {
|
|
84
|
+
let timing = PNAnimator.timingParameters(kind: "timing", spec: ["duration_ms": 250, "easing": "linear"])
|
|
85
|
+
XCTAssertEqual(timing.duration, 0.25, accuracy: 0.0001)
|
|
86
|
+
let spring = PNAnimator.timingParameters(kind: "spring", spec: ["stiffness": 100, "damping": 10, "mass": 1, "from": 0, "to": 1])
|
|
87
|
+
XCTAssertGreaterThan(spring.duration, 0.15)
|
|
88
|
+
XCTAssertTrue(spring.parameters is UISpringTimingParameters)
|
|
89
|
+
XCTAssertTrue(PNAnimator.curve(for: [0.1, 0.2, 0.3, 0.4]) is UICubicTimingParameters)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func testAnimatorSetAppliesImmediately() {
|
|
93
|
+
PNTransaction.apply("[[\"c\", 50, \"View\", {}]]")
|
|
94
|
+
_ = PNAnimator.shared.handle(tag: 50, request: ["op": "set", "prop": "opacity", "value": 0.25])
|
|
95
|
+
XCTAssertEqual(PNViewRegistry.shared.view(for: 50)?.alpha ?? 0, 0.25, accuracy: 0.001)
|
|
96
|
+
_ = PNAnimator.shared.handle(tag: 50, request: ["op": "set", "prop": "translate_x", "value": 12])
|
|
97
|
+
XCTAssertEqual(PNViewRegistry.shared.view(for: 50)?.transform.tx ?? 0, 12, accuracy: 0.001)
|
|
98
|
+
let rejected = PNAnimator.shared.handle(tag: 50, request: ["op": "start", "id": 1, "prop": "opacity", "spec": ["kind": "mystery"]])
|
|
99
|
+
XCTAssertEqual((rejected as? [String: Any])?["ok"] as? Bool, false)
|
|
100
|
+
}
|
|
101
|
+
}
|
pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import PythonNativeKit
|
|
3
|
+
|
|
4
|
+
final class PNManagerTests: XCTestCase {
|
|
5
|
+
override func tearDown() {
|
|
6
|
+
PNViewRegistry.shared.removeAll()
|
|
7
|
+
super.tearDown()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
func testViewManagerAppliesCommonProps() {
|
|
11
|
+
let manager = PNViewManager()
|
|
12
|
+
let view = manager.createView(tag: 1, props: [
|
|
13
|
+
"background_color": "#ff0000",
|
|
14
|
+
"opacity": 0.5,
|
|
15
|
+
"border_width": 2,
|
|
16
|
+
"border_color": "#00ff00",
|
|
17
|
+
"border_radius": 8,
|
|
18
|
+
"test_id": "hero",
|
|
19
|
+
"accessibility_label": "Hero card",
|
|
20
|
+
])
|
|
21
|
+
XCTAssertEqual(view.alpha, 0.5, accuracy: 0.001)
|
|
22
|
+
XCTAssertEqual(view.layer.borderWidth, 2)
|
|
23
|
+
XCTAssertEqual(view.layer.cornerRadius, 8)
|
|
24
|
+
XCTAssertEqual(view.accessibilityIdentifier, "hero")
|
|
25
|
+
XCTAssertEqual(view.accessibilityLabel, "Hero card")
|
|
26
|
+
XCTAssertEqual(PNColor.hexString(view.backgroundColor ?? .clear).lowercased(), "#ffff0000")
|
|
27
|
+
|
|
28
|
+
manager.update(view: view, changed: ["opacity": 1, "background_color": NSNull(), "display": "none"])
|
|
29
|
+
XCTAssertEqual(view.alpha, 1, accuracy: 0.001)
|
|
30
|
+
XCTAssertTrue(view.isHidden)
|
|
31
|
+
XCTAssertNil(PNViewState.existing(for: view)?.props["background_color"])
|
|
32
|
+
XCTAssertEqual(PNProps.double(PNViewState.existing(for: view)?.props["opacity"]), 1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func testTransformPropBuildsAffineTransform() {
|
|
36
|
+
let manager = PNViewManager()
|
|
37
|
+
let view = manager.createView(tag: 2, props: [
|
|
38
|
+
"transform": [["translate_x": 10], ["translate_y": 5], ["scale": 2]],
|
|
39
|
+
])
|
|
40
|
+
XCTAssertEqual(view.transform.tx, 10, accuracy: 0.001)
|
|
41
|
+
XCTAssertEqual(view.transform.ty, 5, accuracy: 0.001)
|
|
42
|
+
XCTAssertEqual(view.transform.a, 2, accuracy: 0.001)
|
|
43
|
+
XCTAssertEqual(view.transform.d, 2, accuracy: 0.001)
|
|
44
|
+
|
|
45
|
+
manager.update(view: view, changed: ["transform": [["rotate": "90deg"]]])
|
|
46
|
+
XCTAssertEqual(view.transform.b, 1, accuracy: 0.001)
|
|
47
|
+
XCTAssertEqual(view.transform.a, 0, accuracy: 0.001)
|
|
48
|
+
|
|
49
|
+
manager.update(view: view, changed: ["transform": NSNull()])
|
|
50
|
+
XCTAssertTrue(view.transform.isIdentity)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func testFrameSurvivesTransform() {
|
|
54
|
+
let manager = PNViewManager()
|
|
55
|
+
let view = manager.createView(tag: 3, props: ["transform": [["scale": 2]]])
|
|
56
|
+
manager.setFrame(view: view, x: 10, y: 20, w: 100, h: 50)
|
|
57
|
+
XCTAssertEqual(view.bounds.size, CGSize(width: 100, height: 50))
|
|
58
|
+
XCTAssertEqual(view.center, CGPoint(x: 60, y: 45))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func testTextMeasureAndTransform() {
|
|
62
|
+
let manager = PNTextManager()
|
|
63
|
+
let view = manager.createView(tag: 4, props: ["text": "hello world", "font_size": 20, "text_transform": "uppercase"])
|
|
64
|
+
XCTAssertEqual((view as? UILabel)?.text, "HELLO WORLD")
|
|
65
|
+
let unconstrained = manager.measure(view: view, maxW: .infinity, maxH: .infinity)
|
|
66
|
+
XCTAssertGreaterThan(unconstrained.width, 50)
|
|
67
|
+
XCTAssertGreaterThan(unconstrained.height, 15)
|
|
68
|
+
let narrow = manager.measure(view: view, maxW: 60, maxH: 1e6)
|
|
69
|
+
XCTAssertLessThanOrEqual(narrow.width, 60)
|
|
70
|
+
XCTAssertGreaterThan(narrow.height, unconstrained.height, "wrapped text grows in height")
|
|
71
|
+
XCTAssertEqual(PNTextManager.transform("hello big world", mode: "capitalize"), "Hello Big World")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func testTextSpansProduceAttributedString() {
|
|
75
|
+
let manager = PNTextManager()
|
|
76
|
+
let view = manager.createView(tag: 5, props: [
|
|
77
|
+
"spans": [["text": "Hi "], ["text": "there", "bold": true, "color": "#ff0000"]],
|
|
78
|
+
])
|
|
79
|
+
let label = view as? UILabel
|
|
80
|
+
XCTAssertEqual(label?.attributedText?.string, "Hi there")
|
|
81
|
+
var range = NSRange()
|
|
82
|
+
let color = label?.attributedText?.attribute(.foregroundColor, at: 3, effectiveRange: &range) as? UIColor
|
|
83
|
+
XCTAssertNotNil(color)
|
|
84
|
+
XCTAssertEqual(range.location, 3)
|
|
85
|
+
XCTAssertEqual(range.length, 5)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func testVirtualListBindsRowsThroughRegistry() {
|
|
89
|
+
PNTransaction.apply("[[\"c\", 40, \"View\", {\"background_color\": \"#123456\"}]]")
|
|
90
|
+
let manager = PNVirtualListManager()
|
|
91
|
+
guard let table = manager.createView(tag: 41, props: ["count": 3, "row_height": 50]) as? UITableView else {
|
|
92
|
+
return XCTFail("expected a table")
|
|
93
|
+
}
|
|
94
|
+
XCTAssertEqual(table.dataSource?.tableView(table, numberOfRowsInSection: 0), 3)
|
|
95
|
+
XCTAssertEqual(table.delegate?.tableView?(table, heightForRowAt: IndexPath(row: 1, section: 0)), 50)
|
|
96
|
+
manager.update(view: table, changed: ["row_heights": [10, 20, 30]])
|
|
97
|
+
XCTAssertEqual(table.delegate?.tableView?(table, heightForRowAt: IndexPath(row: 2, section: 0)), 30)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func testScrollPayloadShape() {
|
|
101
|
+
let scroll = UIScrollView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
|
|
102
|
+
scroll.contentSize = CGSize(width: 100, height: 800)
|
|
103
|
+
scroll.contentOffset = CGPoint(x: 0, y: 40)
|
|
104
|
+
let payload = PNScrollPayload.make(scroll)
|
|
105
|
+
XCTAssertEqual(payload["y"] as? Double, 40)
|
|
106
|
+
XCTAssertEqual(payload["extent"] as? Double, 200)
|
|
107
|
+
XCTAssertEqual(payload["range"] as? Double, 800)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func testRegistryKnowsEveryBuiltinType() {
|
|
111
|
+
let names = PNRegistry.shared.componentNames
|
|
112
|
+
for type in ["View", "Column", "Row", "Text", "Button", "TextInput", "Image", "Switch", "ProgressBar",
|
|
113
|
+
"ActivityIndicator", "WebView", "Spacer", "ScrollView", "SafeAreaView", "Modal", "Portal",
|
|
114
|
+
"Slider", "TabBar", "Pressable", "StatusBar", "KeyboardAvoidingView", "Picker", "Checkbox",
|
|
115
|
+
"SegmentedControl", "DatePicker", "VirtualList"] {
|
|
116
|
+
XCTAssertTrue(names.contains(type), "missing manager for \(type)")
|
|
117
|
+
}
|
|
118
|
+
for module in ["Host", "Device", "Alert", "Storage", "SecureStore", "Clipboard", "Share", "Linking",
|
|
119
|
+
"Haptics", "Battery", "NetInfo", "AppState", "Permissions", "Notifications", "Camera",
|
|
120
|
+
"Location", "Biometrics"] {
|
|
121
|
+
XCTAssertTrue(PNRegistry.shared.moduleNames.contains(module), "missing module \(module)")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import PythonNativeKit
|
|
3
|
+
|
|
4
|
+
/// A module that settles inline or later depending on the method.
|
|
5
|
+
final class PNTestEchoModule: PNNativeModule {
|
|
6
|
+
static let name = "TestEcho"
|
|
7
|
+
static var pending: PNPromise?
|
|
8
|
+
|
|
9
|
+
init() {}
|
|
10
|
+
|
|
11
|
+
func call(_ method: String, args: [String: Any], promise: PNPromise) {
|
|
12
|
+
switch method {
|
|
13
|
+
case "echo": promise.resolve(args["value"])
|
|
14
|
+
case "fail": promise.reject("nope", code: "test_code")
|
|
15
|
+
case "later": PNTestEchoModule.pending = promise
|
|
16
|
+
default: promise.reject("unknown method \(method)")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
final class PNModuleTests: XCTestCase {
|
|
22
|
+
override func setUp() {
|
|
23
|
+
super.setUp()
|
|
24
|
+
PNRegistry.shared.registerModule(PNTestEchoModule.self)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testSynchronousResolveProducesOkEnvelope() {
|
|
28
|
+
let result = PNModuleDispatcher.shared.call(module: "TestEcho", method: "echo", envelope: ["call_id": 7, "args": ["value": 42]])
|
|
29
|
+
XCTAssertEqual(result["ok"] as? Bool, true)
|
|
30
|
+
XCTAssertEqual(result["value"] as? Int, 42)
|
|
31
|
+
XCTAssertNil(result["pending"])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func testRejectProducesErrorEnvelope() {
|
|
35
|
+
let result = PNModuleDispatcher.shared.call(module: "TestEcho", method: "fail", envelope: ["call_id": 8, "args": [:]])
|
|
36
|
+
XCTAssertEqual(result["ok"] as? Bool, false)
|
|
37
|
+
XCTAssertEqual(result["error"] as? String, "nope")
|
|
38
|
+
XCTAssertEqual(result["code"] as? String, "test_code")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testUnknownModuleAndMethod() {
|
|
42
|
+
let missing = PNModuleDispatcher.shared.call(module: "Nope", method: "x", envelope: [:])
|
|
43
|
+
XCTAssertEqual(missing["ok"] as? Bool, false)
|
|
44
|
+
XCTAssertEqual(missing["code"] as? String, "unknown_module")
|
|
45
|
+
let method = PNModuleDispatcher.shared.call(module: "TestEcho", method: "zzz", envelope: [:])
|
|
46
|
+
XCTAssertEqual(method["ok"] as? Bool, false)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func testAsynchronousSettlementReturnsPending() {
|
|
50
|
+
let result = PNModuleDispatcher.shared.call(module: "TestEcho", method: "later", envelope: ["call_id": 9, "args": [:]])
|
|
51
|
+
XCTAssertEqual(result["pending"] as? Bool, true)
|
|
52
|
+
guard let promise = PNTestEchoModule.pending else { return XCTFail("promise not captured") }
|
|
53
|
+
XCTAssertFalse(promise.isSettled)
|
|
54
|
+
XCTAssertEqual(promise.callId, 9)
|
|
55
|
+
promise.resolve("done")
|
|
56
|
+
XCTAssertTrue(promise.isSettled)
|
|
57
|
+
promise.resolve("again")
|
|
58
|
+
XCTAssertEqual(promise.result?["value"] as? String, "done", "second settlement is ignored")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func testEnvelopeJSONRoundTrip() {
|
|
62
|
+
let ok = PNJSON.encode(["ok": true, "value": ["a": 1]])
|
|
63
|
+
let decoded = PNJSON.decodeObject(ok)
|
|
64
|
+
XCTAssertEqual(decoded["ok"] as? Bool, true)
|
|
65
|
+
XCTAssertEqual((decoded["value"] as? [String: Any])?["a"] as? Int, 1)
|
|
66
|
+
XCTAssertEqual(PNJSON.decodeObject(PNJSON.encode(["pending": true]))["pending"] as? Bool, true)
|
|
67
|
+
XCTAssertEqual(PNJSON.encode(Double.infinity), "null", "non-finite numbers never leave Swift as JSON numbers")
|
|
68
|
+
XCTAssertEqual(PNJSON.resolveInfinity("inf") as? Double, Double.infinity)
|
|
69
|
+
XCTAssertEqual(PNJSON.encode(nil), "null")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func testDeviceInfoShape() {
|
|
73
|
+
let info = DeviceModule.info()
|
|
74
|
+
XCTAssertEqual(info["os"] as? String, "ios")
|
|
75
|
+
XCTAssertNotNil(info["os_version"])
|
|
76
|
+
XCTAssertNotNil(info["app_dir"])
|
|
77
|
+
XCTAssertNotNil(info["cache_dir"])
|
|
78
|
+
XCTAssertNotNil(info["scale"])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func testStorageModuleRoundTrip() {
|
|
82
|
+
let storage = StorageModule()
|
|
83
|
+
_ = PNModuleDispatcher.shared.call(module: "Storage", method: "clear", envelope: [:])
|
|
84
|
+
let set = PNPromise(callId: 1, module: "Storage", method: "set")
|
|
85
|
+
storage.call("set", args: ["key": "k", "value": "v"], promise: set)
|
|
86
|
+
let get = PNPromise(callId: 2, module: "Storage", method: "get")
|
|
87
|
+
storage.call("get", args: ["key": "k"], promise: get)
|
|
88
|
+
XCTAssertEqual(get.result?["value"] as? String, "v")
|
|
89
|
+
let keys = PNPromise(callId: 3, module: "Storage", method: "all_keys")
|
|
90
|
+
storage.call("all_keys", args: [:], promise: keys)
|
|
91
|
+
XCTAssertEqual(keys.result?["value"] as? [String], ["k"])
|
|
92
|
+
let del = PNPromise(callId: 4, module: "Storage", method: "delete")
|
|
93
|
+
storage.call("delete", args: ["key": "k"], promise: del)
|
|
94
|
+
let missing = PNPromise(callId: 5, module: "Storage", method: "get")
|
|
95
|
+
storage.call("get", args: ["key": "k"], promise: missing)
|
|
96
|
+
XCTAssertTrue(missing.result?["value"] is NSNull)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import PythonNativeKit
|
|
3
|
+
|
|
4
|
+
final class PNTransactionTests: XCTestCase {
|
|
5
|
+
override func tearDown() {
|
|
6
|
+
PNViewRegistry.shared.removeAll()
|
|
7
|
+
super.tearDown()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
func testDecodesEveryOpcode() throws {
|
|
11
|
+
let json = """
|
|
12
|
+
[["c", 1, "View", {"background_color": "#ff0000"}],
|
|
13
|
+
["u", 1, {"opacity": 0.5}],
|
|
14
|
+
["c", 2, "Text", {"text": "hi"}],
|
|
15
|
+
["i", 1, 2, 0],
|
|
16
|
+
["f", 2, 1.5, 2.5, 100, 20],
|
|
17
|
+
["d", 2]]
|
|
18
|
+
"""
|
|
19
|
+
let ops = try PNTransaction.decode(json)
|
|
20
|
+
XCTAssertEqual(ops.count, 6)
|
|
21
|
+
XCTAssertEqual(ops[0], .create(tag: 1, type: "View", props: ["background_color": "#ff0000"]))
|
|
22
|
+
XCTAssertEqual(ops[1], .update(tag: 1, changed: ["opacity": 0.5]))
|
|
23
|
+
XCTAssertEqual(ops[3], .insert(parent: 1, child: 2, index: 0))
|
|
24
|
+
XCTAssertEqual(ops[4], .frame(tag: 2, x: 1.5, y: 2.5, w: 100, h: 20))
|
|
25
|
+
XCTAssertEqual(ops[5], .destroy(tag: 2))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testInfinityStringsBecomeInfinity() throws {
|
|
29
|
+
let ops = try PNTransaction.decode("[[\"c\", 5, \"View\", {\"max_width\": \"inf\", \"nested\": {\"h\": \"inf\"}}]]")
|
|
30
|
+
guard case let .create(_, _, props) = ops[0] else { return XCTFail("expected create") }
|
|
31
|
+
XCTAssertEqual(props["max_width"] as? Double, Double.infinity)
|
|
32
|
+
XCTAssertEqual((props["nested"] as? [String: Any])?["h"] as? Double, Double.infinity)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func testMalformedOpsAreSkippedNotFatal() throws {
|
|
36
|
+
let ops = try PNTransaction.decode("[[\"c\", 1, \"View\", {}], [\"zz\", 9], [\"u\"], 42, [\"d\", 1]]")
|
|
37
|
+
XCTAssertEqual(ops.count, 2)
|
|
38
|
+
XCTAssertEqual(ops[0], .create(tag: 1, type: "View", props: [:]))
|
|
39
|
+
XCTAssertEqual(ops[1], .destroy(tag: 1))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func testNonArrayDocumentThrows() {
|
|
43
|
+
XCTAssertThrowsError(try PNTransaction.decode("{\"not\": \"an array\"}"))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func testApplyBuildsHierarchyAndIsolatesFailures() {
|
|
47
|
+
PNTransaction.apply("""
|
|
48
|
+
[["c", 10, "View", {"background_color": "#00ff00"}],
|
|
49
|
+
["c", 11, "Text", {"text": "hello"}],
|
|
50
|
+
["u", 999, {"opacity": 0.1}],
|
|
51
|
+
["i", 10, 11, 0],
|
|
52
|
+
["i", 10, 12345, 0],
|
|
53
|
+
["f", 11, 4, 8, 120, 30],
|
|
54
|
+
["c", 13, "NoSuchType", {}]]
|
|
55
|
+
""")
|
|
56
|
+
let parent = PNViewRegistry.shared.view(for: 10)
|
|
57
|
+
let child = PNViewRegistry.shared.view(for: 11)
|
|
58
|
+
XCTAssertNotNil(parent)
|
|
59
|
+
XCTAssertNotNil(child)
|
|
60
|
+
XCTAssertTrue(child?.superview === parent)
|
|
61
|
+
XCTAssertEqual(child?.frame, CGRect(x: 4, y: 8, width: 120, height: 30))
|
|
62
|
+
XCTAssertNotNil(PNViewRegistry.shared.view(for: 13), "unknown types get a placeholder view")
|
|
63
|
+
XCTAssertEqual(PNViewRegistry.shared.resolve(13)?.typeName, "NoSuchType")
|
|
64
|
+
|
|
65
|
+
PNTransaction.apply("[[\"d\", 11]]")
|
|
66
|
+
XCTAssertNil(PNViewRegistry.shared.view(for: 11))
|
|
67
|
+
XCTAssertEqual(parent?.subviews.count, 0)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func testInsertIsMoveAwareAndClamps() {
|
|
71
|
+
PNTransaction.apply("""
|
|
72
|
+
[["c", 20, "View", {}], ["c", 21, "View", {}], ["c", 22, "View", {}], ["c", 23, "View", {}],
|
|
73
|
+
["i", 20, 21, 0], ["i", 20, 22, 1], ["i", 20, 23, 99]]
|
|
74
|
+
""")
|
|
75
|
+
let parent = PNViewRegistry.shared.view(for: 20)
|
|
76
|
+
XCTAssertEqual(parent?.subviews.map { PNViewState.existing(for: $0)?.tag }, [21, 22, 23])
|
|
77
|
+
PNTransaction.apply("[[\"i\", 20, 23, 0]]")
|
|
78
|
+
XCTAssertEqual(parent?.subviews.map { PNViewState.existing(for: $0)?.tag }, [23, 21, 22])
|
|
79
|
+
XCTAssertEqual(parent?.subviews.count, 3)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func testFrameClampsNonFiniteValues() {
|
|
83
|
+
PNTransaction.apply("[[\"c\", 30, \"View\", {}]]")
|
|
84
|
+
guard let record = PNViewRegistry.shared.resolve(30) else { return XCTFail("missing view") }
|
|
85
|
+
record.manager.setFrame(view: record.view, x: .nan, y: 5, w: .infinity, h: -3)
|
|
86
|
+
XCTAssertEqual(record.view.frame.origin.x, 0)
|
|
87
|
+
XCTAssertEqual(record.view.frame.origin.y, 5)
|
|
88
|
+
XCTAssertEqual(record.view.frame.size.height, 0)
|
|
89
|
+
XCTAssertTrue(record.view.frame.size.width.isFinite)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// AppDelegate.swift
|
|
3
3
|
// ios_template
|
|
4
4
|
//
|
|
5
|
-
// Application-level callbacks.
|
|
6
|
-
// results are forwarded to
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// Application-level callbacks. Battery monitoring and APNs
|
|
6
|
+
// registration results are forwarded to the Swift native modules in
|
|
7
|
+
// PythonNativeKit (BatteryModule, NotificationsModule), which relay
|
|
8
|
+
// them to Python over the bridge.
|
|
9
9
|
//
|
|
10
10
|
|
|
11
|
+
import PythonNativeKit
|
|
11
12
|
import UIKit
|
|
12
13
|
|
|
13
14
|
@main
|
|
@@ -16,67 +17,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
16
17
|
_ application: UIApplication,
|
|
17
18
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
18
19
|
) -> Bool {
|
|
19
|
-
|
|
20
|
+
BatteryModule.startMonitoring()
|
|
20
21
|
return true
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
// MARK: - Battery forwarding
|
|
24
|
-
|
|
25
|
-
// UIDevice only posts battery notifications while monitoring is on;
|
|
26
|
-
// Battery.add_listener subscribers receive {"level", "state"}.
|
|
27
|
-
private func registerBatteryObservers() {
|
|
28
|
-
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
29
|
-
let center = NotificationCenter.default
|
|
30
|
-
for name in [UIDevice.batteryLevelDidChangeNotification, UIDevice.batteryStateDidChangeNotification] {
|
|
31
|
-
center.addObserver(forName: name, object: nil, queue: .main) { _ in
|
|
32
|
-
self.dispatchBattery()
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private func dispatchBattery() {
|
|
38
|
-
guard PythonRuntime.shared.started else { return }
|
|
39
|
-
let device = UIDevice.current
|
|
40
|
-
let state: String
|
|
41
|
-
switch device.batteryState {
|
|
42
|
-
case .unplugged: state = "unplugged"
|
|
43
|
-
case .charging: state = "charging"
|
|
44
|
-
case .full: state = "full"
|
|
45
|
-
default: state = "unknown"
|
|
46
|
-
}
|
|
47
|
-
PythonRuntime.shared.notify(
|
|
48
|
-
module: "pythonnative.native_modules.battery",
|
|
49
|
-
function: "dispatch_battery",
|
|
50
|
-
Double(device.batteryLevel),
|
|
51
|
-
state
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
24
|
// MARK: - Remote notifications (APNs)
|
|
56
25
|
|
|
57
26
|
func application(
|
|
58
27
|
_ application: UIApplication,
|
|
59
28
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
|
60
29
|
) {
|
|
61
|
-
|
|
62
|
-
guard PythonRuntime.shared.started else { return }
|
|
63
|
-
PythonRuntime.shared.notify(
|
|
64
|
-
module: "pythonnative.native_modules.notifications",
|
|
65
|
-
function: "dispatch_device_token",
|
|
66
|
-
token
|
|
67
|
-
)
|
|
30
|
+
NotificationsModule.deliverToken(deviceToken)
|
|
68
31
|
}
|
|
69
32
|
|
|
70
33
|
func application(
|
|
71
34
|
_ application: UIApplication,
|
|
72
35
|
didFailToRegisterForRemoteNotificationsWithError error: Error
|
|
73
36
|
) {
|
|
74
|
-
|
|
75
|
-
PythonRuntime.shared.notify(
|
|
76
|
-
module: "pythonnative.native_modules.notifications",
|
|
77
|
-
function: "dispatch_device_token_error",
|
|
78
|
-
String(describing: error)
|
|
79
|
-
)
|
|
37
|
+
NotificationsModule.deliverError(error)
|
|
80
38
|
}
|
|
81
39
|
|
|
82
40
|
// MARK: - UISceneSession lifecycle
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
|
-
<!-- PythonNative's StatusBar element
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<!-- PythonNative's StatusBar element is applied through PNViewController's
|
|
6
|
+
preferredStatusBarStyle / prefersStatusBarHidden overrides, so the
|
|
7
|
+
modern view-controller-based appearance must stay enabled. -->
|
|
8
8
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
9
|
-
<
|
|
9
|
+
<true/>
|
|
10
10
|
<key>UIApplicationSceneManifest</key>
|
|
11
11
|
<dict>
|
|
12
12
|
<key>UIApplicationSupportsMultipleScenes</key>
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
// Python.xcframework is linked at build time, so a missing runtime is
|
|
7
7
|
// a build error, never a silent runtime fallback.
|
|
8
8
|
//
|
|
9
|
+
// After the interpreter starts, `pythonnative.bootstrap.start()` binds
|
|
10
|
+
// the PythonNativeKit C exports, registers the Python callback, and
|
|
11
|
+
// verifies the protocol version. Everything else (screens, modules,
|
|
12
|
+
// deep links) flows over that bridge; nothing else here calls Python
|
|
13
|
+
// functions by name.
|
|
14
|
+
//
|
|
9
15
|
|
|
10
16
|
import Foundation
|
|
17
|
+
import PythonNativeKit
|
|
11
18
|
|
|
12
19
|
enum PythonRuntimeError: Error, CustomStringConvertible {
|
|
13
20
|
case startup(String)
|
|
@@ -62,28 +69,54 @@ final class PythonRuntime {
|
|
|
62
69
|
|
|
63
70
|
private(set) var started = false
|
|
64
71
|
private(set) var startupError: String? = nil
|
|
65
|
-
private var pendingURLs: [String] = []
|
|
66
72
|
|
|
67
73
|
private init() {}
|
|
68
74
|
|
|
69
75
|
// MARK: - Startup
|
|
70
76
|
|
|
71
|
-
/// Initialize the embedded interpreter once
|
|
72
|
-
/// description when the bundle is not
|
|
77
|
+
/// Initialize the embedded interpreter once and complete the bridge
|
|
78
|
+
/// handshake. Throws with a full description when the bundle is not
|
|
79
|
+
/// packaged correctly or the protocol versions disagree; there is
|
|
73
80
|
/// deliberately no fallback mode.
|
|
74
81
|
func ensureStarted() throws {
|
|
75
|
-
if started { return }
|
|
76
82
|
if let error = startupError { throw PythonRuntimeError.startup(error) }
|
|
83
|
+
if started { return }
|
|
77
84
|
do {
|
|
78
85
|
try start()
|
|
86
|
+
// The interpreter is live from here on; a failed handshake is
|
|
87
|
+
// recorded as a startup error but Python is never re-initialized.
|
|
79
88
|
started = true
|
|
80
|
-
|
|
89
|
+
try connectBridge()
|
|
81
90
|
} catch let error as PythonRuntimeError {
|
|
82
91
|
startupError = error.description
|
|
83
92
|
throw error
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
/// Run `pythonnative.bootstrap.start(dev, strict=True)`: it binds the
|
|
97
|
+
/// `pn_bridge_*` symbols, registers the native -> Python callback,
|
|
98
|
+
/// verifies the protocol version against `pn_bridge_protocol_version()`,
|
|
99
|
+
/// routes `print()` to the console, and warms the asyncio runtime.
|
|
100
|
+
private func connectBridge() throws {
|
|
101
|
+
#if DEBUG
|
|
102
|
+
let dev = true
|
|
103
|
+
#else
|
|
104
|
+
let dev = false
|
|
105
|
+
#endif
|
|
106
|
+
do {
|
|
107
|
+
try callList(module: "pythonnative.bootstrap", function: "start", [dev, true])
|
|
108
|
+
} catch let error as PythonRuntimeError {
|
|
109
|
+
throw PythonRuntimeError.startup(
|
|
110
|
+
"Bridge bootstrap failed (native protocol v\(pn_bridge_protocol_version())):\n\(error.description)"
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
guard PNBridge.shared.hasCallback else {
|
|
114
|
+
throw PythonRuntimeError.startup(
|
|
115
|
+
"pythonnative.bridge.handshake() returned without registering the native callback."
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
87
120
|
private func start() throws {
|
|
88
121
|
guard let resourcePath = Bundle.main.resourcePath else {
|
|
89
122
|
throw PythonRuntimeError.startup("The app bundle has no resource path; the build is corrupt.")
|
|
@@ -250,16 +283,6 @@ final class PythonRuntime {
|
|
|
250
283
|
return try Self.invoke(fnObj, args: args, context: "\(module).\(function)")
|
|
251
284
|
}
|
|
252
285
|
|
|
253
|
-
/// Like `call`, but logs failures instead of throwing. For host
|
|
254
|
-
/// notifications where the app must keep running.
|
|
255
|
-
func notify(module: String, function: String, _ args: Any...) {
|
|
256
|
-
do {
|
|
257
|
-
_ = try callList(module: module, function: function, args)
|
|
258
|
-
} catch {
|
|
259
|
-
NSLog("[PN] \(module).\(function) failed: \(error)")
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
286
|
/// Shared invocation helper. The caller must hold the GIL.
|
|
264
287
|
fileprivate static func invoke(
|
|
265
288
|
_ callable: UnsafeMutablePointer<PyObject>,
|
|
@@ -300,26 +323,6 @@ final class PythonRuntime {
|
|
|
300
323
|
}
|
|
301
324
|
}
|
|
302
325
|
|
|
303
|
-
// MARK: - Deep links
|
|
304
|
-
|
|
305
|
-
/// Deliver a deep-link URL to the Python side, buffering it until
|
|
306
|
-
/// the interpreter is running.
|
|
307
|
-
func deliverURL(_ url: String) {
|
|
308
|
-
guard started else {
|
|
309
|
-
pendingURLs.append(url)
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
notify(module: "pythonnative.native_modules.linking", function: "dispatch_url", url)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private func flushPendingURLs() {
|
|
316
|
-
let urls = pendingURLs
|
|
317
|
-
pendingURLs = []
|
|
318
|
-
for url in urls {
|
|
319
|
-
notify(module: "pythonnative.native_modules.linking", function: "dispatch_url", url)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
326
|
// MARK: - Error formatting
|
|
324
327
|
|
|
325
328
|
/// Format and clear the pending Python exception (caller holds the GIL).
|
|
@@ -384,14 +387,3 @@ final class PythonRuntime {
|
|
|
384
387
|
return "unknown error"
|
|
385
388
|
}
|
|
386
389
|
}
|
|
387
|
-
|
|
388
|
-
/// Exported for the Python side: `pythonnative.runtime` wakes the guest
|
|
389
|
-
/// asyncio loop by resolving this symbol via `ctypes.CDLL(None)` and
|
|
390
|
-
/// calling it whenever loop work is scheduled.
|
|
391
|
-
@_cdecl("pn_schedule_render_drain")
|
|
392
|
-
public func pn_schedule_render_drain() {
|
|
393
|
-
DispatchQueue.main.async {
|
|
394
|
-
guard PythonRuntime.shared.started else { return }
|
|
395
|
-
PythonRuntime.shared.notify(module: "pythonnative.hosts", function: "drain_ios_scheduled_renders")
|
|
396
|
-
}
|
|
397
|
-
}
|