pythonnative 0.25.0__py3-none-any.whl → 0.27.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 (50) hide show
  1. pythonnative/__init__.py +1 -1
  2. pythonnative/animated.py +697 -62
  3. pythonnative/cli/pn.py +172 -12
  4. pythonnative/components.py +61 -7
  5. pythonnative/diagnostics.py +27 -0
  6. pythonnative/gestures.py +446 -34
  7. pythonnative/native_modules/app_state.py +2 -1
  8. pythonnative/native_modules/battery.py +3 -2
  9. pythonnative/native_modules/biometrics.py +2 -1
  10. pythonnative/native_modules/camera.py +16 -7
  11. pythonnative/native_modules/clipboard.py +3 -2
  12. pythonnative/native_modules/file_system.py +3 -2
  13. pythonnative/native_modules/haptics.py +7 -6
  14. pythonnative/native_modules/linking.py +62 -6
  15. pythonnative/native_modules/location.py +11 -8
  16. pythonnative/native_modules/net_info.py +75 -5
  17. pythonnative/native_modules/notifications.py +134 -15
  18. pythonnative/native_modules/permissions.py +3 -2
  19. pythonnative/native_modules/share.py +2 -1
  20. pythonnative/native_views/android.py +461 -137
  21. pythonnative/native_views/desktop.py +816 -96
  22. pythonnative/native_views/ios.py +750 -193
  23. pythonnative/project/android.py +15 -3
  24. pythonnative/project/builder.py +172 -70
  25. pythonnative/project/config.py +28 -6
  26. pythonnative/project/devices.py +291 -0
  27. pythonnative/project/doctor.py +5 -4
  28. pythonnative/project/ios.py +51 -77
  29. pythonnative/project/permissions.py +19 -1
  30. pythonnative/project/runtime_assets.py +78 -166
  31. pythonnative/reconciler.py +28 -0
  32. pythonnative/style.py +19 -0
  33. pythonnative/templates/android_template/app/build.gradle +9 -0
  34. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +5 -0
  35. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +148 -12
  36. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
  37. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
  38. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  39. pythonnative/templates/ios_template/ios_template/Info.plist +5 -2
  40. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
  41. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
  42. pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
  43. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
  44. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
  45. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +49 -46
  46. pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
  47. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
  48. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
  49. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
  50. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
@@ -14,20 +14,38 @@ is forwarded through
14
14
  view's reconciler-assigned tag.
15
15
 
16
16
  **Layout** is owned by the pure-Python flex engine in
17
- `pythonnative.layout`: container handlers create plain `UIView`s, the
18
- engine computes per-child frames in points, and
17
+ `pythonnative.layout`: container handlers create hit-test-aware
18
+ ``UIView`` subclasses (see ``_PNInteractiveViewCTypes``), the engine
19
+ computes per-child frames in points, and
19
20
  [`set_frame`][pythonnative.native_views.ios.IOSViewHandler.set_frame]
20
21
  applies those frames via UIKit's classic ``frame`` property (with Auto
21
22
  Layout disabled).
22
23
 
23
24
  **Gestures** attach real ``UIGestureRecognizer`` instances (pan, pinch,
24
- rotation, tap, long-press, swipe) configured from the serialized
25
- gesture specs, so recognition runs fully natively.
25
+ rotation, tap, long-press, swipe, fling) configured from the
26
+ serialized gesture specs, so recognition runs fully natively. The
27
+ composition metadata emitted by
28
+ [`serialize_gestures`][pythonnative.gestures.serialize_gestures] is
29
+ honored natively too: the shared delegate answers
30
+ ``shouldRecognizeSimultaneously...`` from each spec's
31
+ ``"simultaneous"`` set, and ``"wait_for"`` relationships map onto
32
+ ``requireGestureRecognizerToFail:``.
33
+
34
+ **Interaction props**: ``pointer_events`` and ``hit_slop`` are
35
+ implemented by overriding ``pointInside:withEvent:`` and
36
+ ``hitTest:withEvent:`` on the container subclass; ``z_index`` maps to
37
+ ``layer.zPosition``. Per-corner radii (``border_top_left_radius`` and
38
+ friends) use ``layer.maskedCorners`` when one radius is shared and a
39
+ ``CAShapeLayer`` mask when radii differ.
26
40
 
27
41
  **Animations**: ``timing`` and ``spring`` specs are driven by UIKit
28
42
  block animations with completion callbacks reported back through
29
43
  ``pythonnative.animated.native_animation_completed``; ``decay`` falls
30
- back to the Python ticker.
44
+ back to the Python ticker. Python-driven frames arriving through
45
+ ``set_animated_property`` accept floats for opacity and transform
46
+ channels, numeric degrees for ``rotate``, and ``"#RRGGBB"`` /
47
+ ``"#AARRGGBB"`` strings for ``background_color`` and ``color``;
48
+ unknown properties are ignored.
31
49
 
32
50
  This module is only imported on iOS at runtime. Desktop tests inject a
33
51
  mock registry via
@@ -42,6 +60,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
42
60
 
43
61
  from rubicon.objc import SEL, ObjCClass, ObjCInstance, objc_method
44
62
 
63
+ from .. import diagnostics
45
64
  from ..events import dispatch_event, event_names
46
65
  from . import _tripwire_log
47
66
  from .base import ViewHandler, _safe_max, parse_color_int
@@ -80,7 +99,7 @@ try:
80
99
  _UIView = ObjCClass("UIView")
81
100
  _UIView.declare_property("superview")
82
101
  except Exception:
83
- pass
102
+ diagnostics.swallowed("ios.<module>")
84
103
 
85
104
 
86
105
  def _objc_ptr(obj: Any) -> Optional[int]:
@@ -204,6 +223,10 @@ _add_method = _libobjc.class_addMethod
204
223
  _add_method.restype = _ct.c_bool
205
224
  _add_method.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_char_p]
206
225
 
226
+ _get_method_imp = _libobjc.class_getMethodImplementation
227
+ _get_method_imp.restype = _ct.c_void_p
228
+ _get_method_imp.argtypes = [_ct.c_void_p, _ct.c_void_p]
229
+
207
230
  _objc_msgSend = _libobjc.objc_msgSend
208
231
 
209
232
  _SEL_ALLOC = _sel_reg(b"alloc")
@@ -225,6 +248,11 @@ _SEL_TEXT_VIEW_DID_END = _sel_reg(b"textViewDidEndEditing:")
225
248
  _SEL_TEXT_VIEW_DID_CHANGE = _sel_reg(b"textViewDidChange:")
226
249
 
227
250
  _NS_OBJECT_CLS = _get_cls(b"NSObject")
251
+ _UIVIEW_CLS = _get_cls(b"UIView")
252
+
253
+
254
+ class _PNCGPoint(_ct.Structure):
255
+ _fields_ = [("x", _ct.c_double), ("y", _ct.c_double)]
228
256
 
229
257
 
230
258
  # ======================================================================
@@ -267,17 +295,17 @@ def _apply_border(layer: Any, props: Dict[str, Any]) -> None:
267
295
  # asks for corners (matches iOS UIKit common practice).
268
296
  layer.setMasksToBounds_(True)
269
297
  except Exception:
270
- pass
298
+ diagnostics.swallowed("ios._apply_border")
271
299
  if "border_width" in props and props["border_width"] is not None:
272
300
  try:
273
301
  layer.setBorderWidth_(float(props["border_width"]))
274
302
  except Exception:
275
- pass
303
+ diagnostics.swallowed("ios._apply_border")
276
304
  if "border_color" in props and props["border_color"] is not None:
277
305
  try:
278
306
  layer.setBorderColor_(_cgcolor(props["border_color"]))
279
307
  except Exception:
280
- pass
308
+ diagnostics.swallowed("ios._apply_border")
281
309
 
282
310
 
283
311
  def _apply_view_border(view: Any, props: Dict[str, Any]) -> None:
@@ -294,13 +322,13 @@ def _apply_view_border(view: Any, props: Dict[str, Any]) -> None:
294
322
  if width > 0.0 and height > 0.0:
295
323
  radius = min(requested, min(width, height) / 2.0)
296
324
  except Exception:
297
- pass
325
+ diagnostics.swallowed("ios._apply_view_border")
298
326
  border_props = dict(props)
299
327
  border_props["border_radius"] = radius
300
328
  _apply_border(view.layer, border_props)
301
329
  return
302
330
  except Exception:
303
- pass
331
+ diagnostics.swallowed("ios._apply_view_border")
304
332
  _apply_border(view.layer, props)
305
333
 
306
334
 
@@ -344,7 +372,7 @@ def _apply_side_borders(view: Any, props: Dict[str, Any]) -> None:
344
372
  try:
345
373
  view.layer.setBorderWidth_(0.0)
346
374
  except Exception:
347
- pass
375
+ diagnostics.swallowed("ios._apply_side_borders")
348
376
  try:
349
377
  bounds = view.bounds
350
378
  w = float(bounds.size.width)
@@ -352,7 +380,7 @@ def _apply_side_borders(view: Any, props: Dict[str, Any]) -> None:
352
380
  if w > 0.0 and h > 0.0:
353
381
  _update_side_border_layers(view, w, h)
354
382
  except Exception:
355
- pass
383
+ diagnostics.swallowed("ios._apply_side_borders")
356
384
 
357
385
 
358
386
  def _update_side_border_layers(view: Any, width: float, height: float) -> None:
@@ -377,7 +405,7 @@ def _update_side_border_layers(view: Any, width: float, height: float) -> None:
377
405
  try:
378
406
  layer.removeFromSuperlayer()
379
407
  except Exception:
380
- pass
408
+ diagnostics.swallowed("ios._update_side_border_layers")
381
409
  layers[i] = None
382
410
  continue
383
411
  try:
@@ -388,7 +416,7 @@ def _update_side_border_layers(view: Any, width: float, height: float) -> None:
388
416
  layer.setBackgroundColor_(_cgcolor(entry["colors"][i]))
389
417
  layer.setFrame_(frames[i])
390
418
  except Exception:
391
- pass
419
+ diagnostics.swallowed("ios._update_side_border_layers")
392
420
 
393
421
 
394
422
  def _clamp_view_corner_radius(view: Any, width: float, height: float) -> None:
@@ -402,7 +430,7 @@ def _clamp_view_corner_radius(view: Any, width: float, height: float) -> None:
402
430
  try:
403
431
  view.layer.setCornerRadius_(min(float(requested), max_radius))
404
432
  except Exception:
405
- pass
433
+ diagnostics.swallowed("ios._clamp_view_corner_radius")
406
434
 
407
435
 
408
436
  def _clamp_layer_corner_radius(layer: Any, width: float, height: float) -> None:
@@ -418,7 +446,140 @@ def _clamp_layer_corner_radius(layer: Any, width: float, height: float) -> None:
418
446
  try:
419
447
  layer.setCornerRadius_(min(requested, max_radius))
420
448
  except Exception:
