UIControl 錯誤處理介紹

通常,當使用 UIControlUIButton 時,我們新增 selector 作為回撥動作,用於在按鈕或控制元件上發生事件時,例如使用者按下按鈕或觸控控制元件。

例如,我們將執行以下操作:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
        button.addTarget(self, action: #selector(self.onButtonPress(_:)), for: .touchUpInside)
        self.view.addSubview(button)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func onButtonPress(_ button: UIButton!) {
        print("PRESSED")
    }
}

說到 selector,編譯器只需要知道它存在。這可以通過 protocol 來完成而不是實現。

例如,以下內容會使你的應用程式崩潰:

import UIKit

@objc
protocol ButtonEvent {
    @objc optional func onButtonPress(_ button: UIButton)
}

class ViewController: UIViewController, ButtonEvent {
    @IBOutlet weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
        button.addTarget(self, action: #selector(ButtonEvent.onButtonPress(_:)), for: .touchUpInside)
        self.view.addSubview(button)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

這是因為你的應用程式沒有實現 onButtonPress 功能。

現在如果你可以在按鈕初始化的同時完成所有這些操作呢?如果你不必指定回撥並且可以指定可以隨時新增和刪除的塊,該怎麼辦?為什麼要擔心實現選擇器?

import Foundation
import UIKit

protocol RemovableTarget {
    func enable();
    func disable();
}

extension UIControl {
    func addEventHandler(event: UIControlEvents, runnable: (control: UIControl) -> Void) -> RemovableTarget {
        
        class Target : RemovableTarget {
            private var event: UIControlEvents
            private weak var control: UIControl?
            private var runnable: (control: UIControl) -> Void
            
            private init(event: UIControlEvents, control: UIControl, runnable: (control: UIControl) -> Void) {
                self.event = event
                self.control = control
                self.runnable = runnable
            }
            
            @objc
            private func run(_ control: UIControl) {
                runnable(control: control)
            }
            
            private func enable() {
                control?.addTarget(self, action: #selector(Target.run(_:)), for: event)
                objc_setAssociatedObject(self, unsafeAddress(of: self), self, .OBJC_ASSOCIATION_RETAIN)
            }
            
            private func disable() {
                control?.removeTarget(self, action: #selector(Target.run(_:)), for: self.event)
                objc_setAssociatedObject(self, unsafeAddress(of: self), nil, .OBJC_ASSOCIATION_ASSIGN)
            }
        }
        
        let target = Target(event: event, control: self, runnable: runnable)
        target.enable()
        return target
    }
}

以上是 UIControl 的簡單擴充套件。它新增了一個內部私有類,它具有一個回撥 func run(_ control: UIControl),用作事件的動作。

接下來我們使用 object association 新增和刪除目標,因為它不會被 UIControl 保留。

事件處理函式返回 Protocol 以隱藏 Target 類的內部工作,但也允許你在任何給定時間 enabledisable 目標。

用法示例:

import Foundation
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        

        //Create a button.
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
        
        //Add an event action block/listener -- Handles Button Press.
        let target = button.addEventHandler(event: .touchUpInside) { (control) in
            print("Pressed")
        }
        

        self.view.addSubview(button)
        
        //Example of enabling/disabling the listener/event-action-block.
        DispatchQueue.main.after(when: DispatchTime.now() + 5) {
            target.disable() //Disable the listener.
            
            DispatchQueue.main.after(when: DispatchTime.now() + 5) {
                target.enable() //Enable the listener.
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}