How to use UIAccessibility.post(notification: .layoutChanged, argument: nil) in SwiftUI to move focus to specific view?
Asked Answered
A

2

7

I have a button on screen. When a user taps that button, my app opens a modal (view). When the user then closes that view, the focus of accessibility VoiceOver goes to the top of the screen. In UIKit, I can use UIAccessibility.post(notification:argument:) passing .layoutChanged into the notification parameter and passing a reference to one of my views into the argument parameter. How can I achieve this same behaviour in SwiftUI?

Ancilin answered 4/3, 2021 at 7:3 Comment(0)
I
1

How I managed this was using a .accessibilityHidden wrapper on the very top level of the parent view and then used a @State variable as the value to pass into accessibilityHidden. This way the parent view is ignored while the modal is showing. And then reintroduced into the view once the modal is closed again.

struct MainView: View {
    @State var showingModal = false
    var body: some View {
        VStack {
            Button(action: {
                showingModal = true
            }, label: {
                Text("Open Modal")
            })
            .fullScreenCover(isPresented: $showingModal, onDismiss: {
               print("Focus coming back to main view")
            } content: {
                Modal()
            })
        }
        .accessibilityHidden(self.showingModal)
    }
}

struct Modal: View {
    @Environment(\.presentationMode) var presentationMode
    var body: some View {
        VStack {
            Text("Focus will move here")
            Button(action: {
                presentationMode.wrappedValue.dismiss()
            }) {
                Text("Close Modal to Refocus Back")
            }
        }
    }
}

You can also chain multiple modal / alerts as long at you have @State values to handle the changes to them so the focus moves properly

.accessibilityHidden(self.showingModel1 || self.showingModel2 || self.showingAlert1 || self.showingAlert2)

I know this question is really old, but I literally just was handling this and thought if someone else stumbled onto this question there would be an answer here.

Isochronal answered 26/4, 2022 at 3:20 Comment(0)
C
0

From iOS 15 we can(and I even think should) use @AccessibilityFocusState, which allows to control accessibility focus programmatically.

You can define @AccessibilityFocusState property as a simple boolean and turn it to true once you dismissed modal. With the help of accessibilityFocused(_:) SwiftUI takes care to move focus on desired element, example with button and sheet would be:

struct ContentView: View {
    
    @AccessibilityFocusState private var accessibilityFocus: Bool
    @State private var isSheetPresented: Bool = false
    
    var body: some View {
        NavigationView {
            VStack {
                Spacer()
                Button("Open Sheet") {
                    isSheetPresented.toggle()
                }
                .accessibilityFocused($accessibilityFocus)
                Spacer()
            }
            .navigationTitle("Title")
            .sheet(isPresented: $isSheetPresented, content: {
                Button("Dismiss") {
                    accessibilityFocus = true
                    isSheetPresented.toggle()
                }
            })
        }
    }
}

Approach for advanced accessibility focus handling

In case if you want to move focus dynamically on different views depending on pressed button or whatever, you would create an enum and use it as a value of @AccessibilityFocusState like I did in the example below by using accessibilityFocused(_:equals:):

fileprivate enum FocusType: Int, Hashable {
    case subtitle
    case button
}

struct ContentView: View {
    
    @AccessibilityFocusState private var accessibilityFocus: FocusType?
    @State private var isSheetPresented: Bool = false
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Subtitle")
                    .font(.title2)
                    .accessibilityFocused($accessibilityFocus, equals: .subtitle)
                
                Spacer()
                Button("Open Sheet") {
                    isSheetPresented.toggle()
                }
                .accessibilityFocused($accessibilityFocus, equals: .button)
                Spacer()
                
                Divider()
                
                VStack {
                    Button("Subtitle") { accessibilityFocus = .subtitle }
                    Button("Button") { accessibilityFocus = .button }
                }
            }
            .navigationTitle("Title")
            .sheet(isPresented: $isSheetPresented, content: {
                SheetContent(focusType: _accessibilityFocus, isSheetPresented: $isSheetPresented)
            })
        }
    }
}

fileprivate struct SheetContent: View {
    @AccessibilityFocusState var focusType: FocusType?
    @Binding var isSheetPresented: Bool
    
    var body: some View {
        VStack {
            Button("Subtitle") { changeState(.subtitle)}
            Button("Button") { changeState(.button)}
        }
    }
    
    func changeState(_ focusType: FocusType) {
        self.focusType = focusType
        self.isSheetPresented.toggle()
    }
}

I added as an example two buttons to switch the state on the same view and also I passed @AccessibilityFocusState to sheet's content where you can dismiss the sheet and move accessibility focus depending on which button you pressed. Note that you need to pass your @AccessibilityFocusState with accessed synthesized property.

Carissacarita answered 30/4, 2024 at 16:4 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.