Swift Programmatically Create Function for Button with a Closure

Swift programmatically create function for button with a closure

Create your own UIButton subclass to do this:

class MyButton: UIButton {
var action: (() -> Void)?

func whenButtonIsClicked(action: @escaping () -> Void) {
self.action = action
self.addTarget(self, action: #selector(MyButton.clicked), for: .touchUpInside)
}

// Button Event Handler:
// I have not marked this as @IBAction because it is not intended to
// be hooked up to Interface Builder
@objc func clicked() {
action?()
}
}

Substitute MyButton for UIButton when you create buttons programmatically and then call whenButtonIsClicked to set up its functionality.

You can also use this with UIButtons in a Storyboard (just change their class to MyButton) and then call whenButtonIsClicked in viewDidLoad.

@IBOutlet weak var theButton: MyButton!

var count = 0

override func viewDidLoad() {
super.viewDidLoad()

// be sure to declare [unowned self] if you access
// properties or methods of the class so that you
// don't create a strong reference cycle
theButton.whenButtonIsClicked { [unowned self] in
self.count += 1
print("count = \(self.count)")
}

A much more capable implementation

Recognizing the fact that programmers might want to handle more events than just .touchUpInside, I wrote this more capable version which supports multiple closures per UIButton and multiple closures per event type.

class ClosureButton: UIButton {
private var actions = [UInt : [((UIControl.Event) -> Void)]]()

private let funcDict: [UInt : Selector] = [
UIControl.Event.touchCancel.rawValue: #selector(eventTouchCancel),
UIControl.Event.touchDown.rawValue: #selector(eventTouchDown),
UIControl.Event.touchDownRepeat.rawValue: #selector(eventTouchDownRepeat),
UIControl.Event.touchUpInside.rawValue: #selector(eventTouchUpInside),
UIControl.Event.touchUpOutside.rawValue: #selector(eventTouchUpOutside),
UIControl.Event.touchDragEnter.rawValue: #selector(eventTouchDragEnter),
UIControl.Event.touchDragExit.rawValue: #selector(eventTouchDragExit),
UIControl.Event.touchDragInside.rawValue: #selector(eventTouchDragInside),
UIControl.Event.touchDragOutside.rawValue: #selector(eventTouchDragOutside)
]

func handle(events: [UIControl.Event], action: @escaping (UIControl.Event) -> Void) {
for event in events {
if var closures = actions[event.rawValue] {
closures.append(action)
actions[event.rawValue] = closures
} else {
guard let sel = funcDict[event.rawValue] else { continue }
self.addTarget(self, action: sel, for: event)
actions[event.rawValue] = [action]
}
}
}

private func callActions(for event: UIControl.Event) {
guard let actions = actions[event.rawValue] else { return }
for action in actions {
action(event)
}
}

@objc private func eventTouchCancel() { callActions(for: .touchCancel) }
@objc private func eventTouchDown() { callActions(for: .touchDown) }
@objc private func eventTouchDownRepeat() { callActions(for: .touchDownRepeat) }
@objc private func eventTouchUpInside() { callActions(for: .touchUpInside) }
@objc private func eventTouchUpOutside() { callActions(for: .touchUpOutside) }
@objc private func eventTouchDragEnter() { callActions(for: .touchDragEnter) }
@objc private func eventTouchDragExit() { callActions(for: .touchDragExit) }
@objc private func eventTouchDragInside() { callActions(for: .touchDragInside) }
@objc private func eventTouchDragOutside() { callActions(for: .touchDragOutside) }
}

Demo

class ViewController: UIViewController {

var count = 0

override func viewDidLoad() {
super.viewDidLoad()

let button = ClosureButton(frame: CGRect(x: 50, y: 100, width: 60, height: 40))
button.setTitle("press me", for: .normal)
button.setTitleColor(.blue, for: .normal)

// Demonstration of handling a single UIControl.Event type.
// If your closure accesses self, be sure to declare [unowned self]
// to prevent a strong reference cycle
button.handle(events: [.touchUpInside]) { [unowned self] _ in
self.count += 1
print("count = \(self.count)")
}

// Define a second handler for touchUpInside:
button.handle(events: [.touchUpInside]) { _ in
print("I'll be called on touchUpInside too")
}

let manyEvents: [UIControl.Event] = [.touchCancel, .touchUpInside, .touchDown, .touchDownRepeat, .touchUpOutside, .touchDragEnter,
.touchDragExit, .touchDragInside, .touchDragOutside]

// Demonstration of handling multiple events
button.handle(events: manyEvents) { event in
switch event {
case .touchCancel:
print("touchCancel")
case .touchDown:
print("touchDown")
case .touchDownRepeat:
print("touchDownRepeat")
case .touchUpInside:
print("touchUpInside")
case .touchUpOutside:
print("touchUpOutside")
case .touchDragEnter:
print("touchDragEnter")
case .touchDragExit:
print("touchDragExit")
case .touchDragInside:
print("touchDragInside")
case .touchDragOutside:
print("touchDragOutside")
default:
break
}
}

self.view.addSubview(button)
}
}

Adding a closure as target to a UIButton

Do Not Use This Answer, See Note Below

NOTE:
like @EthanHuang said
"This solution doesn't work if you have more than two instances. All actions will be overwrite by the last assignment."

Keep in mind this when you develop, i will post another solution soon.

If you want to add a closure as target to a UIButton, you must add a function to UIButton class by using extension

Swift 5

import UIKit    
extension UIButton {
private func actionHandler(action:(() -> Void)? = nil) {
struct __ { static var action :(() -> Void)? }
if action != nil { __.action = action }
else { __.action?() }
}
@objc private func triggerActionHandler() {
self.actionHandler()
}
func actionHandler(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
self.actionHandler(action: action)
self.addTarget(self, action: #selector(triggerActionHandler), for: control)
}
}

Older

import UIKit

extension UIButton {
private func actionHandleBlock(action:(() -> Void)? = nil) {
struct __ {
static var action :(() -> Void)?
}
if action != nil {
__.action = action
} else {
__.action?()
}
}

@objc private func triggerActionHandleBlock() {
self.actionHandleBlock()
}

func actionHandle(controlEvents control :UIControlEvents, ForAction action:() -> Void) {
self.actionHandleBlock(action)
self.addTarget(self, action: "triggerActionHandleBlock", forControlEvents: control)
}
}

and the call:

 let button = UIButton()
button.actionHandle(controlEvents: .touchUpInside,
ForAction:{() -> Void in
print("Touch")
})

Make a UIButton programmatically in Swift

You're just missing the colon at the end of the selector name. Since pressed takes a parameter the colon must be there. Also your pressed function shouldn't be nested inside viewDidLoad.

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let myFirstLabel = UILabel()
let myFirstButton = UIButton()
myFirstLabel.text = "I made a label on the screen #toogood4you"
myFirstLabel.font = UIFont(name: "MarkerFelt-Thin", size: 45)
myFirstLabel.textColor = .red
myFirstLabel.textAlignment = .center
myFirstLabel.numberOfLines = 5
myFirstLabel.frame = CGRect(x: 15, y: 54, width: 300, height: 500)
myFirstButton.setTitle("✸", for: .normal)
myFirstButton.setTitleColor(.blue, for: .normal)
myFirstButton.frame = CGRect(x: 15, y: -50, width: 300, height: 500)
myFirstButton.addTarget(self, action: #selector(pressed), for: .touchUpInside)
}

@objc func pressed() {
var alertView = UIAlertView()
alertView.addButtonWithTitle("Ok")
alertView.title = "title"
alertView.message = "message"
alertView.show()
}

EDIT: Updated to reflect best practices in Swift 2.2. #selector() should be used rather than a literal string which is deprecated.

Execute code when pressing button created programmatically in function

What you are looking for are closures:

  var some_code_block = {
print("my awesome code block!")
}

Put this at a higher / global scope, then you can run / change the block at will, simply by adding '()' at the end of the closure (variable name)

 func addCode() {
// Wont execute:
some_code_block = { print("new code inserted") }
}

func buttonPressed() {
// Executes:
some_code_block()
}

Personally, I use arrays / dictionaries to store / update code blocks on the fly.. then you can iterate through / match indices/keys to get the code block you want.

https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html

If your problem is actually making different buttons, then you can store your newly created instance of a UI button into an array or dictionary

[UIButton]

An array / dictionary of buttons, matching with an array / dictionary of code blocks can certainly work... You just have to make sure they are in the right scope / and synchronize indices / keys.

Or, you could create a class / struct that holds a UIButton instance, and a code block, then put the class / struct into an array / dictionary

// Make your own initializer for your purposes
struct ButtonStuff {
var button: UIButton?
var code_block: ()?
}

// Make empty array that holds type ButtonStuff (global / outer scope)
var buttons: [ButtonStuff] = []

func addButton(pram1:Pram1, pram2:Pram2) {
// Make a new button (called locally inside of a func)
var new_button = ButtonStuff()

// Put stuff in here...
// new_button.button = pram1
// new_button.code_block = pram2

// Add our locally made new_button to the Global buttons array
buttons.append(new_buttons)
}

NOTE: The pram1, pram2 can be whatever you want, but to store to the code block (without running it) you want type (). If you want to automatically run the code block, you would use type ()->()

There are many ways of doing the above (you don't have to use optionals); just a quick example. You would then just have to make some logic / algo to find the right button, display it, then call the code block.

If you have any questions, need me to edit my answer, please leave a message in the comments with @fluidity

Custom anonymous closure navigation back button inside of UiView()

First add a callback function in the CustomView. Then call this callback closure from goToBack() method.

class CustomView: UIView {

var backButtonTapped: (() -> Void)?

lazy var backButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(systemName: "chevron.left")
button.setImage(image, for: UIControl.State())
button.tintColor = .white
button.isHidden = true
button.addTarget(self, action: #selector(goToBack), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()

@objc func goToBack() {
backButtonTapped?()
}
}

In UIViewController where you initialise this CustomView, give the action of the closure.

let view = CustomView()
view.backButtonTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}

Adding a target to a button programmatically throws an error unrecognized selector sent to class

It's actually all a matter of timing. You are declaring an instance property along with a define-and-call function that initializes that property, and you proceed to say button.addTarget(self... in that function. The question then is: when will that function's code run? Incredibly, that matters a lot, so much so that it actually changes the meaning of self.

When you say let to make this declaration, the code runs at a time when the instance does not yet exist. The instance is exactly what we are in the process of forming at that moment; it isn't "cooked" yet. In fact, unbeknownst to you, self at that moment means the UIButton class! Therefore the resulting target-action is invalid; you have no class method postComment; it's an instance method (rightly). And so later on when you tap the button and we try to say postComment to the class, we crash. "Unrecognized selector sent to class", exactly as you say.

On the other hand, when you say lazy var, the code is not called until after the instance self does exist, self means the instance, and all is well.

It's terribly confusing to beginners, and to not-so-beginners; even after pointing out the problem, I've proceeded to make exactly the same mistake myself. In my opinion the compiler should catch this and stop you, but it doesn't. I've filed a bug: http://bugs.swift.org/browse/SR-4865



Related Topics



Leave a reply



Submit