diff --git a/ios/extensions/UIView.swift b/ios/extensions/UIView.swift index f47e185385..c488ac0929 100644 --- a/ios/extensions/UIView.swift +++ b/ios/extensions/UIView.swift @@ -39,6 +39,27 @@ public extension UIView { if isFirstResponder { return self } + #if targetEnvironment(macCatalyst) + // On Mac Catalyst running in the Mac idiom, `view.subviews` can contain + // host containers (e.g. UIRemoteView wrappers around Mac-side AppKit + // views) whose pointers cannot be bridged to UIView at the Swift level. + // Iterating them with the standard `for subview in subviews` loop trips + // a `swift_dynamicCast` SIGSEGV whenever findFirstResponder() is called + // during normal UIKit operations such as -becomeFirstResponder. + // Route through UIKit's responder chain via sendAction(_:to: nil, …) + // instead, which locates the first responder without touching the + // subview tree. + if UIDevice.current.userInterfaceIdiom == .mac { + UIResponder._kbcCapturedFirstResponder = nil + UIApplication.shared.sendAction( + #selector(UIResponder._kbcRecordFirstResponder(_:)), + to: nil, + from: nil, + for: nil + ) + return UIResponder._kbcCapturedFirstResponder as? UIView + } + #endif for subview in subviews { if let responder = subview.findFirstResponder() { return responder @@ -48,6 +69,21 @@ public extension UIView { } } +#if targetEnvironment(macCatalyst) +private var _kbcCapturedFirstResponderKey: UInt8 = 0 + +extension UIResponder { + fileprivate static var _kbcCapturedFirstResponder: UIResponder? { + get { objc_getAssociatedObject(UIResponder.self, &_kbcCapturedFirstResponderKey) as? UIResponder } + set { objc_setAssociatedObject(UIResponder.self, &_kbcCapturedFirstResponderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + @objc fileprivate func _kbcRecordFirstResponder(_ sender: Any?) { + UIResponder._kbcCapturedFirstResponder = self + } +} +#endif + public extension Optional where Wrapped == UIView { var frameTransitionInWindow: (Double, Double) { let areCrossFadeTransitionsEnabled = (self?.layer.presentation()?.animationKeys() ?? []).contains("opacity") diff --git a/ios/traversal/ViewHierarchyNavigator.swift b/ios/traversal/ViewHierarchyNavigator.swift index 29164a8576..b636be3b43 100644 --- a/ios/traversal/ViewHierarchyNavigator.swift +++ b/ios/traversal/ViewHierarchyNavigator.swift @@ -13,6 +13,35 @@ import UIKit public class ViewHierarchyNavigator: NSObject { private static let groupViewTypeName = "KeyboardToolbarGroupView" + /// Returns the subviews of the given view in a way that's safe to iterate + /// from Swift on Mac Catalyst. + /// + /// On Catalyst running in the Mac idiom, `view.subviews` can contain host + /// containers (e.g. UIRemoteView wrappers around Mac-side AppKit views) + /// whose pointers cannot be bridged to UIView at the Swift level. Iterating + /// the standard Swift `[UIView]` array trips a `swift_dynamicCast` SIGSEGV + /// whenever one of those entries is encountered. Enumerating via Objective-C + /// KVC instead gives us an untyped NSArray we can defensively cast and skip. + /// + /// On any other platform this returns `view.subviews` directly — no behaviour + /// or performance change. + private static func safeSubviews(of view: UIView) -> [UIView] { + #if targetEnvironment(macCatalyst) + if UIDevice.current.userInterfaceIdiom == .mac { + guard let raw = view.value(forKey: "subviews") as? NSArray else { return [] } + var result: [UIView] = [] + result.reserveCapacity(raw.count) + for entry in raw { + if let subview = entry as? UIView { + result.append(subview) + } + } + return result + } + #endif + return view.subviews + } + @objc public static func setFocusTo(direction: String) { DispatchQueue.main.async { if direction == "current" { @@ -58,7 +87,7 @@ public class ViewHierarchyNavigator: NSObject { if let textInput = isValidTextInput(view) { textInputs.append(textInput) } else if !isGroupView(view) { - for subview in view.subviews { + for subview in safeSubviews(of: view) { findTextInputs(in: subview) } } @@ -66,7 +95,7 @@ public class ViewHierarchyNavigator: NSObject { if isGroupRoot { // When root is a group, search its children directly - for subview in rootView.subviews { + for subview in safeSubviews(of: rootView) { findTextInputs(in: subview) } } else { @@ -138,7 +167,8 @@ public class ViewHierarchyNavigator: NSObject { guard !isGroupView(view) else { return nil } // Determine the iteration order based on the direction - let subviews = direction == "next" ? view.subviews : view.subviews.reversed() + let children = safeSubviews(of: view) + let subviews: [UIView] = direction == "next" ? children : children.reversed() // Iterate over subviews for subview in subviews {