SwiftUI on Mac - How do I designate a button as being the primary?
macOS 11.0 / iOS 14:
As of Xcode 12 beta, new methods are exposed on Button() allowing assignment of keyEquivalent (either by enum case or explicit key and modifiers).
Setting as default:
Button( ... )
.keyboardShortcut(.defaultAction)
Setting as cancel:
Button( ... )
.keyboardShortcut(.cancelAction)
It's currently not possible. I have reported it to Apple.
However, for now, you can wrap NSButton.
Usage:
@available(macOS 10.15, *)
struct ContentView: View {
var body: some View {
NativeButton("Submit", keyEquivalent: .return) {
// Some action
}
.padding()
}
}
Implementation:
// MARK: - Action closure for controls
private var controlActionClosureProtocolAssociatedObjectKey: UInt8 = 0
protocol ControlActionClosureProtocol: NSObjectProtocol {
var target: AnyObject? { get set }
var action: Selector? { get set }
}
private final class ActionTrampoline<T>: NSObject {
let action: (T) -> Void
init(action: @escaping (T) -> Void) {
self.action = action
}
@objc
func action(sender: AnyObject) {
action(sender as! T)
}
}
extension ControlActionClosureProtocol {
func onAction(_ action: @escaping (Self) -> Void) {
let trampoline = ActionTrampoline(action: action)
self.target = trampoline
self.action = #selector(ActionTrampoline<Self>.action(sender:))
objc_setAssociatedObject(self, &controlActionClosureProtocolAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
}
}
extension NSControl: ControlActionClosureProtocol {}
// MARK: -
@available(macOS 10.15, *)
struct NativeButton: NSViewRepresentable {
enum KeyEquivalent: String {
case escape = "\u{1b}"
case `return` = "\r"
}
var title: String?
var attributedTitle: NSAttributedString?
var keyEquivalent: KeyEquivalent?
let action: () -> Void
init(
_ title: String,
keyEquivalent: KeyEquivalent? = nil,
action: @escaping () -> Void
) {
self.title = title
self.keyEquivalent = keyEquivalent
self.action = action
}
init(
_ attributedTitle: NSAttributedString,
keyEquivalent: KeyEquivalent? = nil,
action: @escaping () -> Void
) {
self.attributedTitle = attributedTitle
self.keyEquivalent = keyEquivalent
self.action = action
}
func makeNSView(context: NSViewRepresentableContext<Self>) -> NSButton {
let button = NSButton(title: "", target: nil, action: nil)
button.translatesAutoresizingMaskIntoConstraints = false
button.setContentHuggingPriority(.defaultHigh, for: .vertical)
button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return button
}
func updateNSView(_ nsView: NSButton, context: NSViewRepresentableContext<Self>) {
if attributedTitle == nil {
nsView.title = title ?? ""
}
if title == nil {
nsView.attributedTitle = attributedTitle ?? NSAttributedString(string: "")
}
nsView.keyEquivalent = keyEquivalent?.rawValue ?? ""
nsView.onAction { _ in
self.action()
}
}
}