pythonnative 0.5.0__py3-none-any.whl → 0.7.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 (28) hide show
  1. pythonnative/__init__.py +53 -15
  2. pythonnative/cli/pn.py +150 -30
  3. pythonnative/components.py +217 -107
  4. pythonnative/element.py +14 -8
  5. pythonnative/hooks.py +334 -0
  6. pythonnative/hot_reload.py +143 -0
  7. pythonnative/native_modules/__init__.py +19 -0
  8. pythonnative/native_modules/camera.py +105 -0
  9. pythonnative/native_modules/file_system.py +131 -0
  10. pythonnative/native_modules/location.py +61 -0
  11. pythonnative/native_modules/notifications.py +151 -0
  12. pythonnative/native_views.py +638 -34
  13. pythonnative/page.py +138 -171
  14. pythonnative/reconciler.py +153 -20
  15. pythonnative/style.py +135 -0
  16. pythonnative/templates/android_template/app/build.gradle +2 -7
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
  20. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
  21. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
  22. pythonnative/collection_view.py +0 -0
  23. pythonnative/material_bottom_navigation_view.py +0 -0
  24. pythonnative/material_toolbar.py +0 -0
  25. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
  26. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
  27. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
  28. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
@@ -123,6 +123,21 @@ def _resolve_padding(
123
123
  return (0, 0, 0, 0)
124
124
 
125
125
 
126
+ _LAYOUT_KEYS = frozenset(
127
+ {
128
+ "width",
129
+ "height",
130
+ "flex",
131
+ "margin",
132
+ "min_width",
133
+ "max_width",
134
+ "min_height",
135
+ "max_height",
136
+ "align_self",
137
+ }
138
+ )
139
+
140
+
126
141
  # ======================================================================
127
142
  # Platform handler registration (lazy imports inside functions)
128
143
  # ======================================================================
@@ -142,15 +157,52 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
142
157
  def _dp(value: float) -> int:
143
158
  return int(value * _density())
144
159
 
160
+ def _apply_layout(view: Any, props: Dict[str, Any]) -> None:
161
+ """Apply common layout properties to an Android view."""
162
+ lp = view.getLayoutParams()
163
+ LayoutParams = jclass("android.widget.LinearLayout$LayoutParams")
164
+ ViewGroupLP = jclass("android.view.ViewGroup$LayoutParams")
165
+ needs_set = False
166
+
167
+ if lp is None:
168
+ lp = LayoutParams(ViewGroupLP.WRAP_CONTENT, ViewGroupLP.WRAP_CONTENT)
169
+ needs_set = True
170
+
171
+ if "width" in props and props["width"] is not None:
172
+ lp.width = _dp(float(props["width"]))
173
+ needs_set = True
174
+ if "height" in props and props["height"] is not None:
175
+ lp.height = _dp(float(props["height"]))
176
+ needs_set = True
177
+ if "flex" in props and props["flex"] is not None:
178
+ try:
179
+ lp.weight = float(props["flex"])
180
+ needs_set = True
181
+ except Exception:
182
+ pass
183
+ if "margin" in props and props["margin"] is not None:
184
+ left, top, right, bottom = _resolve_padding(props["margin"])
185
+ try:
186
+ lp.setMargins(_dp(left), _dp(top), _dp(right), _dp(bottom))
187
+ needs_set = True
188
+ except Exception:
189
+ pass
190
+
191
+ if needs_set:
192
+ view.setLayoutParams(lp)
193
+
145
194
  # ---- Text -----------------------------------------------------------
146
195
  class AndroidTextHandler(ViewHandler):
147
196
  def create(self, props: Dict[str, Any]) -> Any:
148
197
  tv = jclass("android.widget.TextView")(_ctx())
149
198
  self._apply(tv, props)
199
+ _apply_layout(tv, props)
150
200
  return tv
151
201
 
152
202
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
153
203
  self._apply(native_view, changed)
204
+ if changed.keys() & _LAYOUT_KEYS:
205
+ _apply_layout(native_view, changed)
154
206
 
155
207
  def _apply(self, tv: Any, props: Dict[str, Any]) -> None:
156
208
  if "text" in props:
@@ -175,10 +227,13 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
175
227
  def create(self, props: Dict[str, Any]) -> Any:
176
228
  btn = jclass("android.widget.Button")(_ctx())
177
229
  self._apply(btn, props)
230
+ _apply_layout(btn, props)
178
231
  return btn
179
232
 
180
233
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
181
234
  self._apply(native_view, changed)
235
+ if changed.keys() & _LAYOUT_KEYS:
236
+ _apply_layout(native_view, changed)
182
237
 
183
238
  def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
184
239
  if "title" in props:
@@ -213,12 +268,16 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
213
268
  ll = jclass("android.widget.LinearLayout")(_ctx())
214
269
  ll.setOrientation(jclass("android.widget.LinearLayout").VERTICAL)
215
270
  self._apply(ll, props)
271
+ _apply_layout(ll, props)
216
272
  return ll
217
273
 
218
274
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
219
275
  self._apply(native_view, changed)
276
+ if changed.keys() & _LAYOUT_KEYS:
277
+ _apply_layout(native_view, changed)
220
278
 
221
279
  def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
280
+ Gravity = jclass("android.view.Gravity")
222
281
  if "spacing" in props and props["spacing"]:
223
282
  px = _dp(float(props["spacing"]))
224
283
  GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
@@ -230,17 +289,31 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
230
289
  if "padding" in props:
231
290
  left, top, right, bottom = _resolve_padding(props["padding"])
232
291
  ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
233
- if "alignment" in props and props["alignment"]:
234
- Gravity = jclass("android.view.Gravity")
235
- mapping = {
292
+ gravity = 0
293
+ ai = props.get("align_items") or props.get("alignment")
294
+ if ai:
295
+ cross_map = {
296
+ "stretch": Gravity.FILL_HORIZONTAL,
236
297
  "fill": Gravity.FILL_HORIZONTAL,
237
- "center": Gravity.CENTER_HORIZONTAL,
298
+ "flex_start": Gravity.START,
238
299
  "leading": Gravity.START,
239
300
  "start": Gravity.START,
301
+ "center": Gravity.CENTER_HORIZONTAL,
302
+ "flex_end": Gravity.END,
240
303
  "trailing": Gravity.END,
241
304
  "end": Gravity.END,
242
305
  }
243
- ll.setGravity(mapping.get(props["alignment"], Gravity.FILL_HORIZONTAL))
306
+ gravity |= cross_map.get(ai, 0)
307
+ jc = props.get("justify_content")
308
+ if jc:
309
+ main_map = {
310
+ "flex_start": Gravity.TOP,
311
+ "center": Gravity.CENTER_VERTICAL,
312
+ "flex_end": Gravity.BOTTOM,
313
+ }
314
+ gravity |= main_map.get(jc, 0)
315
+ if gravity:
316
+ ll.setGravity(gravity)
244
317
  if "background_color" in props and props["background_color"] is not None:
245
318
  ll.setBackgroundColor(parse_color_int(props["background_color"]))
246
319
 
@@ -259,12 +332,16 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
259
332
  ll = jclass("android.widget.LinearLayout")(_ctx())
260
333
  ll.setOrientation(jclass("android.widget.LinearLayout").HORIZONTAL)
261
334
  self._apply(ll, props)
335
+ _apply_layout(ll, props)
262
336
  return ll
263
337
 
264
338
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
265
339
  self._apply(native_view, changed)
340
+ if changed.keys() & _LAYOUT_KEYS:
341
+ _apply_layout(native_view, changed)
266
342
 
267
343
  def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
344
+ Gravity = jclass("android.view.Gravity")
268
345
  if "spacing" in props and props["spacing"]:
269
346
  px = _dp(float(props["spacing"]))
270
347
  GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
@@ -276,15 +353,29 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
276
353
  if "padding" in props:
277
354
  left, top, right, bottom = _resolve_padding(props["padding"])
278
355
  ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
279
- if "alignment" in props and props["alignment"]:
280
- Gravity = jclass("android.view.Gravity")
281
- mapping = {
356
+ gravity = 0
357
+ ai = props.get("align_items") or props.get("alignment")
358
+ if ai:
359
+ cross_map = {
360
+ "stretch": Gravity.FILL_VERTICAL,
282
361
  "fill": Gravity.FILL_VERTICAL,
283
- "center": Gravity.CENTER_VERTICAL,
362
+ "flex_start": Gravity.TOP,
284
363
  "top": Gravity.TOP,
364
+ "center": Gravity.CENTER_VERTICAL,
365
+ "flex_end": Gravity.BOTTOM,
285
366
  "bottom": Gravity.BOTTOM,
286
367
  }
287
- ll.setGravity(mapping.get(props["alignment"], Gravity.FILL_VERTICAL))
368
+ gravity |= cross_map.get(ai, 0)
369
+ jc = props.get("justify_content")
370
+ if jc:
371
+ main_map = {
372
+ "flex_start": Gravity.START,
373
+ "center": Gravity.CENTER_HORIZONTAL,
374
+ "flex_end": Gravity.END,
375
+ }
376
+ gravity |= main_map.get(jc, 0)
377
+ if gravity:
378
+ ll.setGravity(gravity)
288
379
  if "background_color" in props and props["background_color"] is not None:
289
380
  ll.setBackgroundColor(parse_color_int(props["background_color"]))
290
381
 
@@ -303,11 +394,14 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
303
394
  sv = jclass("android.widget.ScrollView")(_ctx())
304
395
  if "background_color" in props and props["background_color"] is not None:
305
396
  sv.setBackgroundColor(parse_color_int(props["background_color"]))
397
+ _apply_layout(sv, props)
306
398
  return sv
307
399
 
308
400
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
309
401
  if "background_color" in changed and changed["background_color"] is not None:
310
402
  native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
403
+ if changed.keys() & _LAYOUT_KEYS:
404
+ _apply_layout(native_view, changed)
311
405
 
312
406
  def add_child(self, parent: Any, child: Any) -> None:
313
407
  parent.addView(child)
@@ -315,15 +409,18 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
315
409
  def remove_child(self, parent: Any, child: Any) -> None:
316
410
  parent.removeView(child)
317
411
 
318
- # ---- TextInput (EditText) -------------------------------------------
412
+ # ---- TextInput (EditText) with on_change ----------------------------
319
413
  class AndroidTextInputHandler(ViewHandler):
320
414
  def create(self, props: Dict[str, Any]) -> Any:
321
415
  et = jclass("android.widget.EditText")(_ctx())
322
416
  self._apply(et, props)
417
+ _apply_layout(et, props)
323
418
  return et
324
419
 
325
420
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
326
421
  self._apply(native_view, changed)
422
+ if changed.keys() & _LAYOUT_KEYS:
423
+ _apply_layout(native_view, changed)
327
424
 
328
425
  def _apply(self, et: Any, props: Dict[str, Any]) -> None:
329
426
  if "value" in props:
@@ -339,26 +436,113 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
339
436
  if "secure" in props and props["secure"]:
340
437
  InputType = jclass("android.text.InputType")
341
438
  et.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
439
+ if "on_change" in props:
440
+ cb = props["on_change"]
441
+ if cb is not None:
442
+ TextWatcher = jclass("android.text.TextWatcher")
443
+
444
+ class ChangeProxy(dynamic_proxy(TextWatcher)):
445
+ def __init__(self, callback: Callable[[str], None]) -> None:
446
+ super().__init__()
447
+ self.callback = callback
342
448
 
343
- # ---- Image ----------------------------------------------------------
449
+ def afterTextChanged(self, s: Any) -> None:
450
+ self.callback(str(s))
451
+
452
+ def beforeTextChanged(self, s: Any, start: int, count: int, after: int) -> None:
453
+ pass
454
+
455
+ def onTextChanged(self, s: Any, start: int, before: int, count: int) -> None:
456
+ pass
457
+
458
+ et.addTextChangedListener(ChangeProxy(cb))
459
+
460
+ # ---- Image (with URL loading) ---------------------------------------
344
461
  class AndroidImageHandler(ViewHandler):
345
462
  def create(self, props: Dict[str, Any]) -> Any:
346
463
  iv = jclass("android.widget.ImageView")(_ctx())
347
464
  self._apply(iv, props)
465
+ _apply_layout(iv, props)
348
466
  return iv
349
467
 
350
468
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
351
469
  self._apply(native_view, changed)
470
+ if changed.keys() & _LAYOUT_KEYS:
471
+ _apply_layout(native_view, changed)
352
472
 
353
473
  def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
354
474
  if "background_color" in props and props["background_color"] is not None:
355
475
  iv.setBackgroundColor(parse_color_int(props["background_color"]))
356
-
357
- # ---- Switch ---------------------------------------------------------
476
+ if "source" in props and props["source"]:
477
+ self._load_source(iv, props["source"])
478
+ if "scale_type" in props and props["scale_type"]:
479
+ ScaleType = jclass("android.widget.ImageView$ScaleType")
480
+ mapping = {
481
+ "cover": ScaleType.CENTER_CROP,
482
+ "contain": ScaleType.FIT_CENTER,
483
+ "stretch": ScaleType.FIT_XY,
484
+ "center": ScaleType.CENTER,
485
+ }
486
+ st = mapping.get(props["scale_type"])
487
+ if st:
488
+ iv.setScaleType(st)
489
+
490
+ def _load_source(self, iv: Any, source: str) -> None:
491
+ try:
492
+ if source.startswith(("http://", "https://")):
493
+ Thread = jclass("java.lang.Thread")
494
+ Runnable = jclass("java.lang.Runnable")
495
+ URL = jclass("java.net.URL")
496
+ BitmapFactory = jclass("android.graphics.BitmapFactory")
497
+ Handler = jclass("android.os.Handler")
498
+ Looper = jclass("android.os.Looper")
499
+ handler = Handler(Looper.getMainLooper())
500
+
501
+ class LoadTask(dynamic_proxy(Runnable)):
502
+ def __init__(self, image_view: Any, url_str: str, main_handler: Any) -> None:
503
+ super().__init__()
504
+ self.image_view = image_view
505
+ self.url_str = url_str
506
+ self.main_handler = main_handler
507
+
508
+ def run(self) -> None:
509
+ try:
510
+ url = URL(self.url_str)
511
+ stream = url.openStream()
512
+ bitmap = BitmapFactory.decodeStream(stream)
513
+ stream.close()
514
+
515
+ class SetImage(dynamic_proxy(Runnable)):
516
+ def __init__(self, view: Any, bmp: Any) -> None:
517
+ super().__init__()
518
+ self.view = view
519
+ self.bmp = bmp
520
+
521
+ def run(self) -> None:
522
+ self.view.setImageBitmap(self.bmp)
523
+
524
+ self.main_handler.post(SetImage(self.image_view, bitmap))
525
+ except Exception:
526
+ pass
527
+
528
+ Thread(LoadTask(iv, source, handler)).start()
529
+ else:
530
+ ctx = _ctx()
531
+ res = ctx.getResources()
532
+ pkg = ctx.getPackageName()
533
+ res_name = source.rsplit(".", 1)[0] if "." in source else source
534
+ res_id = res.getIdentifier(res_name, "drawable", pkg)
535
+ if res_id != 0:
536
+ iv.setImageResource(res_id)
537
+ except Exception:
538
+ pass
539
+
540
+ # ---- Switch (with on_change) ----------------------------------------
358
541
  class AndroidSwitchHandler(ViewHandler):
359
542
  def create(self, props: Dict[str, Any]) -> Any:
360
543
  sw = jclass("android.widget.Switch")(_ctx())
361
544
  self._apply(sw, props)
545
+ _apply_layout(sw, props)
362
546
  return sw
363
547
 
364
548
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -387,6 +571,7 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
387
571
  pb = jclass("android.widget.ProgressBar")(_ctx(), None, 0, style)
388
572
  pb.setMax(1000)
389
573
  self._apply(pb, props)
574
+ _apply_layout(pb, props)
390
575
  return pb
391
576
 
392
577
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -402,6 +587,7 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
402
587
  pb = jclass("android.widget.ProgressBar")(_ctx())
403
588
  if not props.get("animating", True):
404
589
  pb.setVisibility(jclass("android.view.View").GONE)
590
+ _apply_layout(pb, props)
405
591
  return pb
406
592
 
407
593
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -415,6 +601,7 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
415
601
  wv = jclass("android.webkit.WebView")(_ctx())
416
602
  if "url" in props and props["url"]:
417
603
  wv.loadUrl(str(props["url"]))
604
+ _apply_layout(wv, props)
418
605
  return wv
419
606
 
420
607
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -429,6 +616,12 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
429
616
  px = _dp(float(props["size"]))
430
617
  lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
431
618
  v.setLayoutParams(lp)
619
+ if "flex" in props and props["flex"] is not None:
620
+ lp = v.getLayoutParams()
621
+ if lp is None:
622
+ lp = jclass("android.widget.LinearLayout$LayoutParams")(0, 0)
623
+ lp.weight = float(props["flex"])
624
+ v.setLayoutParams(lp)
432
625
  return v
433
626
 
434
627
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -437,6 +630,156 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
437
630
  lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
438
631
  native_view.setLayoutParams(lp)
439
632
 
633
+ # ---- View (generic container FrameLayout) ---------------------------
634
+ class AndroidViewHandler(ViewHandler):
635
+ def create(self, props: Dict[str, Any]) -> Any:
636
+ fl = jclass("android.widget.FrameLayout")(_ctx())
637
+ if "background_color" in props and props["background_color"] is not None:
638
+ fl.setBackgroundColor(parse_color_int(props["background_color"]))
639
+ if "padding" in props:
640
+ left, top, right, bottom = _resolve_padding(props["padding"])
641
+ fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
642
+ _apply_layout(fl, props)
643
+ return fl
644
+
645
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
646
+ if "background_color" in changed and changed["background_color"] is not None:
647
+ native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
648
+ if "padding" in changed:
649
+ left, top, right, bottom = _resolve_padding(changed["padding"])
650
+ native_view.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
651
+ if changed.keys() & _LAYOUT_KEYS:
652
+ _apply_layout(native_view, changed)
653
+
654
+ def add_child(self, parent: Any, child: Any) -> None:
655
+ parent.addView(child)
656
+
657
+ def remove_child(self, parent: Any, child: Any) -> None:
658
+ parent.removeView(child)
659
+
660
+ def insert_child(self, parent: Any, child: Any, index: int) -> None:
661
+ parent.addView(child, index)
662
+
663
+ # ---- SafeAreaView (FrameLayout with fitsSystemWindows) ---------------
664
+ class AndroidSafeAreaViewHandler(ViewHandler):
665
+ def create(self, props: Dict[str, Any]) -> Any:
666
+ fl = jclass("android.widget.FrameLayout")(_ctx())
667
+ fl.setFitsSystemWindows(True)
668
+ if "background_color" in props and props["background_color"] is not None:
669
+ fl.setBackgroundColor(parse_color_int(props["background_color"]))
670
+ if "padding" in props:
671
+ left, top, right, bottom = _resolve_padding(props["padding"])
672
+ fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
673
+ return fl
674
+
675
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
676
+ if "background_color" in changed and changed["background_color"] is not None:
677
+ native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
678
+
679
+ def add_child(self, parent: Any, child: Any) -> None:
680
+ parent.addView(child)
681
+
682
+ def remove_child(self, parent: Any, child: Any) -> None:
683
+ parent.removeView(child)
684
+
685
+ # ---- Modal (AlertDialog) -------------------------------------------
686
+ class AndroidModalHandler(ViewHandler):
687
+ def create(self, props: Dict[str, Any]) -> Any:
688
+ placeholder = jclass("android.view.View")(_ctx())
689
+ placeholder.setVisibility(jclass("android.view.View").GONE)
690
+ return placeholder
691
+
692
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
693
+ pass
694
+
695
+ def add_child(self, parent: Any, child: Any) -> None:
696
+ pass
697
+
698
+ # ---- Slider (SeekBar) -----------------------------------------------
699
+ class AndroidSliderHandler(ViewHandler):
700
+ def create(self, props: Dict[str, Any]) -> Any:
701
+ sb = jclass("android.widget.SeekBar")(_ctx())
702
+ sb.setMax(1000)
703
+ self._apply(sb, props)
704
+ _apply_layout(sb, props)
705
+ return sb
706
+
707
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
708
+ self._apply(native_view, changed)
709
+
710
+ def _apply(self, sb: Any, props: Dict[str, Any]) -> None:
711
+ min_val = float(props.get("min_value", 0))
712
+ max_val = float(props.get("max_value", 1))
713
+ rng = max_val - min_val if max_val != min_val else 1
714
+ if "value" in props:
715
+ normalized = (float(props["value"]) - min_val) / rng
716
+ sb.setProgress(int(normalized * 1000))
717
+ if "on_change" in props and props["on_change"] is not None:
718
+ cb = props["on_change"]
719
+
720
+ class SeekProxy(dynamic_proxy(jclass("android.widget.SeekBar").OnSeekBarChangeListener)):
721
+ def __init__(self, callback: Callable[[float], None], mn: float, rn: float) -> None:
722
+ super().__init__()
723
+ self.callback = callback
724
+ self.mn = mn
725
+ self.rn = rn
726
+
727
+ def onProgressChanged(self, seekBar: Any, progress: int, fromUser: bool) -> None:
728
+ if fromUser:
729
+ self.callback(self.mn + (progress / 1000.0) * self.rn)
730
+
731
+ def onStartTrackingTouch(self, seekBar: Any) -> None:
732
+ pass
733
+
734
+ def onStopTrackingTouch(self, seekBar: Any) -> None:
735
+ pass
736
+
737
+ sb.setOnSeekBarChangeListener(SeekProxy(cb, min_val, rng))
738
+
739
+ # ---- Pressable (FrameLayout with click listener) --------------------
740
+ class AndroidPressableHandler(ViewHandler):
741
+ def create(self, props: Dict[str, Any]) -> Any:
742
+ fl = jclass("android.widget.FrameLayout")(_ctx())
743
+ fl.setClickable(True)
744
+ self._apply(fl, props)
745
+ return fl
746
+
747
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
748
+ self._apply(native_view, changed)
749
+
750
+ def _apply(self, fl: Any, props: Dict[str, Any]) -> None:
751
+ if "on_press" in props and props["on_press"] is not None:
752
+ cb = props["on_press"]
753
+
754
+ class PressProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
755
+ def __init__(self, callback: Callable[[], None]) -> None:
756
+ super().__init__()
757
+ self.callback = callback
758
+
759
+ def onClick(self, view: Any) -> None:
760
+ self.callback()
761
+
762
+ fl.setOnClickListener(PressProxy(cb))
763
+ if "on_long_press" in props and props["on_long_press"] is not None:
764
+ cb = props["on_long_press"]
765
+
766
+ class LongPressProxy(dynamic_proxy(jclass("android.view.View").OnLongClickListener)):
767
+ def __init__(self, callback: Callable[[], None]) -> None:
768
+ super().__init__()
769
+ self.callback = callback
770
+
771
+ def onLongClick(self, view: Any) -> bool:
772
+ self.callback()
773
+ return True
774
+
775
+ fl.setOnLongClickListener(LongPressProxy(cb))
776
+
777
+ def add_child(self, parent: Any, child: Any) -> None:
778
+ parent.addView(child)
779
+
780
+ def remove_child(self, parent: Any, child: Any) -> None:
781
+ parent.removeView(child)
782
+
440
783
  registry.register("Text", AndroidTextHandler())
441
784
  registry.register("Button", AndroidButtonHandler())
442
785
  registry.register("Column", AndroidColumnHandler())
@@ -449,6 +792,11 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
449
792
  registry.register("ActivityIndicator", AndroidActivityIndicatorHandler())
450
793
  registry.register("WebView", AndroidWebViewHandler())
451
794
  registry.register("Spacer", AndroidSpacerHandler())
795
+ registry.register("View", AndroidViewHandler())
796
+ registry.register("SafeAreaView", AndroidSafeAreaViewHandler())
797
+ registry.register("Modal", AndroidModalHandler())
798
+ registry.register("Slider", AndroidSliderHandler())
799
+ registry.register("Pressable", AndroidPressableHandler())
452
800
 
453
801
 
454
802
  def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
@@ -468,15 +816,37 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
468
816
  b = (argb & 0xFF) / 255.0
469
817
  return UIColor.colorWithRed_green_blue_alpha_(r, g, b, a)
470
818
 
819
+ def _apply_ios_layout(view: Any, props: Dict[str, Any]) -> None:
820
+ """Apply common layout constraints to an iOS view."""
821
+ if "width" in props and props["width"] is not None:
822
+ try:
823
+ for c in list(view.constraints or []):
824
+ if c.firstAttribute == 7: # NSLayoutAttributeWidth
825
+ c.setActive_(False)
826
+ view.widthAnchor.constraintEqualToConstant_(float(props["width"])).setActive_(True)
827
+ except Exception:
828
+ pass
829
+ if "height" in props and props["height"] is not None:
830
+ try:
831
+ for c in list(view.constraints or []):
832
+ if c.firstAttribute == 8: # NSLayoutAttributeHeight
833
+ c.setActive_(False)
834
+ view.heightAnchor.constraintEqualToConstant_(float(props["height"])).setActive_(True)
835
+ except Exception:
836
+ pass
837
+
471
838
  # ---- Text -----------------------------------------------------------
472
839
  class IOSTextHandler(ViewHandler):
473
840
  def create(self, props: Dict[str, Any]) -> Any:
474
841
  label = ObjCClass("UILabel").alloc().init()
475
842
  self._apply(label, props)
843
+ _apply_ios_layout(label, props)
476
844
  return label
477
845
 
478
846
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
479
847
  self._apply(native_view, changed)
848
+ if changed.keys() & _LAYOUT_KEYS:
849
+ _apply_ios_layout(native_view, changed)
480
850
 
481
851
  def _apply(self, label: Any, props: Dict[str, Any]) -> None:
482
852
  if "text" in props:
@@ -501,10 +871,6 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
501
871
 
502
872
  # ---- Button ---------------------------------------------------------
503
873
 
504
- # btn id(ObjCInstance) -> _PNButtonTarget. Keeps a strong ref to
505
- # each handler (preventing GC) and lets us swap the callback on
506
- # re-render without calling removeTarget/addTarget (which crashes
507
- # due to rubicon-objc wrapper lifecycle issues).
508
874
  _pn_btn_handler_map: dict = {}
509
875
 
510
876
  class _PNButtonTarget(NSObject): # type: ignore[valid-type]
@@ -515,9 +881,6 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
515
881
  if self._callback is not None:
516
882
  self._callback()
517
883
 
518
- # Strong refs to retained UIButton wrappers so the ObjCInstance
519
- # (and its prevent-deallocation retain) stays alive for the
520
- # lifetime of the app.
521
884
  _pn_retained_views: list = []
522
885
 
523
886
  class IOSButtonHandler(ViewHandler):
@@ -528,10 +891,13 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
528
891
  _ios_blue = UIColor.colorWithRed_green_blue_alpha_(0.0, 0.478, 1.0, 1.0)
529
892
  btn.setTitleColor_forState_(_ios_blue, 0)
530
893
  self._apply(btn, props)
894
+ _apply_ios_layout(btn, props)
531
895
  return btn
532
896
 
533
897
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
534
898
  self._apply(native_view, changed)
899
+ if changed.keys() & _LAYOUT_KEYS:
900
+ _apply_ios_layout(native_view, changed)
535
901
 
536
902
  def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
537
903
  if "title" in props:
@@ -563,17 +929,40 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
563
929
  sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
564
930
  sv.setAxis_(1) # vertical
565
931
  self._apply(sv, props)
932
+ _apply_ios_layout(sv, props)
566
933
  return sv
567
934
 
568
935
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
569
936
  self._apply(native_view, changed)
937
+ if changed.keys() & _LAYOUT_KEYS:
938
+ _apply_ios_layout(native_view, changed)
570
939
 
571
940
  def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
572
941
  if "spacing" in props and props["spacing"]:
573
942
  sv.setSpacing_(float(props["spacing"]))
574
- if "alignment" in props and props["alignment"]:
575
- mapping = {"fill": 0, "leading": 1, "top": 1, "center": 3, "trailing": 4, "bottom": 4}
576
- sv.setAlignment_(mapping.get(props["alignment"], 0))
943
+ ai = props.get("align_items") or props.get("alignment")
944
+ if ai:
945
+ alignment_map = {
946
+ "stretch": 0,
947
+ "fill": 0,
948
+ "flex_start": 1,
949
+ "leading": 1,
950
+ "center": 3,
951
+ "flex_end": 4,
952
+ "trailing": 4,
953
+ }
954
+ sv.setAlignment_(alignment_map.get(ai, 0))
955
+ jc = props.get("justify_content")
956
+ if jc:
957
+ distribution_map = {
958
+ "flex_start": 0,
959
+ "center": 0,
960
+ "flex_end": 0,
961
+ "space_between": 3,
962
+ "space_around": 4,
963
+ "space_evenly": 4,
964
+ }
965
+ sv.setDistribution_(distribution_map.get(jc, 0))
577
966
  if "background_color" in props and props["background_color"] is not None:
578
967
  sv.setBackgroundColor_(_uicolor(props["background_color"]))
579
968
  if "padding" in props:
@@ -600,17 +989,40 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
600
989
  sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
601
990
  sv.setAxis_(0) # horizontal
602
991
  self._apply(sv, props)
992
+ _apply_ios_layout(sv, props)
603
993
  return sv
604
994
 
605
995
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
606
996
  self._apply(native_view, changed)
997
+ if changed.keys() & _LAYOUT_KEYS:
998
+ _apply_ios_layout(native_view, changed)
607
999
 
608
1000
  def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
609
1001
  if "spacing" in props and props["spacing"]:
610
1002
  sv.setSpacing_(float(props["spacing"]))
611
- if "alignment" in props and props["alignment"]:
612
- mapping = {"fill": 0, "leading": 1, "top": 1, "center": 3, "trailing": 4, "bottom": 4}
613
- sv.setAlignment_(mapping.get(props["alignment"], 0))
1003
+ ai = props.get("align_items") or props.get("alignment")
1004
+ if ai:
1005
+ alignment_map = {
1006
+ "stretch": 0,
1007
+ "fill": 0,
1008
+ "flex_start": 1,
1009
+ "top": 1,
1010
+ "center": 3,
1011
+ "flex_end": 4,
1012
+ "bottom": 4,
1013
+ }
1014
+ sv.setAlignment_(alignment_map.get(ai, 0))
1015
+ jc = props.get("justify_content")
1016
+ if jc:
1017
+ distribution_map = {
1018
+ "flex_start": 0,
1019
+ "center": 0,
1020
+ "flex_end": 0,
1021
+ "space_between": 3,
1022
+ "space_around": 4,
1023
+ "space_evenly": 4,
1024
+ }
1025
+ sv.setDistribution_(distribution_map.get(jc, 0))
614
1026
  if "background_color" in props and props["background_color"] is not None:
615
1027
  sv.setBackgroundColor_(_uicolor(props["background_color"]))
616
1028
 
@@ -630,6 +1042,7 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
630
1042
  sv = ObjCClass("UIScrollView").alloc().init()
631
1043
  if "background_color" in props and props["background_color"] is not None:
632
1044
  sv.setBackgroundColor_(_uicolor(props["background_color"]))
1045
+ _apply_ios_layout(sv, props)
633
1046
  return sv
634
1047
 
635
1048
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -650,16 +1063,33 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
650
1063
  def remove_child(self, parent: Any, child: Any) -> None:
651
1064
  child.removeFromSuperview()
652
1065
 
653
- # ---- TextInput (UITextField) ----------------------------------------
1066
+ # ---- TextInput (UITextField with on_change) -------------------------
1067
+ _pn_tf_handler_map: dict = {}
1068
+
1069
+ class _PNTextFieldTarget(NSObject): # type: ignore[valid-type]
1070
+ _callback: Optional[Callable[[str], None]] = None
1071
+
1072
+ @objc_method
1073
+ def onEdit_(self, sender: object) -> None:
1074
+ if self._callback is not None:
1075
+ try:
1076
+ text = str(sender.text) if sender and hasattr(sender, "text") else ""
1077
+ self._callback(text)
1078
+ except Exception:
1079
+ pass
1080
+
654
1081
  class IOSTextInputHandler(ViewHandler):
655
1082
  def create(self, props: Dict[str, Any]) -> Any:
656
1083
  tf = ObjCClass("UITextField").alloc().init()
657
1084
  tf.setBorderStyle_(2) # RoundedRect
658
1085
  self._apply(tf, props)
1086
+ _apply_ios_layout(tf, props)
659
1087
  return tf
660
1088
 
661
1089
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
662
1090
  self._apply(native_view, changed)
1091
+ if changed.keys() & _LAYOUT_KEYS:
1092
+ _apply_ios_layout(native_view, changed)
663
1093
 
664
1094
  def _apply(self, tf: Any, props: Dict[str, Any]) -> None:
665
1095
  if "value" in props:
@@ -674,20 +1104,72 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
674
1104
  tf.setBackgroundColor_(_uicolor(props["background_color"]))
675
1105
  if "secure" in props and props["secure"]:
676
1106
  tf.setSecureTextEntry_(True)
1107
+ if "on_change" in props:
1108
+ existing = _pn_tf_handler_map.get(id(tf))
1109
+ if existing is not None:
1110
+ existing._callback = props["on_change"]
1111
+ else:
1112
+ handler = _PNTextFieldTarget.new()
1113
+ handler._callback = props["on_change"]
1114
+ _pn_tf_handler_map[id(tf)] = handler
1115
+ tf.addTarget_action_forControlEvents_(handler, SEL("onEdit:"), 1 << 17)
677
1116
 
678
- # ---- Image ----------------------------------------------------------
1117
+ # ---- Image (with URL loading) ---------------------------------------
679
1118
  class IOSImageHandler(ViewHandler):
680
1119
  def create(self, props: Dict[str, Any]) -> Any:
681
1120
  iv = ObjCClass("UIImageView").alloc().init()
682
- if "background_color" in props and props["background_color"] is not None:
683
- iv.setBackgroundColor_(_uicolor(props["background_color"]))
1121
+ self._apply(iv, props)
1122
+ _apply_ios_layout(iv, props)
684
1123
  return iv
685
1124
 
686
1125
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
687
- if "background_color" in changed and changed["background_color"] is not None:
688
- native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
1126
+ self._apply(native_view, changed)
1127
+ if changed.keys() & _LAYOUT_KEYS:
1128
+ _apply_ios_layout(native_view, changed)
1129
+
1130
+ def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
1131
+ if "background_color" in props and props["background_color"] is not None:
1132
+ iv.setBackgroundColor_(_uicolor(props["background_color"]))
1133
+ if "source" in props and props["source"]:
1134
+ self._load_source(iv, props["source"])
1135
+ if "scale_type" in props and props["scale_type"]:
1136
+ mapping = {"cover": 2, "contain": 1, "stretch": 0, "center": 4}
1137
+ iv.setContentMode_(mapping.get(props["scale_type"], 1))
1138
+
1139
+ def _load_source(self, iv: Any, source: str) -> None:
1140
+ try:
1141
+ if source.startswith(("http://", "https://")):
1142
+ NSURL = ObjCClass("NSURL")
1143
+ NSData = ObjCClass("NSData")
1144
+ UIImage = ObjCClass("UIImage")
1145
+ url = NSURL.URLWithString_(source)
1146
+ data = NSData.dataWithContentsOfURL_(url)
1147
+ if data:
1148
+ image = UIImage.imageWithData_(data)
1149
+ if image:
1150
+ iv.setImage_(image)
1151
+ else:
1152
+ UIImage = ObjCClass("UIImage")
1153
+ image = UIImage.imageNamed_(source)
1154
+ if image:
1155
+ iv.setImage_(image)
1156
+ except Exception:
1157
+ pass
1158
+
1159
+ # ---- Switch (with on_change) ----------------------------------------
1160
+ _pn_switch_handler_map: dict = {}
1161
+
1162
+ class _PNSwitchTarget(NSObject): # type: ignore[valid-type]
1163
+ _callback: Optional[Callable[[bool], None]] = None
1164
+
1165
+ @objc_method
1166
+ def onToggle_(self, sender: object) -> None:
1167
+ if self._callback is not None:
1168
+ try:
1169
+ self._callback(bool(sender.isOn()))
1170
+ except Exception:
1171
+ pass
689
1172
 
690
- # ---- Switch ---------------------------------------------------------
691
1173
  class IOSSwitchHandler(ViewHandler):
692
1174
  def create(self, props: Dict[str, Any]) -> Any:
693
1175
  sw = ObjCClass("UISwitch").alloc().init()
@@ -700,6 +1182,15 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
700
1182
  def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
701
1183
  if "value" in props:
702
1184
  sw.setOn_animated_(bool(props["value"]), False)
1185
+ if "on_change" in props:
1186
+ existing = _pn_switch_handler_map.get(id(sw))
1187
+ if existing is not None:
1188
+ existing._callback = props["on_change"]
1189
+ else:
1190
+ handler = _PNSwitchTarget.new()
1191
+ handler._callback = props["on_change"]
1192
+ _pn_switch_handler_map[id(sw)] = handler
1193
+ sw.addTarget_action_forControlEvents_(handler, SEL("onToggle:"), 1 << 12)
703
1194
 
704
1195
  # ---- ProgressBar (UIProgressView) -----------------------------------
705
1196
  class IOSProgressBarHandler(ViewHandler):
@@ -707,6 +1198,7 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
707
1198
  pv = ObjCClass("UIProgressView").alloc().init()
708
1199
  if "value" in props:
709
1200
  pv.setProgress_(float(props["value"]))
1201
+ _apply_ios_layout(pv, props)
710
1202
  return pv
711
1203
 
712
1204
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -737,6 +1229,7 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
737
1229
  NSURLRequest = ObjCClass("NSURLRequest")
738
1230
  url_obj = NSURL.URLWithString_(str(props["url"]))
739
1231
  wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
1232
+ _apply_ios_layout(wv, props)
740
1233
  return wv
741
1234
 
742
1235
  def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
@@ -760,6 +1253,112 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
760
1253
  size = float(changed["size"])
761
1254
  native_view.setFrame_(((0, 0), (size, size)))
762
1255
 
1256
+ # ---- View (generic UIView) -----------------------------------------
1257
+ class IOSViewHandler(ViewHandler):
1258
+ def create(self, props: Dict[str, Any]) -> Any:
1259
+ v = ObjCClass("UIView").alloc().init()
1260
+ if "background_color" in props and props["background_color"] is not None:
1261
+ v.setBackgroundColor_(_uicolor(props["background_color"]))
1262
+ _apply_ios_layout(v, props)
1263
+ return v
1264
+
1265
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1266
+ if "background_color" in changed and changed["background_color"] is not None:
1267
+ native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
1268
+ if changed.keys() & _LAYOUT_KEYS:
1269
+ _apply_ios_layout(native_view, changed)
1270
+
1271
+ def add_child(self, parent: Any, child: Any) -> None:
1272
+ parent.addSubview_(child)
1273
+
1274
+ def remove_child(self, parent: Any, child: Any) -> None:
1275
+ child.removeFromSuperview()
1276
+
1277
+ # ---- SafeAreaView ---------------------------------------------------
1278
+ class IOSSafeAreaViewHandler(ViewHandler):
1279
+ def create(self, props: Dict[str, Any]) -> Any:
1280
+ v = ObjCClass("UIView").alloc().init()
1281
+ if "background_color" in props and props["background_color"] is not None:
1282
+ v.setBackgroundColor_(_uicolor(props["background_color"]))
1283
+ return v
1284
+
1285
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1286
+ if "background_color" in changed and changed["background_color"] is not None:
1287
+ native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
1288
+
1289
+ def add_child(self, parent: Any, child: Any) -> None:
1290
+ parent.addSubview_(child)
1291
+
1292
+ def remove_child(self, parent: Any, child: Any) -> None:
1293
+ child.removeFromSuperview()
1294
+
1295
+ # ---- Modal ----------------------------------------------------------
1296
+ class IOSModalHandler(ViewHandler):
1297
+ def create(self, props: Dict[str, Any]) -> Any:
1298
+ v = ObjCClass("UIView").alloc().init()
1299
+ v.setHidden_(True)
1300
+ return v
1301
+
1302
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1303
+ pass
1304
+
1305
+ # ---- Slider (UISlider) ----------------------------------------------
1306
+ _pn_slider_handler_map: dict = {}
1307
+
1308
+ class _PNSliderTarget(NSObject): # type: ignore[valid-type]
1309
+ _callback: Optional[Callable[[float], None]] = None
1310
+
1311
+ @objc_method
1312
+ def onSlide_(self, sender: object) -> None:
1313
+ if self._callback is not None:
1314
+ try:
1315
+ self._callback(float(sender.value))
1316
+ except Exception:
1317
+ pass
1318
+
1319
+ class IOSSliderHandler(ViewHandler):
1320
+ def create(self, props: Dict[str, Any]) -> Any:
1321
+ sl = ObjCClass("UISlider").alloc().init()
1322
+ self._apply(sl, props)
1323
+ _apply_ios_layout(sl, props)
1324
+ return sl
1325
+
1326
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1327
+ self._apply(native_view, changed)
1328
+
1329
+ def _apply(self, sl: Any, props: Dict[str, Any]) -> None:
1330
+ if "min_value" in props:
1331
+ sl.setMinimumValue_(float(props["min_value"]))
1332
+ if "max_value" in props:
1333
+ sl.setMaximumValue_(float(props["max_value"]))
1334
+ if "value" in props:
1335
+ sl.setValue_(float(props["value"]))
1336
+ if "on_change" in props:
1337
+ existing = _pn_slider_handler_map.get(id(sl))
1338
+ if existing is not None:
1339
+ existing._callback = props["on_change"]
1340
+ else:
1341
+ handler = _PNSliderTarget.new()
1342
+ handler._callback = props["on_change"]
1343
+ _pn_slider_handler_map[id(sl)] = handler
1344
+ sl.addTarget_action_forControlEvents_(handler, SEL("onSlide:"), 1 << 12)
1345
+
1346
+ # ---- Pressable (UIView with tap gesture) ----------------------------
1347
+ class IOSPressableHandler(ViewHandler):
1348
+ def create(self, props: Dict[str, Any]) -> Any:
1349
+ v = ObjCClass("UIView").alloc().init()
1350
+ v.setUserInteractionEnabled_(True)
1351
+ return v
1352
+
1353
+ def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1354
+ pass
1355
+
1356
+ def add_child(self, parent: Any, child: Any) -> None:
1357
+ parent.addSubview_(child)
1358
+
1359
+ def remove_child(self, parent: Any, child: Any) -> None:
1360
+ child.removeFromSuperview()
1361
+
763
1362
  registry.register("Text", IOSTextHandler())
764
1363
  registry.register("Button", IOSButtonHandler())
765
1364
  registry.register("Column", IOSColumnHandler())
@@ -772,6 +1371,11 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
772
1371
  registry.register("ActivityIndicator", IOSActivityIndicatorHandler())
773
1372
  registry.register("WebView", IOSWebViewHandler())
774
1373
  registry.register("Spacer", IOSSpacerHandler())
1374
+ registry.register("View", IOSViewHandler())
1375
+ registry.register("SafeAreaView", IOSSafeAreaViewHandler())
1376
+ registry.register("Modal", IOSModalHandler())
1377
+ registry.register("Slider", IOSSliderHandler())
1378
+ registry.register("Pressable", IOSPressableHandler())
775
1379
 
776
1380
 
777
1381
  # ======================================================================