fix(ios): avoid swift_dynamicCast crash walking subviews on Mac Catalyst#1469
Open
skrtdev wants to merge 1 commit into
Open
fix(ios): avoid swift_dynamicCast crash walking subviews on Mac Catalyst#1469skrtdev wants to merge 1 commit into
skrtdev wants to merge 1 commit into
Conversation
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.
There was a problem hiding this comment.
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 inViewHierarchyNavigatorthat enumerates subviews via KVC on Catalyst/Mac idiom and defensively filters toUIView. - Updates multiple subview-walking sites in
ViewHierarchyNavigatorto usesafeSubviews(of:)instead ofview.subviews. - Updates
UIView.findFirstResponder()on Catalyst/Mac idiom to find the first responder viaUIApplication.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) |
Owner
Author
|
I'll check if my crash is still reproducible in 1.21.8 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Mac Catalyst running in the Mac idiom,
view.subviewscan contain host containers (e.g.UIRemoteViewwrappers around Mac-side AppKit views) whose pointers cannot be bridged toUIViewat the Swift level. Iterating them with the standard:trips a
swift_dynamicCastSIGSEGV — which fires from this library's two subview-walking entry points whenever something on screen calls-becomeFirstResponderor asks for the input-field hierarchy.Two fixes, both gated to
#if targetEnvironment(macCatalyst)+ auserInterfaceIdiom == .macruntime 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 toUIView, silently skipping non-UIViewhost pointers. Outside Catalyst it's a one-linereturn view.subviews— no behaviour or perf change.The three subview-iteration sites (
findTextInputsbody,isGroupRootbranch,findTextInputInHierarchydirection loop) all switch tosafeSubviews(of:).Test plan
userInterfaceIdiom == .mac— both fixes compile cleanly.TextInputon a Catalyst Mac build — noswift_dynamicCastcrash; keyboard toolbar previous/next still navigates between fields.findFirstResponder()returns the focused field on Catalyst Mac.🤖 Generated with Claude Code