I built a NativeButton based on a NSButton with a NSViewRepresentable to detect Rightclicks.
It works fine, but I do not get "visual feedback" as with a normal Button, if it's clicked.
How can I accomplish that the Button get dark, if it's clicked (as any normal Button)?
struct TEST_NativeButton: View {
var body: some View {
VStack{
Text("Test Native Button")
NativeButton(
rightClickAction: {print("rightclick")},
leftClickAction: {print("leftclick")}
)
{ print("standard action")
}
}.frame(width: 200, height: 200)
}
}
struct NativeButton: NSViewRepresentable {
typealias NSViewType = NativeNSButton
let rightClickAction: ActionFunction
let leftClickAction: ActionFunction
let standardAction: ActionFunction
init(
rightClickAction : ActionFunction? = nil,
leftClickAction : ActionFunction? = nil,
standardAction: #escaping ActionFunction
)
{
if let rightClickAction = rightClickAction{
self.rightClickAction = rightClickAction
} else {
self.rightClickAction = {}
}
if let leftClickAction = leftClickAction{
self.leftClickAction = leftClickAction
} else {
self.leftClickAction = standardAction
}
self.standardAction = standardAction
}
func makeNSView(context: Context) -> NativeNSButton {
NativeNSButton(
rightClickAction: rightClickAction,
leftClickAction: leftClickAction,
standardAction: standardAction)
}
func updateNSView(_ nsView: NativeNSButton, context: Context) {
//ToDo auf änderungen am Titel reagieren
return
}
}
class NativeNSButton: NSButton {
let standardAction: () -> Void
let rightClickAction: () -> Void
let leftClickAction: () -> Void
init(
rightClickAction : #escaping () -> Void,
leftClickAction : #escaping () -> Void,
standardAction: #escaping () -> Void) {
self.standardAction = standardAction
self.rightClickAction = rightClickAction
self.leftClickAction = leftClickAction
super.init(frame: .zero)
self.title = title
self.alignment = alignment
self.target = self
self.action = #selector(clickButton(_:))
bezelStyle = .rounded
isBordered = true
focusRingType = .none
self.translatesAutoresizingMaskIntoConstraints = false
self.setContentHuggingPriority(.defaultHigh, for: .vertical)
self.setContentHuggingPriority(.defaultHigh, for: .horizontal)
}
required init?(coder: NSCoder) {
fatalError()
}
override func mouseDown(with theEvent: NSEvent) {
//print("left mouse")
leftClickAction()
}
override func rightMouseDown(with theEvent: NSEvent) {
//print("right mouse")
rightClickAction()
}
#objc func clickButton(_ sender: BbcNSButton) {
//print("standard action")
standardAction()
}
}
The standard action is used for detection Keyboard-Shortcuts (removed in this example).
Call super method
override func mouseDown(with theEvent: NSEvent) {
super.mouseDown(with: theEvent)
//print("left mouse")
leftClickAction()
}
Related
I have tried to get the NSContactPicker to display a picker window in SwiftUI on macOS. Here is my code. If you click on the button nothing happens. What am I missing?
import SwiftUI
import Contacts
import ContactsUI
let d = MyContactPicker()
class MyContactPicker: NSObject, CNContactPickerDelegate
{
var contactName: String = "No user selected"
func pickContact()
{
let contactPicker = CNContactPicker()
contactPicker.delegate = self
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact)
{
contactName = contact.givenName
}
}
struct ContentView: View
{
#State var contact: CNContact?
var picker = MyContactPicker()
var body: some View
{
VStack
{
Text(picker.contactName)
Button("Select Contact")
{
picker.pickContact()
}
}
}
}
Here's a possible starting point using NSViewRepresentable and an NSView subclass
class NSContactPickerView: NSView, CNContactPickerDelegate {
let didSelectContact: (CNContact) -> Void
init(didSelectContact: #escaping (CNContact) -> Void) {
self.didSelectContact = didSelectContact
super.init(frame: .zero)
Task {
showPicker()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showPicker() {
let picker = CNContactPicker()
picker.delegate = self
picker.showRelative(to: .zero, of: self, preferredEdge: .maxY)
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact) {
didSelectContact(contact)
}
}
struct ContactPicker: NSViewRepresentable {
let didSelectContact: (CNContact) -> Void
func makeNSView(context: Context) -> NSContactPickerView {
NSContactPickerView(didSelectContact: didSelectContact)
}
func updateNSView(_ nsView: NSContactPickerView, context: Context) {
}
}
struct ContentView: View {
#State var contact: CNContact?
#State private var showPicker = false
var body: some View {
VStack {
Text(contact?.givenName ?? "")
Button("Select Contact") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker { contact in
self.contact = contact
}
.frame(width: 1, height: 1)
}
}
}
It works, but it's not very elegant. Maybe someone else can improve on this.
I try to add additional functionality when doing a right click. Unfortunately, this will break the .context modifier and I don't understand why. This is the code I am using:
extension View {
func onRightClick(handler: #escaping () -> Void) -> some View {
modifier(RightClickHandler(handler: handler))
}
}
struct RightClickHandler: ViewModifier {
let handler: () -> Void
func body(content: Content) -> some View {
content.overlay(RightClickListeningViewRepresentable(handler: handler), alignment: .center)
}
}
struct RightClickListeningViewRepresentable: NSViewRepresentable {
let handler: () -> Void
func makeNSView(context: Context) -> RightClickListeningView {
RightClickListeningView(handler: handler)
}
func updateNSView(_ nsView: RightClickListeningView, context: Context) {}
}
class RightClickListeningView: NSView {
let handler: () -> Void
init(handler: #escaping () -> Void) {
self.handler = handler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func rightMouseDown(with event: NSEvent) {
handler()
super.rightMouseDown(with: event)
if let menu = super.menu(for: event) {
print(menu)
let location = event.locationInWindow
menu.popUp(positioning: nil, at: location, in: self)
}
}
}
Later then, I have this:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.contextMenu(menuItems: {
Button("Test") { }
})
.onRightClick {
print("right click detcted")
}
.padding()
}
}
If I remove the onRightClick-modifier, the context menu works again.
I know it's 6 months since you've asked this but if you swap around the modifiers it will work as intended (assuming you are trying to detect when context menu is opened). When you right click, the context menu will open and the right click will be detected. Hope this helps!
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.onRightClick {
print("right click detcted")
}
.contextMenu(menuItems: {
Button("Test") { }
})
.padding()
}
}
I am trying to extend my KeyboardView view with rx action with no success. According to debugging with breakpoints value is passed to the relay but extension is not called despite further subscription in a view controller. What might be a problem and how to fix it?
final class KeyboardView: UIView {
private let disposeBag = DisposeBag()
fileprivate let buttonTappedRelay = PublishRelay<ActionType>()
private let digitButtons: [KeyboardButton] = {
return stride(from: 0, through: 9, by: 1)
.compactMap { $0 }
.map { KeyboardButton(actionType: .digit($0)) }
}()
private let eraseButton: KeyboardButton = {
let button = KeyboardButton(actionType: .erase)
return button
}()
public override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
setupActions()
}
private func setupViews() { ... }
private func setupConstraints() { ... }
private func setupActions() {
eraseButton.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
for button in digitButtons {
button.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
}
}
}
extension Reactive where Base: KeyboardView {
internal var buttonTap: ControlEvent<ActionType> {
return ControlEvent<ActionType>(events: base.buttonTappedRelay.asObservable() )
}
}
Your problem is likely in code you haven't shown. Note that the below code compiles, runs and works:
final class KeyboardView: UIView {
private let disposeBag = DisposeBag()
fileprivate let buttonTappedRelay = PublishRelay<ActionType>()
private let digitButtons: [KeyboardButton] = {
return stride(from: 0, through: 9, by: 1)
.compactMap { $0 }
.map { KeyboardButton(actionType: .digit($0)) }
}()
private let eraseButton: KeyboardButton = {
let button = KeyboardButton(actionType: .erase)
return button
}()
public override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupActions()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
let stack = UIStackView(frame: bounds)
stack.distribution = .equalSpacing
stack.addArrangedSubview(eraseButton)
for each in digitButtons {
stack.addArrangedSubview(each)
}
addSubview(stack)
}
private func setupActions() {
eraseButton.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
for button in digitButtons {
button.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
}
}
}
final class ViewController: UIViewController {
weak var keyboard: KeyboardView?
override func loadView() {
super.loadView()
let keyboard = KeyboardView(frame: view.bounds)
view.addSubview(keyboard)
self.keyboard = keyboard
}
override func viewDidLoad() {
super.viewDidLoad()
keyboard!.rx.buttonTap
.debug("đŸŸ£")
.subscribe()
}
}
extension Reactive where Base: KeyboardView {
internal var buttonTap: ControlEvent<ActionType> {
return ControlEvent<ActionType>(events: base.buttonTappedRelay.asObservable() )
}
}
enum ActionType {
case digit(Int)
case erase
}
class KeyboardButton: UIButton {
let actionType: ActionType
init(actionType: ActionType) {
self.actionType = actionType
super.init(frame: CGRect.zero)
backgroundColor = .red
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension Reactive where Base: KeyboardButton {
var buttonTap: Observable<ActionType> {
base.rx.tap.map { base.actionType }
}
}
Because the ScrollView does not provide a function to set the contentOffset, I'm trying to use the UIScrollView as UIViewRepresentable. The attached code shows both, the caller and the definition of the view and the view controller.
When running the code in simulator or previews, just a blue area is shown. When debugging the display, the Text is shown, as expected.
If have no idea about the reason - is it because I'm doing something wrong, or because there's a bug in Xcode or SwiftUI?
Here the custom scroll view:
struct PositionableScrollView<Content>: UIViewRepresentable where Content: View {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: UIViewRepresentableContext<PositionableScrollView<Content>>) -> UIScrollView {
let scrollViewVC = PositionableScrollViewVC<Content>(nibName: nil, bundle: nil)
scrollViewVC.add(content: content)
let control = scrollViewVC.scrollView
return control
}
func updateUIView(_ uiView: UIScrollView, context: UIViewRepresentableContext<PositionableScrollView<Content>>) {
// Do nothing at the moment.
}
}
The view controller:
final class PositionableScrollViewVC<Content>: UIViewController where Content: View {
var scrollView: UIScrollView = UIScrollView()
var contentView: UIView!
var contentVC: UIViewController!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
self.view.addSubview(self.scrollView)
self.scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
self.scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
self.scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
debugPrint("self:", self.frame())
debugPrint("self.view:", self.view!.frame)
debugPrint("self.view.subviews:", self.view.subviews)
// debugPrint("self.view.subviews[0]:", self.view.subviews[0])
// debugPrint("self.view.subviews[0].subviews:", self.view.subviews[0].subviews)
}
func add(#ViewBuilder content: #escaping () -> Content) {
self.contentVC = UIHostingController(rootView: content())
self.contentView = self.contentVC.view!
self.scrollView.addSubview(contentView)
self.contentView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor).isActive = true
self.contentView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor).isActive = true
self.contentView.topAnchor.constraint(equalTo: self.scrollView.topAnchor).isActive = true
self.contentView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor).isActive = true
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.widthAnchor.constraint(greaterThanOrEqualTo: self.scrollView.widthAnchor).isActive = true
}
}
extension PositionableScrollViewVC: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<PositionableScrollViewVC>) -> PositionableScrollViewVC {
let vc = PositionableScrollViewVC()
return vc
}
func updateUIViewController(_ uiViewController: PositionableScrollViewVC, context: UIViewControllerRepresentableContext<PositionableScrollViewVC>) {
// Do nothing at the moment.
}
}
The callers:
struct TimelineView: View {
#State private var posX: CGFloat = 0
var body: some View {
GeometryReader { geo in
VStack {
Text("\(self.posX) || \(geo.frame(in: .global).width)")
PositionableScrollView() {
VStack {
Spacer()
Text("Hallo")
.background(Color.yellow)
Spacer()
}
.frame(width: 1000, height: 200, alignment: .bottomLeading)
}
.background(Color.blue)
}
}
}
}
struct TimelineView_Previews: PreviewProvider {
static var previews: some View {
TimelineView()
}
}
The display, when run, and in debugger:
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?
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()
}
}
}
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()
}
}