1

I'm trying to make an App that simulates the behavior of Calculator app from Apple. On the Calculator, when you tap = (Equals), the App reads "Result, Number.", from the main display view. How can I reproduce this behavior in SwiftUI ?

My Test Scenario:

struct ContentView: View {
    
    @State var result = 0
    
    var body: some View {
        VStack {
            Text("\(result)")
                .font(.largeTitle)
                .frame(height: 100)
                .padding()
            Spacer()
            Button(action: {
                self.result += 1
            }, label: {
                Text("Add one")
                    .font(.title)
                    .padding()
            })
            Button(action: {
                self.result -= 1
            }, label: {
                Text("Minus one")
                    .font(.title)
                    .padding()
            })
        }
    }
}

I want every time result changes, from the interaction of either Add or Minus Buttons, the VoiceOver system detects the Result changes and read the Result.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
Allan Garcia
  • 550
  • 3
  • 16

3 Answers3

2

You can post a notification for Voice over to announce something. But I am not familiar with SwiftUI, so not sure where would be best to place this code in your example

The code to post the notificaiton is:

if UIAccessibility.isVoiceOverRunning {
    UIAccessibility.post(notification: .announcement, argument: "message to announce")
}

Maybe you can do it this way:

function announceResult() {
    if UIAccessibility.isVoiceOverRunning {
        UIAccessibility.post(notification: .announcement, argument: self.result)
    }
}

Button(action: {
     self.result += 1
     announceResult()

}
Scriptable
  • 19,402
  • 5
  • 56
  • 72
  • @AllanGarcia `UIAccessibility` is part of `UIKit`, so you can access it from `SwiftUI` if you're developing for iOS - otherwise you can't. – Dávid Pásztor Aug 18 '20 at 11:25
1

You can simply use the accessibility(value:) modifier on the body of your view.

var body: some View {
    VStack {
        Text("\(result)")
            .font(.largeTitle)
            .frame(height: 100)
            .padding()
        Spacer()
        Button(action: {
            self.result += 1
        }, label: {
            Text("Add one")
                .font(.title)
                .padding()
        })
        Button(action: {
            self.result -= 1
        }, label: {
            Text("Minus one")
                .font(.title)
                .padding()
        })
    }
    .accessibility(value: Text("\(result)"))
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • This quite not work for me, when you add accessibility value to a VStack it gets propagated downwards to its children's, making every interaction with any view disruptive. It's reads "Add one, Five, Button." and it's not what I want. I want something like "Add one, Button" -> when tapped -> "Added one, new result, 1". – Allan Garcia Aug 18 '20 at 11:21
  • @AllanGarcia You can modify the exact value you want to be read out loud and also use the same modifier to add the appropriate accessibility value to each child view instead of adding it to the `VStack`. – Dávid Pásztor Aug 18 '20 at 11:24
0

Here is my final solution:

struct ContentView: View {

@State var result = 0 {
    didSet {
        announce(this: "\(result)")
    }
}

var body: some View {
    VStack {
        Text("\(result)")
            .font(.largeTitle)
            .frame(height: 100)
            .padding()
        Spacer()
        Button(action: {
            self.result += 1
        }, label: {
            Text("Add one")
                .font(.title)
                .padding()
        })
        Button(action: {
            self.result -= 1
        }, label: {
            Text("Minus one")
                .font(.title)
                .padding()
        })
    }
    //.accessibility(value: Text("\(result)"))
}

private func announce(this: String) {
    if UIAccessibility.isVoiceOverRunning {
        UIAccessibility.post(notification: .screenChanged, argument: "The new result is \(this).")
    }
}

}

The notification i'm looking for was the .screenChanged, but I'm also wanting to use the .announcement as mention by @Scriptable.

Sometimes there's a race condition you have to solve with DispatchQueue.main.asyncAfter, as mention at: Why is UIAccessibility.post(notification: .announcement, argument: "arg") not announced in voice over?

Here where I found the others notifications: Default UIAccessibilityElement after screen change

Allan Garcia
  • 550
  • 3
  • 16