@@ -19,16 +19,14 @@ public struct AccessibilityDeferral {
1919 var rotorSequencer : AccessibilityComposition . RotorSequencer ? { get set }
2020
2121 /// Custom content that may be supplied in addition to the deferred content
22- var customContent : [ Accessibility . CustomContent ] ? { get set }
22+ var customContent : [ BlueprintUI . Accessibility . CustomContent ] ? { get set }
2323
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 he 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,12 @@ 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+ /// TODO Consider using the identifiers?
151+ public func deferredAccessibilityReceiver( identifiers: [ AnyHashable ] ) -> AccessibilityDeferral . ReceiverContainer {
152+ AccessibilityDeferral . ReceiverContainer ( wrapping: { self } )
153+ }
101154}
102155
103156extension AccessibilityDeferral {
@@ -130,7 +183,6 @@ extension AccessibilityDeferral {
130183
131184 private final class DeferralContainerView : UIView {
132185
133- var useContainerFrame : Bool = true
134186 var contents : [ Content ] ? {
135187 didSet {
136188 if oldValue != contents {
@@ -177,9 +229,7 @@ extension AccessibilityDeferral {
177229
178230 guard receivers. count <= 1 else {
179231 // We cannot reasonably determine which receiver to apply the content to.
180- receivers. forEach { $0. applyDeferredAccessibility (
181- content: nil
182- ) }
232+ receivers. forEach { $0. apply ( content: nil , frameProvider: nil ) }
183233 return
184234 }
185235
@@ -199,14 +249,113 @@ extension AccessibilityDeferral {
199249 }
200250
201251 // Apply content to receiver.
202- receiver. applyDeferredAccessibility (
203- content: deferredContent
204- )
252+ receiver. apply ( content: deferredContent, frameProvider: . view( self ) )
253+
205254 }
206255 }
207256 }
208257}
209258
259+ extension AccessibilityDeferral {
260+
261+ public struct ReceiverContainer : Element {
262+ public var wrappedElement : Element
263+
264+ init ( wrapping: @escaping ( ) -> Element ) {
265+ wrappedElement = wrapping ( )
266+ }
267+
268+ public var content : ElementContent {
269+ ElementContent ( measuring: wrappedElement)
270+ }
271+
272+ public func backingViewDescription( with context: BlueprintUI . ViewDescriptionContext ) -> BlueprintUI . ViewDescription ? {
273+ ReceiverContainerView . describe { config in
274+ config. apply { view in
275+ view. isAccessibilityElement = true
276+ view. needsAccessibilityUpdate = true
277+ view. layoutDirection = context. environment. layoutDirection
278+ view. element = wrappedElement
279+ }
280+ }
281+ }
282+
283+ private final class ReceiverContainerView : AccessibilityComposition . CombinableView , AccessibilityDeferral . Receiver {
284+ var element : Element ? {
285+ didSet {
286+ blueprintView. element = element
287+ blueprintView. setNeedsLayout ( )
288+ }
289+ }
290+
291+ private var blueprintView = BlueprintView ( )
292+
293+ override init ( frame: CGRect ) {
294+ super. init ( frame: frame)
295+ isAccessibilityElement = true
296+ mergeInteractiveSingleChild = false
297+
298+ blueprintView. backgroundColor = . clear
299+ addSubview ( blueprintView)
300+ }
301+
302+ @MainActor required init ? ( coder: NSCoder ) {
303+ fatalError ( " init(coder:) has not been implemented " )
304+ }
305+
306+ override func layoutSubviews( ) {
307+ super. layoutSubviews ( )
308+ blueprintView. frame = bounds
309+ needsAccessibilityUpdate = true
310+ }
311+
312+ // MARK: - Accessibility Deferral and Custom Content
313+ internal var frameProvider : FrameProvider ?
314+
315+ var customContent : [ Accessibility . CustomContent ] ?
316+
317+ var deferredAccessibilityContent : [ AccessibilityDeferral . Content ] ?
318+
319+ public override var accessibilityCustomRotors : [ UIAccessibilityCustomRotor ] ? {
320+ get { super. accessibilityCustomRotors + rotorSequencer? . rotors }
321+ set { super. accessibilityCustomRotors = newValue }
322+ }
323+
324+ public override var accessibilityPath : UIBezierPath ? {
325+ get { frameProvider ? ( ) ?? UIBezierPath ( rect: super. accessibilityFrame) }
326+ set { fatalError ( " Not settable, please use frameProvider instead. " ) }
327+ }
328+
329+ public override var accessibilityCustomContent : [ AXCustomContent ] ! {
330+ get {
331+ let existing = super. accessibilityCustomContent
332+ let applied = customContent? . map { AXCustomContent ( $0) }
333+ return ( existing + applied) ? . removingDuplicates ?? [ ]
334+ }
335+ set { super. accessibilityCustomContent = newValue }
336+ }
337+
338+ public func updateDeferredAccessibility( frameProvider: FrameProvider ? ) {
339+ needsAccessibilityUpdate = true
340+
341+ self . frameProvider = frameProvider
342+
343+ if var deferred = deferredAccessibilityContent? . compactMap ( { $0. inheritedAccessibility } ) ,
344+ let first = deferred. first
345+ {
346+
347+ mergeValues = deferred. dropFirst ( )
348+ . reduce ( into: first) { result, value in
349+ result. merge ( with: value)
350+ }
351+ }
352+ needsAccessibilityUpdate = true
353+ }
354+ }
355+ }
356+ }
357+
358+
210359
211360extension AccessibilityDeferral {
212361
@@ -261,10 +410,6 @@ extension AccessibilityDeferral {
261410 addSubview ( blueprintView)
262411 }
263412
264- override func addSubview( _ view: UIView ) {
265- super. addSubview ( view)
266- }
267-
268413 required init ? ( coder: NSCoder ) {
269414 fatalError ( " init(coder:) has not been implemented " )
270415 }
@@ -325,11 +470,15 @@ extension AccessibilityComposition.CompositeRepresentation {
325470 }
326471}
327472
328- /// Default Implementation
329473extension AccessibilityDeferral . Receiver {
330474
331- public func applyDeferredAccessibility(
332- content: [ AccessibilityDeferral . Content ] ?
475+ // Default implementation ignores frame
476+ public func updateDeferredAccessibility( frameProvider: AccessibilityDeferral . FrameProvider ? ) { }
477+
478+
479+ internal func apply(
480+ content: [ AccessibilityDeferral . Content ] ? ,
481+ frameProvider: AccessibilityDeferral . FrameProvider ?
333482 ) {
334483 guard let content, !content. isEmpty else { replaceContent ( [ ] ) ; return }
335484 guard let updateID = content. first? . updateIdentifier, content. allSatisfy ( { $0. updateIdentifier == updateID } ) else {
@@ -342,30 +491,32 @@ extension AccessibilityDeferral.Receiver {
342491 } else {
343492 replaceContent ( content)
344493 }
494+ updateDeferredAccessibility ( frameProvider: frameProvider)
495+ }
345496
346- func replaceContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
347- deferredAccessibilityContent = content
497+ internal func replaceContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
498+ deferredAccessibilityContent = content
348499
349- accessibilityCustomActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 } . removingDuplicateActions ( )
500+ accessibilityCustomActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 } . removingDuplicateActions ( )
350501
351- if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
352- rotorSequencer = . init( rotors: rotors)
353- } else {
354- rotorSequencer = nil
355- }
502+ if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
503+ rotorSequencer = . init( rotors: rotors)
504+ } else {
505+ rotorSequencer = nil
356506 }
507+ }
357508
358- func mergeContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
359- deferredAccessibilityContent = ( deferredAccessibilityContent + content) ? . removingDuplicates
509+ internal func mergeContent( _ content: [ AccessibilityDeferral . Content ] ? ) {
510+ deferredAccessibilityContent = ( deferredAccessibilityContent + content) ? . removingDuplicates
360511
361- let contentActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 }
362- accessibilityCustomActions = ( accessibilityCustomActions + contentActions) ? . removingDuplicateActions ( )
512+ let contentActions = content? . compactMap { $0. inheritedAccessibility? . allActions } . flatMap { $0 }
513+ accessibilityCustomActions = ( accessibilityCustomActions + contentActions) ? . removingDuplicateActions ( )
363514
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- }
515+ if let rotors = content? . compactMap ( { $0. inheritedAccessibility? . rotors } ) . flatMap ( { $0 } ) , !rotors. isEmpty {
516+ let mergedRotors = ( rotorSequencer? . rotors ?? [ ] ) + rotors
517+ rotorSequencer = . init( rotors: mergedRotors)
518+ accessibilityCustomRotors = rotorSequencer? . rotors
369519 }
370520 }
521+
371522}
0 commit comments