How to disable the default focus effect and detect keyboard focus in SwiftUI?

I’m trying to customize the keyboard focus appearance in SwiftUI.

In UIKit (see WWDC 2021 session Focus on iPad keyboard navigation), it’s possible to remove the default UIFocusHaloEffect and change a view’s appearance depending on whether it has focus or not.

In SwiftUI I’ve tried the following:

.focusable() // .focusable(true, interactions: .activate)
.focusEffectDisabled()
.focused($isFocused)

However, I’m running into several issues:

  • .focusable(true, interactions: .activate) causes an infinite loop, so keyboard navigation stops responding
  • .focusEffectDisabled() doesn’t seem to remove the default focus effect on iOS
  • Using @FocusState prevents Space from triggering the action when the view has keyboard focus

My main questions:

  1. How can I reliably detect whether a SwiftUI view has keyboard focus? (Is there an alternative to FocusState that integrates better with keyboard navigation on iOS?)
  2. What’s the recommended way in SwiftUI to disable the default focus effect (the blue overlay) and replace it with a custom border?

Any guidance or best practices would be greatly appreciated!

Here's my sample code:

import SwiftUI

struct KeyboardFocusExample: View {

    var body: some View {
        // The ScrollView is required, otherwise the custom focus value resets to false after a few seconds. I also need it for my actual use case
        ScrollView {
            VStack {
                Text("First button")
                    .keyboardFocus()
                    .button {
                        print("First button tapped")
                    }
                
                Text("Second button")
                    .keyboardFocus()
                    .button {
                        print("Second button tapped")
                    }
            }
        }
    }
}

// MARK: - Focus Modifier
struct KeyboardFocusModifier: ViewModifier {
    @FocusState private var isFocused: Bool
    
    func body(content: Content) -> some View {
        content
            .focusable() // ⚠️ Must come before .focused(), otherwise the FocusState won’t be recognized
//            .focusable(true, interactions: .activate) // ⚠️ This causes an infinite loop, so keyboard navigation no longer responds
            .focusEffectDisabled() // ⚠️ Has no effect on iOS
            .focused($isFocused)
        
            // Custom Halo effect
            .padding(4)
            .overlay(
                RoundedRectangle(cornerRadius: 18)
                    .strokeBorder(
                        isFocused ? .red : .clear,
                        lineWidth: 2
                    )
            )
            .padding(-4)
    }
}
extension View {
    public func keyboardFocus() -> some View {
        modifier(KeyboardFocusModifier())
    }
}

// MARK: - Button Modifier
/// ⚠️ Using a Button view makes no difference
struct ButtonModifier: ViewModifier {
    let action: () -> Void
    
    func body(content: Content) -> some View {
        content
            .contentShape(Rectangle())
            .onTapGesture {
                action()
            }
            .accessibilityAction {
                action()
            }
            .accessibilityAddTraits(.isButton)
            .accessibilityElement(children: .combine)
            .accessibilityRespondsToUserInteraction()
    }
}
extension View {
    public func button(action: @escaping () -> Void) -> some View {
        modifier(ButtonModifier(action: action))
    }
}

Thanks for writing about your experience developing for keyboard navigation! Quick question because this can slightly affect the user experience and development approach: are you using the Accessibility feature Full Keyboard Access? Or just using UIKit keyboard navigation?

How to disable the default focus effect and detect keyboard focus in SwiftUI?
 
 
Q