Search code examples
swifttoast

Argument of '#selector' cannot refer to global function


I have made a Toast function as below

CommonFunctions.swift

import UIKit

func showToast(toastTitle: String, toastText: String, type : ToastType, position : ToastPosition = .center, style : ToastStyle = .defaultToast){
    
    if (style == .defaultToast) {
        
        if let foundView = topVC?.view.viewWithTag(toastTagNumber) {
            foundView.removeFromSuperview()
        }
        
        toastTimer?.invalidate()
        toastTimer = nil
        
        if let currentView : UIView = topVC?.view {
            
            let toastView : UIView = UIView()
            currentView.addSubview(toastView)
            toastView.translatesAutoresizingMaskIntoConstraints = false
            
            toastView.tag = toastTagNumber
            toastView.backgroundColor = AppColors.topBarBackgroundColor
            toastView.fillHorizontally(padding: 10*iPhoneFactorX)
            toastView.Bottom == currentView.Bottom - (bottomBarInnerHeight + safeAreaBottomHeight + (10*iPhoneFactorX))
            toastView.addRadius(radius: 15*iPhoneFactorX)
            
            let toast_headerLabel : UILabel = UILabel()
            toastView.addSubview(toast_headerLabel)
            toast_headerLabel.translatesAutoresizingMaskIntoConstraints = false
            toast_headerLabel.Top == toastView.Top + (10*iPhoneFactorX)
            toast_headerLabel.fillHorizontally(padding: 5*iPhoneFactorX)
            toast_headerLabel.adjustDefaultLabel(fontColor: AppColors.whiteColor_FFFFFF, fontSize: 15, fontType: .bold)
            toast_headerLabel.text = toastTitle
            toast_headerLabel.isHidden = toastTitle.isEmpty
            toast_headerLabel.textAlignment = .center
            
            let toast_textLabel : UILabel = UILabel()
            toastView.addSubview(toast_textLabel)
            toast_textLabel.translatesAutoresizingMaskIntoConstraints = false
            toast_textLabel.Top == toast_headerLabel.Bottom + (10*iPhoneFactorX)
            toast_textLabel.fillHorizontally(padding: 5*iPhoneFactorX)
            toast_textLabel.adjustDefaultLabel(fontColor: AppColors.whiteColor_FFFFFF, fontSize: 14)
            toast_textLabel.text = toastText
            toast_textLabel.textAlignment = .center
            
            toastView.Bottom == toast_textLabel.Bottom - (10*iPhoneFactorX)
            
            toastTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(toastDismissDuration), repeats: false) { (Timer) in
                hideToast()
            }
            
            let toast_button : UIButton = UIButton(type: .custom)
            toastView.addSubview(toast_button)
            toast_button.translatesAutoresizingMaskIntoConstraints = false
            toast_button.setTitle("", for: .normal)
            toast_button.fillContainer()
            toast_button.backgroundColor = .clear
            // problem comes here
            toast_button.addTarget(currentView, action: #selector(clickedDefaultToast), for: .touchUpInside)
            toastView.addSubview(toast_button)
            
        }
        
    }
}

func clickedDefaultToast() {
    print("fapa=clickedDefaultToast==0001")
    
}

Calling it as below.

showToast(toastTitle: "error", toastText: "toast text comes here", type: .error)

Toast is showing and goes away in 3 seconds which is not an issue.

I want to remove Toast if anyone click on it. So I add a button. However issue coming when adding target on it.

Error I get is as below.

Argument of '#selector' cannot refer to global function 'clickedDefaultToast()'

If I add @objc for function, I get error as

@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

How can I achieve this to dismiss when clicked?


Solution

  • Targets respond to selectors. It doesn't make sense to pass a selector of clickedDefaultToast, when the target currentView doesn't respond to it, regardless of whether Swift allows you to do this.

    Selectors are usually methods declared in a class (and instances of that class would respond to the selector), and so Swift has the #selector(...) syntax to let you get a Selector from an instance method.

    If you just want to call a global function when the button is clicked, addAction instead of addTarget. This allows you to just pass a Swift closure instead of a selector.

    toast_button.addAction(UIAction { clickedDefaultToast() }, for: .touchUpInside)
    

    Otherwise, you would need to change your design to use target-action pairs. The usage could be something like:

    // declare a Toast property at the class level
    let toast = Toast()
    
    // in some method...
    toast.showToast(...)
    

    You'd move showToast and clickedDefaultToast into a new Toast class. You would use self as the target of the button, and self can respond to the clickedDefaultToast selector.

    In my opinion, this design is better because you can also store the currently displayed toast UIView in the Toast instance, so dismissing it is trivial - just remove the UIView from its superview. With your current design, finding the currently displayed toast would be rather hard.