Skip to content

fix(ios): avoid swift_dynamicCast crash walking subviews on Mac Catalyst#1469

Open
skrtdev wants to merge 1 commit into
kirillzyusko:mainfrom
skrtdev:fix/mac-catalyst-firstresponder-crash
Open

fix(ios): avoid swift_dynamicCast crash walking subviews on Mac Catalyst#1469
skrtdev wants to merge 1 commit into
kirillzyusko:mainfrom
skrtdev:fix/mac-catalyst-firstresponder-crash

Conversation

@skrtdev
Copy link
Copy Markdown

@skrtdev skrtdev commented May 21, 2026

Summary

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 {  }

trips a swift_dynamicCast SIGSEGV — which fires from this library's two subview-walking entry points whenever something on screen calls -becomeFirstResponder or asks for the input-field hierarchy.

Two fixes, both gated to #if targetEnvironment(macCatalyst) + a userInterfaceIdiom == .mac runtime check, so iOS / iPadOS / Catalyst-as-iPad take the existing code paths byte-for-byte.

1. UIView.findFirstResponder()

Route through UIKit's responder chain via sendAction(_:to: nil, from: nil, for:) instead of recursing through subviews. The nil-target send walks the responder chain and stops at the first responder, which records itself into a static slot via a small selector helper. No subview tree walk.

2. ViewHierarchyNavigator (getAllInputFields, findTextInputInHierarchy)

Add a private safeSubviews(of:) helper that, on Catalyst-in-Mac-idiom, enumerates subviews via Objective-C KVC (view.value(forKey: \"subviews\") as? NSArray) and casts each entry defensively to UIView, silently skipping non-UIView host pointers. Outside Catalyst it's a one-line return view.subviews — no behaviour or perf change.

The three subview-iteration sites (findTextInputs body, isGroupRoot branch, findTextInputInHierarchy direction loop) all switch to safeSubviews(of:).

Test plan

  • Build for iOS Simulator — no diff in any iOS code path, no behaviour change.
  • Build for Mac Catalyst with userInterfaceIdiom == .mac — both fixes compile cleanly.
  • Tap into a TextInput on a Catalyst Mac build — no swift_dynamicCast crash; keyboard toolbar previous/next still navigates between fields.
  • findFirstResponder() returns the focused field on Catalyst Mac.

🤖 Generated with Claude Code

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 — which fires from this library's two
subview-walking entry points whenever something on screen calls
-becomeFirstResponder or asks for the input-field hierarchy.

Two fixes, both gated to `#if targetEnvironment(macCatalyst)` +
`userInterfaceIdiom == .mac`, so all other platforms keep the existing
code paths byte-for-byte:

1. UIView.findFirstResponder():
   Route through UIKit's responder chain via
   sendAction(_:to: nil, from: nil, for:) instead of recursing through
   subviews. The first responder records itself into a static slot via
   a small selector helper, so we never touch the subview tree.

2. ViewHierarchyNavigator (getAllInputFields, findTextInputInHierarchy):
   Add a private safeSubviews(of:) helper that enumerates subviews via
   Objective-C KVC and casts each entry defensively to UIView. The
   three subview-iteration sites use it instead of `view.subviews`.
   Outside Catalyst the helper is a single-line `return view.subviews`
   so there's no behaviour or performance change.
Copilot AI review requested due to automatic review settings May 21, 2026 11:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens iOS view traversal code paths to avoid swift_dynamicCast crashes on Mac Catalyst (Mac idiom) when Swift iterates UIView.subviews that may contain non-bridgeable host/container pointers.

Changes:

  • Adds a safeSubviews(of:) helper in ViewHierarchyNavigator that enumerates subviews via KVC on Catalyst/Mac idiom and defensively filters to UIView.
  • Updates multiple subview-walking sites in ViewHierarchyNavigator to use safeSubviews(of:) instead of view.subviews.
  • Updates UIView.findFirstResponder() on Catalyst/Mac idiom to find the first responder via UIApplication.sendAction (responder chain) rather than recursing through subviews.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ios/traversal/ViewHierarchyNavigator.swift Adds safeSubviews(of:) and uses it in key traversal flows to prevent Catalyst/Mac-idiom subview iteration crashes.
ios/extensions/UIView.swift Switches first-responder discovery to responder-chain action dispatch on Catalyst/Mac idiom and adds a helper capture mechanism.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +72 to +79
#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) }
}
Comment on lines +28 to +33
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)
@kirillzyusko
Copy link
Copy Markdown
Owner

Hey @skrtdev

Aren't you trying to fix the problem that has been fixed in #1454? Which library version do you use when crash is reproducible?

@skrtdev
Copy link
Copy Markdown
Author

skrtdev commented May 21, 2026

I'll check if my crash is still reproducible in 1.21.8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants