如何将侦听器添加到 UIControl 的子类

How to add a listener to a subclass of UIControl

为了自定义原生 iOS 日期选择器(UIDatePicker)的外观,我在 GitHub 上找到了一段代码。 (参考:https://github.com/xiaosao6/CCDatePicker

现在我想为选择器添加一个事件侦听器。 selected 日期将更新为 UILabel。

我怎样才能像 UIDatePicker 那样做:

picker.addTarget(self, action: #selector(timeChanged(ccPicker:)), for: .valueChanged)
@objc func timeChanged(datePicker: UIDatePicker) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = NumberUtils.format["time"]
        label.text = dateFormatter.string(from: datePicker.date)
    }

这是我的做法:

let picker = CCDatePicker.init(minDate: minDate, maxDate: maxDate)!
            picker.frame = CGRect(x: 0, y: 0, width: pickerContainer.bounds.width, height: pickerContainer.bounds.height)
            picker.isUserInteractionEnabled = true
            picker.addTarget(self, action: #selector(timeChanged(ccPicker:)), for: .valueChanged)
pickerContainer.addSubview(picker)
@objc func timeChanged(ccPicker: CCDatePicker) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = NumberUtils.format["date"]
        label.text = ccPicker.date
    }

但是,当我在 CCDatePicker 实例中 select 一个新行时,此函数 timeChanged(ccPicker: CCDatePicker) 从未被调用。

如何将 .valueChanged 的事件侦听器添加到我的自定义日期选择器?

//
//  CCDatePicker.swift
//  test
//
//  Created by RenYuan on 5/11/19.
//  Copyright © 2019 RenYuan. All rights reserved.
//

import UIKit


protocol CCDatePickerDelegate: class {
    func didSelectDate(at picker: CCDatePicker)
}

protocol CCDatePickerDataSource: class {
    func datepicker(_ picker: CCDatePicker, numberOfRowsInComponent component: Int) -> Int
    func datepicker(_ picker: CCDatePicker, intValueForRow row: Int, forComponent component: Int) -> Int
}


class CCDatePicker: UIControl {

    weak var delegate: CCDatePickerDelegate?
    weak var dataSource: CCDatePickerDataSource?

    let pickerDataSize = 120_000  // for loop loading

    /// 单位字符
    //    var unitName: (year: String?, month: String?, day: String?) = ("年", "月", "日")
    var unitName: (year: String?, month: String?, day: String?) = ("", "", "")

    /// 标题字体
    var titleFont  = UIFont.systemFont(ofSize: 20)

    /// 标题颜色
    var titleColor = UIColor.white

    /// 中心行高
    var rowHeight: CGFloat = 45

    /// 分割线颜色
    var separatorColor = UIColor.black {
        didSet{
            setNeedsLayout()
            layoutIfNeeded()
        }
    }

    /// 当前选择的日期
    var currentDate: Date {
        let year  = currentYearInt()
        let month = currentMonthInt()
        let day   = currentDayInt()
        let date  = Date.cc_defaultFormatter.date(from: "\(month)-\(day)-\(year)")
        return date!
    }


    fileprivate let componentCount = 3

    fileprivate var manager: CCDateManager?

    fileprivate lazy var pickerview: UIPickerView = {
        let tmpv = UIPickerView.init()
        tmpv.delegate = self
        tmpv.dataSource = self
        return tmpv
    }()

    required init?(frame: CGRect = .zero, minDate: Date, maxDate: Date) {
        super.init(frame: frame)

        if minDate.compare(maxDate) == .orderedDescending {
            return nil
        }

        manager = CCDateManager.init(minDate: minDate, maxDate: maxDate)
        manager?.delegate = self
        self.dataSource = manager

        pickerview.frame = frame
        self.addSubview(pickerview)
        self.isUserInteractionEnabled = true
        self.isEnabled = true
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder) // 暂不支持xib
    }

    override func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) {
        super.addTarget(target, action: action, for: controlEvents)
        if controlEvents == .valueChanged {
            print("123")
            print(action.description)
        }
    }
}

//MARK: ------------------------ Public
extension CCDatePicker {

    /// 设置日期,例如`"2007-8-20"`或`"2007-11-9"`
    func setDate(_ dateString: String, animated: Bool = false) {
        guard let date = Date.cc_defaultFormatter.date(from: dateString) else { return }
        setDate(date, animated: animated)
    }

    func setDate(_ date: Date, animated: Bool = false) {
        let rowInfo = manager?.setDate(date)
        if let info = rowInfo {
            pickerview.selectRow(info.mRow, inComponent: 0, animated: animated)
            pickerview.selectRow(info.dRow, inComponent: 1, animated: animated)
            pickerview.selectRow(info.yRow, inComponent: 2, animated: animated)
            pickerview.reloadAllComponents()
        }
    }
}

//MARK: ------------------------ Private
extension CCDatePicker{
    /// 分割线views
    fileprivate var separatorLines: [UIView] {
        return pickerview.subviews.filter {
            [=14=].bounds.height < 1.0 && [=14=].bounds.width == pickerview.bounds.width
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        pickerview.frame = self.bounds
        separatorLines.forEach { [=14=].backgroundColor = separatorColor }
    }
}

extension CCDatePicker: CCDateSelectionDelegate {

    func currentMonthInt() -> Int {
        let row = pickerview.selectedRow(inComponent: 0)
        let attrStr = self.pickerView(pickerview, attributedTitleForRow: row, forComponent: 0)
        let value: Int = attrStr?.string.getInt() ?? 1
        return value
    }

    func currentDayInt() -> Int {
        let row = pickerview.selectedRow(inComponent: 1)
        let attrStr = self.pickerView(pickerview, attributedTitleForRow: row, forComponent: 1)
        let value: Int = attrStr?.string.getInt() ?? 1
        return value
    }

    func currentYearInt() -> Int {
        let row = pickerview.selectedRow(inComponent: 2)
        let attrStr = self.pickerView(pickerview, attributedTitleForRow: row, forComponent: 2)
        let value: Int = attrStr?.string.getInt() ?? 1
        return value
    }
}

extension CCDatePicker: UIPickerViewDelegate{
    func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        switch component {
        case 0: return pickerView.bounds.width * 0.5 * (1 - 0.45) // 根据字符宽度测得比例
        case 1: return pickerView.bounds.width * 0.5 * (1 - 0.45)
        case 2: return pickerView.bounds.width * 0.45
        default: return 0
        }
    }
    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return rowHeight
    }
    func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
        var ostr = ""
        let intValue = self.dataSource?.datepicker(self, intValueForRow: row, forComponent: component) ?? 1
        switch component {
        case 0: ostr = String(intValue) + (unitName.month ?? "")
        case 1: ostr = String(intValue) + (unitName.day ?? "")
        case 2: ostr = String(intValue) + (unitName.year ?? "")
        default: break
        }
        let attStr = NSMutableAttributedString(string: ostr)
        attStr.addAttributes([.foregroundColor: titleColor, .font: titleFont], range: NSMakeRange(0, ostr.count))
        return attStr
    }

    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        let attrText = self.pickerView(pickerView, attributedTitleForRow: row, forComponent: component)
        if let label = view as? UILabel {
            label.attributedText = attrText
            return label
        }
        let newlabel = UILabel.init()
        newlabel.backgroundColor = UIColor.clear
        newlabel.textAlignment = .center
        newlabel.attributedText = attrText
        return newlabel
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        switch component {
        case 0: // month
            let position = pickerDataSize / 2 + row % (manager?.months_available.count)!
            pickerView.selectRow(position, inComponent: 0, animated: false)

            // call the funtion to reload the days_available array accodrig to the diffrent moth
            // so that the position will fall into the right day
            if let dRow = manager?.onMonthRefreshed() {
                pickerView.reloadComponent(1)

                let day_position = dRow % (manager?.days_available.count)!
                pickerview.selectRow(day_position, inComponent: 1, animated: false)
                self.pickerView(pickerView, didSelectRow: day_position, inComponent: 1)
            }
        case 1: // day
            self.delegate?.didSelectDate(at: self)
        case 2: // year

            if let mRow = manager?.onYearRefreshed(){
                pickerView.reloadComponent(0)
                pickerview.selectRow(mRow, inComponent: 0, animated: false)
                self.pickerView(pickerView, didSelectRow: mRow, inComponent: 0)
            }
        default: break
        }
    }
}

extension CCDatePicker: UIPickerViewDataSource{
    func numberOfComponents(in pickerView: UIPickerView) -> Int { return componentCount }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        let rowCount = self.dataSource?.datepicker(self, numberOfRowsInComponent: component) ?? 0
        return rowCount
    }
}

extension String {
    fileprivate func getInt() -> Int {
        let scanner = Scanner(string: self)
        scanner.scanUpToCharacters(from: CharacterSet.decimalDigits, into: nil)
        var number: Int = 0
        scanner.scanInt(&number)
        return number
    }
}




protocol CCDateSelectionDelegate: class {
    func currentYearInt() -> Int
    func currentMonthInt() -> Int
    func currentDayInt() -> Int
}


/// 日期数据管理类
class CCDateManager {

    fileprivate lazy var months_: [Int] = {
        var arr = [Int]()
        for i in 1...12 { arr.append(i) }
        return arr
    }()
    fileprivate lazy var days_: [Int] = {
        var arr = [Int]()
        for i in 1...31 { arr.append(i) }
        return arr
    }()


    /// 最小的日期
    fileprivate let minDate: Date
    /// 最大的日期
    fileprivate let maxDate: Date

    fileprivate var months_available   :[Int]
    fileprivate var days_available     :[Int]

    weak var delegate: CCDateSelectionDelegate?

    init(minDate: Date, maxDate: Date) {
        self.minDate = minDate
        self.maxDate = maxDate
        self.months_available = []
        self.days_available   = []
    }
}

extension CCDateManager {
    @discardableResult
    func setDate(_ date: Date) -> (yRow: Int, mRow: Int, dRow: Int)? {
        if date.compare(minDate) == .orderedAscending || date.compare(maxDate) == .orderedDescending {
            NSLog("指定日期超过了可选范围")
            return nil
        }

        let result = refreshCurrent(year: date.year, month: date.month, day: date.day)
        return result
    }

    /// 更新`年`的选择,返回新的`月`index
    func onYearRefreshed() -> Int {
        let year  = self.delegate?.currentYearInt() ?? 1
        let month = self.delegate?.currentMonthInt() ?? 1

        handleRefreshMonthsOf(year: year)

        var mRow = months_available.index(of: month) ?? 0
        if let monthLast = months_available.last, let monthFirst = months_available.first {
            if month < monthFirst {
                mRow = 0
            } else if month > monthLast {
                mRow = months_available.count - 1
            }
        }
        return mRow
    }

    /// 更新`月`的选择,返回新的`日`index
    func onMonthRefreshed() -> Int {
        let year  = self.delegate?.currentYearInt() ?? 1
        let month = self.delegate?.currentMonthInt() ?? 1
        let day   = self.delegate?.currentDayInt() ?? 1

        handleRefreshDaysOf(year: year, month: month)

        var dRow = days_available.index(of: day) ?? 0
        if let dayLast = days_available.last, let dayFirst = days_available.first {
            if day < dayFirst {
                dRow = 0
            } else if day > dayLast {
                dRow = days_available.count - 1
            }
        }
        return dRow
    }
}

extension CCDateManager {
    fileprivate func refreshCurrent(year: Int, month: Int, day: Int) -> (yRow: Int, mRow: Int, dRow: Int) {
        handleRefreshMonthsOf(year: year)
        handleRefreshDaysOf(year: year, month: month)

        var mRow = months_available.index(of: month) ?? 0
        if let monthLast = months_available.last, let monthFirst = months_available.first {
            if month < monthFirst {
                mRow = 0
            } else if month > monthLast {
                mRow = months_available.count - 1
            }
        }

        var dRow = days_available.index(of: day) ?? 0
        if let dayLast = days_available.last, let dayFirst = days_available.first {
            if day < dayFirst {
                dRow = 0
            } else if day > dayLast {
                dRow = days_available.count - 1
            }
        }

        let yRow = year - minDate.year
        return (yRow, mRow, dRow)
    }

    /// 处理`月`范围
    fileprivate func handleRefreshMonthsOf(year: Int) {

        if (maxDate.year == minDate.year) {
            months_available = months_.filter({ [=14=] >= minDate.month && [=14=] <= maxDate.month })
        } else {
            if year == minDate.year {
                months_available = months_.filter({ [=14=] >= minDate.month })
            } else if year == maxDate.year {
                months_available = months_.filter({ [=14=] <= maxDate.month })
            } else {
                months_available = months_
            }
        }
    }
    /// 处理`日`范围
    fileprivate func handleRefreshDaysOf(year: Int, month: Int) {
        let fullDays = Date.fullDaysOf(year: year, month: month)

        if (maxDate.year == minDate.year) {
            if (maxDate.month == minDate.month){
                days_available = days_.filter({ [=14=] >= minDate.day && [=14=] <= maxDate.day })
            } else {
                if (month == minDate.month) {
                    days_available = days_.filter({ [=14=] >= minDate.day && [=14=] <= fullDays })
                } else if (month == maxDate.month) {
                    days_available = days_.filter({ [=14=] <= maxDate.day })
                } else {
                    days_available = days_.filter({ [=14=] <= fullDays })
                }
            }
        } else {
            if year == minDate.year {
                if month == minDate.month {
                    days_available = days_.filter({ [=14=] >= minDate.day && [=14=] <= fullDays })
                } else {
                    days_available = days_.filter({ [=14=] <= fullDays })
                }
            } else if year == maxDate.year {
                if month == maxDate.month {
                    days_available = days_.filter({ [=14=] <= maxDate.day })
                } else {
                    days_available = days_.filter({ [=14=] <= fullDays })
                }
            } else {
                days_available = days_.filter({ [=14=] <= fullDays })
            }
        }
    }

}

extension CCDateManager {
    fileprivate func numberOfRowsInComponent(_ component: Int) -> Int {
        switch component {
        case 0:
//            return months_available.count
            return 120_000  // for loop loading
        case 1:
//            return days_available.count
            return 120_000 // for loop loading
        case 2:
            return (maxDate.year - minDate.year) + 1
        default: return 0
        }
    }

    fileprivate func intValueForRow(row: Int, forComponent component: Int) -> Int{
        switch component {
        case 0:
            return months_available[row % months_available.count]
        case 1:
            return days_available[row % days_available.count ]
        case 2:
            return minDate.year + row
        default: return 1
        }
    }
}

extension CCDateManager: CCDatePickerDataSource {
    func datepicker(_ picker: CCDatePicker, numberOfRowsInComponent component: Int) -> Int {
        return self.numberOfRowsInComponent(component)
    }

    func datepicker(_ picker: CCDatePicker, intValueForRow row: Int, forComponent component: Int) -> Int {
        return self.intValueForRow(row: row, forComponent: component)
    }
}


extension Date {
    static var cc_defaultFormatter: DateFormatter {
        return self.dateFormatterWith("MM-dd-yyyy")
    }

    /// 自定义时间格式的格式化器
    fileprivate static func dateFormatterWith(_ formatString: String) -> DateFormatter {
        let threadDic = Thread.current.threadDictionary
        if let fmt = threadDic.object(forKey: formatString) as? DateFormatter {
            return fmt
        }
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = formatString
        threadDic.setObject(dateFormatter, forKey: formatString as NSCopying)
        return dateFormatter
    }

    /// 指定年月的天数
    fileprivate static func fullDaysOf(year: Int, month: Int) -> Int {
        if [1, 3, 5, 7, 8, 10, 12].contains(month) { return 31 }
        if [4, 6, 9, 11].contains(month) { return 30 }
        let isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
        return isLeapYear ? 29 : 28 // 二月
    }

    fileprivate var year: Int {
        return NSCalendar.current.component(.year, from: self)
    }
    fileprivate var month: Int {
        return NSCalendar.current.component(.month, from: self)
    }
    fileprivate var day: Int {
        return NSCalendar.current.component(.day, from: self)
    }
}

您希望自定义控件在用户在内部选择器视图中选择一行时发出 valueChanged 事件。

添加行:

sendActions(for: .valueChanged)

您希望您的自定义控件在任何时候告诉所有已注册的目标值已更改。至少这将在您的 pickerView:didSelectRow 委托方法的实现中。

一些注意事项:

  • 除调试目的外,您无需在自定义控件中重写 addTarget
  • 奇怪的是,由于您的自定义控件提供了 CCDatePickerDelegate 委托协议,因此您想使用 addTarget 在值更改时收到通知。