@@ -1266,30 +1266,55 @@ def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
12661266 btn .addTarget_action_forControlEvents_ (handler , SEL ("onTap:" ), 1 << 6 )
12671267
12681268
1269- # Maps ``id(delegate)`` -> ``{"on_scroll": cb, "delegate": obj}`` so the
1270- # UIScrollViewDelegate can forward ``scrollViewDidScroll:`` back to Python.
1271- _pn_scroll_delegate_map : dict = {}
1269+ # ``scrollViewDidScroll:`` hands the delegate a ``UIScrollView*``. rubicon's
1270+ # ``@objc_method`` FFI bridge is unreliable for delegate callbacks that take
1271+ # ObjC object arguments on arm64 (see the module header note) — on the arm64
1272+ # simulator the callback simply never reaches Python, so ``on_scroll`` would
1273+ # silently never fire. Exactly like the UITabBar delegate, we therefore build
1274+ # the delegate class with raw libobjc and dispatch through a CFUNCTYPE IMP.
1275+ #
1276+ # We read ``contentOffset`` off the *retained rubicon* scroll view we already
1277+ # hold (keyed by the delegate instance pointer) rather than off the raw
1278+ # callback argument: that sidesteps both the object-arg marshaling issue and
1279+ # the CGPoint struct-return ABI quirks of calling ``contentOffset`` via raw
1280+ # ``objc_msgSend``.
1281+ _pn_scroll_imp_map : Dict [int , Dict [str , Any ]] = {}
12721282
1283+ _SCROLL_IMP_TYPE = _ct .CFUNCTYPE (None , _ct .c_void_p , _ct .c_void_p , _ct .c_void_p )
12731284
1274- class _PNScrollDelegate (NSObject ): # type: ignore[valid-type]
1275- @objc_method
1276- def scrollViewDidScroll_ (self , scroll_view : object ) -> None :
1277- info = _pn_scroll_delegate_map .get (id (self ))
1278- if not info :
1279- return
1280- cb = info .get ("on_scroll" )
1281- if cb is None :
1282- return
1283- try :
1284- offset = scroll_view .contentOffset
1285- x = float (offset .x )
1286- y = float (offset .y )
1287- except Exception :
1288- return
1289- try :
1290- cb (x , y )
1291- except Exception :
1292- pass
1285+
1286+ def _scroll_did_scroll_imp (self_ptr : int , _cmd_ptr : int , _scroll_view_ptr : int ) -> None :
1287+ """Raw C callback for ``scrollViewDidScroll:``."""
1288+ info = _pn_scroll_imp_map .get (self_ptr )
1289+ if not info :
1290+ return
1291+ cb = info .get ("on_scroll" )
1292+ sv = info .get ("sv" )
1293+ if cb is None or sv is None :
1294+ return
1295+ try :
1296+ offset = sv .contentOffset
1297+ x = float (offset .x )
1298+ y = float (offset .y )
1299+ except Exception :
1300+ return
1301+ try :
1302+ cb (x , y )
1303+ except Exception :
1304+ pass
1305+
1306+
1307+ _scroll_imp_ref = _SCROLL_IMP_TYPE (_scroll_did_scroll_imp )
1308+
1309+ _PN_SCROLL_DELEGATE_CLS = _alloc_cls (_NS_OBJECT_CLS , b"_PNScrollDelegateCTypes" , 0 )
1310+ if _PN_SCROLL_DELEGATE_CLS :
1311+ _add_method (
1312+ _PN_SCROLL_DELEGATE_CLS ,
1313+ _sel_reg (b"scrollViewDidScroll:" ),
1314+ _ct .cast (_scroll_imp_ref , _ct .c_void_p ),
1315+ b"v@:@" ,
1316+ )
1317+ _reg_cls (_PN_SCROLL_DELEGATE_CLS )
12931318
12941319
12951320class ScrollViewHandler (IOSViewHandler ):
@@ -1361,21 +1386,26 @@ def _apply_scroll_props(self, sv: Any, props: Dict[str, Any]) -> None:
13611386 self ._wire_scroll (sv , props ["on_scroll" ])
13621387
13631388 def _wire_scroll (self , sv : Any , on_scroll : Any ) -> None :
1364- delegate_id = getattr (sv , "_pn_scroll_delegate_id" , None )
1365- if delegate_id is None :
1366- delegate = _PNScrollDelegate .new ()
1367- delegate .retain ()
1368- _pn_retained_views .append (delegate )
1369- delegate_id = id (delegate )
1370- sv ._pn_scroll_delegate_id = delegate_id
1371- _pn_scroll_delegate_map [delegate_id ] = {"on_scroll" : on_scroll , "delegate" : delegate }
1372- try :
1373- sv .setDelegate_ (delegate )
1374- except Exception :
1375- pass
1389+ delegate_ptr = getattr (sv , "_pn_scroll_delegate_ptr" , None )
1390+ if delegate_ptr is None :
1391+ if not _PN_SCROLL_DELEGATE_CLS :
1392+ return
1393+ _objc_msgSend .restype = _ct .c_void_p
1394+ _objc_msgSend .argtypes = [_ct .c_void_p , _ct .c_void_p ]
1395+ d = _objc_msgSend (_PN_SCROLL_DELEGATE_CLS , _SEL_ALLOC )
1396+ d = _objc_msgSend (d , _SEL_INIT )
1397+ d = _objc_msgSend (d , _SEL_RETAIN )
1398+ delegate_ptr = int (d )
1399+ sv ._pn_scroll_delegate_ptr = delegate_ptr
1400+ _pn_scroll_imp_map [delegate_ptr ] = {"on_scroll" : on_scroll , "sv" : sv }
1401+ _objc_msgSend .restype = None
1402+ _objc_msgSend .argtypes = [_ct .c_void_p , _ct .c_void_p , _ct .c_void_p ]
1403+ sv_ptr = sv .ptr if hasattr (sv , "ptr" ) else sv
1404+ _objc_msgSend (sv_ptr , _SEL_SET_DELEGATE , _ct .c_void_p (delegate_ptr ))
13761405 else :
1377- info = _pn_scroll_delegate_map .setdefault (delegate_id , {})
1406+ info = _pn_scroll_imp_map .setdefault (delegate_ptr , {})
13781407 info ["on_scroll" ] = on_scroll
1408+ info ["sv" ] = sv
13791409
13801410 def _apply_refresh (self , sv : Any , props : Dict [str , Any ]) -> None :
13811411 spec = props .get ("refresh_control" )
0 commit comments