SwiftUI accessibility: Beyond the basics

RSS for tag

Discuss the WWDC21 session SwiftUI accessibility: Beyond the basics.

Posts under wwdc21-10119 tag

8 Posts

Post

Replies

Boosts

Views

Activity

Observing UserDefaults on iOS 13
Hello there, I stumbled on the issue of observing UserDefaults. My need is to "listening"/observing UD key named "com.apple.configuration.managed" which is responsible for reading provided MDM external plist. I checked that on the opened app it is possible to provide that plist, and app read this payload correctly. My problem is to observing that change, when plist is uploading. Requirement is to support iOS 13, this is why I can't use AppStorage. Despite using some similar solutions like: someone own implementation of AppStorage, and using StackOverflow solutions like this, it still doesn't work. My, I feel, the closest one solution was:   @objc dynamic var mdmConfiguration: Dictionary<String, String> {     get { (dictionary(forKey: MDM.ConfigurationPayloadKey) != nil) ? dictionary(forKey: MDM.ConfigurationPayloadKey)! as! Dictionary<String, String> : Dictionary<String, String>() }     set { setValue(newValue, forKey: MDM.ConfigurationPayloadKey)}   } } class MDMConfiguration: ObservableObject {       //@Binding private var bindedValue: Bool       @Published var configuration: Dictionary = UserDefaults.standard.mdmConfiguration {     didSet {       UserDefaults.standard.mdmConfiguration = configuration     //  bindedValue.toggle()     }   }   private var cancelable: AnyCancellable?   init() {  // init(toggle: Binding<Bool>) {     //_bindedValue = toggle     cancelable = UserDefaults.standard.publisher(for: \.mdmConfiguration)       .sink(receiveValue: { [weak self] newValue in         guard let self = self else { return }         if newValue != self.configuration { // avoid cycling !!           self.configuration = newValue         }       })   } } struct ContentView: View {       @State private var isConfigurationAvailable: Bool = false   @State private var showLoadingIndicator: Bool = true   @ObservedObject var configuration = MDMConfiguration()       var body: some View {           GeometryReader { geometry in               let width = geometry.size.width       let height = geometry.size.height                             VStack {         Text("CONTENT -> \(configuration.configuration.debugDescription)").padding()         Spacer()         if !configuration.configuration.isEmpty {           Text("AVAILABLE").padding()         } else {           Text("NIL NULL ZERO EMPTY")             .padding()         }       }        }   } } But it still doesn't ensure any changes in view, when I manually click on f.e. button, which prints the configuration in the console when it has uploaded, it does it well. Please help, my headache is reaching the zenith. I am a newbie in Swift development, maybe I did something weird and stupid. I hope so :D Thank in advance!
0
0
1.6k
Mar ’22
accessibilityRepresentation view modifier unexpected behavior when Text provided for label
Hi there! I'm working on using the .accessibilityRepresentation view modifier for a custom slider, and I'm having some unexpected behavior. When I run my app with VoiceOver and swipe over the custom slider view, it will only announce the adjustable trait on a fresh run of VoiceOver, otherwise the VoiceOver needs to be turned off and back on again to hear the adjustable trait announcement and use the adjustable a11y action. The only way that I found to get the correct announcement is to leave out the label specified in the .accessibilityRepresentation closure. See below for reference...          .accessibilityRepresentation {              Slider(value: $value, in: 0...12, step: 1.0) {                    // Text("Window Distance")                 }             } I should note that this workaround causes these issues: the slider thumb control doesn't update its position when the a11y action is performed - I figure this is probably because we don't have anything specified in the label param of the a11y representation. VoiceOver does not announce the new value when the a11y action is performed - I have to double tap it again to hear the announcement If I uncomment the Text modifier, then the following happens: the adjustable trait and action will either be completely unavailable OR I can adjust one time, then have to turn it off and back on again. the .staticTrait is added there is no .adjustable trait Additional Information that may be relevant: this is how I'm making the slider and where the a11y representation is being used:  GeometryReader { geometry in          ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {                 RoundedRectangle(cornerRadius: 20)                    .frame(minHeight: 44)                   .foregroundStyle(self.trackGradient)                   .overlay(                        RoundedRectangle(cornerRadius: 30)                         .foregroundColor(.clear)                           .shadow(color: .black, radius: 5)               .clipShape(RoundedRectangle(cornerRadius: 30))                    )                              HStack {                    RoundedRectangle(cornerRadius: 50)                       .frame(width: self.thumbSize.width, height: self.thumbSize.height)                        .foregroundColor(.white)                       .offset(x: lastOffset)                        .shadow(radius: 8)                       .gesture(                            DragGesture(minimumDistance: 0.1)           .onChanged { value in       if value.location.x >= 0 && value.location.x <= geometry.size.width - self.thumbSize.width {                self.lastOffset = value.location.x   let sliderPosition = max(0 + self.leadingOffset, min(self.lastOffset + value.translation.width, geometry.size.width - self.trailingOffset)) let sliderValue = sliderPosition.map(from: self.leadingOffset...(geometry.size.width - self.trailingOffset), to: self.range) self.value = sliderValue                                    }                               }                        )               }            }            .accessibilityRepresentation {               Slider(value: $value, in: 0...12, step: 1.0) {                  // Text("Window Distance")               }            }         }    } And finally, here's the view where the CustomSlider lives...     var body: some View {         VStack {             HStack(alignment: .center) {                 Text("Window Distance")                     .font(.body)                 Spacer()                 Text($value.wrappedValue == 0 ? "In window" : "\(Int($value.wrappedValue)) ft.")             }            // .accessibilityElement(children: .ignore) // I've experimented with this .ignore trait so that once the a11yRepresentation is working correctly, we won't have redundant info             CustomSlider(value: $value)                // .accessibilityElement(children: .contain) // I've tried also adding the .contain behavior here because I thought that maybe we'd want to ignore the texts and contain the custom slider view since it should have all the a11y info we need in the a11y representation             Spacer()         }         .padding(.vertical)         .padding(.bottom)         .padding(.bottom)         .accessibilityElement(children: .combine)     } Has anyone had a similar issue before? Thanks, everyone!
0
0
878
Jan ’22
How can I create a custom NSCursor that supports the new Monterey custom cursor colors?
macOS Monterey introduced support for user-defined cursor colors (e.g., I can change my mouse pointer to have a yellow outline and a magenta fill to make it easier to see). In a sample app, if I change the cursor to any of the built-in NSCursors everything works fine, but if I use my own cursor then none of the Accessibility colors get applied. I couldn't find any documentation on how to create an NSCursor that honors the new user-defined cursor colors. I've tried both PDF and PNG cursor files (but I assume we need to use a vector-based format for the recoloring to work) Would appreciate any guidance on how to get this to work, so our app can be more accessible and support the latest awesomeness in Monterey :)
0
0
842
Oct ’21
trouble enabling two finger gesture to go back to previous page on a webview app
I have enabled the following swipe gesture and it works however with voiceover it doesnt respond and doesnt allow the user to go back with the two finger swipe from left to right Heres the code that i have written so far - import UIKit import WebKit class ViewController: UIViewController { let webView: WKWebView = { let prefs = WKWebpagePreferences() prefs.allowsContentJavaScript = true let configuration = WKWebViewConfiguration() configuration.defaultWebpagePreferences = prefs let webView = WKWebView(frame: .zero, configuration: configuration) return webView }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(webView) guard let url = URL(string: "https://sindhisystems.co.uk") else { return } func goBack(_ sender: Any) { webView.goBack() } webView.load(URLRequest(url: url)) webView.customUserAgent = "iPad/Chrome/SomethingRandom" webView.allowsBackForwardNavigationGestures = true DispatchQueue.main.asyncAfter(deadline: .now()+5) { self.webView.evaluateJavaScript("document.body.innerHTML") { result, error in guard let html = result as? String, error == nil else { return } print(html) } } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() webView.frame = view.bounds } }
0
0
657
Aug ’21
.accessilityIdentifier not working for buttons in .toolbar
Updating my code to use .toolbar (with ToolbarItem) rather than .navigationBarItems I've run into a problem with the accessibilityIdentifiers. They no longer seem to show up in the view hierarchy, and so all UI tests are breaking. Anyone know how to fix this? (I would like to avoid having to change all UI tests to search for button label, since the use of accessibilityIdentifiers generally gives more stable UI-tests) struct SwiftUIView: View {          let myFunction: () -> Void     let myOtherFunction: () -> Void          var body: some View {             Form {                 Text("Hello World!")             }             .navigationBarTitle("My Title", displayMode: .inline)                          //VERSION 1: navigationBar-version (works, but deprecated)             .navigationBarItems(leading: cancelButton,                                 trailing: addButton)                          //VERSION 2: toolbar-version (.accessibilityIdentifiers do not work)             .toolbar {                 ToolbarItem(placement: .cancellationAction) { cancelButton }                 ToolbarItem(placement: .confirmationAction) { addButton }             }     }          var cancelButton: some View {         Button(action: myFunction,                label: { Text("Cancel") })             .accessibilityIdentifier("MyTitleView_CancelButton") }          var addButton: some View {         Button(action: myOtherFunction,                label: { Text("Add") })             .accessibilityIdentifier("MyTitleView_AddButton") } }
1
0
1.2k
Jul ’21
Observing UserDefaults on iOS 13
Hello there, I stumbled on the issue of observing UserDefaults. My need is to "listening"/observing UD key named "com.apple.configuration.managed" which is responsible for reading provided MDM external plist. I checked that on the opened app it is possible to provide that plist, and app read this payload correctly. My problem is to observing that change, when plist is uploading. Requirement is to support iOS 13, this is why I can't use AppStorage. Despite using some similar solutions like: someone own implementation of AppStorage, and using StackOverflow solutions like this, it still doesn't work. My, I feel, the closest one solution was:   @objc dynamic var mdmConfiguration: Dictionary<String, String> {     get { (dictionary(forKey: MDM.ConfigurationPayloadKey) != nil) ? dictionary(forKey: MDM.ConfigurationPayloadKey)! as! Dictionary<String, String> : Dictionary<String, String>() }     set { setValue(newValue, forKey: MDM.ConfigurationPayloadKey)}   } } class MDMConfiguration: ObservableObject {       //@Binding private var bindedValue: Bool       @Published var configuration: Dictionary = UserDefaults.standard.mdmConfiguration {     didSet {       UserDefaults.standard.mdmConfiguration = configuration     //  bindedValue.toggle()     }   }   private var cancelable: AnyCancellable?   init() {  // init(toggle: Binding<Bool>) {     //_bindedValue = toggle     cancelable = UserDefaults.standard.publisher(for: \.mdmConfiguration)       .sink(receiveValue: { [weak self] newValue in         guard let self = self else { return }         if newValue != self.configuration { // avoid cycling !!           self.configuration = newValue         }       })   } } struct ContentView: View {       @State private var isConfigurationAvailable: Bool = false   @State private var showLoadingIndicator: Bool = true   @ObservedObject var configuration = MDMConfiguration()       var body: some View {           GeometryReader { geometry in               let width = geometry.size.width       let height = geometry.size.height                             VStack {         Text("CONTENT -> \(configuration.configuration.debugDescription)").padding()         Spacer()         if !configuration.configuration.isEmpty {           Text("AVAILABLE").padding()         } else {           Text("NIL NULL ZERO EMPTY")             .padding()         }       }        }   } } But it still doesn't ensure any changes in view, when I manually click on f.e. button, which prints the configuration in the console when it has uploaded, it does it well. Please help, my headache is reaching the zenith. I am a newbie in Swift development, maybe I did something weird and stupid. I hope so :D Thank in advance!
Replies
0
Boosts
0
Views
1.6k
Activity
Mar ’22
accessibilityRepresentation view modifier unexpected behavior when Text provided for label
Hi there! I'm working on using the .accessibilityRepresentation view modifier for a custom slider, and I'm having some unexpected behavior. When I run my app with VoiceOver and swipe over the custom slider view, it will only announce the adjustable trait on a fresh run of VoiceOver, otherwise the VoiceOver needs to be turned off and back on again to hear the adjustable trait announcement and use the adjustable a11y action. The only way that I found to get the correct announcement is to leave out the label specified in the .accessibilityRepresentation closure. See below for reference...          .accessibilityRepresentation {              Slider(value: $value, in: 0...12, step: 1.0) {                    // Text("Window Distance")                 }             } I should note that this workaround causes these issues: the slider thumb control doesn't update its position when the a11y action is performed - I figure this is probably because we don't have anything specified in the label param of the a11y representation. VoiceOver does not announce the new value when the a11y action is performed - I have to double tap it again to hear the announcement If I uncomment the Text modifier, then the following happens: the adjustable trait and action will either be completely unavailable OR I can adjust one time, then have to turn it off and back on again. the .staticTrait is added there is no .adjustable trait Additional Information that may be relevant: this is how I'm making the slider and where the a11y representation is being used:  GeometryReader { geometry in          ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {                 RoundedRectangle(cornerRadius: 20)                    .frame(minHeight: 44)                   .foregroundStyle(self.trackGradient)                   .overlay(                        RoundedRectangle(cornerRadius: 30)                         .foregroundColor(.clear)                           .shadow(color: .black, radius: 5)               .clipShape(RoundedRectangle(cornerRadius: 30))                    )                              HStack {                    RoundedRectangle(cornerRadius: 50)                       .frame(width: self.thumbSize.width, height: self.thumbSize.height)                        .foregroundColor(.white)                       .offset(x: lastOffset)                        .shadow(radius: 8)                       .gesture(                            DragGesture(minimumDistance: 0.1)           .onChanged { value in       if value.location.x >= 0 && value.location.x <= geometry.size.width - self.thumbSize.width {                self.lastOffset = value.location.x   let sliderPosition = max(0 + self.leadingOffset, min(self.lastOffset + value.translation.width, geometry.size.width - self.trailingOffset)) let sliderValue = sliderPosition.map(from: self.leadingOffset...(geometry.size.width - self.trailingOffset), to: self.range) self.value = sliderValue                                    }                               }                        )               }            }            .accessibilityRepresentation {               Slider(value: $value, in: 0...12, step: 1.0) {                  // Text("Window Distance")               }            }         }    } And finally, here's the view where the CustomSlider lives...     var body: some View {         VStack {             HStack(alignment: .center) {                 Text("Window Distance")                     .font(.body)                 Spacer()                 Text($value.wrappedValue == 0 ? "In window" : "\(Int($value.wrappedValue)) ft.")             }            // .accessibilityElement(children: .ignore) // I've experimented with this .ignore trait so that once the a11yRepresentation is working correctly, we won't have redundant info             CustomSlider(value: $value)                // .accessibilityElement(children: .contain) // I've tried also adding the .contain behavior here because I thought that maybe we'd want to ignore the texts and contain the custom slider view since it should have all the a11y info we need in the a11y representation             Spacer()         }         .padding(.vertical)         .padding(.bottom)         .padding(.bottom)         .accessibilityElement(children: .combine)     } Has anyone had a similar issue before? Thanks, everyone!
Replies
0
Boosts
0
Views
878
Activity
Jan ’22
How to use TableColumnBuilder and TableRowBuilder in SwiftUI 3.0 ?
Are there any relevant code examples to show how to use TableColumnBuilder and TableRowBuilder ?Thank you.
Replies
0
Boosts
0
Views
567
Activity
Nov ’21
How to drag the table header to reorder the columns?
Using NSTableView in Cocoa framework, it is easy to implement drag and drop sorting of table headers (not row sorting). But I didn't find out how to use the Table component in SwiftUI.
Replies
0
Boosts
0
Views
772
Activity
Nov ’21
How can I create a custom NSCursor that supports the new Monterey custom cursor colors?
macOS Monterey introduced support for user-defined cursor colors (e.g., I can change my mouse pointer to have a yellow outline and a magenta fill to make it easier to see). In a sample app, if I change the cursor to any of the built-in NSCursors everything works fine, but if I use my own cursor then none of the Accessibility colors get applied. I couldn't find any documentation on how to create an NSCursor that honors the new user-defined cursor colors. I've tried both PDF and PNG cursor files (but I assume we need to use a vector-based format for the recoloring to work) Would appreciate any guidance on how to get this to work, so our app can be more accessible and support the latest awesomeness in Monterey :)
Replies
0
Boosts
0
Views
842
Activity
Oct ’21
trouble enabling two finger gesture to go back to previous page on a webview app
I have enabled the following swipe gesture and it works however with voiceover it doesnt respond and doesnt allow the user to go back with the two finger swipe from left to right Heres the code that i have written so far - import UIKit import WebKit class ViewController: UIViewController { let webView: WKWebView = { let prefs = WKWebpagePreferences() prefs.allowsContentJavaScript = true let configuration = WKWebViewConfiguration() configuration.defaultWebpagePreferences = prefs let webView = WKWebView(frame: .zero, configuration: configuration) return webView }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(webView) guard let url = URL(string: "https://sindhisystems.co.uk") else { return } func goBack(_ sender: Any) { webView.goBack() } webView.load(URLRequest(url: url)) webView.customUserAgent = "iPad/Chrome/SomethingRandom" webView.allowsBackForwardNavigationGestures = true DispatchQueue.main.asyncAfter(deadline: .now()+5) { self.webView.evaluateJavaScript("document.body.innerHTML") { result, error in guard let html = result as? String, error == nil else { return } print(html) } } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() webView.frame = view.bounds } }
Replies
0
Boosts
0
Views
657
Activity
Aug ’21
swift ui
how to start with swift ui
Replies
1
Boosts
0
Views
708
Activity
Jul ’21
.accessilityIdentifier not working for buttons in .toolbar
Updating my code to use .toolbar (with ToolbarItem) rather than .navigationBarItems I've run into a problem with the accessibilityIdentifiers. They no longer seem to show up in the view hierarchy, and so all UI tests are breaking. Anyone know how to fix this? (I would like to avoid having to change all UI tests to search for button label, since the use of accessibilityIdentifiers generally gives more stable UI-tests) struct SwiftUIView: View {          let myFunction: () -> Void     let myOtherFunction: () -> Void          var body: some View {             Form {                 Text("Hello World!")             }             .navigationBarTitle("My Title", displayMode: .inline)                          //VERSION 1: navigationBar-version (works, but deprecated)             .navigationBarItems(leading: cancelButton,                                 trailing: addButton)                          //VERSION 2: toolbar-version (.accessibilityIdentifiers do not work)             .toolbar {                 ToolbarItem(placement: .cancellationAction) { cancelButton }                 ToolbarItem(placement: .confirmationAction) { addButton }             }     }          var cancelButton: some View {         Button(action: myFunction,                label: { Text("Cancel") })             .accessibilityIdentifier("MyTitleView_CancelButton") }          var addButton: some View {         Button(action: myOtherFunction,                label: { Text("Add") })             .accessibilityIdentifier("MyTitleView_AddButton") } }
Replies
1
Boosts
0
Views
1.2k
Activity
Jul ’21