Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions ios/extensions/UIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) }
}
Comment on lines +72 to +79

@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")
Expand Down
36 changes: 33 additions & 3 deletions ios/traversal/ViewHierarchyNavigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +28 to +33
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" {
Expand Down Expand Up @@ -58,15 +87,15 @@ 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)
}
}
}

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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading