Catching double click in Cocoa OSX - cocoa

NSResponder seems to have no mouse double click event. Is there an easy way to catch a double click?

The mouseDown: and mouseUp: methods take an NSEvent object as an argument with information about the clicks, including the clickCount.

The problem with plain clickCount solution is that double click is considered simply as two single clicks. I mean you still get the single click. And if you want to react differently to that single click, you need something on top of mere click counting. Here's what I've ended up with (in Swift):
private var _doubleClickTimer: NSTimer?
// a way to ignore first click when listening for double click
override func mouseDown(theEvent: NSEvent) {
if theEvent.clickCount > 1 {
_doubleClickTimer!.invalidate()
onDoubleClick(theEvent)
} else if theEvent.clickCount == 1 { // can be 0 - if delay was big between down and up
_doubleClickTimer = NSTimer.scheduledTimerWithTimeInterval(
0.3, // NSEvent.doubleClickInterval() - too long
target: self,
selector: "onDoubleClickTimeout:",
userInfo: theEvent,
repeats: false
)
}
}
func onDoubleClickTimeout(timer: NSTimer) {
onClick(timer.userInfo as! NSEvent)
}
func onClick(theEvent: NSEvent) {
println("single")
}
func onDoubleClick(theEvent: NSEvent) {
println("double")
}

Generally applications look at clickCount == 2 on -[mouseUp:] to determine a double-click.
One refinement is to keep track of the location of the mouse click on the -[mouseDown:] and see that the delta on the mouse up location is small (5 points or less in both the x and the y).

The NSEvents generated for mouseDown: and mouseUp: have a property called clickCount. Check if it's two to determine if a double click has occurred.
Sample implementation:
- (void)mouseDown:(NSEvent *)event {
if (event.clickCount == 2) {
NSLog(#"Double click!");
}
}
Just place that in your NSResponder (such as an NSView) subclass.

An alternative to the mouseDown: + NSTimer method that I prefer is NSClickGestureRecognizer.
let doubleClickGestureRecognizer = NSClickGestureRecognizer(target: self, action: #selector(self.myCustomMethod))
doubleClickGestureRecognizer.numberOfClicksRequired = 2
self.myView.addGestureRecognizer(doubleClickGestureRecognizer)

I implemented something similar to #jayarjo except this is a bit more modular in that you could use it for any NSView or a subclass of it. This is a custom gesture recognizer that will recognize both click and double actions but not single clicks until the double click threshold has passed:
//
// DoubleClickGestureRecognizer.swift
//
import Foundation
/// gesture recognizer to detect two clicks and one click without having a massive delay or having to implement all this annoying `requireFailureOf` boilerplate code
final class DoubleClickGestureRecognizer: NSClickGestureRecognizer {
private let _action: Selector
private let _doubleAction: Selector
private var _clickCount: Int = 0
override var action: Selector? {
get {
return nil /// prevent base class from performing any actions
} set {
if newValue != nil { // if they are trying to assign an actual action
fatalError("Only use init(target:action:doubleAction) for assigning actions")
}
}
}
required init(target: AnyObject, action: Selector, doubleAction: Selector) {
_action = action
_doubleAction = doubleAction
super.init(target: target, action: nil)
}
required init?(coder: NSCoder) {
fatalError("init(target:action:doubleAction) is only support atm")
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
_clickCount += 1
let delayThreshold = 0.15 // fine tune this as needed
perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
if _clickCount == 2 {
_ = target?.perform(_doubleAction)
}
}
#objc private func _resetAndPerformActionIfNecessary() {
if _clickCount == 1 {
_ = target?.perform(_action)
}
_clickCount = 0
}
}
USAGE :
let gesture = DoubleClickGestureRecognizer(target: self, action: #selector(mySingleAction), doubleAction: #selector(myDoubleAction))
button.addGestureRecognizer(gesture)
#objc func mySingleAction() {
// ... single click handling code here
}
#objc func myDoubleAction() {
// ... double click handling code here
}

Personally, I check the double click into mouseUp functions:
- (void)mouseUp:(NSEvent *)theEvent
{
if ([theEvent clickCount] == 2)
{
CGPoint point = [theEvent locationInWindow];
NSLog(#"Double click on: %f, %f", point.x, point.y);
}
}

Related

Implementing NSSegmentedControl in NSToolbar to control NSTabViewController

In my macOS application, I'm trying to replicate the Photos.app implementation of NSSegmentedControl in NSToolbar to control an NSTabViewController. For reference, here's what that looks like:
So, my approach was as follows:
Hide the default NSTabView header using the Interface Builder
Programmatically add an NSToolbar
Insert NSSegmentedControl as an NSToolbarItem.
Use a #selector to listen for changes to NSSegmentedControl.
Here's the current implementation:
class WindowController: NSWindowController, NSToolbarDelegate {
// MARK: - Identifiers
let mainToolbarIdentifier = NSToolbar.Identifier("MAIN_TOOLBAR")
let segmentedControlIdentifier = NSToolbarItem.Identifier("MAIN_TABBAR")
// MARK: - Properties
var tabBar: NSSegmentedControl? = NSSegmentedControl(labels: ["One", "Two"], trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(didSwitchTabs))
var toolbar: NSToolbar?
var tabBarController: NSTabViewController?
// MARK: - Life Cycle
override func windowDidLoad() {
super.windowDidLoad()
self.toolbar = NSToolbar(identifier: mainToolbarIdentifier)
self.toolbar?.allowsUserCustomization = false
self.toolbar?.delegate = self
self.tabBar?.setSelected(true, forSegment: 0)
self.tabBarController = self.window?.contentViewController as? NSTabViewController
self.tabBarController?.selectedTabViewItemIndex = 0
self.window?.toolbar = self.toolbar
}
// MARK: - NSToolbarDelegate
public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
var toolbarItem: NSToolbarItem
switch itemIdentifier {
case segmentedControlIdentifier:
toolbarItem = NSToolbarItem(itemIdentifier: segmentedControlIdentifier)
toolbarItem.view = self.tabBar
case NSToolbarItem.Identifier.flexibleSpace:
toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
default:
fatalError()
}
return toolbarItem
}
public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
// MARK: - Selectors
#objc func didSwitchTabs(sender: Any) {
let segmentedControl = sender as! NSSegmentedControl
if (segmentedControl.selectedSegment == 0) {
self.tabBarController?.selectedTabViewItemIndex = 0
} else if (segmentedControl.selectedSegment == 1) {
self.tabBarController?.selectedTabViewItemIndex = 1
}
}
}
And, here it is in action:
Now, I am new to macOS development and this feels like it's a very complicated and convoluted way of solving this problem. Is there an easier way I could achieve the same thing ? Perhaps somehow in Interface Builder ? What could be done to improve here ? What have I done wrong ?
Thanks for your time.
For anybody implementing NSSegmentedControl on the toolbar and it did not trigger IBAction, I got the same problem and pay my half-day to resolve this.
The problem is I connect my segmented with the NSWindowController class.
To fix this, create a subclass of NSWindow, set that class to base class of window on your storyboard, then create #IBOutlet #IBAction link to NSWindow. Remember, link it with NSWindowController will not work.

NSView does not receive mouseUp: event when mouse button is released outside of view

I have a custom NSView subclass with (for example) the following methods:
override func mouseDown(with event: NSEvent) { Swift.print("mouseDown") }
override func mouseDragged(with event: NSEvent) { Swift.print("mouseDragged") }
override func mouseUp(with event: NSEvent) { Swift.print("mouseUp") }
As long as the mouse (button) is pressed, dragged and released all inside the view, this works fine. However, when the mouse is depressed inside the view, moved outside the view, and only then released, I never receive the mouseUp event.
P.S.: Calling the super implementations does not help.
The Handling Mouse Dragging Operations section of Apple's mouse events documentation provided a solution: Apparently, we do receive the mouseUp event when tracking events with a mouse-tracking loop.
Here's a variant of the sample code from the documentation, adapted for Swift 3:
override func mouseDown(with event: NSEvent) {
var keepOn = true
mouseDownImpl(with: event)
// We need to use a mouse-tracking loop as otherwise mouseUp events are not delivered when the mouse button is
// released outside the view.
while true {
guard let nextEvent = self.window?.nextEvent(matching: [.leftMouseUp, .leftMouseDragged]) else { continue }
let mouseLocation = self.convert(nextEvent.locationInWindow, from: nil)
let isInside = self.bounds.contains(mouseLocation)
switch nextEvent.type {
case .leftMouseDragged:
if isInside {
mouseDraggedImpl(with: nextEvent)
}
case .leftMouseUp:
mouseUpImpl(with: nextEvent)
return
default: break
}
}
}
func mouseDownImpl(with event: NSEvent) { Swift.print("mouseDown") }
func mouseDraggedImpl(with event: NSEvent) { Swift.print("mouseDragged") }
func mouseUpImpl(with event: NSEvent) { Swift.print("mouseUp") }
Am posting this as an answer to a similar question that I had where I needed to know that the user had stopped using a slider. I needed to capture the mouseUp event from NSSlider or actually NSView. The solution that worked out for me was to simply capture the mouseDown event and add some code when it exited and does the job that I needed. Hope that this is of use to somebody else who needs to do a similar thing. Code written using XCode 11.3.1 Swift 5
import Cocoa
class SMSlider: NSSlider {
var calledOnExit:(()->())?
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if self.calledOnExit != nil {
self.calledOnExit!()
}
}
}
// In my main swift app
func sliderStopped() {
print("Slider stopped moving")
}
//...
if slider == nil {
slider = SMSlider()
}
slider?.isContinuous = true
slider?.target = self
slider?.calledOnExit = sliderStopped
//...

Changing NSCursor for NSView above an NSTextView

Found a similar question to mine(this),
but my issues seems to be a bit more associated with view hierarchy.
I have a NSTextView, then as sibling views, several other NSViews on top of it.
Similar to the question linked above, I setup a tracking area, and applied the cursor as such:
class CursorChangingView: NSView {
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect:
}
override func cursorUpdate(event: NSEvent) {
NSCursor.arrowCursor().set()
}
}
It does seem to work when hovering, but immediately goes back to the IBeam Cursor, which is the default cursor for NSTextViews under this CursorChangingView.
Is this the proper way of applying changing the cursor when hovering over a certain NSView, and is the NSTextView under it overriding my overrriding?
All you need is to subclass a custom view, override awakeFromNib method, add the custom tracking area for [.mouseMoved, .activeAlways] events: NSTrackingArea Info there. There is no need to override resetCursorRects and/or updateTrackingAreas. All you need is to override mouseMoved method and set the desired cursor there:
Note about discardCursorRects method:
From the docs
You need never invoke this method directly
Xcode 9 • Swift 4
import Cocoa
class CursorChangingView: NSView {
override func awakeFromNib() {
addTrackingArea(NSTrackingArea(rect: bounds, options: [.activeAlways, .mouseMoved], owner: self, userInfo: nil))
wantsLayer = true
layer?.backgroundColor = NSColor.cyan.cgColor
layer?.borderColor = NSColor.black.cgColor
layer?.borderWidth = 1
}
#objc override func mouseMoved(with theEvent: NSEvent) {
NSCursor.pointingHand.set()
}
}
Sample
Thanks #Leo Dabus for your answer,
but I managed to solve it, so I will post my answer too.
In my case, for some reason, mouseEntered and mouseEntered did not work at all.
So here is my code that finally got it to work:
class CursorChangingView: NSView {
let trackingArea: NSTrackingArea?
func setupTracking() {
if self.trackingArea == nil {
self.trackingArea = NSTrackingArea(rect: self.bounds, options: NSTrackingAreaOptions.ActiveAlways | NSTrackingAreaOptions.MouseMoved | NSTrackingAreaOptions.CursorUpdate | NSTrackingAreaOptions.MouseEnteredAndExited | NSTrackingAreaOptions.ActiveInActiveApp, owner: self, userInfo: nil)
self.addTrackingArea(self.trackingArea!)
}
}
override func updateTrackingAreas() {
self.trackingArea = NSTrackingArea(rect: self.bounds, options: NSTrackingAreaOptions.ActiveAlways | NSTrackingAreaOptions.CursorUpdate | NSTrackingAreaOptions.MouseEnteredAndExited | NSTrackingAreaOptions.ActiveInActiveApp, owner: self, userInfo: nil)
self.addTrackingArea(self.trackingArea!)
}
override func resetCursorRects() {
self.discardCursorRects()
self.addCursorRect(self.bounds, cursor: NSCursor.arrowCursor())
}
override func mouseMoved(theEvent: NSEvent) {
NSCursor.arrowCursor().set()
}
}
It might be a little excessive, but worked, so will share this as my own solution.
A few important notes:
Be careful calling super on your mouseMoved or similar events, or the cursor might just get reset by the base class implementation.
Only reset your tracking area when the parent view size changes; if you try to do this by overriding layout() it's going to be happening all the time which is not great
Here's an example class that you can just use as a base class in your storyboards.
Swift 4 code:
import Cocoa
final class MouseTrackingTextView: NSTextView {
// MARK: - Lifecycle
override func awakeFromNib() {
setupTrackingArea()
}
// MARK: - Resizing
// Call this in your controller's `viewDidLayout`
// so it only gets called when the view resizes
func superviewResized() {
resetTrackingArea()
}
// MARK: - Mouse Events
override func resetCursorRects() {
addCursorRect(bounds, cursor: cursorType)
}
override func mouseMoved(with event: NSEvent) {
cursorType.set()
}
// MARK: - Private Properties
private var currentTrackingArea: NSTrackingArea?
private var cursorType: NSCursor {
return isEditable ? .iBeam : .pointingHand
}
// MARK: - Private API
private func setupTrackingArea() {
let trackingArea = NSTrackingArea(rect: bounds,
options: [.activeAlways, .mouseMoved],
owner: self, userInfo: nil)
currentTrackingArea = trackingArea
addTrackingArea(trackingArea)
}
private func resetTrackingArea() {
if let trackingArea = currentTrackingArea {
removeTrackingArea(trackingArea)
}
setupTrackingArea()
}
}

How can the context menu in WKWebView on the Mac be modified or overridden?

I'm using a WKWebView in a Mac OS X application. I want to override the contextual menu that appears when the user Control + clicks or right clicks in the WKWebView, but I cannot find a way to accomplish this.
It should be noted that the context menu changes depending on the state of the WKWebView and what element is under the mouse when the context menu is invoked. For example, the context menu only has a single "Reload" item when the mouse is over an "empty" part of the content, whereas right clicking a link presents the options "Open Link", "Open Link In New Window", and so on. It would be helpful to have granular control over these different menus if possible.
The older WebUIDelegate provides the - webView:contextMenuItemsForElement:defaultMenuItems:
method that allows you to customize the context menu for WebView instances; I'm essentially looking for the analog to this method for WKWebView, or any way to duplicate the functionality.
You can do this by intercepting the contextmenu event in your javascript, reporting the event back to your OSX container through a scriptMessageHandler, then popping up a menu from OSX. You can pass context back through the body field of the script message to show an appropriate menu, or use a different handler for each one.
Setting up callback handler in Objective C:
WKUserContentController *contentController = [[WKUserContentController alloc]init];
[contentController addScriptMessageHandler:self name:#"callbackHandler"];
config.userContentController = contentController;
self.mainWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config];
Javascript code using jquery:
$(nodeId).on("contextmenu", function (evt) {
window.webkit.messageHandlers.callbackHandler.postMessage({body: "..."});
evt.preventDefault();
});
Responding to it from Objective C:
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:#"callbackHandler"]) {
[self popupMenu:message.body];
}
}
-(void)popupMenu:(NSString *)context {
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:#"Context Menu"];
[theMenu insertItemWithTitle:#"Beep" action:#selector(beep:) keyEquivalent:#"" atIndex:0];
[theMenu insertItemWithTitle:#"Honk" action:#selector(honk:) keyEquivalent:#"" atIndex:1];
[theMenu popUpMenuPositioningItem:theMenu.itemArray[0] atLocation:NSPointFromCGPoint(CGPointMake(0,0)) inView:self.view];
}
-(void)beep:(id)val {
NSLog(#"got beep %#", val);
}
-(void)honk:(id)val {
NSLog(#"got honk %#", val);
}
You can intercept context menu items of the WKWebView class by subclassing it and implementing the willOpenMenu method like this:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(_:))
menuItem.target = self
}
}
}
#objc func menuClick(_ sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
Instead of this you can also simply hide the menu items with menuItem.isHidden = true
Detecting the chosen menu item is one thing, but knowing what the user actually clicked in the WKWebView control is the next challenge :)
It's also possible to add new menu items to the menu.items array.
Objective C solution. The best solution is to subclass WKWebView and intercept mouse clicks. It works great.
#implementation WKReportWebView
// Ctrl+click seems to send this not rightMouse
-(void)mouseDown:(NSEvent *)event
{
if(event.modifierFlags & NSEventModifierFlagControl)
return [self rightMouseDown:event];
[super mouseDown:event]; // Catch scrollbar mouse events
}
-(void)rightMouseDown:(NSEvent *)theEvent
{
NSMenu *rightClickMenu = [[NSMenu alloc] initWithTitle:#"Print Menu"];
[rightClickMenu insertItemWithTitle:NSLocalizedString(#"Print", nil) action:#selector(print:) keyEquivalent:#"" atIndex:0];
[NSMenu popUpContextMenu:rightClickMenu withEvent:theEvent forView:self];
}
#end
This answer builds on the excellent answers in this thread.
The challenges in working with the WKWebView's context menu are:
It can only be manipulated in a subclass of WKWebView
WebKit does not expose any information about the HTML element that the user right-clicked on. Thus, information about the element must be intercepted in JavaScript and plumbed back into Swift.
Intercepting and finding information about the element the user clicked on happens by injecting JavaScript into the page prior to rendering, and then by establishing a callback into Swift. Here is the class that I wrote to do this. It works on the WKWebView's configuration object. It also assumes that there is only one context menu available at a time:
class GlobalScriptMessageHandler: NSObject, WKScriptMessageHandler {
public private(set) static var instance = GlobalScriptMessageHandler()
public private(set) var contextMenu_nodeName: String?
public private(set) var contextMenu_nodeId: String?
public private(set) var contextMenu_hrefNodeName: String?
public private(set) var contextMenu_hrefNodeId: String?
public private(set) var contextMenu_href: String?
static private var WHOLE_PAGE_SCRIPT = """
window.oncontextmenu = (event) => {
var target = event.target
var href = target.href
var parentElement = target
while (href == null && parentElement.parentElement != null) {
parentElement = parentElement.parentElement
href = parentElement.href
}
if (href == null) {
parentElement = null;
}
window.webkit.messageHandlers.oncontextmenu.postMessage({
nodeName: target.nodeName,
id: target.id,
hrefNodeName: parentElement?.nodeName,
hrefId: parentElement?.id,
href
});
}
"""
private override init() {
super.init()
}
public func ensureHandles(configuration: WKWebViewConfiguration) {
var alreadyHandling = false
for userScript in configuration.userContentController.userScripts {
if userScript.source == GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT {
alreadyHandling = true
}
}
if !alreadyHandling {
let userContentController = configuration.userContentController
userContentController.add(self, name: "oncontextmenu")
let userScript = WKUserScript(source: GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? NSDictionary {
contextMenu_nodeName = body["nodeName"] as? String
contextMenu_nodeId = body["id"] as? String
contextMenu_hrefNodeName = body["hrefNodeName"] as? String
contextMenu_hrefNodeId = body["hrefId"] as? String
contextMenu_href = body["href"] as? String
}
}
Next, to enable this in your WKWebView, you must subclass it and call GlobalScriptMessageHandler.instance.ensureHandles in your constructor:
class WebView: WKWebView {
public var webViewDelegate: WebViewDelegate?
init() {
super.init(frame: CGRect(), configuration: WKWebViewConfiguration())
GlobalScriptMessageHandler.instance.ensureHandles(configuration: self.configuration)
}
Finally, (as other answers have pointed out,) you override the context menu handler. In this case I changed the action in target for the "Open Link" menu item. You can change them as you see fit:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for index in 0...(menu.items.count - 1) {
let menuItem = menu.items[index]
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierOpenLink" {
menuItem.action = #selector(openLink(_:))
menuItem.target = self
And then, in your method to handle the menu item, use GlobalScriptMessageHandler.instance.contextMenu_href to get the URL that the user right-clicked:
#objc func openLink(_ sender: AnyObject) {
if let url = GlobalScriptMessageHandler.instance.contextMenu_href {
let url = URL(string: url)!
self.load(URLRequest(url: url))
}
}
Following the answers already given I was able to modify the menu and also found a way get the URL that was selected by the user. I suppose this approach can also be used to get an image or any other similar content selected, and I'm hoping this can help other folks.
This is written using Swift 5
This approach consists on performing the action from the menu item "Copy Link", so that the URL gets copied into the paste board, then retrieving the URL from the paste board to use it on a new menu item.
Note: Retrieving the URL from the pasteboard needs to be called on an async closure, allowing time for the URL to first be copied into it.
final class WebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with: NSEvent) {
menu.items.first { $0.identifier?.rawValue == "WKMenuItemIdentifierCopyLink" }.map {
guard let action = $0.action else { return }
NSApp.sendAction(action, to: $0.target, from: $0)
DispatchQueue.main.async { [weak self] in
let newTab = NSMenuItem(title: "Open Link in New Tab", action: #selector(self?.openInNewTab), keyEquivalent: "")
newTab.target = self
newTab.representedObject = NSPasteboard.general.string(forType: .string)
menu.items.append(newTab)
}
}
}
#objc private func openInNewTab(_ item: NSMenuItem) {
print(item.representedObject as? String)
}
}

Set Action Listener Programmatically in Swift

I saw some example codes that assign the same OnClick event to all the buttons in Android (even if they perform completely different action) . How can do it with Swift
Android Example:
#Override
public void onCreate(Bundle savedInstanceState) {
button1.setOnClickListener(onClickListener);
button2.setOnClickListener(onClickListener);
button3.setOnClickListener(onClickListener);
}
private OnClickListener onClickListener = new OnClickListener() {
#Override
public void onClick(View v) {
switch(v.getId()){
case R.id.button1:
//DO something
break;
case R.id.button2:
//DO something
break;
case R.id.button3:
//DO something
break;
}
}
};
Note: I don't want create the button programatically.
On iOS, you're not setting a listener; you add a target (an object) and an action (method signature, "selector" in iOS parlance) to your UIControl (which UIButton is a subclass of):
button1.addTarget(self, action: "buttonClicked:", for: .touchUpInside)
button2.addTarget(self, action: "buttonClicked:", for: .touchUpInside)
button3.addTarget(self, action: "buttonClicked:", for: .touchUpInside)
The first parameter is the target object, in this case self. The action is a selector (method signature) and there are basically two options (more on that later). The control event is a bit specific to the UIControl - .TouchUpInside is commonly used for tapping a button.
Now, the action. That's a method (the name is your choice) of one of the following formats:
func buttonClicked()
func buttonClicked(_ sender: AnyObject?)
To use the first one use "buttonClicked", for the second one (which you want here) use "buttonClicked:" (with trailing colon). The sender will be the source of the event, in other words, your button.
func buttonClicked(_ sender: AnyObject?) {
if sender === button1 {
// do something
} else if sender === button2 {
// do something
} else if sender === button3 {
// do something
}
}
(this assumes that button1, button2 and button3 are instance variables).
Instead of this one method with the big switch statement consider using separate methods for each button. Based on your specific use case either approach might be better:
func button1Clicked() {
// do something
}
func button2Clicked() {
// do something
}
func button3Clicked() {
// do something
}
Here, I'm not even using the sender argument because I don't need it.
P.S.: Instead of adding targets and actions programmatically you can do so in your Storyboard or nib file. In order to expose the actions you put IBAction in front of your function, e.g.:
#IBAction func button1Clicked() {
// do something
}
Swift 4.*
button.addTarget(self, action: #selector(didButtonClick), for: .touchUpInside)
And the button triggers this function:
#objc func didButtonClick(_ sender: UIButton) {
// your code goes here
}
An Swift 5 Extension Solution
Create A SwiftFile "SetOnClickListener.swift"
copy paste this code
import UIKit
class ClosureSleeve {
let closure: () -> ()
init(attachTo: AnyObject, closure: #escaping () -> ()) {
self.closure = closure
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
}
#objc func invoke() {
closure()
}
}
extension UIControl {
func setOnClickListener(for controlEvents: UIControl.Event = .primaryActionTriggered, action: #escaping () -> ()) {
let sleeve = ClosureSleeve(attachTo: self, closure: action)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
}
}
How To use
for example buttonA is a UIButton
buttonA.setOnClickListener {
print("button A clicked")
}

Resources