macosswiftui

Button with PlainButtonStyle doesn't get tapped when the window is inactive


I'm writing a MacOS app. I'm displaying a button. When the app is inactive, if I click the button, the button's tap event gets triggered as the same time as the window is activated. So far so good.

However if I now add .buttonStyle(PlainButtonStyle()) to my button which is the desired look, it doesn't get tapped when the window is inactive. The first click activates the window and I need a second click to trigger the button.

Button {
  print("tapped!")
} label: {
  Image(systemName: "doc.on.doc")
    .resizable()
    .frame(width: 14, height: 14)
    .padding(5)
}
.buttonStyle(PlainButtonStyle())

Solution

  • Whether a view can receive the click event without their window being activated is controlled by the acceptsFirstMouse(for:) method. When the button style is .plain, the button is no longer backed by an NSButton (whose acceptsFirstMouse method would have returned true), so you can no longer "click-through" the button.

    You can wrap a NSViewRepresentable around the button, and override acceptsFirstMouse to return true.

    This is the code from this blog post

    extension SwiftUI.View {
        public func acceptClickThrough() -> some View {
            ClickThroughBackdrop(self)
        }
    }
    
    fileprivate struct ClickThroughBackdrop<Content: SwiftUI.View>: NSViewRepresentable {
        final class Backdrop: NSHostingView<Content> {
            override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
                return true
            }
        }
    
        let content: Content
    
        init(_ content: Content) {
            self.content = content
        }
    
        func makeNSView(context: Context) -> Backdrop {
            let backdrop = Backdrop(rootView: content)
            backdrop.translatesAutoresizingMaskIntoConstraints = false
            return backdrop
        }
    
        func updateNSView(_ nsView: Backdrop, context: Context) {
            nsView.rootView = content
        }
    }
    

    Example usage:

    Button("Foo") {
        print("triggered")
    }
    .buttonStyle(.plain)
    .acceptClickThrough()