Overriden method not being called - ios

I have a problem that I can't override function.
My parent class below:
class A: UIViewController {
func parentalGate() {
let appearance = SCLAlertView.SCLAppearance(
kTitleFont: UIFont(name: "Futura", size: 20)!,
kTextFont: UIFont(name: "Futura", size: 14)!,
kButtonFont: UIFont(name: "Futura", size: 14)!,
showCloseButton: false
)
let alert = SCLAlertView(appearance: appearance)
let alert2 = SCLAlertView(appearance: appearance)
if (UserDefaults.standard.object(forKey: "language") as? String == "english") {
let txt = alert.addTextField("Enter third letter")
alert.addButton("Done") {
if ((txt.text == "P") || (txt.text == "p")) {
self.parentalGatefunctions()
} else {
alert2.addButton("Close", target: SCLAlertView(), selector: #selector(SCLAlertView.hideView))
alert2.showError("Incorrect letter entered", subTitle: "Try again")
}
}
alert.addButton("Close", target: SCLAlertView(), selector: #selector(SCLAlertView.hideView))
alert.showWarning("We need to be sure that You are an adult", subTitle: "Enter the third letter of word: HAPPY")
}
}
func parentalGatefunctions(){
// Parental gate functions are overriten in classes where they are used for
}
}
Child class below:
class B: A {
#IBAction func unlockAllCategoriesBtnPressed(_ sender: Any) {
A().parentalGate()
}
override func parentalGatefunctions() {
print ("Do something in class B")
}
}
Another class:
class C: A {
#IBAction func unlockAllCategoriesBtnPressed(_ sender: Any) {
A().parentalGate()
}
override func parentalGatefunctions() {
print ("Do something in class C")
}
}
All the time when parentalGate() method is called in classes B and C only empty parentalGateFunctions() is called without the overriten methods are not called in classes.
Am I doing something wrong? I just want to avoid repetitive code in my classes.
Thanks!

Your mistake is here.
#IBAction func unlockAllCategoriesBtnPressed(_ sender: Any) {
A().parentalGate()
}
Since you already have subclassed A, that method is available to you. So, just call the method in your subclass rather than the one in a new instance of the superclass! (which would obviously call the empty method)
#IBAction func unlockAllCategoriesBtnPressed(_ sender: Any) {
parentalGate()
}

Related

How to change language (localisation) within the app in swift 5?

I am trying to localise iOS app which is developed in Swift 5. I have done with all localisation things in code as well as in storyboard. But I am not sure how to change language within the app when i click on Language Button.
Is this possible to change app language within app? if yes How?
Please suggest best possible way to do same
I just did a similar implementation. Glad you asked and I saw this. Here is my implementation. You can modify.
enum Language: String, CaseIterable {
case english, german
var code: String {
switch self {
case .english: return "en"
case .german: return "de"
}
}
static var selected: Language {
set {
UserDefaults.standard.set([newValue.code], forKey: "AppleLanguages")
UserDefaults.standard.set(newValue.rawValue, forKey: "language")
}
get {
return Language(rawValue: UserDefaults.standard.string(forKey: "language") ?? "") ?? .english
}
}
static func switchLanguageBetweenEnglishAndGerman() {
selected = selected == .english ? .german : .english
}
}
Now you just need to call Language.selected == .german and reload the views.
To change localization throughout the app. For that, You need to follow the below step.
Create a Parent class of every UIViewController and define setupLocasitation method for further usage.
ParentViewController.swift
class ParentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func setupLocasitation(){
}
}
All other class of UIViewController should be a subclass of ParentViewController and override setupLocasitation method
ViewController1.swift
class ViewController1: ParentViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupLocasitation()
}
override func setupLocasitation() {
super.setupLocasitation()
print("Your localisation specifi code here...")
}
}
ViewController2.swift
class ViewController2: ParentViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupLocasitation()
}
override func setupLocasitation() {
super.setupLocasitation()
print("Your localisation specifi code here...")
}
}
ChangeLanguageVC.swift
You need to grab all instances of ParentViewController and force-fully call the setupLocasitation method.
class ChangeLanguageVC: ParentViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupLocasitation()
}
#IBAction func btnChangeLanguageTap(){
//Code for your language changes here...
let viewControllers = self.navigationController?.viewControllers ?? []
for vc in viewControllers{
if let parent = vc as? ParentViewController{
parent.setupLocasitation()
}
}
}
}
//
// LanguageExtensions.swift
// Flourish
//
// Created by Janko on 11/11/2020.
//
import Foundation
import UIKit
let languageKey = "languageKey"
var language : Int {
switch UserDefaults.standard.string(forKey: languageKey) {
case "en":
return 0
case "dutch":
return 1
default:
return 0
}
}
extension String {
func localizedLanguage()->String?{
var defaultLanguage = "en"
if let selectedLanguage = UserDefaults.standard.string(forKey: languageKey){
defaultLanguage = selectedLanguage
}
return NSLocalizedString(self, tableName: defaultLanguage, comment: "")
}
}
class LanguageLabel: UILabel{
required init?(coder: NSCoder) {
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: AppNotification.changeLanguage, object: nil)
}
#IBInspectable var localizedLanguage: String? {
didSet{
updateUI()
}
}
#objc func updateUI(){
if let string = localizedLanguage {
text = string.localizedLanguage()
}
}
}
class LanguageButton: UIButton{
required init?(coder: NSCoder) {
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: AppNotification.changeLanguage, object: nil)
}
#IBInspectable var localizedLanguage: String? {
didSet{
updateUI()
}
}
#objc func updateUI(){
if let string = localizedLanguage {
setTitle(string.localizedLanguage(), for: .normal)
}
}
}
struct AppNotification{
static let changeLanguage = Notification.Name("changeLanguage")
}
extension UIViewController{
func changeLanguage(){
let alert = UIAlertController(title: "Change Language", message: "Change it", preferredStyle: .alert)
let actionEnglish = UIAlertAction(title: "English", style: .default) { (action) in
UserDefaults.standard.setValue("en", forKey: languageKey)
NotificationCenter.default.post(name: AppNotification.changeLanguage , object: nil)
}
let actionMontenegrin = UIAlertAction(title: "Montenegrinish", style: .default) { (action) in
UserDefaults.standard.setValue("dutch", forKey: languageKey)
NotificationCenter.default.post(name: AppNotification.changeLanguage , object: nil)
}
alert.addAction(actionEnglish)
alert.addAction(actionMontenegrin)
present(alert, animated: true, completion: nil)
}
}

How to return bool from a button click in alert with Swift

override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject!) -> Bool {
let appearance = SCLAlertView.SCLAppearance(
showCloseButton: false,
showCircularIcon: false
)
let alertView = SCLAlertView(appearance: appearance)
alertView.addButton("First") {
return true
}
alertView.addButton("Second") {
return false
}
alertView.showSuccess("warning", subTitle: "something")
}
This code above wants a return true/false before the last curly bracket but if I do that I lose the button return.
So is there any way to archive this?
I use SCLAlertView for my alert (if someone knows how to do it with uialert I would like to see it)
Thanks in advance.
to add to the first comment, I did it like this with the use of a UISwitch in a header cell with a bool variable declared in the top of the class
#IBAction func `switchAction`(_ sender: UISwitch) {
selected = sender.isOn
if selected == false {
//do you stuff here change false to true if you need it that way
}
}
then in this method setup like this
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
var enable: Bool = false
if selected == false {
enable = true
} else {
enable = false
}
return enable
}
you can use block- closesure
var alertAction :((result:Bool)->())?
use it :
alertView.addButton("Second") {
alertAction?(false)
}
implement:
alertAction = {result in
// do s.t
}

UITextField delegate method not called

I am presenting an alert with UITextfield, but its delegate methods are not getting called. What wrong I might be doing. I am using the below code to show the alert with textfield.
func takePasscodeToEnableTouch(){
self.passcodeInputOperationType = .EnableTouchID
alertControllerPassCodeEntry = UIAlertController(title: "", message: "Enter Passcode to enable the Touch Id.", preferredStyle: UIAlertControllerStyle.Alert)
let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel) { (action) -> Void in
}
alertControllerPassCodeEntry!.addAction(cancelAction)
alertControllerPassCodeEntry!.addTextFieldWithConfigurationHandler { (txtField) -> Void in
txtField.placeholder = "Enter passcode"
txtField.delegate = self
txtField.tag = TextFieldTag.EnterPassCode
txtField.keyboardType = UIKeyboardType.NumbersAndPunctuation
txtField.accessibilityIdentifier = "PassCode"
txtField.secureTextEntry = true
txtField.addTarget(self, action:"textFieldDidChange:", forControlEvents: UIControlEvents.EditingChanged)
}
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController?.presentViewController(alertControllerPassCodeEntry!, animated: true, completion: nil )
}
And the textField delegate methods are :
func textFieldShouldBeginEditing(textField: UITextField) -> Bool
{
return true
}
func textFieldDidBeginEditing(textField: UITextField) // became first responder
{
}
func textFieldShouldEndEditing(textField: UITextField) -> Bool
{
return true
}
func textFieldDidEndEditing(textField: UITextField)
{
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
var isLimitExist: Bool
var accessIndentifier: String
if let str = textField.accessibilityIdentifier
{
accessIndentifier = str
}
else
{
accessIndentifier = ""
}
//checkFieldLimit function is used to check the limit of text and restrict
isLimitExist = UIUtils.checkFieldLimit(accessIndentifier, stringToMatch: textField.text!, rangeLength: range.length, stringLength: string.characters.count)
if !isLimitExist
{
return false
}
return true
}
Ok, so with information from the comments, everything seems clear now. To recap, you call the method showing the alert like this :
#IBAction func swtchTouchAction(sender: UISwitch) {
if sender.on {
let passCodeManager = PasscodeManager()
passCodeManager.delegate = self
passCodeManager.takePasscodeToEnableTouch()
} else {
let passCodeManager = PasscodeManager()
passCodeManager.delegate = self
passCodeManager.authenticatePasscodeToDisalbeTouch()
}
}
Now, you don't retain (meaning - assign to a strong property) the passCodeManager anywhere in here. This means, that at the end of this method this object gets destroyed (thanks to ARC - Automatic Reference Counting). One may think that it would get retained because you assigned it as a delegate of the text field, but delegates are weak proeprties 99.99% of time - this means that they don't bump the retain count of objects assigned to them.
To solve your immediate issue you should make a property in your class in which you have swtchTouchAction method and change your code like this :
var passCodeManager: PasscodeManager?
#IBAction func swtchTouchAction(sender: UISwitch) {
if sender.on {
self.passCodeManager = PasscodeManager()
self.passCodeManager?.delegate = self
self.passCodeManager?.takePasscodeToEnableTouch()
} else {
self.passCodeManager = PasscodeManager()
self.passCodeManager?.delegate = self
self.passCodeManager?.authenticatePasscodeToDisalbeTouch()
}
}
This will be enough to retain your passcode manager.
I'd also suggest you read up on how memory management is done in Swift.

In CosmicMind / Material SnackbarController, how do I start (show) the snackbar?

In CosmicMind / Material library, how do I start (show) the snackbar?
I have tried to prepare the snackbarController and then show it whenever the user clicks on a button.
As shown in their example:
private var undoButton: FlatButton!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
prepareSnackbar()
animateSnackbar()
}
#IBAction func loginBtnTapped(_ sender: AnyObject) {
sc?.show(vc: UIViewControlle, sender: Any)//doesn't show
more code... (which works)
}
private func prepareUndoButton() {
undoButton = FlatButton(title: "Undo", titleColor: Color.yellow.base)
undoButton.pulseAnimation = .backing
undoButton.titleLabel?.font = RobotoFont.regular(with: 14)
}
private func prepareSnackbar() {
guard let sc = snackbarController else {
return
}
sc.snackbar.text = "Reminder saved."
sc.snackbar.rightViews = [undoButton]
}
private func animateSnackbar() {
guard let sc = snackbarController else {
return
}
_ = sc.animate(snackbar: .visible, delay: 1)
_ = sc.animate(snackbar: .hidden, delay: 4)
}
So I did try it on an empty project and the snackbar still doesn't work. Could you please point out what I am doing wrong?
import Foundation
import UIKit
import Material
class MainViewController: UIViewController {
private var undoButton: FlatButton!
open override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Color.red.accent1
prepareUndoButton()
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//prepareSnackbar()
//animateSnackbar()
}
private func prepareUndoButton() {
undoButton = FlatButton(title: "Undo", titleColor: Color.yellow.base)
undoButton.pulseAnimation = .backing
undoButton.titleLabel?.font = RobotoFont.regular(with: 14)
}
private func prepareSnackbar() {
guard let sc = snackbarController else {
return
}
sc.snackbar.text = "Reminder saved."
sc.snackbar.rightViews = [undoButton]
}
private func animateSnackbar() {
guard let sc = snackbarController else {
return
}
_ = sc.animate(snackbar: .visible, delay: 1)
_ = sc.animate(snackbar: .hidden, delay: 4)
}
#IBAction func testBtn(_ sender: AnyObject) {
print("TEST TEST TEST")
prepareSnackbar()
animateSnackbar()
}
}
If you notice in the viewDidAppear function you have an animateSnackbar call, which calls the lines
_ = sc.animate(snackbar: .visible, delay: 1)
_ = sc.animate(snackbar: .hidden, delay: 4)
You can basically use the animateSnackbar function in your button handler, like so:
#IBAction func loginBtnTapped(_ sender: AnyObject) {
animateSnackbar()
}
That's it. There are two animations playing, one to show and one to hide the snackbar. Set the delay you would like to have them appear and hide automatically.
You will probably want to remove the animateSnackbar call from the viewDidAppear method, as that was put there for the example.

Adding a closure as target to a UIButton

I have a generic control class which needs to set the completion of the button depending on the view controller.Due to that setLeftButtonActionWithClosure function needs to take as parameter a closure which should be set as action to an unbutton.How would it be possible in Swift since we need to pass the function name as String to action: parameter.
func setLeftButtonActionWithClosure(completion: () -> Void)
{
self.leftButton.addTarget(<#target: AnyObject?#>, action: <#Selector#>, forControlEvents: <#UIControlEvents#>)
}
With iOS 14 Apple has finally added this feature to UIKit. However, someone might still want to use this extension because Apple's method signature is suboptimal.
iOS 14:
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: #escaping()->()) {
addAction(UIAction { (action: UIAction) in closure() }, for: controlEvents)
}
}
pre-iOS 14:
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: #escaping()->()) {
#objc class ClosureSleeve: NSObject {
let closure:()->()
init(_ closure: #escaping()->()) { self.closure = closure }
#objc func invoke() { closure() }
}
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, "\(UUID())", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.addAction {
print("Hello, Closure!")
}
or:
button.addAction(for: .touchUpInside) {
print("Hello, Closure!")
}
or if avoiding retain loops:
self.button.addAction(for: .touchUpInside) { [unowned self] in
self.doStuff()
}
(Extension is included here: https://github.com/aepryus/Acheron)
Also note, in theory .primaryActionTriggered could replace .touchUpInside, but it seems to be currently bugged in catalyst, so I'll leave it as is for now.
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")
})
You can effectively achieve this by subclassing UIButton:
class ActionButton: UIButton {
var touchDown: ((button: UIButton) -> ())?
var touchExit: ((button: UIButton) -> ())?
var touchUp: ((button: UIButton) -> ())?
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:)") }
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
func setupButton() {
//this is my most common setup, but you can customize to your liking
addTarget(self, action: #selector(touchDown(_:)), forControlEvents: [.TouchDown, .TouchDragEnter])
addTarget(self, action: #selector(touchExit(_:)), forControlEvents: [.TouchCancel, .TouchDragExit])
addTarget(self, action: #selector(touchUp(_:)), forControlEvents: [.TouchUpInside])
}
//actions
func touchDown(sender: UIButton) {
touchDown?(button: sender)
}
func touchExit(sender: UIButton) {
touchExit?(button: sender)
}
func touchUp(sender: UIButton) {
touchUp?(button: sender)
}
}
Use:
let button = ActionButton(frame: buttonRect)
button.touchDown = { button in
print("Touch Down")
}
button.touchExit = { button in
print("Touch Exit")
}
button.touchUp = { button in
print("Touch Up")
}
Similar solution to those already listed, but perhaps lighter weight and doesn't rely on randomness to generate unique ids:
class ClosureSleeve {
let closure: ()->()
init (_ closure: #escaping ()->()) {
self.closure = closure
}
#objc func invoke () {
closure()
}
}
extension UIControl {
func add (for controlEvents: UIControlEvents, _ closure: #escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, String(ObjectIdentifier(self).hashValue) + String(controlEvents.rawValue), sleeve,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.add(for: .touchUpInside) {
print("Hello, Closure!")
}
Or if avoiding retain loops:
button.add(for: .touchUpInside) { [unowned self] in
self.doStuff()
}
This is now possible on iOS 14. You can pass a UIAction, which has a handler closure, when you create the UIButton:
let action = UIAction(title: "") { action in
print("Button tapped!")
}
UIButton(type: .system, primaryAction: action)
Or shorter:
UIButton(type: .system, primaryAction: UIAction(title: "") { action in
print("Button tapped!")
})
This is basically Armanoide's answer, above, but with a couple slight changes that are useful for me:
the passed-in closure can take a UIButton argument, allowing you to pass in self
the functions and arguments are renamed in a way that, for me, clarifies what's going on, for instance by distinguishing a Swift closure from a UIButton action.
private func setOrTriggerClosure(closure:((button:UIButton) -> Void)? = nil) {
//struct to keep track of current closure
struct __ {
static var closure :((button:UIButton) -> Void)?
}
//if closure has been passed in, set the struct to use it
if closure != nil {
__.closure = closure
} else {
//otherwise trigger the closure
__. closure?(button: self)
}
}
#objc private func triggerActionClosure() {
self.setOrTriggerClosure()
}
func setActionTo(closure:(UIButton) -> Void, forEvents :UIControlEvents) {
self.setOrTriggerClosure(closure)
self.addTarget(self, action:
#selector(UIButton.triggerActionClosure),
forControlEvents: forEvents)
}
Much props to Armanoide though for some heavy-duty magic here.
#Armanoide solution is cool cause it uses trick with struct and static var inside it but it is not perfect if you're reusing one button a few times cause in this case action closure will always store the last handler.
I've fixed it for UIKitPlus library
import UIKit
extension UIControl {
private func actionHandler(action: (() -> Void)? = nil) {
struct Storage { static var actions: [Int: (() -> Void)] = [:] }
if let action = action {
Storage.actions[hashValue] = action
} else {
Storage.actions[hashValue]?()
}
}
#objc func triggerActionHandler() {
actionHandler()
}
func actionHandler(controlEvents control: UIControl.Event, forAction action: #escaping () -> Void) {
actionHandler(action: action)
addTarget(self, action: #selector(triggerActionHandler), for: control)
}
}
I put together a little extension for UIControl that will let you use closures for any action on any UIControl really easily.
You can find it here: https://gist.github.com/nathan-fiscaletti/8308f00ff364b72b6a6dec57c4b13d82
Here are some examples of it in practice:
Setting a Button Action
myButton.action(.touchUpInside, { (sender: UIControl) in
// do something
})
Detecting a Switch changing Values
mySwitch.action(.valueChanged, { (sender: UIControl) in
print("Switch State:", mySwitch.isOn)
})
Here is a generic swift 5 approach. It has a sender inside action block and eliminates adding action for same event twice
import UIKit
protocol Actionable {
associatedtype T = Self
func addAction(for controlEvent: UIControl.Event, action: ((T) -> Void)?)
}
private class ClosureSleeve<T> {
let closure: ((T) -> Void)?
let sender: T
init (sender: T, _ closure: ((T) -> Void)?) {
self.closure = closure
self.sender = sender
}
#objc func invoke() {
closure?(sender)
}
}
extension Actionable where Self: UIControl {
func addAction(for controlEvent: UIControl.Event, action: ((Self) -> Void)?) {
let previousSleeve = objc_getAssociatedObject(self, String(controlEvent.rawValue))
objc_removeAssociatedObjects(previousSleeve as Any)
removeTarget(previousSleeve, action: nil, for: controlEvent)
let sleeve = ClosureSleeve(sender: self, action)
addTarget(sleeve, action: #selector(ClosureSleeve<Self>.invoke), for: controlEvent)
objc_setAssociatedObject(self, String(controlEvent.rawValue), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
extension UIControl: Actionable {}
Here is a fun variant to the answer by aepryus. My version uses Combine's Cancellable protocol to:
Support removing the registered closure.
Handle memory management thus avoiding the need to use objc_setAssociatedObject.
// Swift 5
import Combine
import UIKit
class BlockObject: NSObject {
let block: () -> Void
init(block: #escaping () -> Void) {
self.block = block
}
#objc dynamic func execute() {
block()
}
}
extension UIControl {
func addHandler(
for controlEvents: UIControl.Event,
block: #escaping () -> Void)
-> Cancellable
{
let blockObject = BlockObject(block: block)
addTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)
return AnyCancellable {
self.removeTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)
}
}
}
Usage:
let button = UIButton(type: .system)
// Add the handler
let cancellable = button.addHandler(for: .touchUpInside) {
print("Button pressed!")
}
// Remove the handler
cancellable.cancel()
Don't forget to store a reference to the Cancellable or else the handler will be immediately unregistered.
I change a little extension for UIControl that was posted #Nathan F.
here
I used objc_setAssociatedObject and objc_getAssociatedObject to get/set closure and i removed global static variable with all created buttons's keys.
So now event stored for each instance and released after dealloc
extension UIControl {
typealias Handlers = [UInt:((UIControl) -> Void)]
private enum AssociatedKey {
static var actionHandlers = "UIControl.actionHandlers"
}
/**
* A map of closures, mapped as [ event : action ] .
*/
private var actionHandlers: Handlers {
get {
return objc_getAssociatedObject(self, &AssociatedKey.actionHandlers) as? Handlers ?? [:]
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKey.actionHandlers, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
You can find it here: https://gist.github.com/desyatov/6ed83de58ca1146d85fedab461a69b12
Here are some example:
myButton.action(.touchUpInside, { (sender: UIControl) in
// do something
})
Swift
After trying all the solutions, this one worked for me for all cases, even when the button in reusable table view cell
import UIKit
typealias UIButtonTargetClosure = UIButton -> ()
class ClosureWrapper: NSObject {
let closure: UIButtonTargetClosure
init(_ closure: UIButtonTargetClosure) {
self.closure = closure
}
}
extension UIButton {
private struct AssociatedKeys {
static var targetClosure = "targetClosure"
}
private var targetClosure: UIButtonTargetClosure? {
get {
guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? ClosureWrapper else { return nil }
return closureWrapper.closure
}
set(newValue) {
guard let newValue = newValue else { return }
objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, ClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addTargetClosure(closure: UIButtonTargetClosure) {
targetClosure = closure
addTarget(self, action: #selector(UIButton.closureAction), forControlEvents: .TouchUpInside)
}
func closureAction() {
guard let targetClosure = targetClosure else { return }
targetClosure(self)
}
}
And then you call it like this:
loginButton.addTargetClosure { _ in
// login logics
}
Resource:
https://medium.com/#jackywangdeveloper/swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455
My solution.
typealias UIAction = () -> Void;
class Button: UIButton {
public var touchUp :UIAction? {
didSet {
self.setup()
}
}
func setup() -> Void {
self.addTarget(self, action: #selector(touchInside), for: .touchUpInside)
}
#objc private func touchInside() -> Void {
self.touchUp!()
}
}
Swift 4.2 for UIControl and UIGestureRecognizer, and and remove targets through swift extension stored property paradigm.
Wrapper class for the selector
class Target {
private let t: () -> ()
init(target t: #escaping () -> ()) { self.t = t }
#objc private func s() { t() }
public var action: Selector {
return #selector(s)
}
}
Protocols with associatedtypes so we can hide hide the objc_ code
protocol PropertyProvider {
associatedtype PropertyType: Any
static var property: PropertyType { get set }
}
protocol ExtensionPropertyStorable: class {
associatedtype Property: PropertyProvider
}
Extension to make the property default and available
extension ExtensionPropertyStorable {
typealias Storable = Property.PropertyType
var property: Storable {
get { return objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) as? Storable ?? Property.property }
set { return objc_setAssociatedObject(self, String(describing: type(of: Storable.self)), newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}
Let us apply the magic
extension UIControl: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property = [String: Target]()
}
func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: #escaping () ->()) {
let key = String(describing: controlEvent)
let target = Target(target: target)
addTarget(target, action: target.action, for: controlEvent)
property[key] = target
}
func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
let key = String(describing: controlEvent)
let target = property[key]
removeTarget(target, action: target?.action, for: controlEvent)
property[key] = nil
}
}
And to the gestures
extension UIGestureRecognizer: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property: Target?
}
func addTarget(target: #escaping () -> ()) {
let target = Target(target: target)
addTarget(target, action: target.action)
property = target
}
func removeTarget() {
let target = property
removeTarget(target, action: target?.action)
property = nil
}
}
Example usage:
button.addTarget {
print("touch up inside")
}
button.addTarget { [weak self] in
print("this will only happen once")
self?.button.removeTarget()
}
button.addTarget(for: .touchDown) {
print("touch down")
}
slider.addTarget(for: .valueChanged) {
print("value changed")
}
textView.addTarget(for: .allEditingEvents) { [weak self] in
self?.editingEvent()
}
gesture.addTarget { [weak self] in
self?.gestureEvent()
self?.otherGestureEvent()
self?.gesture.removeTarget()
}
Here's a nice framework for doing this: HandlersKit. The biggest advantage is that you can access to the sender inside the closure without typecasting or optional unwrapping.
Example for UIButton:
import HandlersKit
let button = MyActivityIndicatorButton()
button.onTap { (sender: MyActivityIndicatorButton) in
sender.showActivityIndicator()
}
Example for UISwitch:
let switchView = UISwitch(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 50.0))
switchView.onChange { isOn in
print("SwitchView is: \(isOn)")
}
I have started to use Armanoide's answer disregarding the fact that it'll be overridden by the second assignment, mainly because at first I needed it somewhere specific which it didn't matter much. But it started to fall apart.
I've came up with a new implementation using AssicatedObjects which doesn't have this limitation, I think has a smarter syntax, but it's not a complete solution:
Here it is:
typealias ButtonAction = () -> Void
fileprivate struct AssociatedKeys {
static var touchUp = "touchUp"
}
fileprivate class ClosureWrapper {
var closure: ButtonAction?
init(_ closure: ButtonAction?) {
self.closure = closure
}
}
extension UIControl {
#objc private func performTouchUp() {
guard let action = touchUp else {
return
}
action()
}
var touchUp: ButtonAction? {
get {
let closure = objc_getAssociatedObject(self, &AssociatedKeys.touchUp)
guard let action = closure as? ClosureWrapper else{
return nil
}
return action.closure
}
set {
if let action = newValue {
let closure = ClosureWrapper(action)
objc_setAssociatedObject(
self,
&AssociatedKeys.touchUp,
closure as ClosureWrapper,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
self.addTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
} else {
self.removeTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
}
}
}
}
As you can see, I've decided to make a dedicated case for touchUpInside. I know controls have more events than this one, but who are we kidding? do we need actions for every one of them?! It's much simpler this way.
Usage example:
okBtn.touchUp = {
print("OK")
}
In any case, if you want to extend this answer you can either make a Set of actions for all the event types, or add more event's properties for other events, it's relatively straightforward.
Cheers,
M.
One more optimisation (useful if you use it in many places and don't want to duplicate call to objc_setAssociatedObject). It allows us to not worry about a dirty part of objc_setAssociatedObject and keeps it inside ClosureSleeve's constructor:
class ClosureSleeve {
let closure: () -> Void
init(
for object: AnyObject,
_ closure: #escaping () -> Void
) {
self.closure = closure
objc_setAssociatedObject(
object,
String(format: "[%d]", arc4random()),
self,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
)
}
#objc func invoke () {
closure()
}
}
So your extension will look a tiny bit cleaner:
extension UIControl {
func add(
for controlEvents: UIControlEvents,
_ closure: #escaping ()->()
) {
let sleeve = ClosureSleeve(
for: self,
closure
)
addTarget(
sleeve,
action: #selector(ClosureSleeve.invoke),
for: controlEvents
)
}
}
class ViewController : UIViewController {
var aButton: UIButton!
var assignedClosure: (() -> Void)? = nil
override func loadView() {
let view = UIView()
view.backgroundColor = .white
aButton = UIButton()
aButton.frame = CGRect(x: 95, y: 200, width: 200, height: 20)
aButton.backgroundColor = UIColor.red
aButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)
view.addSubview(aButton)
self.view = view
}
func fizzleButtonOn(events: UIControlEvents, with: #escaping (() -> Void)) {
assignedClosure = with
aButton.removeTarget(self, action: .buttonTapped, for: .allEvents)
aButton.addTarget(self, action: .buttonTapped, for: events)
}
#objc func buttonTapped() {
guard let closure = assignedClosure else {
debugPrint("original tap")
return
}
closure()
}
}
fileprivate extension Selector {
static let buttonTapped = #selector(ViewController.buttonTapped)
}
Then at some point in your app's lifecycle, you'll mutate the instances' closure. Here's an example
fizzleButtonOn(events: .touchUpInside, with: { debugPrint("a new tap action") })
Below extension is for add tap gesture to UIView's level, which will work on anything that based of UIView.
Note: I found this solution years ago on StackOverflow too, but now I can't seem to find the original source.
extension UIView {
// In order to create computed properties for extensions, we need a key to
// store and access the stored property
fileprivate struct AssociatedObjectKeys {
static var tapGestureRecognizer = "MediaViewerAssociatedObjectKey_mediaViewer"
}
fileprivate typealias Action = (() -> Void)?
// Set our computed property type to a closure
fileprivate var tapGestureRecognizerAction: Action? {
set {
if let newValue = newValue {
// Computed properties get stored as associated objects
objc_setAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
get {
let tapGestureRecognizerActionInstance = objc_getAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer) as? Action
return tapGestureRecognizerActionInstance
}
}
// This is the meat of the sauce, here we create the tap gesture recognizer and
// store the closure the user passed to us in the associated object we declared above
public func addTapGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.tapGestureRecognizerAction = action
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
tapGestureRecognizer.cancelsTouchesInView = false
self.addGestureRecognizer(tapGestureRecognizer)
}
// Every time the user taps on the UIImageView, this function gets called,
// which triggers the closure we stored
#objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
if let action = self.tapGestureRecognizerAction {
action?()
} else {
print("no action")
}
}
}
Usage example:
let button = UIButton()
button.addTapGestureRecognizer {
print("tapped")
}
let label = UILabel()
label.addTapGestureRecognizer {
print("label tapped")
}

Resources