421
- pass
449
+ diagnostics.swallowed("ios._clamp_layer_corner_radius")
450
+
451
+
452
+ # ----------------------------------------------------------------------
453
+ # Per-corner radii
454
+ # ----------------------------------------------------------------------
455
+ #
456
+ # When every rounded corner shares one radius the cheap path applies:
457
+ # ``layer.cornerRadius`` restricted to the rounded corners through
458
+ # ``layer.maskedCorners``. When the radii differ, UIKit has no direct
459
+ # API, so the view's layer gets a ``CAShapeLayer`` mask built from a
460
+ # ``UIBezierPath`` with one arc per corner; the mask depends on the
461
+ # view's bounds, so ``set_frame`` rebuilds it on every frame change via
462
+ # ``_update_corner_mask``.
463
+
464
+ # Order matches ``_MASKED_CORNER_BITS``: top-left, top-right,
465
+ # bottom-left, bottom-right.
466
+ _CORNER_RADIUS_KEYS = (
467
+ "border_top_left_radius",
468
+ "border_top_right_radius",
469
+ "border_bottom_left_radius",
470
+ "border_bottom_right_radius",
471
+ )
472
+ # CACornerMask bits: kCALayerMinXMinYCorner (top-left) = 1,
473
+ # MaxXMinY (top-right) = 2, MinXMaxY (bottom-left) = 4,
474
+ # MaxXMaxY (bottom-right) = 8.
475
+ _MASKED_CORNER_BITS = (1, 2, 4, 8)
476
+ _ALL_MASKED_CORNERS = 15
477
+
478
+ # id(view) -> (tl, tr, bl, br) radii for views on the shape-mask path.
479
+ _pn_corner_radii_map: dict = {}
480
+
481
+
482
+ def _apply_corner_radii(view: Any, merged: Dict[str, Any]) -> None:
483
+ """Apply per-corner radii from the *merged* prop set.
484
+
485
+ Unspecified corners inherit the uniform ``border_radius`` (else 0),
486
+ matching the Android backend. With every per-corner key absent the
487
+ entry is dropped and the uniform path owns the corners again.
488
+ """
489
+ if not any(merged.get(k) is not None for k in _CORNER_RADIUS_KEYS):
490
+ if _pn_corner_radii_map.pop(id(view), None) is not None:
491
+ try:
492
+ view.layer.setMask_(None)
493
+ view.layer.setMaskedCorners_(_ALL_MASKED_CORNERS)
494
+ except Exception:
495
+ diagnostics.swallowed("ios._apply_corner_radii")
496
+ _apply_view_border(view, merged)
497
+ return
498
+ base = float(merged.get("border_radius") or 0.0)
499
+ radii = tuple(
500
+ max(0.0, _safe_finite(merged[k], base)) if merged.get(k) is not None else max(0.0, base)
501
+ for k in _CORNER_RADIUS_KEYS
502
+ )
503
+ distinct = {r for r in radii if r > 0.0}
504
+ if len(distinct) <= 1:
505
+ # One shared radius (or all zero): plain cornerRadius limited
506
+ # to the rounded corners.
507
+ _pn_corner_radii_map.pop(id(view), None)
508
+ value = distinct.pop() if distinct else 0.0
509
+ mask = 0
510
+ for bit, r in zip(_MASKED_CORNER_BITS, radii):
511
+ if r > 0.0:
512
+ mask |= bit
513
+ try:
514
+ view.layer.setMask_(None)
515
+ view.layer.setCornerRadius_(value)
516
+ view.layer.setMaskedCorners_(mask if mask else _ALL_MASKED_CORNERS)
517
+ view.layer.setMasksToBounds_(True)
518
+ except Exception:
519
+ diagnostics.swallowed("ios._apply_corner_radii")
520
+ # Route the shared value through the frame-time pill clamp.
521
+ _pn_view_border_radius_map[id(view)] = value
522
+ return
523
+ # Differing radii: shape-layer mask, rebuilt at frame time.
524
+ _pn_view_border_radius_map.pop(id(view), None)
525
+ _pn_corner_radii_map[id(view)] = radii
526
+ try:
527
+ view.layer.setCornerRadius_(0.0)
528
+ view.layer.setMaskedCorners_(_ALL_MASKED_CORNERS)
529
+ except Exception:
530
+ diagnostics.swallowed("ios._apply_corner_radii")
531
+ try:
532
+ bounds = view.bounds
533
+ w = float(bounds.size.width)
534
+ h = float(bounds.size.height)
535
+ if w > 0.0 and h > 0.0:
536
+ _update_corner_mask(view, w, h)
537
+ except Exception:
538
+ diagnostics.swallowed("ios._apply_corner_radii")
539
+
540
+
541
+ def _update_corner_mask(view: Any, width: float, height: float) -> None:
542
+ """(Re)build the CAShapeLayer corner mask for the current bounds."""
543
+ radii = _pn_corner_radii_map.get(id(view))
544
+ if radii is None or width <= 0.0 or height <= 0.0:
545
+ return
546
+ tl, tr, bl, br = radii
547
+ # CSS-style scaling so adjacent radii never overlap along an edge.
548
+ scale = 1.0
549
+ for pair_sum, extent in ((tl + tr, width), (bl + br, width), (tl + bl, height), (tr + br, height)):
550
+ if pair_sum > extent > 0.0:
551
+ scale = min(scale, extent / pair_sum)
552
+ tl, tr, bl, br = (r * scale for r in radii)
553
+ try:
554
+ path = ObjCClass("UIBezierPath").bezierPath()
555
+ half_pi = math.pi / 2.0
556
+ path.moveToPoint_((tl, 0.0))
557
+ path.addLineToPoint_((width - tr, 0.0))
558
+ if tr > 0.0:
559
+ path.addArcWithCenter_radius_startAngle_endAngle_clockwise_((width - tr, tr), tr, -half_pi, 0.0, True)
560
+ path.addLineToPoint_((width, height - br))
561
+ if br > 0.0:
562
+ path.addArcWithCenter_radius_startAngle_endAngle_clockwise_(
563
+ (width - br, height - br), br, 0.0, half_pi, True
564
+ )
565
+ path.addLineToPoint_((bl, height))
566
+ if bl > 0.0:
567
+ path.addArcWithCenter_radius_startAngle_endAngle_clockwise_((bl, height - bl), bl, half_pi, math.pi, True)
568
+ path.addLineToPoint_((0.0, tl))
569
+ if tl > 0.0:
570
+ path.addArcWithCenter_radius_startAngle_endAngle_clockwise_((tl, tl), tl, math.pi, math.pi + half_pi, True)
571
+ path.closePath()
572
+ # On raw-registered outbound wrappers rubicon sometimes resolves
573
+ # ``CGPath`` as a bound method rather than a property.
574
+ cgpath = path.CGPath
575
+ if callable(cgpath):
576
+ cgpath = cgpath()
577
+ shape = ObjCClass("CAShapeLayer").layer()
578
+ shape.setFrame_(((0.0, 0.0), (width, height)))
579
+ shape.setPath_(cgpath)
580
+ view.layer.setMask_(shape)
581
+ except Exception:
582
+ diagnostics.swallowed("ios._update_corner_mask")
422
583
 
423
584
 
424
585
  def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
@@ -434,22 +595,22 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
434
595
  layer.setMasksToBounds_(False)
435
596
  view.setClipsToBounds_(False)
436
597
  except Exception:
437
- pass
598
+ diagnostics.swallowed("ios._apply_shadow")
438
599
  if "shadow_color" in props and props["shadow_color"] is not None:
439
600
  try:
440
601
  layer.setShadowColor_(_cgcolor(props["shadow_color"]))
441
602
  except Exception:
442
- pass
603
+ diagnostics.swallowed("ios._apply_shadow")
443
604
  if "shadow_opacity" in props and props["shadow_opacity"] is not None:
444
605
  try:
445
606
  layer.setShadowOpacity_(float(props["shadow_opacity"]))
446
607
  except Exception:
447
- pass
608
+ diagnostics.swallowed("ios._apply_shadow")
448
609
  if "shadow_radius" in props and props["shadow_radius"] is not None:
449
610
  try:
450
611
  layer.setShadowRadius_(float(props["shadow_radius"]))
451
612
  except Exception:
452
- pass
613
+ diagnostics.swallowed("ios._apply_shadow")
453
614
  if "shadow_offset" in props and props["shadow_offset"] is not None:
454
615
  offset = props["shadow_offset"]
455
616
  try:
@@ -460,7 +621,7 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
460
621
  w, h = float(offset[0]), float(offset[1])
461
622
  layer.setShadowOffset_((w, h))
462
623
  except Exception:
463
- pass
624
+ diagnostics.swallowed("ios._apply_shadow")
464
625
 
465
626
 
466
627
  def _make_transform(spec: Any) -> Any:
@@ -534,7 +695,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
534
695
  try:
535
696
  view.setTransform_((1.0, 0.0, 0.0, 1.0, 0.0, 0.0))
536
697
  except Exception:
537
- pass
698
+ diagnostics.swallowed("ios._apply_transform")
538
699
  return
539
700
  try:
540
701
  transform = _make_transform(spec)
@@ -565,7 +726,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
565
726
  # rubicon-objc accepts the C struct as a tuple of its fields.
566
727
  view.setTransform_((a, b, c, d, tx, ty))
567
728
  except Exception:
568
- pass
729
+ diagnostics.swallowed("ios._apply_transform")
569
730
 
570
731
 
571
732
  # UIAccessibilityTraits bitmask values (UIKit constants).
@@ -604,25 +765,25 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
604
765
  try:
605
766
  view.setIsAccessibilityElement_(bool(props["accessible"]))
606
767
  except Exception:
607
- pass
768
+ diagnostics.swallowed("ios._apply_accessibility")
608
769
  if "accessibility_label" in props:
609
770
  v = props["accessibility_label"]
610
771
  try:
611
772
  view.setAccessibilityLabel_(str(v) if v is not None else "")
612
773
  except Exception:
613
- pass
774
+ diagnostics.swallowed("ios._apply_accessibility")
614
775
  if "accessibility_hint" in props:
615
776
  v = props["accessibility_hint"]
616
777
  try:
617
778
  view.setAccessibilityHint_(str(v) if v is not None else "")
618
779
  except Exception:
619
- pass
780
+ diagnostics.swallowed("ios._apply_accessibility")
620
781
  if "test_id" in props:
621
782
  v = props["test_id"]
622
783
  try:
623
784
  view.setAccessibilityIdentifier_(str(v) if v is not None else None)
624
785
  except Exception:
625
- pass
786
+ diagnostics.swallowed("ios._apply_accessibility")
626
787
 
627
788
  role = props.get("accessibility_role")
628
789
  state = props.get("accessibility_state")
@@ -642,11 +803,11 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
642
803
  try:
643
804
  view.setAccessibilityValue_("expanded" if state["expanded"] else "collapsed")
644
805
  except Exception:
645
- pass
806
+ diagnostics.swallowed("ios._apply_accessibility")
646
807
  try:
647
808
  view.setAccessibilityTraits_(traits)
648
809
  except Exception:
649
- pass
810
+ diagnostics.swallowed("ios._apply_accessibility")
650
811
 
651
812
 
652
813
  def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
@@ -657,21 +818,242 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
657
818
  try:
658
819
  view.layer.setBackgroundColor_(color.CGColor)
659
820
  except Exception:
660
- pass
821
+ diagnostics.swallowed("ios._apply_common_visual")
661
822
  if "overflow" in props:
662
823
  view.setClipsToBounds_(props["overflow"] == "hidden")
663
824
  if "opacity" in props and props["opacity"] is not None:
664
825
  try:
665
826
  view.setAlpha_(float(props["opacity"]))
666
827
  except Exception:
667
- pass
828
+ diagnostics.swallowed("ios._apply_common_visual")
829
+ if "z_index" in props:
830
+ # A removed prop (None) restores the default plane.
831
+ try:
832
+ z = props["z_index"]
833
+ view.layer.setZPosition_(float(z) if z is not None else 0.0)
834
+ except Exception:
835
+ diagnostics.swallowed("ios._apply_common_visual")
836
+ if "pointer_events" in props or "hit_slop" in props:
837
+ _apply_interaction_props(view, props)
668
838
  _apply_view_border(view, props)
669
839
  _apply_side_borders(view, props)
840
+ # Per-corner radii read the merged prop set (an update may carry
841
+ # only one key); a lone border_radius change must also re-run the
842
+ # per-corner path when corner keys are active, or the uniform
843
+ # applier above would stomp the masked corners.
844
+ merged = _state_of(view).get("props") or props
845
+ if any(k in props for k in _CORNER_RADIUS_KEYS) or (
846
+ "border_radius" in props and any(merged.get(k) is not None for k in _CORNER_RADIUS_KEYS)
847
+ ):
848
+ _apply_corner_radii(view, merged)
670
849
  _apply_shadow(view, props)
671
850
  _apply_transform(view, props)
672
851
  _apply_accessibility(view, props)
673
852
 
674
853
 
