In AppKit I would do this by assigning its key equivalent to be ↩ or making its cell the window's default. However, neither of these seems possible in SwiftUI, so how do I make a button the default window button?
4 Answers
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)
-
1But this works only on menus, and doesnt works inside of window or sheet, so, this is not solution even for SwiftUI 2. Commented Aug 15, 2020 at 8:06
-
1Works now in Xcode Version 12.2 beta 3 (12B5035g) and Big Sur bêta 11.0 (20A5395g)– u0cramCommented Oct 25, 2020 at 14:56
-
3
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()
}
}
}
-
Still not possible in pure SwiftUI. Hopefully, we'll get this at WWDC this June. Commented Feb 21, 2020 at 8:41
-
I really hope so, many features are missing like SearchBar, these Buttons and more. Does your solution colors the button aswell? Will try it out later. Thanks for the answer– davidevCommented Feb 21, 2020 at 8:52
-
If you
KeyEquivalent.return
, the button will be highlighted with the user's accent color (usually blue). Commented Feb 21, 2020 at 11:44 -
-
It's been possible in native SwiftUI for a while now, since Xcode 12 beta. See my answer for details.– stefCommented Oct 26, 2020 at 19:51
iOS 15.0+ / macOS 12.0+
If you just want the style, use .borderedProminent
to indicate the primary button:
Button( ... ) {
}.buttonStyle(.borderedProminent)
If you want it to respond to the Return key on iOS or macOS, use:
Button( ... ) {
}.keyboardShortcut(.defaultAction)
It will apply a .borderedProminent
style on macOS, but not on iOS. If you want that style on iOS and macOS, use both settings:
Button( ... ) {
}.buttonStyle(.borderedProminent)
.keyboardShortcut(.defaultAction)
-
While that does color the button like I wanted, it doesn't make it the default button (e.g. the one which is automatically pressed if the user presses ↩ return)– Ky -Commented Apr 27, 2023 at 22:51
-
In that case, .keyboardShortcut(.defaultAction) will work. I just tested it working in a sheet for an "iOS macOS" app.– radleyCommented Apr 28, 2023 at 6:03
-
Indeed that works well! Which is why that is the accepted answer, which was written by stef in 2020– Ky -Commented May 2, 2023 at 21:18
Here is a shorter, but less generic solution to create a primary button with return key equivalent, and default button blue tinting.
struct PrimaryButtonView: NSViewRepresentable {
typealias NSViewType = PrimaryButton
let title: String
let action: () -> Void
init(_ title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
func makeNSView(context: Context) -> PrimaryButton {
PrimaryButton(title, action: action)
}
func updateNSView(_ nsView: PrimaryButton, context: Context) {
return
}
}
class PrimaryButton: NSButton {
let buttonAction: () -> Void
init(_ title: String, action: @escaping () -> Void) {
self.buttonAction = action
super.init(frame: .zero)
self.title = title
self.action = #selector(clickButton(_:))
bezelStyle = .rounded //Only this style results in blue tint for button
isBordered = true
focusRingType = .none
keyEquivalent = "\r"
}
required init?(coder: NSCoder) {
fatalError()
}
@objc func clickButton(_ sender: PrimaryButton) {
buttonAction()
}
}
-
Very good solution. The Button takes all the available space. How can I change it, that takes only the space of the title?– micaCommented Jan 7, 2021 at 15:00
-
1I do know whether there is a way to make the button size to the text. I explicitly set the frame of the PrimaryButtonView within the parent view.– jbaragaCommented Jan 10, 2021 at 17:48
-
although this is a good example of wrapping a NSView for this purpose, it has a bug with action not being called. it is missing setting target to self, hence clickButton function is never called. Commented Apr 20, 2023 at 12:38