I am building an SwiftUI-App that should work with VoiceOver, but when VoiceOver is active parts of my UI is not rendered on screen. It works when I open the view and then activate VoiceOver. It just does not load correctly in combination with VoiceOver. I tried making a simplified example of what I do, but with it evereything seems to be working fine, so I do not know what to do.
I have a tree of Elements that I want to draw a Form for each on the screen. therefore I use Rectangle/Circle/Path and others (but mostly Path). Each element has a function that creates the view associated to it, what automatically also creates the views of the children. There are Tap and DragGestures associated with the drawn Elements, so these are important, if they are not rendered on screen, the hole thing does not work anymore. The actual system is far more complex and uses Subclasses for the TreeElements. I was not able to make a simple example reproducing the problem.
So what I wanted to ask, is if someone would know of anything that could pose problems with my kind of appoach or knows something that would in general not work, when VoiceOver is active?
Here is the simplified example that shown the general Idea of my code:
struct TestView: View {
let tree: TreeElement = createTree()
let gesture: DragGesture = DragGesture()
var body: some View {
tree.createView(with: gesture)
}
}
// This function is only there to create a Tree for testing
func createTree() -> TreeElement {
let tree = TreeElement()
var counter: Int = 1
for _ in stride(from: 0, to: 2, by: 1) {
counter += 1
let childTree = TreeElement()
for _ in stride(from: 0, to: 2, by: 1) {
counter += 1
childTree.children.append(TreeElement())
}
tree.children.append(childTree)
}
print("There are \(counter) Tree Elements.")
return tree
}
class TreeElement: ObservableObject, Identifiable {
@Published public var children: [TreeElement] = []
public var id: UUID = UUID()
func createView(with gesture: DragGesture) -> AnyView {
return AnyView(TreeView(tree: self, gesture: gesture))
}
}
struct TreeView: View {
@ObservedObject var tree: TreeElement
var gesture: DragGesture
var body: some View {
ZStack {
Rectangle()
.onTapGesture(count: 2) { print("A Treeview was tapped") }
.onAppear { print("A Treeview was created") }
ForEach(tree.children) { child in
child.createView(with: gesture)
}
}
}
}
I found the Problem, it had nothing to do with the structure used, but rather with the way I opened the view:
The variable for the tree in the opening view was set with a default empty Tree, and when the Button was tapped, the actual tree was computed. But this was done in a buttons action inside of a navigationLink. Once VoiceOver was active only the NavigationLink was used to enter the View where the Tree should be displayed and so the Tree was not created.
The way I fixed this was to add an Accessibility action to the NavigationLink, so to make sure, the needed computation is done before the link is opened.
struct OpenerView: View {
var file: String
@State var showTreeView: Bool = false
@State var myTree: TreeRoot = TreeRoot(withParent: nil)
var body: some View {
NavigationLink(
destination: ActualTreeView(show: $showTreeView, myTree: myTree)
.navigationBarHidden(true)
.accessibility(addTraits: [.allowsDirectInteraction]),
isActive: $showTreeView,
label: {
Button {
let data: Data = TreeViewHelper.createDataFromAsset(asset: file)
myTree = TreeViewHelper.createUITreeFromData(data: data)
showTreeView.toggle()
} label: {
MenuButtonLook(name: LocalizedStringKey(file))
}
}).accessibilityAction {
let data: Data = TreeViewHelper.createDataFromAsset(asset: file)
myTree = TreeViewHelper.createUITreeFromData(data: data)
showTreeView.toggle()
}
}
}