854
+ # ======================================================================
855
+ # Interaction props: pointer_events / hit_slop (raw libobjc subclass)
856
+ # ======================================================================
857
+ #
858
+ # ``pointer_events`` and ``hit_slop`` need to change how UIKit
859
+ # hit-tests a view, which requires overriding ``pointInside:withEvent:``
860
+ # and ``hitTest:withEvent:``. Like ``_PNPortalViewCTypes``, the
861
+ # ``_PNInteractiveViewCTypes`` class is registered with raw libobjc
862
+ # (CFUNCTYPE IMPs) rather than rubicon's ``@objc_method``, for the same
863
+ # arm64 FFI reliability reasons documented in the module header.
864
+ #
865
+ # Container handlers (View / Row / Column, Pressable, SafeAreaView,
866
+ # KeyboardAvoidingView) *always* instantiate this subclass. That is the
867
+ # low-risk choice: with no entry in ``_pn_interaction_state`` both
868
+ # overrides reproduce stock UIView behavior exactly, and it means the
869
+ # props can appear, change, or disappear on any later render without
870
+ # recreating the view. Leaf handlers (labels, controls) keep their
871
+ # stock classes; if the props show up there, ``_apply_interaction_props``
872
+ # warns once and the props are ignored.
873
+ #
874
+ # The overrides read per-view state synchronously from
875
+ # ``_pn_interaction_state``, a Python dict keyed by the raw view
876
+ # pointer (the ``self`` of every IMP), following the same routing
877
+ # pattern as ``_pn_action_handlers``.
878
+ #
879
+ # Semantics (matching React Native and the Android backend):
880
+ #
881
+ # - ``"none"``: the view and its whole subtree are transparent to
882
+ # touches (``hitTest`` returns nil, ``pointInside`` returns NO).
883
+ # - ``"box_none"``: the view itself never receives touches, but
884
+ # subviews still do (``hitTest`` returns the subview result and
885
+ # converts a self result to nil).
886
+ # - ``"box_only"``: the view receives touches, subviews never do.
887
+ # - ``hit_slop`` expands ``pointInside`` by the slop on each edge.
888
+ # As on every platform, the expanded area only works where the
889
+ # parent's own bounds still contain the point.
890
+
891
+ # Maps view ptr -> {"pointer_events": mode or None,
892
+ # "hit_slop": (top, left, bottom, right) or None}.
893
+ _pn_interaction_state: Dict[int, Dict[str, Any]] = {}
894
+
895
+ _POINTER_EVENT_MODES = ("none", "box_none", "box_only")
896
+
897
+
898
+ def _hit_slop_insets(value: Any) -> Optional[Tuple[float, float, float, float]]:
899
+ """Normalize a ``hit_slop`` prop into ``(top, left, bottom, right)`` points.
900
+
901
+ Accepts a uniform number or a dict with any of ``top`` / ``left`` /
902
+ ``bottom`` / ``right``. Returns ``None`` for absent or unparseable
903
+ values.
904
+ """
905
+ if value is None:
906
+ return None
907
+ if isinstance(value, dict):
908
+ try:
909
+ return (
910
+ float(value.get("top", 0.0) or 0.0),
911
+ float(value.get("left", 0.0) or 0.0),
912
+ float(value.get("bottom", 0.0) or 0.0),
913
+ float(value.get("right", 0.0) or 0.0),
914
+ )
915
+ except Exception:
916
+ return None
917
+ try:
918
+ uniform = float(value)
919
+ except Exception:
920
+ return None
921
+ return (uniform, uniform, uniform, uniform)
922
+
923
+
924
+ _INTERACTIVE_POINT_INSIDE_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
925
+ _INTERACTIVE_HITTEST_TYPE = _ct.CFUNCTYPE(_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
926
+
927
+ _SEL_HITTEST_WITH_EVENT = _sel_reg(b"hitTest:withEvent:")
928
+
929
+ # UIView's own ``hitTest:withEvent:`` IMP, called directly (the
930
+ # superclass is known to be exactly UIView, so this is equivalent to a
931
+ # ``objc_msgSendSuper`` call without the extra struct plumbing). Bound
932
+ # after the class registration below.
933
+ _uiview_hittest_imp: Any = None
934
+
935
+
936
+ def _interactive_point_inside_imp(self_ptr: int, _cmd_ptr: int, point: Any, _event_ptr: int) -> bool:
937
+ """``pointInside:withEvent:`` honoring pointer_events and hit_slop."""
938
+ try:
939
+ info = _pn_interaction_state.get(int(self_ptr or 0)) or {}
940
+ if info.get("pointer_events") == "none":
941
+ return False
942
+ top = left = bottom = right = 0.0
943
+ slop = info.get("hit_slop")
944
+ if slop is not None:
945
+ top, left, bottom, right = slop
946
+ view = ObjCInstance(_ct.c_void_p(self_ptr))
947
+ bounds = view.bounds
948
+ w = float(bounds.size.width)
949
+ h = float(bounds.size.height)
950
+ px = float(point.x)
951
+ py = float(point.y)
952
+ return (-left <= px <= w + right) and (-top <= py <= h + bottom)
953
+ except Exception:
954
+ return False
955
+
956
+
957
+ def _interactive_hittest_imp(self_ptr: int, _cmd_ptr: int, point: Any, event_ptr: int) -> int:
958
+ """``hitTest:withEvent:`` honoring the pointer_events mode.
959
+
960
+ Delegates to UIView's stock implementation (which consults our
961
+ ``pointInside:`` override, so hit_slop is already accounted for),
962
+ then filters the result per the mode.
963
+ """
964
+ try:
965
+ info = _pn_interaction_state.get(int(self_ptr or 0)) or {}
966
+ mode = info.get("pointer_events")
967
+ if mode == "none":
968
+ return 0
969
+ if _uiview_hittest_imp is None:
970
+ return 0
971
+ result = int(_uiview_hittest_imp(self_ptr, _SEL_HITTEST_WITH_EVENT, point, event_ptr) or 0)
972
+ if mode == "box_none" and result == int(self_ptr or 0):
973
+ return 0
974
+ if mode == "box_only":
975
+ return int(self_ptr or 0) if result else 0
976
+ return result
977
+ except Exception:
978
+ return 0
979
+
980
+
981
+ _interactive_point_inside_imp_ref = _INTERACTIVE_POINT_INSIDE_TYPE(_interactive_point_inside_imp)
982
+ _interactive_hittest_imp_ref = _INTERACTIVE_HITTEST_TYPE(_interactive_hittest_imp)
983
+
984
+ _PN_INTERACTIVE_VIEW_CLS = _alloc_cls(_UIVIEW_CLS, b"_PNInteractiveViewCTypes", 0) if _UIVIEW_CLS else None
985
+ if _PN_INTERACTIVE_VIEW_CLS:
986
+ _add_method(
987
+ _PN_INTERACTIVE_VIEW_CLS,
988
+ _sel_reg(b"pointInside:withEvent:"),
989
+ _ct.cast(_interactive_point_inside_imp_ref, _ct.c_void_p),
990
+ b"c@:{CGPoint=dd}@",
991
+ )
992
+ _add_method(
993
+ _PN_INTERACTIVE_VIEW_CLS,
994
+ _SEL_HITTEST_WITH_EVENT,
995
+ _ct.cast(_interactive_hittest_imp_ref, _ct.c_void_p),
996
+ b"@@:{CGPoint=dd}@",
997
+ )
998
+ _reg_cls(_PN_INTERACTIVE_VIEW_CLS)
999
+ try:
1000
+ _raw_hittest = _get_method_imp(_UIVIEW_CLS, _SEL_HITTEST_WITH_EVENT)
1001
+ if _raw_hittest:
1002
+ _uiview_hittest_imp = _ct.cast(_ct.c_void_p(_raw_hittest), _INTERACTIVE_HITTEST_TYPE)
1003
+ except Exception:
1004
+ _uiview_hittest_imp = None
1005
+
1006
+
1007
+ def _new_container_view() -> Any:
1008
+ """Instantiate the hit-test-aware container class (UIView fallback).
1009
+
1010
+ When the ``_PNInteractiveViewCTypes`` registration is unavailable
1011
+ the container degrades to a plain ``UIView``: everything works
1012
+ except ``pointer_events`` / ``hit_slop``, which warn when applied.
1013
+ """
1014
+ view = None
1015
+ if _PN_INTERACTIVE_VIEW_CLS:
1016
+ try:
1017
+ view = ObjCClass("_PNInteractiveViewCTypes").alloc().init()
1018
+ except Exception:
1019
+ view = None
1020
+ if view is None:
1021
+ view = ObjCClass("UIView").alloc().init()
1022
+ view.setTranslatesAutoresizingMaskIntoConstraints_(True)
1023
+ return view
1024
+
1025
+
1026
+ def _apply_interaction_props(view: Any, props: Dict[str, Any]) -> None:
1027
+ """Record pointer_events / hit_slop where the ObjC overrides read them."""
1028
+ ptr = _objc_ptr(view)
1029
+ if ptr is None:
1030
+ return
1031
+ entry = _pn_interaction_state.setdefault(ptr, {})
1032
+ if "pointer_events" in props:
1033
+ mode = props["pointer_events"]
1034
+ entry["pointer_events"] = mode if mode in _POINTER_EVENT_MODES else None
1035
+ if "hit_slop" in props:
1036
+ entry["hit_slop"] = _hit_slop_insets(props["hit_slop"])
1037
+ if entry.get("pointer_events") is None and entry.get("hit_slop") is None:
1038
+ # Both props removed (or "auto"): drop the entry so the
1039
+ # overrides take their stock-behavior fast path.
1040
+ _pn_interaction_state.pop(ptr, None)
1041
+ return
1042
+ is_custom = False
1043
+ try:
1044
+ if _PN_INTERACTIVE_VIEW_CLS:
1045
+ is_custom = bool(view.isKindOfClass_(ObjCClass("_PNInteractiveViewCTypes")))
1046
+ except Exception:
1047
+ is_custom = False
1048
+ if not is_custom:
1049
+ diagnostics.warn_once(
1050
+ "pointer_events / hit_slop only work on container elements "
1051
+ "(View, Row, Column, Pressable) on iOS; this element's native "
1052
+ "class doesn't override hit-testing, so the props are ignored",
1053
+ key=f"interaction_props:{_tag_of(view)}",
1054
+ )
1055
+
1056
+
675
1057
  # ======================================================================
676
1058
  # Retention + shared targets
677
1059
  # ======================================================================
@@ -683,17 +1065,42 @@ _pn_retained_views: list = []
683
1065
  # Simultaneous-recognition gesture delegate (raw libobjc)
684
1066
  # ======================================================================
685
1067
  #
686
- # All PythonNative recognizers on a view should recognize together
687
- # (press feedback + pan + pinch...), matching the GestureArbiter's
688
- # semantics on Android/desktop. UIKit defaults to exclusivity, so every
689
- # recognizer we create gets this delegate, which answers YES to
690
- # ``gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:``.
1068
+ # UIKit defaults to recognizer exclusivity, so every recognizer we
1069
+ # create gets this delegate. The answer to
1070
+ # ``gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:``
1071
+ # mirrors the GestureArbiter's semantics on Android/desktop:
1072
+ #
1073
+ # - Recognizers wired by ``_wire_gestures`` carry an entry in
1074
+ # ``_pn_gesture_relations`` (view pointer, spec index, and the spec's
1075
+ # ``"simultaneous"`` index set). Two recognizers on the *same* view
1076
+ # recognize together only when each is in the other's set; a spec
1077
+ # without the key means "simultaneous with everything" (back-compat
1078
+ # for hand-built spec dicts).
1079
+ # - Any pairing involving an unknown recognizer (Pressable's built-in
1080
+ # press trio, a scroll view's pan, recognizers from another view)
1081
+ # keeps the historical always-YES behavior.
691
1082
 
692
1083
  _GESTURE_SIMUL_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
693
1084
 
1085
+ # Maps recognizer ptr -> (view ptr, spec index, simultaneous set or None).
1086
+ _pn_gesture_relations: Dict[int, Tuple[int, int, Optional[frozenset]]] = {}
1087
+
694
1088
 
695
1089
  def _gesture_simul_imp(self_ptr: int, cmd_ptr: int, g1_ptr: int, g2_ptr: int) -> bool:
696
- return True
1090
+ try:
1091
+ a = _pn_gesture_relations.get(int(g1_ptr or 0))
1092
+ b = _pn_gesture_relations.get(int(g2_ptr or 0))
1093
+ if a is None or b is None:
1094
+ return True
1095
+ if a[0] != b[0]:
1096
+ return True
1097
+ sim_a = a[2]
1098
+ sim_b = b[2]
1099
+ if sim_a is None or sim_b is None:
1100
+ return True
1101
+ return b[1] in sim_a and a[1] in sim_b
1102
+ except Exception:
1103
+ return True
697
1104
 
698
1105
 
699
1106
  _gesture_simul_imp_ref = _GESTURE_SIMUL_TYPE(_gesture_simul_imp)
@@ -733,7 +1140,7 @@ def _set_recognizer_delegate(recognizer: Any) -> None:
733
1140
  rec_ptr = recognizer.ptr if hasattr(recognizer, "ptr") else recognizer
734
1141
  _objc_msgSend(rec_ptr, _SEL_SET_DELEGATE, delegate)
735
1142
  except Exception:
736
- pass
1143
+ diagnostics.swallowed("ios._set_recognizer_delegate")
737
1144
 
738
1145
 
739
1146
  # ======================================================================
@@ -753,6 +1160,7 @@ def _set_recognizer_delegate(recognizer: Any) -> None:
753
1160
  _pn_action_handlers: Dict[int, Any] = {}
754
1161
 
755
1162
  _ACTION_IMP_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
1163
+ _CONTROL_ACTION_IMP_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
756
1164
 
757
1165
 
758
1166
  def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
@@ -763,12 +1171,70 @@ def _action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int) -> None:
763
1171
  try:
764
1172
  handler()
765
1173
  except Exception:
766
- pass
1174
+ diagnostics.swallowed("ios._action_imp")
767
1175
 
768
1176
 
769
1177
  _action_imp_ref = _ACTION_IMP_TYPE(_action_imp)
770
1178
 
1179
+ # ----------------------------------------------------------------------
1180
+ # UIControl action dedupe
1181
+ # ----------------------------------------------------------------------
1182
+ #
1183
+ # The simulator's XCTest event-injection stack sometimes delivers a
1184
+ # single synthesized tap as *two* back-to-back "send control actions"
1185
+ # bursts for the same UIEvent (~1ms apart; the unified log shows one
1186
+ # UIEvent delivered to one window, then the doubled sends). Every
1187
+ # non-idempotent ``on_press`` then fires twice: reducer counters jump
1188
+ # by two, alerts present twice, pickers reopen after selecting. The
1189
+ # control action selector therefore takes the ``forEvent:`` argument
1190
+ # and drops a second send whose UIEvent timestamp matches the last one
1191
+ # delivered to the same control. Distinct taps (even rapid ones) carry
1192
+ # distinct event timestamps, and continuous ValueChanged streams
1193
+ # (slider drags) tick the timestamp per touch move, so only the
1194
+ # duplicated sends are filtered. Programmatic ``sendActions...`` calls
1195
+ # pass a nil event and are never deduped.
1196
+
1197
+ # Maps control ptr -> UIEvent.timestamp of the last delivered action.
1198
+ _pn_last_control_event_ts: Dict[int, float] = {}
1199
+
1200
+ _objc_msgSend_double = _ct.CFUNCTYPE(_ct.c_double, _ct.c_void_p, _ct.c_void_p)(("objc_msgSend", _libobjc))
1201
+
1202
+ _SEL_TIMESTAMP = _sel_reg(b"timestamp")
1203
+
1204
+
1205
+ def _control_action_imp(_self_ptr: int, _cmd_ptr: int, sender_ptr: int, event_ptr: int) -> None:
1206
+ """Raw C callback for every PythonNative UIControl event action."""
1207
+ key = int(sender_ptr or 0)
1208
+ handler = _pn_action_handlers.get(key)
1209
+ if handler is None:
1210
+ return
1211
+ if event_ptr:
1212
+ try:
1213
+ timestamp = float(_objc_msgSend_double(event_ptr, _SEL_TIMESTAMP))
1214
+ except Exception:
1215
+ timestamp = None
1216
+ if timestamp is not None:
1217
+ if _pn_last_control_event_ts.get(key) == timestamp:
1218
+ diagnostics.warn_once(
1219
+ "ios._control_action_imp dropped a duplicated control action "
1220
+ "(one UIEvent, two sends; a simulator event-injection quirk)",
1221
+ key=f"control_action_dedupe:{key}",
1222
+ )
1223
+ return
1224
+ _pn_last_control_event_ts[key] = timestamp
1225
+ try:
1226
+ handler()
1227
+ except Exception:
1228
+ diagnostics.swallowed("ios._control_action_imp")
1229
+
1230
+
1231
+ _control_action_imp_ref = _CONTROL_ACTION_IMP_TYPE(_control_action_imp)
1232
+
1233
+ # Gesture recognizers only support zero- or one-argument action
1234
+ # selectors, so recognizers keep the plain variant while controls use
1235
+ # the two-argument ``forEvent:`` variant for the dedupe above.
771
1236
  _SEL_ON_ACTION = _sel_reg(b"onPNAction:")
