【问题标题】:Swift programmatically create function for button with a closureSwift 以编程方式为带有闭包的按钮创建函数
【发布时间】:2016-10-20 13:42:27
【问题描述】:

在 Swift 中,您可以像这样为按钮创建一个函数:

button.addTarget(self, action: #selector(buttonAction), forControlEvents: .TouchUpInside)

但是有办法让我做这样的事情:

button.whenButtonIsClicked({Insert code here})

这样我什至没有为按钮声明一个显式函数。我知道我可以使用按钮标签,但我更愿意这样做。

【问题讨论】:

  • 在我的回答中,我展示了如何使用 UIButton 的子类来做到这一点。

标签: swift button uibutton closures


【解决方案1】:

创建您自己的UIButton 子类来执行此操作:

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?()
    }
}

当您以编程方式创建按钮然后调用whenButtonIsClicked 来设置其功能时,将MyButton 替换为UIButton

您也可以在 Storyboard 中将其与 UIButtons 一起使用(只需将其类更改为 MyButton),然后在 viewDidLoad 中调用 whenButtonIsClicked

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

更强大的实现

认识到程序员可能想要处理的事件不仅仅是.touchUpInside,我编写了这个功能更强大的版本,它支持每个UIButton 的多个闭包和每个事件类型的多个闭包。

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

演示

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

【讨论】:

  • 如果你使用@IBAction修饰符而不是@objc,它会感觉更好的逻辑。
  • @holex, @IBAction 用于您要在 Interface Builder 中连接的功能。 @objc 用于选择器,所以即使@IBAction 包含@objc,我觉得@objc 在这里更合适,因为clicked 是一种内部方法,不打算通过IB 连接。
  • 对于这种排他性根本没有约定,它也只会被 IB 使用;但在这种情况下,@IBAction 修饰符会向读者表明,实际方法在这里是一个真正的事件处理程序,而不是用于通用回调方法(例如通知、计时器等)的通用 @objc 修饰符
  • @holex,我恭敬地建议我们同意在这一点上不同意。如果我将其设为@IBAction,我相信我会收到有关如何将该方法连接到情节提要中的按钮的问题。
  • 我们可以不同意,我对此很满意——但通常用@IBAction 标记方法表示该方法是一个事件处理程序,并且不会仅标记代码和 IB 之间的排他连接;您可以将它仅用于这样的目的,但这并不普遍,您不能期望@IBAction自动只使用这样的用例。
【解决方案2】:

这个会起作用的! 确保不要更改按钮的标签

extension UIButton {
private func actionHandleBlock(action:(()->())? = nil) {
    struct __ {
        var closure : (() -> Void)?
        typealias EmptyCallback = ()->()
        static var action : [EmptyCallback] = []
    }
    if action != nil {
       // __.action![(__.action?.count)!] = action!
        self.tag = (__.action.count)
        __.action.append(action!)
    } else {
        let exe = __.action[self.tag]
        exe()
    }
}

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

func addAction(forControlEvents control :UIControlEvents, ForAction action:@escaping () -> Void) {
    self.actionHandleBlock(action: action)
    self.addTarget(self, action: #selector(triggerActionHandleBlock), for: control)
}

}

【讨论】:

    【解决方案3】:
    let bt1 = UIButton(type: UIButtonType.InfoDark)
        bt1.frame = CGRectMake(130, 80, 40, 40)
    
        let bt2 = UIButton(type: UIButtonType.RoundedRect)
        bt2.frame = CGRectMake(80, 180, 150, 44)
        bt2.backgroundColor = UIColor.purpleColor()
        bt2.tintColor = UIColor.yellowColor()
        bt2.setTitle("Tap Me", forState: UIControlState.Normal)
        bt2.addTarget(self, action: "buttonTap", forControlEvents: UIControlEvents.TouchUpInside)
    
        let bt3 = UIButton(type: UIButtonType.RoundedRect)
        bt3.backgroundColor = UIColor.brownColor()
        bt3.tintColor = UIColor.redColor()
        bt3.setTitle("Tap Me", forState: UIControlState.Normal)
        bt3.frame = CGRectMake(80, 280, 150, 44)
        bt3.layer.masksToBounds = true
        bt3.layer.cornerRadius = 10
        bt3.layer.borderColor = UIColor.lightGrayColor().CGColor
    
        self.view.addSubview(bt1)
        self.view.addSubview(bt2)
        self.view.addSubview(bt3)
    
    }
    
    func buttonTap(button:UIButton)
    {
        let alert = UIAlertController(title: "Information", message: "UIButton Event", preferredStyle: UIAlertControllerStyle.Alert)
        let OKAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
        alert.addAction(OKAction)
    }
    

    【讨论】:

    • 为您的答案添加一些描述。
    【解决方案4】:

    您也可以只继承 UIView 并拥有一个类似于 vacawama 的闭包属性。

    var action: () -> ()? 
    

    然后重写touchesBegan 方法以在触摸按钮时调用该函数。使用这种方法虽然您不会获得从 UIBitton 开始的所有好处。

    【讨论】:

      【解决方案5】:

      如果您不想做任何“有问题的”事情(即,使用 Objective-C 的动态功能,或添加您自己的触摸处理程序等)并且纯粹在 Swift 中执行此操作,那么很遗憾,这是不可能的。

      任何时候你在 Swift 中看到#selector,编译器都会在后台调用objc_MsgSend。 Swift 不支持 Objective-C 的动态性。无论好坏,这意味着为了将这个选择器的使用替换为一个块,您可能需要执行一些黑魔法才能使其工作,并且您必须使用 Objective-C 构造来这样做。

      如果您对“令人讨厌的动态 Objective-C 东西”没有任何疑虑,您可以通过在 UIButton 上定义扩展来实现这一点,然后使用关联对象动态地将函数关联到对象。我将在此停止,但如果您想了解更多信息,NSHipster 有一个很棒的 overview 关于关联对象以及如何使用它们。

      【讨论】:

      • 我真的不想使用 Objective-C。幸运的是,事实证明有一个更好的解决方案可以更好地利用 IOS“风格”!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-10-14
      • 1970-01-01
      • 2017-12-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多