Set Action Listener Programmatically in Swift - xcode

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")
}

Related

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
//...

NSResponder accepts events when acceptsFirstResponder is false?

Why are the methods moveLeft() and moveRight() being called, I have turned off the first responder ability for the window controller? I haven't added any code in elsewhere, so I'm obviously missing something somewhere...
In the end I do want to accept events, but if I 'enable' them here and deal with overriding keyEvent(), it causes it to be handled twice and a choice being made twice.
import Cocoa
enum UserChoice {
case Left, Right
}
class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
}
override var windowNibName: String? {
return "MainWindowController"
}
override var acceptsFirstResponder: Bool {
return false
}
override func moveLeft(sender: AnyObject?) {
chooseImage(UserChoice.Left)
}
override func moveRight(sender: AnyObject?) {
chooseImage(UserChoice.Right)
}
func chooseImage(choice: UserChoice) {
print("choice made")
}
}
The only other file I have is AppDelegate.swift:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var mainWindowController: MainWindowController!
func applicationDidFinishLaunching(notification: NSNotification) {
mainWindowController = MainWindowController()
mainWindowController.showWindow(self)
}
}
Any comments on my code are welcome too, I'm new to Swift/Cocoa so...
When your controller refuses to be first responder, it's still a responder, just not the first one. Another responder such as the window or a view within it can choose to pass the buck back up the responder chain.
I'm not clear on why you implemented moveLeft and moveRight if you don't want to handle them.

Dropping a token in NSTokenField

I am implementing an app where rows from an NSTableView can be dragged and dropped into an NSTokenField, but I am struggling to implement the drop-side of the interaction. I have subclassed NSTokenField (as shown below in the debugging code below). But I am only seeing calls to draggingEntered: and updateDraggingItemsForDrag: method. Even though I return a valid NSDragOperation (Copy), none of the other methods in NSDraggingDestination are called. The cursor briefly flashes to the copy icon when moving over the token field, but then returns to the normal cursor.
I tried implementing all the methods associated with NSDraggingDestination for debugging purposes, shown in the code below. Is there another class or part of the NSTokenField that is handling the drop? Is it possible to override that?
I have confirmed that the pasteboard does have data with the valid pasteboard type.
let kPasteboardType = "SamplePasteboardType"
class MyTokenField : NSTokenField
{
override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation {
// entered
NSLog("ENTERED")
// must come from same window
guard self.window == sender.draggingDestinationWindow() else {
return super.draggingEntered(sender)
}
// has valid pasteboard data?
let pb = sender.draggingPasteboard()
if let _ = pb.dataForType(kPasteboardType) {
NSLog("MATCHED")
return NSDragOperation.Copy
}
return super.draggingEntered(sender)
}
override func draggingUpdated(sender: NSDraggingInfo) -> NSDragOperation {
NSLog("UPDATED")
// must come from same window
guard self.window == sender.draggingDestinationWindow() else {
return super.draggingUpdated(sender)
}
// has valid pasteboard data?
let pb = sender.draggingPasteboard()
if let _ = pb.dataForType(kPasteboardType) {
return NSDragOperation.Copy
}
return super.draggingUpdated(sender)
}
override func draggingExited(sender: NSDraggingInfo?) {
NSLog("EXITED")
super.draggingExited(sender)
}
override func prepareForDragOperation(sender: NSDraggingInfo) -> Bool {
NSLog("PREPARE")
return super.prepareForDragOperation(sender)
}
override func performDragOperation(sender: NSDraggingInfo) -> Bool {
NSLog("PERFORM")
return super.performDragOperation(sender)
}
override func concludeDragOperation(sender: NSDraggingInfo?) {
NSLog("CONCLUDE")
super.concludeDragOperation(sender)
}
override func draggingEnded(sender: NSDraggingInfo?) {
NSLog("ENDED")
super.draggingEnded(sender)
}
override func updateDraggingItemsForDrag(sender: NSDraggingInfo?) {
// super.updateDraggingItemsForDrag(sender)
guard let drag = sender else {
return
}
let classes: [AnyClass] = [NSPasteboardItem.self]
let options: [String: AnyObject] = [NSPasteboardURLReadingContentsConformToTypesKey: [kPasteboardType]]
drag.enumerateDraggingItemsWithOptions(NSDraggingItemEnumerationOptions.ClearNonenumeratedImages, forView: self, classes: classes, searchOptions: options) {
(item, idx, stop) in
NSLog("\(item)")
}
}
}
Thanks to the comment from #stevesliva, I was able to solve the problem. There are some key caveats that I discovered (they may be partly due to my ignorance of pasteboard and drag/drop interactions).
Subclassing the NSTokenField class is not necessary.
I had to implement the delegate function tokenField(tokenField: NSTokenField, readFromPasteboard pboard: NSPasteboard) -> [AnyObject]? for the token field.
I had to change start of the drag to store a string value to the pasteboard. It seems like if the pasteboard does not have a string value, then the above delegate function is never called.

Add completion handler to presentViewControllerAsSheet(NSViewController)?

I am attempting to present a sheet configuration view (AddSoundEffect) for my main window/view controller (I'm using storyboards), and when the configuration view controller is dismissed, take the values entered in the AddSoundEffect view and pass that back to the main view. My current code in the main view controller:
presentViewControllerAsSheet(self.storyboard!.instantiateControllerWithIdentifier("AddSoundEffect") as! AddSoundViewController
And in the AddSoundViewController.swift file, the code to dismiss it is:
self.dismissViewController(self)
To pass the data, I have a class-independent tuple that I save data to. How do I add a completion handler to presentViewControllerAsSheet, and (optionally) is there a better way to pass the data between view controllers?
Setup: Xcode version 6.4, OS X 10.10.4
Delegation pattern is the easiest way for you.
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
protocol AddSoundViewControllerDelegate: class {
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect)
}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
weak var delegate: AddSoundViewControllerDelegate?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.delegate?.soundViewController(self, didAddSoundEffect: soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController, AddSoundViewControllerDelegate {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.delegate = self
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect) {
// This method is called only when new sound effect is added
}
}
Another way is to use closures:
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
var completionHandler: ((SoundEffect) -> ())?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.completionHandler?(soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.completionHandler = { [weak self] (soundEffect) -> () in
// Called when new sound effect is added
}
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
}
Or many other ways like sending notification, ... Whatever suits your needs. But delegation pattern or closures is the best way to go in this specific case.
I missed that your question is about NSViewController. This example is for iOS, but same pattern can be used on OS X without any issues.
The easiest way to detect sheet opening or closing is to use the Sheet Notifications:
class ViewController: NSViewController, NSWindowDelegate {
override func viewDidLoad(){
NSApplication.sharedApplication().windows.first?.delegate = self
}
func windowDidEndSheet(notification: NSNotification) {
}
func windowWillBeginSheet(notification: NSNotification) {
}
}

Catching double click in Cocoa OSX

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);
}
}

Resources