I have a view shown as a crouton (from bottom view) when tapping a button. I want to move accessibility focus to this view when shown but using AccessibilityFocusState only works if the button is NOT inside a List. If it’s inside a list it does not work.
Is the List “absorbing” the focus event?
Here is the code:
struct ContentView: View {
@State var showCrouton = false
var body: some View {
List { // CHANGING THIS BY "Vstack" WORKS
Section("Section 1") {
VStack(spacing: 2) {
Text("Label 1")
Text("Label 2")
}
}
Section("Buttons") {
Button(action: {
withAnimation {
showCrouton.toggle()
}
}, label: {
Text("Show crouton")
})
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.crouton(isVisible: $showCrouton, config: Config(title: "Test crouton", closeActionTitle: "Close"))
}
}
extension View {
@ViewBuilder
func crouton(
isVisible: Binding<Bool>,
config: Config,
dismissHandlerBlock: DismissHandlerBlock? = nil
) -> some View {
modifier(
SnackbarModifier(
isVisible: isVisible,
config: config,
dismissHandlerBlock: dismissHandlerBlock
)
)
}
}
struct SnackbarModifier: ViewModifier {
@Binding var isVisible: Bool
var config: Config
var dismissHandlerBlock: DismissHandlerBlock? = nil
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
content
.zIndex(0)
if isVisible {
VStack {
Spacer()
Snackbar(
config: config
)
.onClose {
dismiss()
}
}
.zIndex(1)
.edgesIgnoringSafeArea(.bottom)
.transition(.move(edge: .bottom).animation(.timingCurve(0.77, 0, 0.175, 5)))
}
}
}
}
public typealias DismissHandlerBlock = () -> Void
extension SnackbarModifier {
func dismiss() {
withAnimation { isVisible = false }
}
}
public struct Config {
let title: String
let closeActionTitle: String?
}
public struct Snackbar: View {
@State private var isCroutonFocused = false
private let config: Config
private var dismissHandlerBlock: DismissHandlerBlock?
public init(config: Config) {
self.config = config
}
public var body: some View {
VStack(alignment: .trailing, spacing: 18) {
HStack(spacing: 16) {
Text(config.title)
.font(.system(size: 16))
.foregroundColor(.blue)
.frame(maxWidth: .infinity, alignment: .leading)
alignedButton
}
}
.padding(30)
.background(Color.red)
.accessibilityFocusedMod(isFocused: $isCroutonFocused)
.onAppear {
isCroutonFocused = true
}
}
@ViewBuilder
var alignedButton: some View {
if let closeActionTitle = config.closeActionTitle, !closeActionTitle.isEmpty {
Spacer()
Button(closeActionTitle) {
dismissHandlerBlock?()
}
}
}
}
public extension Snackbar {
func onClose(perform action: DismissHandlerBlock? = nil) -> Snackbar {
var snackbar = self
snackbar.dismissHandlerBlock = action
return snackbar
}
}
@available(iOS 15, *)
struct AccessibilityModifier: ViewModifier {
@AccessibilityFocusState var focused: Bool
@Binding var isFocused: Bool
func body(content: Content) -> some View {
content
.accessibilityFocused($focused)
.onChange(of: isFocused) { newValue in
focused = newValue
}
}
}
public extension View {
@ViewBuilder
func accessibilityFocusedMod(isFocused: Binding<Bool>) -> some View {
if #available(iOS 15, *) {
self.modifier(AccessibilityModifier(isFocused: isFocused))
} else {
self
}
}
}