@@ -24,11 +24,9 @@ public struct AccessibilityDeferral {
2424 /// Content from an outside source that will be exposed via AccessibilityCustomContent
2525 var deferredAccessibilityContent : [ AccessibilityDeferral . Content ] ? { get set }
2626
27- /// Called by the parent container. Default implementation provided.
28- /// - parameter content: the accessibility content to apply to the receiver.
29- func applyDeferredAccessibility(
30- content: [ AccessibilityDeferral . Content ] ?
31- )
27+ /// Called by the parent container after deferred value update pass completes.
28+ /// - parameter frameProvider: an optional accessibility frame to apply at the receiver's discretion.
29+ func updateDeferredAccessibility( frameProvider: AccessibilityDeferral . FrameProvider ? )
3230 }
3331
3432 /// An accessibility container wrapping an element that natively provides the deferred accessibility content. This element's accessibility is conditionally exposed based on the presence of a receiver.
@@ -44,20 +42,18 @@ public struct AccessibilityDeferral {
4442
4543 public struct Content : Equatable {
4644 public enum Kind : Equatable {
47- /// Uses accessibility values from the contained element and exposes them as custom via the accessiblity rotor.
45+ /// Uses accessibility values from the contained element and exposes them as custom via the accessibility rotor.
4846 case inherited( Accessibility . CustomContent . Importance = . default)
4947 /// Announces an error message with high importance using accessibility values from the contained element.
5048 case error
51- /// Exposes the custom content provided.
52- case custom( Accessibility . CustomContent )
5349 }
5450
5551 public var kind : Kind
5652
5753 /// Used to identify a specific `Source` element to inherit accessibility from.
5854 public var sourceIdentifier : AnyHashable
5955
60- /// : A stable identifier used to identify a given update pass through he view hierarchy. Content with matching updateIdentifiers should be combined.
56+ /// A stable identifier used to identify a given update pass through the view hierarchy. Content with matching updateIdentifiers should be combined.
6157 internal var updateIdentifier : UUID ?
6258 internal var inheritedAccessibility : AccessibilityComposition . CompositeRepresentation ?
6359
@@ -77,8 +73,59 @@ public struct AccessibilityDeferral {
7773 content? . value = value
7874 content? . label = LocalizedStrings . Accessibility. errorTitle
7975 return content? . axCustomContent
80- case . custom( let customContent) :
81- return customContent. axCustomContent
76+ }
77+ }
78+ }
79+ }
80+
81+ extension AccessibilityDeferral {
82+
83+ // Prefer accessibilityPath API to simplify overrides and provide a common codepath.
84+ public struct FrameProvider {
85+ public static let accessibilityCornerRadius = 8.0 // Matches Voiceover's CGRect API
86+
87+ fileprivate static let accessibilityPathInset = - 2.0
88+
89+ private let provider : ( ) -> UIBezierPath
90+
91+ private init ( _ provider: @escaping ( ) -> UIBezierPath ) {
92+ self . provider = provider
93+ }
94+
95+ public func callAsFunction( ) -> UIBezierPath {
96+ provider ( )
97+ }
98+
99+ /// Creates a container frame from a CGRect with rounded corners
100+ /// - Parameters:
101+ /// - rect: The frame in global coordinate space
102+ /// - cornerRadius: The radius for rounded corners
103+ public static func frame( _ rect: CGRect , cornerRadius: CGFloat = accessibilityCornerRadius) -> Self {
104+ . init {
105+ UIBezierPath ( roundedRect: rect, cornerRadius: max ( 0 , cornerRadius - accessibilityPathInset) )
106+ }
107+ }
108+
109+ /// Creates a container frame from a UIView
110+ /// - Parameters:
111+ /// - view: The view providing the frame geometry
112+ public static func view( _ view: UIView ) -> Self {
113+ . init { [ weak view] in
114+ guard let view else { return UIBezierPath ( ) }
115+
116+ // Prefer the path if it's already set.
117+ guard view. accessibilityPath == nil else { return view. accessibilityPath! }
118+
119+ let bounds = view. bounds
120+ let outsetFrame = bounds. insetBy ( dx: accessibilityPathInset * 2 , dy: accessibilityPathInset * 2 )
121+ let convertedFrame = UIAccessibility . convertToScreenCoordinates ( outsetFrame, in: view)
122+
123+ // Apply corner radius from layer if present, otherwise use default text field radius
124+ let cornerRadius = view. layer. cornerRadius > 0 ? view. layer. cornerRadius : accessibilityCornerRadius
125+ return UIBezierPath (
126+ roundedRect: convertedFrame,
127+ cornerRadius: max ( 0 , cornerRadius - accessibilityPathInset)
128+ )
82129 }
83130 }
84131 }
@@ -98,6 +145,11 @@ extension Element {
98145 public func deferredAccessibilitySource( identifier: AnyHashable ) -> AccessibilityDeferral . SourceContainer {
99146 AccessibilityDeferral . SourceContainer ( wrapping: { self } , identifier: identifier)
100147 }
148+
149+ /// Creates a `ReceiverContainer` element to expose the deferred accessibility.
150+ public func deferredAccessibilityReceiver( ) -> AccessibilityDeferral . ReceiverContainer {
151+ AccessibilityDeferral . ReceiverContainer ( wrapping: { self } )
152+ }
101153}
102154
103155extension AccessibilityDeferral {
@@ -130,7 +182,6 @@ extension AccessibilityDeferral {
130182
131183 private final class DeferralContainerView : UIView {
132184
133- var useContainerFrame : Bool = true
134185 var contents : [ Content ] ? {
135186 didSet {
136187 if oldValue != contents {
@@ -177,9 +228,7 @@ extension AccessibilityDeferral {
177228
178229 guard receivers. count <= 1 else {
179230 // We cannot reasonably determine which receiver to apply the content to.
180- receivers. forEach { $0. applyDeferredAccessibility (
181- content: nil
182- ) }
231+ receivers. forEach { $0. apply ( content: nil , frameProvider: nil ) }
183232 return
184233 }
185234
@@ -199,14 +248,111 @@ extension AccessibilityDeferral {
199248 }
200249
201250 // Apply content to receiver.
202- receiver. applyDeferredAccessibility (
203- content: deferredContent
204- )
251+ receiver. apply ( content: deferredContent, frameProvider: . view( self ) )
252+
205253 }
206254 }
207255 }
208256}
209257
258+ extension AccessibilityDeferral {
259+
260+ public struct ReceiverContainer : Element {
261+ public var wrappedElement : Element
262+
263+ init ( wrapping: @escaping ( ) -> Element ) {
264+ wrappedElement = wrapping ( )
265+ }
266+
267+ public var content : ElementContent {
268+ ElementContent ( measuring: wrappedElement)
269+ }
270+
271+ public func backingViewDescription( with context: BlueprintUI . ViewDescriptionContext ) -> BlueprintUI . ViewDescription ? {
272+ ReceiverContainerView . describe { config in
273+ config. apply { view in
274+ view. isAccessibilityElement = true
275+ view. needsAccessibilityUpdate = true
276+ view. layoutDirection = context. environment. layoutDirection
277+ view. element = wrappedElement
278+ }
279+ }
280+ }
281+
282+ private final class ReceiverContainerView : AccessibilityComposition . CombinableView , AccessibilityDeferral . Receiver {
283+ var element : Element ? {
284+ didSet {
285+ blueprintView. element = element
286+ blueprintView. setNeedsLayout ( )
287+ }
288+ }
289+
290+ private var blueprintView = BlueprintView ( )
291+
292+ override init ( frame: CGRect ) {
293+ super. init ( frame: frame)
294+ isAccessibilityElement = true
295+ mergeInteractiveSingleChild = false
296+
297+ blueprintView. backgroundColor = . clear
298+ addSubview ( blueprintView)
299+ }
300+
301+ @MainActor required init ? ( coder: NSCoder ) {
302+ fatalError ( " init(coder:) has not been implemented " )
303+ }
304+
305+ override func layoutSubviews( ) {
306+ super. layoutSubviews ( )
307+ blueprintView. frame = bounds
308+ needsAccessibilityUpdate = true
309+ }
310+
311+ // MARK: - Accessibility Deferral and Custom Content
312+ internal var frameProvider : FrameProvider ?
313+
314+ var customContent : [ Accessibility . CustomContent ] ?
315+
316+ var deferredAccessibilityContent : [ AccessibilityDeferral . Content ] ?
317+
318+ public override var accessibilityCustomRotors : [ UIAccessibilityCustomRotor ] ? {
319+ get { super. accessibilityCustomRotors + rotorSequencer? . rotors }
320+ set { super. accessibilityCustomRotors = newValue }
321+ }
322+
323+ public override var accessibilityPath : UIBezierPath ? {
324+ get { frameProvider ? ( ) ?? UIBezierPath ( rect: super. accessibilityFrame) }
325+ set { assertionFailure ( " Use frameProvider instead of setting accessibilityPath directly. " ) }
326+ }
327+
328+ public override var accessibilityCustomContent : [ AXCustomContent ] ! {
329+ get {
330+ let existing = super. accessibilityCustomContent
331+ let applied = customContent? . map { AXCustomContent ( $0) }
332+ return ( existing + applied) ? . removingDuplicates ?? [ ]
333+ }
334+ set { super. accessibilityCustomContent = newValue }
335+ }
336+
337+ public func updateDeferredAccessibility( frameProvider: FrameProvider ? ) {
338+ needsAccessibilityUpdate = true
339+
340+ self . frameProvider = frameProvider
341+
342+ if let deferred = deferredAccessibilityContent? . compactMap ( { $0. inheritedAccessibility } ) ,
343+ let first = deferred. first
344+ {
345+ mergeValues = deferred. dropFirst ( )
346+ . reduce ( first) { result, value in
347+ result. merge ( with: value)
348+ }
349+ }
350+ }
351+ }
352+ }
353+ }
354+
355+
210356
211357extension AccessibilityDeferral {
212358
@@ -261,10 +407,6 @@ extension AccessibilityDeferral {
261407 addSubview ( blueprintView)
262408 }
263409
264- override func addSubview( _ view: UIView ) {
265- super. addSubview ( view)
266- }
267-
268410 required init ? ( coder: NSCoder ) {
269411 fatalError ( " init(coder:) has not been implemented " )
270412 }
@@ -325,11 +467,15 @@ extension AccessibilityComposition.CompositeRepresentation {
325467 }
326468}
327469
328- /// Default Implementation
329470extension AccessibilityDeferral . Receiver {
330471
331- public func applyDeferredAccessibility(
332- content: [ AccessibilityDeferral . Content ] ?
472+ // Default implementation ignores frame
473+ public func updateDeferredAccessibility( frameProvider: AccessibilityDeferral . FrameProvider ? ) { }
474+
475+
476+ internal func apply(
477+ content: [ AccessibilityDeferral . Content ] ? ,
478+ frameProvider: AccessibilityDeferral . FrameProvider ?
333479 ) {
334480 guard let content, !content. isEmpty else { replaceContent ( [ ] ) ; return }
335481 guard let updateID = content. first? . updateIdentifier, content. allSatisfy ( { $0. updateIdentifier == updateID } ) else {
@@ -342,30 +488,32 @@ extension AccessibilityDeferral.Receiver {
342488 } else {
343489 replaceContent ( content)
344490 }
491+ updateDeferredAccessibility ( frameProvider: frameProvider)
492+ }
345493
346- func replaceContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
347- deferredAccessibilityContent = content
494+ internal func replaceContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
495+ deferredAccessibilityContent = content
348496
349- accessibilityCustomActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 } . removingDuplicateActions ( )
497+ accessibilityCustomActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 } . removingDuplicateActions ( )
350498
351- if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
352- rotorSequencer = . init( rotors: rotors)
353- } else {
354- rotorSequencer = nil
355- }
499+ if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
500+ rotorSequencer = . init( rotors: rotors)
501+ } else {
502+ rotorSequencer = nil
356503 }
504+ }
357505
358- func mergeContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
359- deferredAccessibilityContent = ( deferredAccessibilityContent + content) ? . removingDuplicates
506+ internal func mergeContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
507+ deferredAccessibilityContent = ( deferredAccessibilityContent + content) ? . removingDuplicates
360508
361- let contentActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 }
362- accessibilityCustomActions = ( accessibilityCustomActions + contentActions) ? . removingDuplicateActions ( )
509+ let contentActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 }
510+ accessibilityCustomActions = ( accessibilityCustomActions + contentActions) ? . removingDuplicateActions ( )
363511
364- if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
365- let mergedRotors = ( rotorSequencer? . rotors ?? [ ] ) + rotors
366- rotorSequencer = . init( rotors: mergedRotors)
367- accessibilityCustomRotors = rotorSequencer? . rotors
368- }
512+ if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
513+ let mergedRotors = ( rotorSequencer? . rotors ?? [ ] ) + rotors
514+ rotorSequencer = . init( rotors: mergedRotors)
515+ accessibilityCustomRotors = rotorSequencer? . rotors
369516 }
370517 }
518+
371519}
0 commit comments