1237
+ _SEL_ON_CONTROL_ACTION = _sel_reg(b"onPNControlAction:forEvent:")
772
1238
 
773
1239
  _PN_ACTION_TARGET_CLS = _alloc_cls(_NS_OBJECT_CLS, b"_PNActionTargetCTypes", 0)
774
1240
  if _PN_ACTION_TARGET_CLS:
@@ -778,6 +1244,12 @@ if _PN_ACTION_TARGET_CLS:
778
1244
  _ct.cast(_action_imp_ref, _ct.c_void_p),
779
1245
  b"v@:@",
780
1246
  )
1247
+ _add_method(
1248
+ _PN_ACTION_TARGET_CLS,
1249
+ _SEL_ON_CONTROL_ACTION,
1250
+ _ct.cast(_control_action_imp_ref, _ct.c_void_p),
1251
+ b"v@:@@",
1252
+ )
781
1253
  _reg_cls(_PN_ACTION_TARGET_CLS)
782
1254
 
783
1255
  _pn_action_target_ptr: Any = None
@@ -830,7 +1302,7 @@ def _register_control_action(control: Any, events_mask: int, handler: Any) -> No
830
1302
  _objc_msgSend.restype = None
831
1303
  _objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_ulong]
832
1304
  ctl_ptr = control.ptr if hasattr(control, "ptr") else control
833
- _objc_msgSend(ctl_ptr, _SEL_ADD_TARGET_ACTION_EVENTS, target_ptr, _SEL_ON_ACTION, events_mask)
1305
+ _objc_msgSend(ctl_ptr, _SEL_ADD_TARGET_ACTION_EVENTS, target_ptr, _SEL_ON_CONTROL_ACTION, events_mask)
834
1306
  _pn_action_handlers[_recognizer_ptr(control)] = handler
835
1307
 
836
1308
 
@@ -846,6 +1318,7 @@ def _make_gesture_handler(
846
1318
  kind: str,
847
1319
  index: int,
848
1320
  direction: Optional[str] = None,
1321
+ n_pointers: Optional[int] = None,
849
1322
  ) -> Any:
850
1323
  """Build the action closure emitting one ``gesture:<i>`` payload."""
851
1324
 
@@ -864,11 +1337,11 @@ def _make_gesture_handler(
864
1337
  payload["x"] = float(location.x)
865
1338
  payload["y"] = float(location.y)
866
1339
  except Exception:
867
- pass
1340
+ diagnostics.swallowed("ios._make_gesture_handler.handler")
868
1341
  try:
869
1342
  payload["pointer_count"] = int(rec.numberOfTouches)
870
1343
  except Exception:
871
- pass
1344
+ diagnostics.swallowed("ios._make_gesture_handler.handler")
872
1345
 
873
1346
  if kind == "pan":
874
1347
  try:
@@ -879,23 +1352,30 @@ def _make_gesture_handler(
879
1352
  payload["velocity_x"] = float(velocity.x)
880
1353
  payload["velocity_y"] = float(velocity.y)
881
1354
  except Exception:
882
- pass
1355
+ diagnostics.swallowed("ios._make_gesture_handler.handler")
883
1356
  elif kind == "pinch":
884
1357
  try:
885
1358
  payload["scale"] = float(rec.scale)
886
1359
  except Exception:
887
- pass
1360
+ diagnostics.swallowed("ios._make_gesture_handler.handler")
888
1361
  elif kind == "rotation":
889
1362
  try:
890
1363
  payload["rotation"] = float(rec.rotation)
891
1364
  except Exception:
892
- pass
1365
+ diagnostics.swallowed("ios._make_gesture_handler.handler")
893
1366
  elif kind == "swipe":
894
1367
  # Discrete: UIKit only calls us on recognition, and only the
895
1368
  # recognizer whose direction matched fires, so the bound
896
1369
  # per-recognizer direction is the actual swipe direction.
897
1370
  payload["state"] = "ended"
898
1371
  payload["direction"] = direction
1372
+ elif kind == "fling":
1373
+ # Same discrete shape as swipe. ``numberOfTouches`` is often
1374
+ # 0 by recognition time, so report the configured pointer
1375
+ # requirement (which UIKit enforced) instead.
1376
+ payload["state"] = "ended"
1377
+ payload["direction"] = direction
1378
+ payload["pointer_count"] = int(n_pointers or 1)
899
1379
  elif kind == "tap":
900
1380
  payload["state"] = "ended"
901
1381
 
@@ -907,9 +1387,17 @@ def _make_gesture_handler(
907
1387
  def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optional[str]]]:
908
1388
  """Build the UIGestureRecognizer(s) for one serialized gesture spec.
909
1389
 
910
- Swipe with ``direction="any"`` needs one recognizer per direction
911
- (UIKit constraint), so this returns ``(recognizer, direction)``
912
- pairs; ``direction`` is ``None`` for non-swipe kinds.
1390
+ Swipe and fling with ``direction="any"`` need one recognizer per
1391
+ direction (a ``UISwipeGestureRecognizer`` reports no resolved
1392
+ direction, only its configured mask), so this returns
1393
+ ``(recognizer, direction)`` pairs; ``direction`` is ``None`` for
1394
+ the other kinds.
1395
+
1396
+ Fling maps to ``UISwipeGestureRecognizer`` with
1397
+ ``numberOfTouchesRequired``. The spec's ``min_velocity`` can't be
1398
+ enforced: UIKit exposes no velocity knob on swipe recognizers, so
1399
+ iOS uses the system's own swipe velocity threshold (a documented
1400
+ approximation; Android and desktop honor the exact value).
913
1401
  """
914
1402
  out: List[Tuple[Any, Optional[str]]] = []
915
1403
  if kind == "tap":
@@ -917,7 +1405,7 @@ def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optiona
917
1405
  try:
918
1406
  rec.setNumberOfTapsRequired_(max(1, int(spec.get("n_taps", 1))))
919
1407
  except Exception:
920
- pass
1408
+ diagnostics.swallowed("ios._make_recognizer")
921
1409
  out.append((rec, None))
922
1410
  elif kind == "long_press":
923
1411
  rec = ObjCClass("UILongPressGestureRecognizer").alloc().init()
@@ -925,14 +1413,14 @@ def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optiona
925
1413
  rec.setMinimumPressDuration_(float(spec.get("min_duration_ms", 500.0)) / 1000.0)
926
1414
  rec.setAllowableMovement_(float(spec.get("max_distance", 12.0)))
927
1415
  except Exception:
928
- pass
1416
+ diagnostics.swallowed("ios._make_recognizer")
929
1417
  out.append((rec, None))
930
1418
  elif kind == "pan":
931
1419
  rec = ObjCClass("UIPanGestureRecognizer").alloc().init()
932
1420
  try:
933
1421
  rec.setMinimumNumberOfTouches_(max(1, int(spec.get("min_pointers", 1))))
934
1422
  except Exception:
935
- pass
1423
+ diagnostics.swallowed("ios._make_recognizer")
936
1424
  out.append((rec, None))
937
1425
  elif kind == "swipe":
938
1426
  direction = str(spec.get("direction", "any"))
@@ -942,7 +1430,19 @@ def _make_recognizer(kind: str, spec: Dict[str, Any]) -> List[Tuple[Any, Optiona
942
1430
  try:
943
1431
  rec.setDirection_(_SWIPE_DIRECTIONS[d])
944
1432
  except Exception:
945
- pass
1433
+ diagnostics.swallowed("ios._make_recognizer")
1434
+ out.append((rec, d))
1435
+ elif kind == "fling":
1436
+ direction = str(spec.get("direction", "any"))
1437
+ directions = [direction] if direction in _SWIPE_DIRECTIONS else list(_SWIPE_DIRECTIONS)
1438
+ n_pointers = max(1, int(spec.get("n_pointers", 1)))
1439
+ for d in directions:
1440
+ rec = ObjCClass("UISwipeGestureRecognizer").alloc().init()
1441
+ try:
1442
+ rec.setDirection_(_SWIPE_DIRECTIONS[d])
1443
+ rec.setNumberOfTouchesRequired_(n_pointers)
1444
+ except Exception:
1445
+ diagnostics.swallowed("ios._make_recognizer")
946
1446
  out.append((rec, d))
947
1447
  elif kind == "pinch":
948
1448
  out.append((ObjCClass("UIPinchGestureRecognizer").alloc().init(), None))
@@ -957,14 +1457,24 @@ def _wire_gestures(view: Any, specs: Any) -> None:
957
1457
  Re-wiring on update removes previously attached PythonNative
958
1458
  recognizers first (configuration changes are rare; correctness
959
1459
  beats incremental patching here).
1460
+
1461
+ Composition metadata is applied in two passes: while creating each
1462
+ recognizer its ``(view, index, simultaneous-set)`` triple is
1463
+ recorded in ``_pn_gesture_relations`` for the shared delegate, and
1464
+ once every recognizer exists, each spec's ``"wait_for"`` indices
1465
+ are wired with ``requireGestureRecognizerToFail:`` (every
1466
+ recognizer of the waiting spec requires every recognizer of each
1467
+ target spec to fail; a spec can own several recognizers, e.g. an
1468
+ any-direction swipe).
960
1469
  """
961
1470
  state = _state_of(view)
962
1471
  for rec in state.get("gesture_recognizers") or []:
963
1472
  try:
964
1473
  view.removeGestureRecognizer_(rec)
965
1474
  except Exception:
966
- pass
1475
+ diagnostics.swallowed("ios._wire_gestures")
967
1476
  _pn_action_handlers.pop(_recognizer_ptr(rec), None)
1477
+ _pn_gesture_relations.pop(_recognizer_ptr(rec), None)
968
1478
  state["gesture_recognizers"] = []
969
1479
  if not isinstance(specs, (list, tuple)) or not specs:
970
1480
  return
@@ -972,20 +1482,25 @@ def _wire_gestures(view: Any, specs: Any) -> None:
972
1482
  try:
973
1483
  view.setUserInteractionEnabled_(True)
974
1484
  except Exception:
975
- pass
1485
+ diagnostics.swallowed("ios._wire_gestures")
976
1486
 
1487
+ view_ptr = _objc_ptr(view) or 0
977
1488
  recognizers: List[Any] = []
1489
+ recs_by_index: Dict[int, List[Any]] = {}
978
1490
  for i, spec in enumerate(specs):
979
1491
  if not isinstance(spec, dict):
980
1492
  continue
981
1493
  kind = str(spec.get("kind", ""))
982
1494
  if not kind:
983
1495
  continue
1496
+ sim = spec.get("simultaneous")
1497
+ sim_set = None if sim is None else frozenset(int(s) for s in sim)
1498
+ n_pointers = max(1, int(spec.get("n_pointers", 1) or 1)) if kind == "fling" else None
984
1499
  for rec, direction in _make_recognizer(kind, spec):
985
1500
  try:
986
1501
  rec.setCancelsTouchesInView_(False)
987
1502
  except Exception:
988
- pass
1503
+ diagnostics.swallowed("ios._wire_gestures")
989
1504
  _set_recognizer_delegate(rec)
990
1505
  try:
991
1506
  view.addGestureRecognizer_(rec)
@@ -993,7 +1508,21 @@ def _wire_gestures(view: Any, specs: Any) -> None:
993
1508
  recognizers.append(rec)
994
1509
  except Exception:
995
1510
  continue
996
- _register_action(rec, _make_gesture_handler(rec, view, kind, i, direction))
1511
+ _pn_gesture_relations[_recognizer_ptr(rec)] = (view_ptr, i, sim_set)
1512
+ recs_by_index.setdefault(i, []).append(rec)
1513
+ _register_action(rec, _make_gesture_handler(rec, view, kind, i, direction, n_pointers))
1514
+
1515
+ for i, spec in enumerate(specs):
1516
+ if not isinstance(spec, dict):
1517
+ continue
1518
+ for target_index in spec.get("wait_for") or ():
1519
+ targets = recs_by_index.get(int(target_index)) or []
1520
+ for waiter in recs_by_index.get(i) or []:
1521
+ for target in targets:
1522
+ try:
1523
+ waiter.requireGestureRecognizerToFail_(target)
1524
+ except Exception:
1525
+ diagnostics.swallowed("ios._wire_gestures")
997
1526
 
998
1527
  state["gesture_recognizers"] = recognizers
999
1528
 
@@ -1010,6 +1539,16 @@ def _is_main_thread() -> bool:
1010
1539
 
1011
1540
 
1012
1541
  def _animated_applier_for(prop: str, value: Any) -> Optional[Callable[[Any], None]]:
1542
+ """Build the per-frame applier for one animated property.
1543
+
1544
+ Color-valued frames (``background_color`` and ``color``) arrive as
1545
+ ``"#RRGGBB"`` / ``"#AARRGGBB"`` strings from ``AnimatedInterpolation``
1546
+ color outputs and go through the same ``_uicolor`` parsing the
1547
+ ordinary update path uses. ``rotate`` receives numeric degrees,
1548
+ which is what ``_apply_transform`` expects for bare numbers. An
1549
+ unknown property returns ``None`` and the frame is silently
1550
+ ignored.
1551
+ """
1013
1552
  if prop == "opacity":
1014
1553
  v = float(value)
1015
1554
 
@@ -1022,6 +1561,18 @@ def _animated_applier_for(prop: str, value: Any) -> Optional[Callable[[Any], Non
1022
1561
  def _apply(view: Any) -> None:
1023
1562
  view.setBackgroundColor_(_uicolor(value))
1024
1563
 
1564
+ return _apply
1565
+ if prop == "color":
1566
+
1567
+ def _apply(view: Any) -> None:
1568
+ color = _uicolor(value)
1569
+ try:
1570
+ view.setTextColor_(color)
1571
+ except Exception:
1572
+ # UIButton has no setTextColor:; views with neither
1573
+ # selector raise into the caller's swallow.
1574
+ view.setTitleColor_forState_(color, 0)
1575
+
1025
1576
  return _apply
1026
1577
  if prop in ("translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"):
1027
1578
  spec = {prop: value}
@@ -1082,13 +1633,13 @@ def _start_native_animation(view: Any, anim_id: int, prop_name: str, spec: Dict[
1082
1633
 
1083
1634
  native_animation_completed(anim_id, bool(finished))
1084
1635
  except Exception:
1085
- pass
1636
+ diagnostics.swallowed("ios._start_native_animation._completion")
1086
1637
 
1087
1638
  def _animations() -> None:
1088
1639
  try:
1089
1640
  applier_to(view)
1090
1641
  except Exception:
1091
- pass
1642
+ diagnostics.swallowed("ios._start_native_animation._animations")
1092
1643
 
1093
1644
  try:
1094
1645
  # Snap to the starting value so the animation covers the full
@@ -1163,15 +1714,19 @@ class IOSViewHandler(ViewHandler):
1163
1714
  state = _state_of(native_view)
1164
1715
  for rec in state.get("gesture_recognizers") or []:
1165
1716
  _pn_action_handlers.pop(_recognizer_ptr(rec), None)
1717
+ _pn_gesture_relations.pop(_recognizer_ptr(rec), None)
1166
1718
  # Controls register their own pointer as the action-handler key
1167
1719
  # (see _register_control_action); drop it with the view.
1168
1720
  _pn_action_handlers.pop(_recognizer_ptr(native_view), None)
1721
+ _pn_last_control_event_ts.pop(_recognizer_ptr(native_view), None)
1722
+ _pn_interaction_state.pop(_objc_ptr(native_view) or 0, None)
1169
1723
  _pn_view_border_radius_map.pop(id(native_view), None)
1724
+ _pn_corner_radii_map.pop(id(native_view), None)
1170
1725
  _pn_side_border_map.pop(id(native_view), None)
1171
1726
  try:
1172
1727
  native_view.removeFromSuperview()
1173
1728
  except Exception:
1174
- pass
1729
+ diagnostics.swallowed("ios.IOSViewHandler.destroy")
1175
1730
  _forget(native_view)
1176
1731
 
1177
1732
  def _build(self, props: Dict[str, Any]) -> Any:
@@ -1187,7 +1742,7 @@ class IOSViewHandler(ViewHandler):
1187
1742
  try:
1188
1743
  child.setTranslatesAutoresizingMaskIntoConstraints_(True)
1189
1744
  except Exception:
1190
- pass
1745
+ diagnostics.swallowed("ios.IOSViewHandler.insert_child")
1191
1746
  try:
1192
1747
  count = len(list(parent.subviews or []))
1193
1748
  except Exception:
@@ -1198,13 +1753,13 @@ class IOSViewHandler(ViewHandler):
1198
1753
  try:
1199
1754
  parent.addSubview_(child)
1200
1755
  except Exception:
1201
- pass
1756
+ diagnostics.swallowed("ios.IOSViewHandler.insert_child")
1202
1757
 
1203
1758
  def remove_child(self, parent: Any, child: Any) -> None:
1204
1759
  try:
1205
1760
  child.removeFromSuperview()
1206
1761
  except Exception:
1207
- pass
1762
+ diagnostics.swallowed("ios.IOSViewHandler.remove_child")
1208
1763
 
1209
1764
  def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
1210
1765
  if native_view is None:
@@ -1217,11 +1772,14 @@ class IOSViewHandler(ViewHandler):
1217
1772
  native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
1218
1773
  native_view.setFrame_(((frame_x, frame_y), (frame_w, frame_h)))
1219
1774
  _clamp_view_corner_radius(native_view, frame_w, frame_h)
1775
+ # The per-corner shape mask is bounds-dependent; rebuild it
1776
+ # whenever the frame changes.
1777
+ _update_corner_mask(native_view, frame_w, frame_h)
1220
1778
  _update_side_border_layers(native_view, frame_w, frame_h)
1221
1779
  try:
1222
1780
  _clamp_layer_corner_radius(native_view.layer, frame_w, frame_h)
1223
1781
  except Exception:
1224
- pass
1782
+ diagnostics.swallowed("ios.IOSViewHandler.set_frame")
1225
1783
  try:
1226
1784
  parent = native_view.superview
1227
1785
  parent_cls = ""
@@ -1238,9 +1796,9 @@ class IOSViewHandler(ViewHandler):
1238
1796
  content_h = max(float(bounds.size.height), frame_y + frame_h)
1239
1797
  parent.setContentSize_((content_w, content_h))
1240
1798
  except Exception:
1241
- pass
1799
+ diagnostics.swallowed("ios.IOSViewHandler.set_frame")
1242
1800
  except Exception:
1243
- pass
1801
+ diagnostics.swallowed("ios.IOSViewHandler.set_frame")
1244
1802
 
1245
1803
  def measure_intrinsic(
1246
1804
  self,
@@ -1261,7 +1819,14 @@ class IOSViewHandler(ViewHandler):
1261
1819
  return (0.0, 0.0)
1262
1820
 
1263
1821
  def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
1264
- """Apply one Python-driven animation frame immediately."""
1822
+ """Apply one Python-driven animation frame immediately.
1823
+
1824
+ Accepts floats for ``opacity`` and the transform channels,
1825
+ numeric degrees for ``rotate``, and ``"#RRGGBB"`` /
1826
+ ``"#AARRGGBB"`` strings for ``background_color`` and ``color``
1827
+ (see ``_animated_applier_for``). Unknown properties are
1828
+ silently ignored.
1829
+ """
1265
1830
  if native_view is None:
1266
1831
  return
1267
1832
  try:
@@ -1273,7 +1838,7 @@ class IOSViewHandler(ViewHandler):
1273
1838
  try:
1274
1839
  applier(native_view)
1275
1840
  except Exception:
1276
- pass
1841
+ diagnostics.swallowed("ios.IOSViewHandler.set_animated_property")
1277
1842
 
1278
1843
  def start_animation(
1279
1844
  self,
@@ -1312,7 +1877,7 @@ class IOSViewHandler(ViewHandler):
1312
1877
  try:
1313
1878
  view.layer.removeAllAnimations()
1314
1879
  except Exception:
1315
- pass
1880
+ diagnostics.swallowed("ios.IOSViewHandler.cancel_animation")
1316
1881
  return value
1317
1882
 
1318
1883
 
@@ -1322,17 +1887,18 @@ class IOSViewHandler(ViewHandler):
1322
1887
 
1323
1888
 
1324
1889
  class FlexContainerHandler(IOSViewHandler):
1325
- """Container for flex layout, a bare `UIView`.
1890
+ """Container for flex layout, a hit-test-aware ``UIView`` subclass.
1326
1891
 
1327
1892
  All flex semantics (direction, alignment, distribution, padding)
1328
1893
  are computed by the layout engine and applied via
1329
1894
  [`set_frame`][pythonnative.native_views.ios.IOSViewHandler.set_frame].
1895
+ The view is a ``_PNInteractiveViewCTypes`` instance so
1896
+ ``pointer_events`` and ``hit_slop`` can be applied on any render
1897
+ without recreating the view (see the interaction-props section).
1330
1898
  """
1331
1899
 
1332
1900
  def _build(self, props: Dict[str, Any]) -> Any:
1333
- v = ObjCClass("UIView").alloc().init()
1334
- v.setTranslatesAutoresizingMaskIntoConstraints_(True)
1335
- return v
1901
+ return _new_container_view()
1336
1902
 
1337
1903
 
1338
1904
  # ======================================================================
@@ -1400,7 +1966,7 @@ class TextHandler(IOSViewHandler):
1400
1966
  if desc is not None:
1401
1967
  font = UIFont.fontWithDescriptor_size_(desc, size)
1402
1968
  except Exception:
1403
- pass
1969
+ diagnostics.swallowed("ios.TextHandler._font_for")
1404
1970
  return font
1405
1971
 
1406
1972
  _SPAN_REBUILD_KEYS = (
@@ -1463,7 +2029,7 @@ class TextHandler(IOSViewHandler):
1463
2029
  try:
1464
2030
  label.setAlpha_(float(props["opacity"]))
1465
2031
  except Exception:
1466
- pass
2032
+ diagnostics.swallowed("ios.TextHandler._apply")
1467
2033
 
1468
2034
  def _apply_spans(self, label: Any, merged: Dict[str, Any]) -> None:
1469
2035
  """Render a rich-text span list as one NSAttributedString.
@@ -1525,13 +2091,13 @@ class TextHandler(IOSViewHandler):
1525
2091
  elif span.get("text_decoration") == "line_through":
1526
2092
  attr.addAttribute_value_range_("NSStrikethrough", 1, rng)
1527
2093
  except Exception:
1528
- pass
2094
+ diagnostics.swallowed("ios.TextHandler._apply_spans")
1529
2095
  label.setAttributedText_(attr)
1530
2096
  except Exception:
1531
2097
  try:
1532
2098
  label.setText_(str(merged.get("text") or ""))
1533
2099
  except Exception:
1534
- pass
2100
+ diagnostics.swallowed("ios.TextHandler._apply_spans")
1535
2101
 
1536
2102
  def _apply_attributed(self, label: Any, props: Dict[str, Any]) -> None:
1537
2103
  """Re-render the label's text as an NSAttributedString.
@@ -1576,7 +2142,7 @@ class TextHandler(IOSViewHandler):
1576
2142
  attr.addAttribute_value_range_("NSStrikethrough", 1, full_range)
1577
2143
  label.setAttributedText_(attr)
1578
2144
  except Exception:
1579
- pass
2145
+ diagnostics.swallowed("ios.TextHandler._apply_attributed")
1580
2146
 
1581
2147
 
1582
2148
  class ButtonHandler(IOSViewHandler):
@@ -1632,7 +2198,7 @@ class ButtonHandler(IOSViewHandler):
1632
2198
  try:
1633
2199
  btn.setAlpha_(float(props["opacity"]))
1634
2200
  except Exception:
1635
- pass
2201
+ diagnostics.swallowed("ios.ButtonHandler._apply")
1636
2202
 
1637
2203
 
1638
2204
  # ``scrollViewDidScroll:`` hands the delegate a ``UIScrollView*``. rubicon's
@@ -1718,24 +2284,24 @@ class ScrollViewHandler(IOSViewHandler):
1718
2284
  sv.setShowsVerticalScrollIndicator_(visible)
1719
2285
  sv.setShowsHorizontalScrollIndicator_(visible)
1720
2286
  except Exception:
1721
- pass
2287
+ diagnostics.swallowed("ios.ScrollViewHandler._apply")
1722
2288
  if "paging_enabled" in props:
1723
2289
  try:
1724
2290
  sv.setPagingEnabled_(bool(props["paging_enabled"]))
1725
2291
  except Exception:
1726
- pass
2292
+ diagnostics.swallowed("ios.ScrollViewHandler._apply")
1727
2293
  if "bounces" in props:
1728
2294
  b = props["bounces"]
1729
2295
  try:
1730
2296
  sv.setBounces_(True if b is None else bool(b))
1731
2297
  except Exception:
1732
- pass
2298
+ diagnostics.swallowed("ios.ScrollViewHandler._apply")
1733
2299
  if "keyboard_dismiss_mode" in props and props["keyboard_dismiss_mode"] is not None:
1734
2300
  mapping = {"none": 0, "on_drag": 1, "interactive": 2}
1735
2301
  try:
1736
2302
  sv.setKeyboardDismissMode_(mapping.get(props["keyboard_dismiss_mode"], 0))
1737
2303
  except Exception:
1738
- pass
2304
+ diagnostics.swallowed("ios.ScrollViewHandler._apply")
1739
2305
 
1740
2306
  def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
1741
2307
  if name == "scroll_to_offset":
@@ -1745,7 +2311,7 @@ class ScrollViewHandler(IOSViewHandler):
1745
2311
  try:
1746
2312
  native_view.setContentOffset_animated_((x, y), animated)
1747
2313
  except Exception:
1748
- pass
2314
+ diagnostics.swallowed("ios.ScrollViewHandler.command")
1749
2315
  return None
1750
2316
  if name == "scroll_to_end":
1751
2317
  animated = args.get("animated", True) is not False
@@ -1760,7 +2326,7 @@ class ScrollViewHandler(IOSViewHandler):
1760
2326
  offset = (target_x, 0.0) if horizontal else (0.0, target_y)
1761
2327
  native_view.setContentOffset_animated_(offset, animated)
1762
2328
  except Exception:
1763
- pass
2329
+ diagnostics.swallowed("ios.ScrollViewHandler.command")
1764
2330
  return None
1765
2331
  if name == "get_scroll_offset":
1766
2332
  try:
@@ -1796,10 +2362,11 @@ class ScrollViewHandler(IOSViewHandler):
1796
2362
  if existing is not None:
1797
2363
  existing.endRefreshing()
1798
2364
  _pn_action_handlers.pop(_recognizer_ptr(existing), None)
2365
+ _pn_last_control_event_ts.pop(_recognizer_ptr(existing), None)
1799
2366
  sv.setRefreshControl_(None)
1800
2367
  sv.setAlwaysBounceVertical_(False)
1801
2368
  except Exception:
1802
- pass
2369
+ diagnostics.swallowed("ios.ScrollViewHandler._apply_refresh")
1803
2370
  return
1804
2371
  try:
1805
2372
  existing = sv.refreshControl
@@ -1816,7 +2383,7 @@ class ScrollViewHandler(IOSViewHandler):
1816
2383
  try:
1817
2384
  sv.setAlwaysBounceVertical_(True)
1818
2385
  except Exception:
1819
- pass
2386
+ diagnostics.swallowed("ios.ScrollViewHandler._apply_refresh")
1820
2387
  existing = rc
1821
2388
  refreshing = bool(spec.get("refreshing")) if isinstance(spec, dict) else False
1822
2389
  if refreshing:
@@ -1824,7 +2391,7 @@ class ScrollViewHandler(IOSViewHandler):
1824
2391
  else:
1825
2392
  existing.endRefreshing()
1826
2393
  except Exception:
1827
- pass
2394
+ diagnostics.swallowed("ios.ScrollViewHandler._apply_refresh")
1828
2395
 
1829
2396
 
1830
2397
  class ImageHandler(IOSViewHandler):
@@ -1842,12 +2409,12 @@ class ImageHandler(IOSViewHandler):
1842
2409
  try:
1843
2410
  iv.setTintColor_(_uicolor(props["tint_color"]))
1844
2411
  except Exception:
1845
- pass
2412
+ diagnostics.swallowed("ios.ImageHandler._apply")
1846
2413
  if "placeholder_color" in props and props["placeholder_color"] is not None:
1847
2414
  try:
1848
2415
  iv.setBackgroundColor_(_uicolor(props["placeholder_color"]))
1849
2416
  except Exception:
1850
- pass
2417
+ diagnostics.swallowed("ios.ImageHandler._apply")
1851
2418
  if "source" in props and props["source"]:
1852
2419
  self._load_source(iv, str(props["source"]))
1853
2420
  if "scale_type" in props and props["scale_type"]:
@@ -1873,7 +2440,7 @@ class ImageHandler(IOSViewHandler):
1873
2440
  w, h = max_width, h * scale
1874
2441
  return (w, h)
1875
2442
  except Exception:
1876
- pass
2443
+ diagnostics.swallowed("ios.ImageHandler.measure_intrinsic")
1877
2444
  return (0.0, 0.0)
1878
2445
 
1879
2446
  def _load_source(self, iv: Any, source: str) -> None:
@@ -1894,7 +2461,7 @@ class ImageHandler(IOSViewHandler):
1894
2461
  else:
1895
2462
  _fire(iv, "on_error", f"image {source!r} not found")
1896
2463
  except Exception:
1897
- pass
2464
+ diagnostics.swallowed("ios.ImageHandler._load_source")
1898
2465
 
1899
2466
  def _load_data_uri(self, iv: Any, source: str) -> None:
1900
2467
  """Decode an inline ``data:`` URI (base64 payload) into the view."""
@@ -1942,7 +2509,7 @@ class ImageHandler(IOSViewHandler):
1942
2509
  iv.setImage_(image)
1943
2510
  _fire(iv, "on_load")
1944
2511
  except Exception:
1945
- pass
2512
+ diagnostics.swallowed("ios.ImageHandler._load_async.on_ready")
1946
2513
 
1947
2514
  def on_error(message: str) -> None:
1948
2515
  if _state_of(iv).get("pending_uri") == source:
@@ -1950,7 +2517,7 @@ class ImageHandler(IOSViewHandler):
1950
2517
 
1951
2518
  fetch(source, on_ready, on_error)
1952
2519
  except Exception:
1953
- pass
2520
+ diagnostics.swallowed("ios.ImageHandler._load_async")
1954
2521
 
1955
2522
  def _decode_downsampled(self, iv: Any, path: str) -> Any:
1956
2523
  """Decode a file, scaling down to the view's bounds if oversized.
@@ -2052,7 +2619,7 @@ def _tf_on_change_imp(self_ptr: int, _cmd: int, sender_ptr: int) -> None:
2052
2619
  try:
2053
2620
  view.setText_(text)
2054
2621
  except Exception:
2055
- pass
2622
+ diagnostics.swallowed("ios._tf_on_change_imp")
2056
2623
  finally:
2057
2624
  state["suppress"] = False
2058
2625
  _fire(view, "on_change", text)
@@ -2077,7 +2644,7 @@ def _tf_should_return_imp(self_ptr: int, _cmd: int, tf_ptr: int) -> bool:
2077
2644
  _objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p]
2078
2645
  _objc_msgSend(_ct.c_void_p(int(tf_ptr or 0)), _SEL_RESIGN_FIRST_RESPONDER)
2079
2646
  except Exception:
2080
- pass
2647
+ diagnostics.swallowed("ios._tf_should_return_imp")
2081
2648
  return True
2082
2649
 
2083
2650
 
@@ -2244,7 +2811,7 @@ class TextInputHandler(IOSViewHandler):
2244
2811
  )
2245
2812
  view.setAttributedPlaceholder_(attr)
2246
2813
  except Exception:
2247
- pass
2814
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2248
2815
  if "font_size" in props and props["font_size"] is not None:
2249
2816
  view.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
2250
2817
  if "color" in props and props["color"] is not None:
@@ -2255,7 +2822,7 @@ class TextInputHandler(IOSViewHandler):
2255
2822
  try:
2256
2823
  view.setSecureTextEntry_(bool(props["secure"]))
2257
2824
  except Exception:
2258
- pass
2825
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2259
2826
  if "keyboard_type" in props and props["keyboard_type"] is not None:
2260
2827
  mapping = {
2261
2828
  "default": 0,
@@ -2270,19 +2837,19 @@ class TextInputHandler(IOSViewHandler):
2270
2837
  try:
2271
2838
  view.setKeyboardType_(mapping.get(props["keyboard_type"], 0))
2272
2839
  except Exception:
2273
- pass
2840
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2274
2841
  if "auto_capitalize" in props and props["auto_capitalize"] is not None:
2275
2842
  # UITextAutocapitalizationType: none=0, words=1, sentences=2, all=3.
2276
2843
  mapping = {"none": 0, "words": 1, "sentences": 2, "characters": 3}
2277
2844
  try:
2278
2845
  view.setAutocapitalizationType_(mapping.get(props["auto_capitalize"], 2))
2279
2846
  except Exception:
2280
- pass
2847
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2281
2848
  if "auto_correct" in props:
2282
2849
  try:
2283
2850
  view.setAutocorrectionType_(1 if props["auto_correct"] else 0)
2284
2851
  except Exception:
2285
- pass
2852
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2286
2853
  if "return_key_type" in props and props["return_key_type"] is not None:
2287
2854
  mapping = {
2288
2855
  "default": 0,
@@ -2299,23 +2866,23 @@ class TextInputHandler(IOSViewHandler):
2299
2866
  try:
2300
2867
  view.setReturnKeyType_(mapping.get(props["return_key_type"], 0))
2301
2868
  except Exception:
2302
- pass
2869
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2303
2870
  if "selection_color" in props and props["selection_color"] is not None:
2304
2871
  try:
2305
2872
  view.setTintColor_(_uicolor(props["selection_color"]))
2306
2873
  except Exception:
2307
- pass
2874
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2308
2875
  if "text_content_type" in props:
2309
2876
  tct = props["text_content_type"]
2310
2877
  try:
2311
2878
  view.setTextContentType_(_ui_text_content_type(str(tct)) if tct is not None else None)
2312
2879
  except Exception:
2313
- pass
2880
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2314
2881
  if "clear_button" in props and is_field:
2315
2882
  try:
2316
2883
  view.setClearButtonMode_(1 if props["clear_button"] else 0) # 1 = WhileEditing
2317
2884
  except Exception:
2318
- pass
2885
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2319
2886
  # ``editable`` is present only when False (read-only). A removed
2320
2887
  # prop arrives as None on update, which means "editable again".
2321
2888
  if "editable" in props:
@@ -2327,12 +2894,12 @@ class TextInputHandler(IOSViewHandler):
2327
2894
  else:
2328
2895
  view.setEditable_(resolved)
2329
2896
  except Exception:
2330
- pass
2897
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2331
2898
  if "auto_focus" in props and props["auto_focus"]:
2332
2899
  try:
2333
2900
  view.becomeFirstResponder()
2334
2901
  except Exception:
2335
- pass
2902
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2336
2903
  _apply_view_border(view, props)
2337
2904
  _apply_shadow(view, props)
2338
2905
  _apply_transform(view, props)
@@ -2341,7 +2908,7 @@ class TextInputHandler(IOSViewHandler):
2341
2908
  try:
2342
2909
  view.setAlpha_(float(props["opacity"]))
2343
2910
  except Exception:
2344
- pass
2911
+ diagnostics.swallowed("ios.TextInputHandler._apply")
2345
2912
 
2346
2913
  def measure_intrinsic(
2347
2914
  self,
@@ -2357,13 +2924,13 @@ class TextInputHandler(IOSViewHandler):
2357
2924
  try:
2358
2925
  native_view.becomeFirstResponder()
2359
2926
  except Exception:
2360
- pass
2927
+ diagnostics.swallowed("ios.TextInputHandler.command")
2361
2928
  return None
2362
2929
  if name == "blur":
2363
2930
  try:
2364
2931
  native_view.resignFirstResponder()
2365
2932
  except Exception:
2366
- pass
2933
+ diagnostics.swallowed("ios.TextInputHandler.command")
2367
2934
  return None
2368
2935
  if name == "clear":
2369
2936
  state = _state_of(native_view)
@@ -2481,7 +3048,7 @@ class ActivityIndicatorHandler(IOSViewHandler):
2481
3048
  try:
2482
3049
  ai.setActivityIndicatorViewStyle_(style)
2483
3050
  except Exception:
2484
- pass
3051
+ diagnostics.swallowed("ios.ActivityIndicatorHandler._apply")
2485
3052
  if "color" in props and props["color"] is not None:
2486
3053
  ai.setColor_(_uicolor(props["color"]))
2487
3054
  animating = props.get("animating")
@@ -2518,8 +3085,7 @@ class PressableHandler(IOSViewHandler):
2518
3085
  """
2519
3086
 
2520
3087
  def _build(self, props: Dict[str, Any]) -> Any:
2521
- v = ObjCClass("UIView").alloc().init()
2522
- v.setTranslatesAutoresizingMaskIntoConstraints_(True)
3088
+ v = _new_container_view()
2523
3089
  v.setUserInteractionEnabled_(True)
2524
3090
  self._wire_press(v)
2525
3091
  return v
@@ -2563,7 +3129,7 @@ class PressableHandler(IOSViewHandler):
2563
3129
  try:
2564
3130
  rec.setCancelsTouchesInView_(False)
2565
3131
  except Exception:
2566
- pass
3132
+ diagnostics.swallowed("ios.PressableHandler._wire_press")
2567
3133
  _set_recognizer_delegate(rec)
2568
3134
  view.addGestureRecognizer_(rec)
2569
3135
  rec.retain()
@@ -2643,7 +3209,7 @@ def _press_feedback(view: Any, pressed: bool) -> None:
2643
3209
  UIView = ObjCClass("UIView")
2644
3210
  UIView.animateWithDuration_animations_(duration, lambda: view.setAlpha_(opacity))
2645
3211
  except Exception:
2646
- pass
3212
+ diagnostics.swallowed("ios._press_feedback")
2647
3213
 
2648
3214
 
2649
3215
  # ======================================================================
@@ -2668,7 +3234,7 @@ class ProgressBarHandler(IOSViewHandler):
2668
3234
  try:
2669
3235
  ai.startAnimating()
2670
3236
  except Exception:
2671
- pass
3237
+ diagnostics.swallowed("ios.ProgressBarHandler._build")
2672
3238
  return ai
2673
3239
  pv = ObjCClass("UIProgressView").alloc().init()
2674
3240
  pv.setTranslatesAutoresizingMaskIntoConstraints_(True)
@@ -2684,27 +3250,27 @@ class ProgressBarHandler(IOSViewHandler):
2684
3250
  try:
2685
3251
  view.setColor_(_uicolor(props["color"]))
2686
3252
  except Exception:
2687
- pass
3253
+ diagnostics.swallowed("ios.ProgressBarHandler._apply")
2688
3254
  try:
2689
3255
  view.startAnimating()
2690
3256
  except Exception:
2691
- pass
3257
+ diagnostics.swallowed("ios.ProgressBarHandler._apply")
2692
3258
  else:
2693
3259
  if "value" in props and props["value"] is not None:
2694
3260
  try:
2695
3261
  view.setProgress_(float(props["value"]))
2696
3262
  except Exception:
2697
- pass
3263
+ diagnostics.swallowed("ios.ProgressBarHandler._apply")
2698
3264
  if "color" in props and props["color"] is not None:
2699
3265
  try:
2700
3266
  view.setProgressTintColor_(_uicolor(props["color"]))
2701
3267
  except Exception:
2702
- pass
3268
+ diagnostics.swallowed("ios.ProgressBarHandler._apply")
2703
3269
  if "track_color" in props and props["track_color"] is not None:
2704
3270
  try:
2705
3271
  view.setTrackTintColor_(_uicolor(props["track_color"]))
2706
3272
  except Exception:
2707
- pass
3273
+ diagnostics.swallowed("ios.ProgressBarHandler._apply")
2708
3274
  _apply_accessibility(view, props)
2709
3275
 
2710
3276
  def measure_intrinsic(
@@ -2771,7 +3337,7 @@ def _webview_did_finish_imp(self_ptr: int, _cmd_ptr: int, _webview_ptr: int, _na
2771
3337
  try:
2772
3338
  wv.evaluateJavaScript_completionHandler_(str(js), None)
2773
3339
  except Exception:
2774
- pass
3340
+ diagnostics.swallowed("ios._webview_did_finish_imp")
2775
3341
  _fire(wv, "on_load", _webview_url(wv))
2776
3342
 
2777
3343
 
@@ -2855,7 +3421,7 @@ class WebViewHandler(IOSViewHandler):
2855
3421
  try:
2856
3422
  cls.declare_property(prop)
2857
3423
  except Exception:
2858
- pass
3424
+ diagnostics.swallowed("ios.WebViewHandler._build")
2859
3425
  _pn_wkwebview_declared = True
2860
3426
  config = WKWebViewConfiguration.alloc().init()
2861
3427
  delegate_ptr = _new_webview_delegate_ptr()
@@ -2867,14 +3433,14 @@ class WebViewHandler(IOSViewHandler):
2867
3433
  try:
2868
3434
  config.userContentController.addScriptMessageHandler_name_(delegate, "pythonnative")
2869
3435
  except Exception:
2870
- pass
3436
+ diagnostics.swallowed("ios.WebViewHandler._build")
2871
3437
  wv = WKWebView.alloc().initWithFrame_configuration_(((0, 0), (0, 0)), config)
2872
3438
  wv.setTranslatesAutoresizingMaskIntoConstraints_(True)
2873
3439
  if delegate is not None:
2874
3440
  try:
2875
3441
  wv.setNavigationDelegate_(delegate)
2876
3442
  except Exception:
2877
- pass
3443
+ diagnostics.swallowed("ios.WebViewHandler._build")
2878
3444
  if delegate_ptr:
2879
3445
  _pn_webview_state[delegate_ptr] = {"view": wv, "inject_js": None}
2880
3446
  self._delegate_ids[_objc_ptr(wv) or 0] = delegate_ptr
@@ -2907,7 +3473,7 @@ class WebViewHandler(IOSViewHandler):
2907
3473
  try:
2908
3474
  wv.loadHTMLString_baseURL_(str(props["html"]), None)
2909
3475
  except Exception:
2910
- pass
3476
+ diagnostics.swallowed("ios.WebViewHandler._apply")
2911
3477
  elif "url" in props and props["url"]:
2912
3478
  try:
2913
3479
  NSURL = ObjCClass("NSURL")
@@ -2915,38 +3481,38 @@ class WebViewHandler(IOSViewHandler):
2915
3481
  url_obj = NSURL.URLWithString_(str(props["url"]))
2916
3482
  wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
2917
3483
  except Exception:
2918
- pass
3484
+ diagnostics.swallowed("ios.WebViewHandler._apply")
2919
3485
  if "scroll_enabled" in props:
2920
3486
  enabled = props["scroll_enabled"]
2921
3487
  try:
2922
3488
  wv.scrollView.setScrollEnabled_(True if enabled is None else bool(enabled))
2923
3489
  except Exception:
2924
- pass
3490
+ diagnostics.swallowed("ios.WebViewHandler._apply")
2925
3491
 
2926
3492
  def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
2927
3493
  if name == "eval_js":
2928
3494
  try:
2929
3495
  native_view.evaluateJavaScript_completionHandler_(str(args.get("source", "")), None)
2930
3496
  except Exception:
2931
- pass
3497
+ diagnostics.swallowed("ios.WebViewHandler.command")
2932
3498
  return None
2933
3499
  if name == "reload":
2934
3500
  try:
2935
3501
  native_view.reload()
2936
3502
  except Exception:
2937
- pass
3503
+ diagnostics.swallowed("ios.WebViewHandler.command")
2938
3504
  return None
2939
3505
  if name == "go_back":
2940
3506
  try:
2941
3507
  native_view.goBack()
2942
3508
  except Exception:
2943
- pass
3509
+ diagnostics.swallowed("ios.WebViewHandler.command")
2944
3510
  return None
2945
3511
  if name == "go_forward":
2946
3512
  try:
2947
3513
  native_view.goForward()
2948
3514
  except Exception:
2949
- pass
3515
+ diagnostics.swallowed("ios.WebViewHandler.command")
2950
3516
  return None
2951
3517
  return None
2952
3518
 
@@ -2976,9 +3542,7 @@ class SafeAreaViewHandler(IOSViewHandler):
2976
3542
  """Plain container; safe-area insets are applied by the layout engine."""
2977
3543
 
2978
3544
  def _build(self, props: Dict[str, Any]) -> Any:
2979
- v = ObjCClass("UIView").alloc().init()
2980
- v.setTranslatesAutoresizingMaskIntoConstraints_(True)
2981
- return v
3545
+ return _new_container_view()
2982
3546
 
2983
3547
 
2984
3548
  # ======================================================================
@@ -3026,13 +3590,13 @@ class ModalHandler(IOSViewHandler):
3026
3590
  try:
3027
3591
  child.setTranslatesAutoresizingMaskIntoConstraints_(True)
3028
3592
  except Exception:
3029
- pass
3593
+ diagnostics.swallowed("ios.ModalHandler.insert_child")
3030
3594
  try:
3031
3595
  content = modal["content_view"]
3032
3596
  count = len(list(content.subviews or []))
3033
3597
  content.insertSubview_atIndex_(child, max(0, min(index, count)))
3034
3598
  except Exception:
3035
- pass
3599
+ diagnostics.swallowed("ios.ModalHandler.insert_child")
3036
3600
  else:
3037
3601
  buf = state.setdefault("pending_children", [])
3038
3602
  buf.insert(max(0, min(index, len(buf))), child)
@@ -3041,7 +3605,7 @@ class ModalHandler(IOSViewHandler):
3041
3605
  try:
3042
3606
  child.removeFromSuperview()
3043
3607
  except Exception:
3044
- pass
3608
+ diagnostics.swallowed("ios.ModalHandler.remove_child")
3045
3609
  buf = _state_of(parent).get("pending_children")
3046
3610
  if buf and child in buf:
3047
3611
  buf.remove(child)
@@ -3082,7 +3646,7 @@ class ModalHandler(IOSViewHandler):
3082
3646
  content.setFrame_(((0, 0), (bounds.size.width, bounds.size.height)))
3083
3647
  content.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
3084
3648
  except Exception:
3085
- pass
3649
+ diagnostics.swallowed("ios.ModalHandler._present")
3086
3650
 
3087
3651
  # UIModalPresentationStyle: fullScreen=0, pageSheet=1,
3088
3652
  # formSheet=2, overCurrentContext=6 (the dimmed overlay).
@@ -3091,7 +3655,7 @@ class ModalHandler(IOSViewHandler):
3091
3655
  try:
3092
3656
  controller.setModalPresentationStyle_(style_int)
3093
3657
  except Exception:
3094
- pass
3658
+ diagnostics.swallowed("ios.ModalHandler._present")
3095
3659
  # For sheet styles, ``dismiss_on_backdrop=False`` locks
3096
3660
  # interactive (swipe / outside-tap) dismissal so the modal
3097
3661
  # stays put until ``visible`` is driven back to False.
@@ -3099,7 +3663,7 @@ class ModalHandler(IOSViewHandler):
3099
3663
  try:
3100
3664
  controller.setModalInPresentation_(True)
3101
3665
  except Exception:
3102
- pass
3666
+ diagnostics.swallowed("ios.ModalHandler._present")
3103
3667
 
3104
3668
  def _on_present_complete() -> None:
3105
3669
  _fire(placeholder, "on_show")
@@ -3115,7 +3679,7 @@ class ModalHandler(IOSViewHandler):
3115
3679
  child.setTranslatesAutoresizingMaskIntoConstraints_(True)
3116
3680
  content.addSubview_(child)
3117
3681
  except Exception:
3118
- pass
3682
+ diagnostics.swallowed("ios.ModalHandler._present")
3119
3683
 
3120
3684
  top = _top_view_controller_for_alert(UIApplication.sharedApplication)
3121
3685
  if top is not None:
@@ -3133,7 +3697,7 @@ class ModalHandler(IOSViewHandler):
3133
3697
  try:
3134
3698
  controller.dismissViewControllerAnimated_completion_(True, None)
3135
3699
  except Exception:
3136
- pass
3700
+ diagnostics.swallowed("ios.ModalHandler._dismiss")
3137
3701
  _fire(placeholder, "on_dismiss")
3138
3702
 
3139
3703
 
@@ -3152,10 +3716,6 @@ class ModalHandler(IOSViewHandler):
3152
3716
  # other delegate classes in this module.
3153
3717
 
3154
3718
 
3155
- class _PNCGPoint(_ct.Structure):
3156
- _fields_ = [("x", _ct.c_double), ("y", _ct.c_double)]
3157
-
3158
-
3159
3719
  _PORTAL_POINT_INSIDE_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
3160
3720
 
3161
3721
 
@@ -3193,7 +3753,6 @@ def _portal_point_inside_imp(self_ptr: int, _cmd_ptr: int, point: Any, _event_pt
3193
3753
 
3194
3754
  _portal_point_inside_imp_ref = _PORTAL_POINT_INSIDE_TYPE(_portal_point_inside_imp)
3195
3755
 
3196
- _UIVIEW_CLS = _get_cls(b"UIView")
3197
3756
  _PN_PORTAL_VIEW_CLS = _alloc_cls(_UIVIEW_CLS, b"_PNPortalViewCTypes", 0) if _UIVIEW_CLS else None
3198
3757
  if _PN_PORTAL_VIEW_CLS:
3199
3758
  _add_method(
@@ -3270,7 +3829,7 @@ class PortalHandler(IOSViewHandler):
3270
3829
  host.addSubview_(overlay)
3271
3830
  self._sync_overlay_frame(overlay, host)
3272
3831
  except Exception:
3273
- pass
3832
+ diagnostics.swallowed("ios.PortalHandler._ensure_attached")
3274
3833
 
3275
3834
  @staticmethod
3276
3835
  def _sync_overlay_frame(overlay: Any, host: Any) -> None:
@@ -3286,7 +3845,7 @@ class PortalHandler(IOSViewHandler):
3286
3845
  overlay.setFrame_(((left, top), (w, h)))
3287
3846
  overlay.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
3288
3847
  except Exception:
3289
- pass
3848
+ diagnostics.swallowed("ios.PortalHandler._sync_overlay_frame")
3290
3849
 
3291
3850
  def insert_child(self, parent: Any, child: Any, index: int) -> None:
3292
3851
  self._ensure_attached(parent)
@@ -3313,7 +3872,7 @@ class PortalHandler(IOSViewHandler):
3313
3872
  try:
3314
3873
  overlay.release()
3315
3874
  except Exception:
3316
- pass
3875
+ diagnostics.swallowed("ios.PortalHandler._teardown")
3317
3876
 
3318
3877
 
3319
3878
  # ======================================================================
@@ -3347,7 +3906,7 @@ class StatusBarHandler(IOSViewHandler):
3347
3906
  mapping = {"default": 3, "light": 1, "dark": 3}
3348
3907
  app.setStatusBarStyle_animated_(mapping.get(props["bar_style"], 0), True)
3349
3908
  except Exception:
3350
- pass
3909
+ diagnostics.swallowed("ios.StatusBarHandler._apply")
3351
3910
 
3352
3911
  def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
3353
3912
  return
@@ -3413,7 +3972,7 @@ def _ensure_keyboard_observer() -> None:
3413
3972
  None,
3414
3973
  )
3415
3974
  except Exception:
3416
- pass
3975
+ diagnostics.swallowed("ios._ensure_keyboard_observer")
3417
3976
 
3418
3977
 
3419
3978
  class KeyboardAvoidingViewHandler(IOSViewHandler):
@@ -3429,9 +3988,7 @@ class KeyboardAvoidingViewHandler(IOSViewHandler):
3429
3988
 
3430
3989
  def _build(self, props: Dict[str, Any]) -> Any:
3431
3990
  _ensure_keyboard_observer()
3432
- v = ObjCClass("UIView").alloc().init()
3433
- v.setTranslatesAutoresizingMaskIntoConstraints_(True)
3434
- return v
3991
+ return _new_container_view()
3435
3992
 
3436
3993
 
3437
3994
  # ======================================================================
@@ -3457,7 +4014,7 @@ def _tabbar_did_select_imp(self_ptr: int, cmd_ptr: int, tabbar_ptr: int, item_pt
3457
4014
  if 0 <= index < len(items):
3458
4015
  _fire_ptr(int(tabbar_ptr), "on_tab_select", items[index].get("name", ""))
3459
4016
  except Exception:
3460
- pass
4017
+ diagnostics.swallowed("ios._tabbar_did_select_imp")
3461
4018
 
3462
4019
 
3463
4020
  _tabbar_imp_ref = _DELEGATE_IMP_TYPE(_tabbar_did_select_imp)
@@ -3543,7 +4100,7 @@ class TabBarHandler(IOSViewHandler):
3543
4100
  try:
3544
4101
  tab_bar.setItems_animated_(bar_items, False)
3545
4102
  except Exception:
3546
- pass
4103
+ diagnostics.swallowed("ios.TabBarHandler._set_bar_items")
3547
4104
 
3548
4105
  def _resolve_icon(self, UIImage: Any, icon: Any) -> Any:
3549
4106
  """Resolve a tab icon spec to a UIImage, or return None.
@@ -3577,7 +4134,7 @@ class TabBarHandler(IOSViewHandler):
3577
4134
  if i < len(all_items):
3578
4135
  tab_bar.setSelectedItem_(all_items[i])
3579
4136
  except Exception:
3580
- pass
4137
+ diagnostics.swallowed("ios.TabBarHandler._set_active")
3581
4138
  break
3582
4139
 
3583
4140
 
@@ -3600,7 +4157,7 @@ def _top_view_controller_for_alert(app: Any) -> Any:
3600
4157
  try:
3601
4158
  window = app.keyWindow
3602
4159
  except Exception:
3603
- pass
4160
+ diagnostics.swallowed("ios._top_view_controller_for_alert")
3604
4161
 
3605
4162
  if window is None:
3606
4163
  try:
@@ -3612,7 +4169,7 @@ def _top_view_controller_for_alert(app: Any) -> Any:
3612
4169
  if window is None and windows:
3613
4170
  window = windows[0]
3614
4171
  except Exception:
3615
- pass
4172
+ diagnostics.swallowed("ios._top_view_controller_for_alert")
3616
4173
 
3617
4174
  if window is None:
3618
4175
  return None
@@ -3629,7 +4186,7 @@ def _top_view_controller_for_alert(app: Any) -> Any:
3629
4186
  if visible is not None:
3630
4187
  top = visible
3631
4188
  except Exception:
3632
- pass
4189
+ diagnostics.swallowed("ios._top_view_controller_for_alert")
3633
4190
 
3634
4191
  while top is not None:
3635
4192
  try:
@@ -3677,7 +4234,7 @@ def _present_alert(
3677
4234
  try:
3678
4235
  on_result(index)
3679
4236
  except Exception:
3680
- pass
4237
+ diagnostics.swallowed("ios._present_alert._deliver")
3681
4238
 
3682
4239
  def _present_on_main() -> None:
3683
4240
  try:
@@ -3778,7 +4335,7 @@ class PickerHandler(IOSViewHandler):
3778
4335
  try:
3779
4336
  btn.setTitle_forState_(_picker_button_title(merged), 0)
3780
4337
  except Exception:
3781
- pass
4338
+ diagnostics.swallowed("ios.PickerHandler._apply")
3782
4339
  _apply_accessibility(btn, props)
3783
4340
 
3784
4341
  def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
@@ -3814,10 +4371,10 @@ def _checkbox_set_image(btn: Any) -> None:
3814
4371
  if tinted is not None:
3815
4372
  image = tinted
3816
4373
  except Exception:
3817
- pass
4374
+ diagnostics.swallowed("ios._checkbox_set_image")
3818
4375
  btn.setImage_forState_(image, 0)
3819
4376
  except Exception:
3820
- pass
4377
+ diagnostics.swallowed("ios._checkbox_set_image")
3821
4378
 
3822
4379
 
3823
4380
  def _checkbox_toggle(btn: Any) -> None:
@@ -3855,7 +4412,7 @@ class CheckboxHandler(IOSViewHandler):
3855
4412
  btn.setTitleColor_forState_(_uicolor("#111111"), 0)
3856
4413
  btn.setTintColor_(_uicolor("#111111"))
3857
4414
  except Exception:
3858
- pass
4415
+ diagnostics.swallowed("ios.CheckboxHandler._apply")
3859
4416
  if "value" in props:
3860
4417
  state["value"] = bool(props["value"])
3861
4418
  if "label" in props:
@@ -3863,14 +4420,14 @@ class CheckboxHandler(IOSViewHandler):
3863
4420
  try:
3864
4421
  btn.setTitle_forState_(str(label) if label is not None else "", 0)
3865
4422
  except Exception:
3866
- pass
4423
+ diagnostics.swallowed("ios.CheckboxHandler._apply")
3867
4424
  # An image-bearing custom button is not exposed to the
3868
4425
  # accessibility tree by title alone; mirror the label
3869
4426
  # explicitly (an accessibility_label prop still wins below).
3870
4427
  try:
3871
4428
  btn.setAccessibilityLabel_(str(label) if label is not None else "")
3872
4429
  except Exception:
3873
- pass
4430
+ diagnostics.swallowed("ios.CheckboxHandler._apply")
3874
4431
  if "disabled" in props:
3875
4432
  # ``disabled`` is present only when True; a removed prop
3876
4433
  # (None) re-enables the control.
@@ -3878,7 +4435,7 @@ class CheckboxHandler(IOSViewHandler):
3878
4435
  try:
3879
4436
  btn.setEnabled_(not disabled)
3880
4437
  except Exception:
3881
- pass
4438
+ diagnostics.swallowed("ios.CheckboxHandler._apply")
3882
4439
  _checkbox_set_image(btn)
3883
4440
  _apply_accessibility(btn, props)
3884
4441
 
@@ -3935,7 +4492,7 @@ class SegmentedControlHandler(IOSViewHandler):
3935
4492
  for i, title in enumerate(new_segments):
3936
4493
  control.insertSegmentWithTitle_atIndex_animated_(title, i, False)
3937
4494
  except Exception:
3938
- pass
4495
+ diagnostics.swallowed("ios.SegmentedControlHandler._apply")
3939
4496
  finally:
3940
4497
  state["suppress"] = False
3941
4498
  rebuilt = True
@@ -3948,7 +4505,7 @@ class SegmentedControlHandler(IOSViewHandler):
3948
4505
  try:
3949
4506
  control.setSelectedSegmentIndex_(int(merged.get("selected_index", 0) or 0))
3950
4507
  except Exception:
3951
- pass
4508
+ diagnostics.swallowed("ios.SegmentedControlHandler._apply")
3952
4509
  finally:
3953
4510
  state["suppress"] = False
3954
4511
  if "tint_color" in props and props["tint_color"] is not None:
@@ -3956,11 +4513,11 @@ class SegmentedControlHandler(IOSViewHandler):
3956
4513
  try:
3957
4514
  control.setSelectedSegmentTintColor_(color) # iOS 13+
3958
4515
  except Exception:
3959
- pass
4516
+ diagnostics.swallowed("ios.SegmentedControlHandler._apply")
3960
4517
  try:
3961
4518
  control.setTintColor_(color)
3962
4519
  except Exception:
3963
- pass
4520
+ diagnostics.swallowed("ios.SegmentedControlHandler._apply")
3964
4521
  if "enabled" in props:
3965
4522
  # ``enabled`` is present only when False; a removed prop
3966
4523
  # (None) re-enables the control.
@@ -3968,7 +4525,7 @@ class SegmentedControlHandler(IOSViewHandler):
3968
4525
  try:
3969
4526
  control.setEnabled_(True if enabled is None else bool(enabled))
3970
4527
  except Exception:
3971
- pass
4528
+ diagnostics.swallowed("ios.SegmentedControlHandler._apply")
3972
4529
  _apply_accessibility(control, props)
3973
4530
 
3974
4531
  def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
@@ -4007,7 +4564,7 @@ def _date_formatter(mode: str) -> Any:
4007
4564
  NSLocale = ObjCClass("NSLocale")
4008
4565
  formatter.setLocale_(NSLocale.alloc().initWithLocaleIdentifier_("en_US_POSIX"))
4009
4566
  except Exception:
4010
- pass
4567
+ diagnostics.swallowed("ios._date_formatter")
4011
4568
  formatter.retain()
4012
4569
  _pn_date_formatters[fmt] = formatter
4013
4570
  return formatter
@@ -4024,7 +4581,7 @@ class DatePickerHandler(IOSViewHandler):
4024
4581
  try:
4025
4582
  picker.setPreferredDatePickerStyle_(2) # UIDatePickerStyleCompact
4026
4583
  except Exception:
4027
- pass
4584
+ diagnostics.swallowed("ios.DatePickerHandler._build")
4028
4585
 
4029
4586
  def _on_change() -> None:
4030
4587
  state = _state_of(picker)
@@ -4049,7 +4606,7 @@ class DatePickerHandler(IOSViewHandler):
4049
4606
  try:
4050
4607
  picker.setDatePickerMode_(mode_map.get(mode, 1))
4051
4608
  except Exception:
4052
- pass
4609
+ diagnostics.swallowed("ios.DatePickerHandler._apply")
4053
4610
  if "minimum" in props:
4054
4611
  self._set_bound(picker, "setMinimumDate_", props["minimum"], mode)
4055
4612
  if "maximum" in props:
@@ -4064,7 +4621,7 @@ class DatePickerHandler(IOSViewHandler):
4064
4621
  try:
4065
4622
  picker.setDate_animated_(date, False)
4066
4623
  except Exception:
4067
- pass
4624
+ diagnostics.swallowed("ios.DatePickerHandler._apply")
4068
4625
  finally:
4069
4626
  state["suppress"] = False
4070
4627
  if "enabled" in props:
@@ -4074,7 +4631,7 @@ class DatePickerHandler(IOSViewHandler):
4074
4631
  try:
4075
4632
  picker.setEnabled_(True if enabled is None else bool(enabled))
4076
4633
  except Exception:
4077
- pass
4634
+ diagnostics.swallowed("ios.DatePickerHandler._apply")
4078
4635
  _apply_accessibility(picker, props)
4079
4636
 
4080
4637
  def _set_bound(self, picker: Any, selector: str, value: Any, mode: str) -> None:
@@ -4085,7 +4642,7 @@ class DatePickerHandler(IOSViewHandler):
4085
4642
  date = _date_formatter(mode).dateFromString_(str(value))
4086
4643
  getattr(picker, selector)(date)
4087
4644
  except Exception:
4088
- pass
4645
+ diagnostics.swallowed("ios.DatePickerHandler._set_bound")
4089
4646
 
4090
4647
  def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
4091
4648
  try:
@@ -4230,7 +4787,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
4230
4787
  cell.setFrame_(((0, 0), (cell_w, row_h)))
4231
4788
  cell.contentView.setFrame_(((0, 0), (cell_w, row_h)))
4232
4789
  except Exception:
4233
- pass
4790
+ diagnostics.swallowed("ios._table_cell_imp")
4234
4791
 
4235
4792
  content = cell.contentView
4236
4793
  try:
@@ -4240,7 +4797,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
4240
4797
  for sub in list(existing_subs or []):
4241
4798
  sub.removeFromSuperview()
4242
4799
  except Exception:
4243
- pass
4800
+ diagnostics.swallowed("ios._table_cell_imp")
4244
4801
 
4245
4802
  # Only rows in (or near) the visible content window get a
4246
4803
  # mounted subtree. UITableView's accessibility container calls
@@ -4270,7 +4827,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
4270
4827
  try:
4271
4828
  cell.setAccessibilityElementsHidden_(not in_window)
4272
4829
  except Exception:
4273
- pass
4830
+ diagnostics.swallowed("ios._table_cell_imp")
4274
4831
 
4275
4832
  if in_window and info is not None:
4276
4833
  render_row = info.get("render_row")
@@ -4289,7 +4846,7 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
4289
4846
  try:
4290
4847
  root.setFrame_(((0, 0), (cell_w, row_h)))
4291
4848
  except Exception:
4292
- pass
4849
+ diagnostics.swallowed("ios._table_cell_imp")
4293
4850
  except Exception:
4294
4851
  print(f"[VirtualList][iOS] mount for row={row} raised:")
4295
4852
  _tb.print_exc()
@@ -4314,7 +4871,7 @@ def _table_did_select_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int)
4314
4871
  _objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_bool]
4315
4872
  _objc_msgSend(_ct.c_void_p(tv_ptr), _SEL_DESELECT_ROW, _ct.c_void_p(ip_ptr), True)
4316
4873
  except Exception:
4317
- pass
4874
+ diagnostics.swallowed("ios._table_did_select_imp")
4318
4875
  _fire_ptr(int(tv_ptr or 0), "on_row_press", row)
4319
4876
 
4320
4877
 
@@ -4440,7 +4997,7 @@ class VirtualListHandler(IOSViewHandler):
4440
4997
  estimate = float(props.get("row_height", 44.0) or 44.0)
4441
4998
  tv.setEstimatedRowHeight_(max(1.0, estimate))
4442
4999
  except Exception:
4443
- pass
5000
+ diagnostics.swallowed("ios.VirtualListHandler._build")
4444
5001
  tv.retain()
4445
5002
  _pn_retained_views.append(tv)
4446
5003
 
@@ -4498,12 +5055,12 @@ class VirtualListHandler(IOSViewHandler):
4498
5055
  try:
4499
5056
  tv.setShowsVerticalScrollIndicator_(visible)
4500
5057
  except Exception:
4501
- pass
5058
+ diagnostics.swallowed("ios.VirtualListHandler._apply")
4502
5059
  if data_changed and not initial:
4503
5060
  try:
4504
5061
  tv.reloadData()
4505
5062
  except Exception:
4506
- pass
5063
+ diagnostics.swallowed("ios.VirtualListHandler._apply")
4507
5064
 
4508
5065
  def _teardown(self, tv: Any) -> None:
4509
5066
  info = self._source_state(tv)
@@ -4512,7 +5069,7 @@ class VirtualListHandler(IOSViewHandler):
4512
5069
  try:
4513
5070
  info["pool"].release_all()
4514
5071
  except Exception:
4515
- pass
5072
+ diagnostics.swallowed("ios.VirtualListHandler._teardown")
4516
5073
  info["tv"] = None
4517
5074
  _pn_table_state.pop(info.get("source_ptr"), None)
4518
5075
 
@@ -4536,7 +5093,7 @@ class VirtualListHandler(IOSViewHandler):
4536
5093
  try:
4537
5094
  native_view.setContentOffset_animated_((0.0, y), animated)
4538
5095
  except Exception:
4539
- pass
5096
+ diagnostics.swallowed("ios.VirtualListHandler.command")
4540
5097
  return None
4541
5098
  if name == "scroll_to_index":
4542
5099
  index = int(args.get("index", 0) or 0)
@@ -4549,7 +5106,7 @@ class VirtualListHandler(IOSViewHandler):
4549
5106
  # 1 = UITableViewScrollPositionTop
4550
5107
  native_view.scrollToRowAtIndexPath_atScrollPosition_animated_(path, 1, animated)
4551
5108
  except Exception:
4552
- pass
5109
+ diagnostics.swallowed("ios.VirtualListHandler.command")
4553
5110
  return None
4554
5111
  if name == "scroll_to_end":
4555
5112
  animated = args.get("animated", True) is not False
@@ -4559,7 +5116,7 @@ class VirtualListHandler(IOSViewHandler):
4559
5116
  target = max(0.0, content_h - bounds_h)
4560
5117
  native_view.setContentOffset_animated_((0.0, target), animated)
4561
5118
  except Exception:
4562
- pass
5119
+ diagnostics.swallowed("ios.VirtualListHandler.command")
4563
5120
  return None
4564
5121
  if name == "get_scroll_offset":
4565
5122
  try: