pythonnative 0.3.0__py3-none-any.whl → 0.5.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 (44) hide show
  1. pythonnative/__init__.py +45 -65
  2. pythonnative/cli/pn.py +16 -10
  3. pythonnative/components.py +241 -0
  4. pythonnative/element.py +47 -0
  5. pythonnative/native_views.py +800 -0
  6. pythonnative/page.py +321 -249
  7. pythonnative/reconciler.py +129 -0
  8. pythonnative/templates/android_template/app/build.gradle +2 -2
  9. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
  10. pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
  11. pythonnative/templates/android_template/build.gradle +3 -3
  12. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
  13. pythonnative/utils.py +21 -29
  14. pythonnative-0.5.0.dist-info/METADATA +161 -0
  15. {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/RECORD +19 -39
  16. {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/WHEEL +1 -1
  17. {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/licenses/LICENSE +1 -1
  18. pythonnative/activity_indicator_view.py +0 -71
  19. pythonnative/button.py +0 -109
  20. pythonnative/date_picker.py +0 -72
  21. pythonnative/image_view.py +0 -76
  22. pythonnative/label.py +0 -66
  23. pythonnative/list_view.py +0 -73
  24. pythonnative/material_activity_indicator_view.py +0 -69
  25. pythonnative/material_button.py +0 -65
  26. pythonnative/material_date_picker.py +0 -85
  27. pythonnative/material_progress_view.py +0 -66
  28. pythonnative/material_search_bar.py +0 -65
  29. pythonnative/material_switch.py +0 -65
  30. pythonnative/material_time_picker.py +0 -72
  31. pythonnative/picker_view.py +0 -65
  32. pythonnative/progress_view.py +0 -68
  33. pythonnative/scroll_view.py +0 -63
  34. pythonnative/search_bar.py +0 -65
  35. pythonnative/stack_view.py +0 -60
  36. pythonnative/switch.py +0 -66
  37. pythonnative/text_field.py +0 -67
  38. pythonnative/text_view.py +0 -70
  39. pythonnative/time_picker.py +0 -73
  40. pythonnative/view.py +0 -25
  41. pythonnative/web_view.py +0 -58
  42. pythonnative-0.3.0.dist-info/METADATA +0 -137
  43. {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/entry_points.txt +0 -0
  44. {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/top_level.txt +0 -0
pythonnative/page.py CHANGED
@@ -1,32 +1,29 @@
1
- """
2
- Your current approach, which involves creating an Android Activity in Kotlin
3
- and then passing it to Python, is necessary due to the restrictions inherent
4
- in Android's lifecycle. You are correctly following the Android way of managing
5
- Activities. In Android, the system is in control of when and how Activities are
6
- created and destroyed. It is not possible to directly create an instance of an
7
- Activity from Python because that would bypass Android's lifecycle management,
8
- leading to unpredictable results.
9
-
10
- Your Button example works because Button is a View, not an Activity. View
11
- instances in Android can be created and managed directly by your code. This is
12
- why you are able to create an instance of Button from Python.
13
-
14
- Remember that Activities in Android are not just containers for your UI like a
15
- ViewGroup, they are also the main entry points into your app and are closely
16
- tied to the app's lifecycle. Therefore, Android needs to maintain tight control
17
- over them. Activities aren't something you instantiate whenever you need them;
18
- they are created in response to a specific intent and their lifecycle is
19
- managed by Android.
20
-
21
- So, to answer your question: Yes, you need to follow this approach for
22
- Activities in Android. You cannot instantiate an Activity from Python like you
23
- do for Views.
24
-
25
- On the other hand, for iOS, you can instantiate a UIViewController directly
26
- from Python. The example code you provided for this is correct.
27
-
28
- Just ensure that your PythonNative UI framework is aware of these platform
29
- differences and handles them appropriately.
1
+ """Page — the root component that bridges native lifecycle and declarative UI.
2
+
3
+ A ``Page`` subclass is the entry point for each screen. It owns a
4
+ :class:`~pythonnative.reconciler.Reconciler` and automatically mounts /
5
+ re-renders the element tree returned by :meth:`render` whenever state
6
+ changes.
7
+
8
+ Usage::
9
+
10
+ import pythonnative as pn
11
+
12
+ class MainPage(pn.Page):
13
+ def __init__(self, native_instance):
14
+ super().__init__(native_instance)
15
+ self.state = {"count": 0}
16
+
17
+ def increment(self):
18
+ self.set_state(count=self.state["count"] + 1)
19
+
20
+ def render(self):
21
+ return pn.Column(
22
+ pn.Text(f"Count: {self.state['count']}", font_size=24),
23
+ pn.Button("Increment", on_click=self.increment),
24
+ spacing=12,
25
+ padding=16,
26
+ )
30
27
  """
31
28
 
32
29
  import json
@@ -34,55 +31,50 @@ from abc import ABC, abstractmethod
34
31
  from typing import Any, Optional, Union
35
32
 
36
33
  from .utils import IS_ANDROID, set_android_context
37
- from .view import ViewBase
38
34
 
39
- # ========================================
40
- # Base class
41
- # ========================================
35
+ # ======================================================================
36
+ # Base class (platform-independent)
37
+ # ======================================================================
42
38
 
43
39
 
44
40
  class PageBase(ABC):
41
+ """Abstract base defining the Page interface."""
42
+
45
43
  @abstractmethod
46
44
  def __init__(self) -> None:
47
45
  super().__init__()
48
46
 
49
47
  @abstractmethod
50
- def set_root_view(self, view) -> None:
51
- pass
48
+ def render(self) -> Any:
49
+ """Return an Element tree describing this page's UI."""
50
+
51
+ def set_state(self, **updates: Any) -> None:
52
+ """Merge *updates* into ``self.state`` and trigger a re-render."""
52
53
 
53
- @abstractmethod
54
54
  def on_create(self) -> None:
55
- pass
55
+ """Called when the page is first created. Triggers initial render."""
56
56
 
57
- @abstractmethod
58
57
  def on_start(self) -> None:
59
58
  pass
60
59
 
61
- @abstractmethod
62
60
  def on_resume(self) -> None:
63
61
  pass
64
62
 
65
- @abstractmethod
66
63
  def on_pause(self) -> None:
67
64
  pass
68
65
 
69
- @abstractmethod
70
66
  def on_stop(self) -> None:
71
67
  pass
72
68
 
73
- @abstractmethod
74
69
  def on_destroy(self) -> None:
75
70
  pass
76
71
 
77
- @abstractmethod
78
72
  def on_restart(self) -> None:
79
73
  pass
80
74
 
81
- @abstractmethod
82
75
  def on_save_instance_state(self) -> None:
83
76
  pass
84
77
 
85
- @abstractmethod
86
78
  def on_restore_instance_state(self) -> None:
87
79
  pass
88
80
 
@@ -99,141 +91,182 @@ class PageBase(ABC):
99
91
  pass
100
92
 
101
93
  def get_args(self) -> dict:
102
- """Return arguments provided to this Page (empty dict if none)."""
103
- # Concrete classes should set self._args; default empty
94
+ """Return navigation arguments (empty dict if none)."""
104
95
  return getattr(self, "_args", {})
105
96
 
106
- # Back-compat: navigate_to delegates to push
107
- def navigate_to(self, page) -> None:
97
+ def navigate_to(self, page: Any) -> None:
108
98
  self.push(page)
109
- pass
110
99
 
111
100
 
112
- if IS_ANDROID:
113
- # ========================================
114
- # Android class
115
- # https://developer.android.com/reference/android/app/Activity
116
- # ========================================
101
+ # ======================================================================
102
+ # Shared declarative rendering helpers
103
+ # ======================================================================
104
+
105
+
106
+ def _init_page_common(page: Any) -> None:
107
+ """Common initialisation shared by both platform Page classes."""
108
+ page.state = {}
109
+ page._args = {}
110
+ page._reconciler = None
111
+ page._root_native_view = None
112
+
113
+
114
+ def _set_state(page: Any, **updates: Any) -> None:
115
+ page.state.update(updates)
116
+ if page._reconciler is not None:
117
+ _re_render(page)
118
+
119
+
120
+ def _on_create(page: Any) -> None:
121
+ from .native_views import get_registry
122
+ from .reconciler import Reconciler
123
+
124
+ page._reconciler = Reconciler(get_registry())
125
+ element = page.render()
126
+ page._root_native_view = page._reconciler.mount(element)
127
+ page._attach_root(page._root_native_view)
128
+
129
+
130
+ def _re_render(page: Any) -> None:
131
+ element = page.render()
132
+ new_root = page._reconciler.reconcile(element)
133
+ if new_root is not page._root_native_view:
134
+ page._detach_root(page._root_native_view)
135
+ page._root_native_view = new_root
136
+ page._attach_root(new_root)
137
+
117
138
 
139
+ def _resolve_page_path(page_ref: Union[str, Any]) -> str:
140
+ if isinstance(page_ref, str):
141
+ return page_ref
142
+ module = getattr(page_ref, "__module__", None)
143
+ name = getattr(page_ref, "__name__", None)
144
+ if module and name:
145
+ return f"{module}.{name}"
146
+ cls = page_ref.__class__
147
+ return f"{cls.__module__}.{cls.__name__}"
148
+
149
+
150
+ def _set_args(page: Any, args: Optional[dict]) -> None:
151
+ if isinstance(args, str):
152
+ try:
153
+ page._args = json.loads(args) or {}
154
+ except Exception:
155
+ page._args = {}
156
+ return
157
+ page._args = args or {}
158
+
159
+
160
+ # ======================================================================
161
+ # Platform implementations
162
+ # ======================================================================
163
+
164
+ if IS_ANDROID:
118
165
  from java import jclass
119
166
 
120
- class Page(PageBase, ViewBase):
121
- def __init__(self, native_instance) -> None:
167
+ class Page(PageBase):
168
+ """Android Page backed by an Activity and Fragment navigation."""
169
+
170
+ def __init__(self, native_instance: Any) -> None:
122
171
  super().__init__()
123
172
  self.native_class = jclass("android.app.Activity")
124
173
  self.native_instance = native_instance
125
- # self.native_instance = self.native_class()
126
- # Stash the Activity so child views can implicitly acquire a Context
127
174
  set_android_context(native_instance)
128
- self._args: dict = {}
175
+ _init_page_common(self)
129
176
 
130
- def set_root_view(self, view) -> None:
131
- # In fragment-based navigation, attach child view to the current fragment container.
132
- try:
133
- from .utils import get_android_fragment_container
177
+ def render(self) -> Any:
178
+ raise NotImplementedError("Page subclass must implement render()")
134
179
 
135
- container = get_android_fragment_container()
136
- # Remove previous children if any, then add the new root
137
- try:
138
- container.removeAllViews()
139
- except Exception:
140
- pass
141
- container.addView(view.native_instance)
142
- except Exception:
143
- # Fallback to setting content view directly on the Activity
144
- self.native_instance.setContentView(view.native_instance)
180
+ def set_state(self, **updates: Any) -> None:
181
+ _set_state(self, **updates)
145
182
 
146
183
  def on_create(self) -> None:
147
- print("Android on_create() called")
184
+ _on_create(self)
148
185
 
149
186
  def on_start(self) -> None:
150
- print("Android on_start() called")
187
+ pass
151
188
 
152
189
  def on_resume(self) -> None:
153
- print("Android on_resume() called")
190
+ pass
154
191
 
155
192
  def on_pause(self) -> None:
156
- print("Android on_pause() called")
193
+ pass
157
194
 
158
195
  def on_stop(self) -> None:
159
- print("Android on_stop() called")
196
+ pass
160
197
 
161
198
  def on_destroy(self) -> None:
162
- print("Android on_destroy() called")
199
+ pass
163
200
 
164
201
  def on_restart(self) -> None:
165
- print("Android on_restart() called")
202
+ pass
166
203
 
167
204
  def on_save_instance_state(self) -> None:
168
- print("Android on_save_instance_state() called")
205
+ pass
169
206
 
170
207
  def on_restore_instance_state(self) -> None:
171
- print("Android on_restore_instance_state() called")
208
+ pass
172
209
 
173
210
  def set_args(self, args: Optional[dict]) -> None:
174
- # Accept dict or JSON string for convenience when crossing language boundaries
175
- if isinstance(args, str):
176
- try:
177
- self._args = json.loads(args) or {}
178
- return
179
- except Exception:
180
- self._args = {}
181
- return
182
- self._args = args or {}
183
-
184
- def _resolve_page_path(self, page: Union[str, Any]) -> str:
185
- if isinstance(page, str):
186
- return page
187
- # If a class or instance is passed, derive dotted path
188
- try:
189
- module = getattr(page, "__module__", None)
190
- name = getattr(page, "__name__", None)
191
- if module and name:
192
- return f"{module}.{name}"
193
- # Instance: use its class
194
- cls = page.__class__
195
- return f"{cls.__module__}.{cls.__name__}"
196
- except Exception:
197
- raise ValueError("Unsupported page reference; expected dotted string or class/instance")
211
+ _set_args(self, args)
198
212
 
199
213
  def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
200
- # Delegate to Navigator.push to navigate to PageFragment with arguments
201
- page_path = self._resolve_page_path(page)
202
- try:
203
- Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
204
- args_json = json.dumps(args) if args else None
205
- Navigator.push(self.native_instance, page_path, args_json)
206
- except Exception:
207
- # As a last resort, do nothing rather than crash
208
- pass
214
+ page_path = _resolve_page_path(page)
215
+ Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
216
+ args_json = json.dumps(args) if args else None
217
+ Navigator.push(self.native_instance, page_path, args_json)
209
218
 
210
219
  def pop(self) -> None:
211
- # Delegate to Navigator.pop for back-stack pop
212
220
  try:
213
221
  Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
214
222
  Navigator.pop(self.native_instance)
215
223
  except Exception:
224
+ self.native_instance.finish()
225
+
226
+ def _attach_root(self, native_view: Any) -> None:
227
+ try:
228
+ from .utils import get_android_fragment_container
229
+
230
+ container = get_android_fragment_container()
216
231
  try:
217
- self.native_instance.finish()
232
+ container.removeAllViews()
218
233
  except Exception:
219
234
  pass
235
+ LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
236
+ lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
237
+ container.addView(native_view, lp)
238
+ except Exception:
239
+ self.native_instance.setContentView(native_view)
240
+
241
+ def _detach_root(self, native_view: Any) -> None:
242
+ try:
243
+ from .utils import get_android_fragment_container
244
+
245
+ container = get_android_fragment_container()
246
+ container.removeAllViews()
247
+ except Exception:
248
+ pass
220
249
 
221
250
  else:
222
- # ========================================
223
- # iOS class
224
- # https://developer.apple.com/documentation/uikit/uiviewcontroller
225
- # ========================================
251
+ from typing import Dict as _Dict
252
+
253
+ _rubicon_available = False
254
+ try:
255
+ from rubicon.objc import ObjCClass, ObjCInstance
226
256
 
227
- from typing import Dict
257
+ _rubicon_available = True
228
258
 
229
- from rubicon.objc import ObjCClass, ObjCInstance
259
+ import gc as _gc
230
260
 
231
- # Global registry mapping native UIViewController pointer address to Page instances.
232
- _IOS_PAGE_REGISTRY: Dict[int, Any] = {}
261
+ _gc.disable()
262
+ except ImportError:
263
+ pass
264
+
265
+ _IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
233
266
 
234
267
  def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
235
268
  try:
236
- ptr = int(vc_instance.ptr) # rubicon ObjCInstance -> c_void_p convertible to int
269
+ ptr = int(vc_instance.ptr)
237
270
  _IOS_PAGE_REGISTRY[ptr] = page_obj
238
271
  except Exception:
239
272
  pass
@@ -246,151 +279,190 @@ else:
246
279
  pass
247
280
 
248
281
  def forward_lifecycle(native_addr: int, event: str) -> None:
249
- """Forward a lifecycle event from Swift ViewController to the registered Page.
250
-
251
- :param native_addr: Integer pointer address of the UIViewController
252
- :param event: One of 'on_start', 'on_resume', 'on_pause', 'on_stop', 'on_destroy',
253
- 'on_save_instance_state', 'on_restore_instance_state'.
254
- """
282
+ """Forward a lifecycle event from Swift ViewController to the registered Page."""
255
283
  page = _IOS_PAGE_REGISTRY.get(int(native_addr))
256
- if not page:
284
+ if page is None:
257
285
  return
258
- try:
259
- handler = getattr(page, event, None)
260
- if handler:
261
- handler()
262
- except Exception:
263
- # Avoid surfacing exceptions across the Swift/Python boundary in lifecycle
264
- pass
286
+ handler = getattr(page, event, None)
287
+ if handler:
288
+ handler()
265
289
 
266
- class Page(PageBase, ViewBase):
267
- def __init__(self, native_instance) -> None:
268
- super().__init__()
269
- self.native_class = ObjCClass("UIViewController")
270
- # If Swift passed us an integer pointer, wrap it as an ObjCInstance.
271
- if isinstance(native_instance, int):
272
- try:
273
- native_instance = ObjCInstance(native_instance)
274
- except Exception:
275
- native_instance = None
276
- self.native_instance = native_instance
277
- # self.native_instance = self.native_class.alloc().init()
278
- self._args: dict = {}
279
- # Register for lifecycle forwarding
280
- if self.native_instance is not None:
281
- _ios_register_page(self.native_instance, self)
282
-
283
- def set_root_view(self, view) -> None:
284
- # UIViewController.view is a property; access without calling.
285
- root_view = self.native_instance.view
286
- # Size the root child to fill the controller's view and enable autoresizing
287
- try:
288
- bounds = root_view.bounds
289
- view.native_instance.setFrame_(bounds)
290
- # UIViewAutoresizingFlexibleWidth (2) | UIViewAutoresizingFlexibleHeight (16)
291
- view.native_instance.setAutoresizingMask_(2 | 16)
292
- except Exception:
293
- pass
294
- root_view.addSubview_(view.native_instance)
290
+ if _rubicon_available:
295
291
 
296
- def on_create(self) -> None:
297
- print("iOS on_create() called")
292
+ class Page(PageBase):
293
+ """iOS Page backed by a UIViewController."""
298
294
 
299
- def on_start(self) -> None:
300
- print("iOS on_start() called")
295
+ def __init__(self, native_instance: Any) -> None:
296
+ super().__init__()
297
+ self.native_class = ObjCClass("UIViewController")
298
+ if isinstance(native_instance, int):
299
+ try:
300
+ native_instance = ObjCInstance(native_instance)
301
+ except Exception:
302
+ native_instance = None
303
+ self.native_instance = native_instance
304
+ _init_page_common(self)
305
+ if self.native_instance is not None:
306
+ _ios_register_page(self.native_instance, self)
301
307
 
302
- def on_resume(self) -> None:
303
- print("iOS on_resume() called")
308
+ def render(self) -> Any:
309
+ raise NotImplementedError("Page subclass must implement render()")
304
310
 
305
- def on_pause(self) -> None:
306
- print("iOS on_pause() called")
311
+ def set_state(self, **updates: Any) -> None:
312
+ _set_state(self, **updates)
307
313
 
308
- def on_stop(self) -> None:
309
- print("iOS on_stop() called")
314
+ def on_create(self) -> None:
315
+ _on_create(self)
310
316
 
311
- def on_destroy(self) -> None:
312
- print("iOS on_destroy() called")
313
- if self.native_instance is not None:
314
- _ios_unregister_page(self.native_instance)
317
+ def on_start(self) -> None:
318
+ pass
315
319
 
316
- def on_restart(self) -> None:
317
- print("iOS on_restart() called")
320
+ def on_resume(self) -> None:
321
+ pass
318
322
 
319
- def on_save_instance_state(self) -> None:
320
- print("iOS on_save_instance_state() called")
323
+ def on_pause(self) -> None:
324
+ pass
321
325
 
322
- def on_restore_instance_state(self) -> None:
323
- print("iOS on_restore_instance_state() called")
326
+ def on_stop(self) -> None:
327
+ pass
324
328
 
325
- def set_args(self, args: Optional[dict]) -> None:
326
- if isinstance(args, str):
327
- try:
328
- self._args = json.loads(args) or {}
329
- return
330
- except Exception:
331
- self._args = {}
332
- return
333
- self._args = args or {}
329
+ def on_destroy(self) -> None:
330
+ if self.native_instance is not None:
331
+ _ios_unregister_page(self.native_instance)
334
332
 
335
- def _resolve_page_path(self, page: Union[str, Any]) -> str:
336
- if isinstance(page, str):
337
- return page
338
- try:
339
- module = getattr(page, "__module__", None)
340
- name = getattr(page, "__name__", None)
341
- if module and name:
342
- return f"{module}.{name}"
343
- cls = page.__class__
344
- return f"{cls.__module__}.{cls.__name__}"
345
- except Exception:
346
- raise ValueError("Unsupported page reference; expected dotted string or class/instance")
333
+ def on_restart(self) -> None:
334
+ pass
347
335
 
348
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
349
- page_path = self._resolve_page_path(page)
350
- # Resolve the Swift ViewController class. Swift classes are namespaced by
351
- # the module name (CFBundleName). Try plain name first, then Module.Name.
352
- ViewController = None
353
- try:
354
- ViewController = ObjCClass("ViewController")
355
- except Exception:
336
+ def on_save_instance_state(self) -> None:
337
+ pass
338
+
339
+ def on_restore_instance_state(self) -> None:
340
+ pass
341
+
342
+ def set_args(self, args: Optional[dict]) -> None:
343
+ _set_args(self, args)
344
+
345
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
346
+ page_path = _resolve_page_path(page)
347
+ ViewController = None
356
348
  try:
357
- NSBundle = ObjCClass("NSBundle")
358
- bundle = NSBundle.mainBundle
359
- module_name = None
349
+ ViewController = ObjCClass("ViewController")
350
+ except Exception:
360
351
  try:
361
- # Prefer CFBundleName; fallback to CFBundleExecutable
352
+ NSBundle = ObjCClass("NSBundle")
353
+ bundle = NSBundle.mainBundle
362
354
  module_name = bundle.objectForInfoDictionaryKey_("CFBundleName")
363
355
  if module_name is None:
364
356
  module_name = bundle.objectForInfoDictionaryKey_("CFBundleExecutable")
357
+ if module_name:
358
+ ViewController = ObjCClass(f"{module_name}.ViewController")
365
359
  except Exception:
366
- module_name = None
367
- if module_name:
368
- ViewController = ObjCClass(f"{module_name}.ViewController")
360
+ pass
361
+
362
+ if ViewController is None:
363
+ raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
364
+
365
+ next_vc = ViewController.alloc().init()
366
+ try:
367
+ next_vc.setValue_forKey_(page_path, "requestedPagePath")
368
+ if args:
369
+ next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
370
+ except Exception:
371
+ pass
372
+ nav = getattr(self.native_instance, "navigationController", None)
373
+ if nav is None:
374
+ raise RuntimeError(
375
+ "No UINavigationController available; ensure template embeds root in navigation controller"
376
+ )
377
+ nav.pushViewController_animated_(next_vc, True)
378
+
379
+ def pop(self) -> None:
380
+ nav = getattr(self.native_instance, "navigationController", None)
381
+ if nav is not None:
382
+ nav.popViewControllerAnimated_(True)
383
+
384
+ def _attach_root(self, native_view: Any) -> None:
385
+ root_view = self.native_instance.view
386
+ native_view.setTranslatesAutoresizingMaskIntoConstraints_(False)
387
+ root_view.addSubview_(native_view)
388
+ try:
389
+ safe = root_view.safeAreaLayoutGuide
390
+ native_view.topAnchor.constraintEqualToAnchor_(safe.topAnchor).setActive_(True)
391
+ native_view.bottomAnchor.constraintEqualToAnchor_(safe.bottomAnchor).setActive_(True)
392
+ native_view.leadingAnchor.constraintEqualToAnchor_(safe.leadingAnchor).setActive_(True)
393
+ native_view.trailingAnchor.constraintEqualToAnchor_(safe.trailingAnchor).setActive_(True)
369
394
  except Exception:
370
- ViewController = None
395
+ native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
396
+ try:
397
+ native_view.setFrame_(root_view.bounds)
398
+ native_view.setAutoresizingMask_(2 | 16)
399
+ except Exception:
400
+ pass
371
401
 
372
- if ViewController is None:
373
- raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
402
+ def _detach_root(self, native_view: Any) -> None:
403
+ try:
404
+ native_view.removeFromSuperview()
405
+ except Exception:
406
+ pass
374
407
 
375
- next_vc = ViewController.alloc().init()
376
- try:
377
- # Use KVC to pass metadata to Swift
378
- next_vc.setValue_forKey_(page_path, "requestedPagePath")
379
- if args:
380
- next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
381
- except Exception:
408
+ else:
409
+
410
+ class Page(PageBase):
411
+ """Desktop stub — no native runtime available.
412
+
413
+ Fully functional for testing with a mock backend via
414
+ ``native_views.set_registry()``.
415
+ """
416
+
417
+ def __init__(self, native_instance: Any = None) -> None:
418
+ super().__init__()
419
+ self.native_instance = native_instance
420
+ _init_page_common(self)
421
+
422
+ def render(self) -> Any:
423
+ raise NotImplementedError("Page subclass must implement render()")
424
+
425
+ def set_state(self, **updates: Any) -> None:
426
+ _set_state(self, **updates)
427
+
428
+ def on_create(self) -> None:
429
+ _on_create(self)
430
+
431
+ def on_start(self) -> None:
382
432
  pass
383
- # On iOS, `navigationController` is exposed as a property; treat it as such.
384
- nav = getattr(self.native_instance, "navigationController", None)
385
- if nav is None:
386
- # If no navigation controller, this push will be a no-op; rely on template to embed one.
387
- raise RuntimeError(
388
- "No UINavigationController available; ensure template embeds root in navigation controller"
389
- )
390
- # Method name maps from pushViewController:animated:
391
- nav.pushViewController_animated_(next_vc, True)
392
433
 
393
- def pop(self) -> None:
394
- nav = getattr(self.native_instance, "navigationController", None)
395
- if nav is not None:
396
- nav.popViewControllerAnimated_(True)
434
+ def on_resume(self) -> None:
435
+ pass
436
+
437
+ def on_pause(self) -> None:
438
+ pass
439
+
440
+ def on_stop(self) -> None:
441
+ pass
442
+
443
+ def on_destroy(self) -> None:
444
+ pass
445
+
446
+ def on_restart(self) -> None:
447
+ pass
448
+
449
+ def on_save_instance_state(self) -> None:
450
+ pass
451
+
452
+ def on_restore_instance_state(self) -> None:
453
+ pass
454
+
455
+ def set_args(self, args: Optional[dict]) -> None:
456
+ _set_args(self, args)
457
+
458
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
459
+ raise RuntimeError("push() requires a native runtime (iOS or Android)")
460
+
461
+ def pop(self) -> None:
462
+ raise RuntimeError("pop() requires a native runtime (iOS or Android)")
463
+
464
+ def _attach_root(self, native_view: Any) -> None:
465
+ pass
466
+
467
+ def _detach_root(self, native_view: Any) -> None:
468
+ pass