Swift 自定义PickerView,支持选择时间、单选、多选

bg:

系统提供了时间选择器UIDatePicker、文本选择器UIPickerView,这两种选择器本身具备了很多特性及优点。但在实际项目开发中有较多地方用到时间选择或数据选择场景,比如商城类App购物车的商品添加、有预约场景的App时间及事务选择等,考虑到UI要求、代码封装易用性等方面,就需要我们自己自定义一些选择控件了。

这里自定义一个简单的选择器:时间选择单选多选(暂未添加多级联动选择模式,后续添加)其中时间选择支持设置时间范围和默认时间

Demo地址:https://github.com/wanghhh/HHPickerViewDemo

先看下效果图:

pickerView_gif.gif
调用示例:

比如选择时间:

let pickerView = HHPickerView.init(frame: CGRect.init(x: marginTop, y: self.view.bounds.size.height, width: self.view.bounds.size.width, height: CGFloat(toolBarH + pickerViewH)), dateFormat: nil,datePickerMode:.dateAndTime, minAndMaxAndCurrentDateArr: nil)
        pickerView.rowAndComponentCallBack = {(resultStr,selectedArr) in
            print("str--->\(String(describing: resultStr))")
            btn.setTitle(resultStr! as String, for: .normal)
        }
pickerView.show()

//多选

let pickerView = HHPickerView.init(frame: CGRect.init(x: marginTop, y: self.view.bounds.size.height, width: self.view.bounds.size.width, height: CGFloat(toolBarH + pickerViewH)), dataSource: data as NSArray, defaultIntegerArr: [1,3,6], pickerType: .mutable)
        
        pickerView.rowAndComponentCallBack = {(resultStr,selectedArr) in
            print("str--->\(String(describing: resultStr))")
            btn.setTitle(resultStr! as String, for: .normal)
        }
 pickerView.show()

代码结构:

ic_picker01.png

调用时将HHPickerView.swift文件拖进项目即可,顾名思义HHPickerView类就是自定义的选择器。
由于要支持时间选择、文本选择,就抽取了对应的2个辅助类(暂且这样叫):HHDatePicker(时间选择类)、HHCollectionView(文本选择类)。其中HHDatePicker直接继承自系统的UIDatePicker,HHCollectionView这里就继承自UICollectionView实现了,也可以直接调用辅助类中的方法。

HHPickerView实现:

//选择器类型

enum HHPickerViewType:NSInteger {
   case single = 0   //只能单选
   case mutable = 1  //可多选、单选
   case time = 2     //选择时间
}
定义一个结果回调闭包

//返回选择结果内容、结果索引数组(针对单选、多选)

typealias HHPickerViewCallBackClosure = (_ resultStr:NSString?,_ resultArr:NSArray?) -> ()

var rowAndComponentCallBack:HHPickerViewCallBackClosure?//选择内容回调

重写init方法来实现基本UI设置,代码如下:

override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = UIColor.white;
    
    // 1 获取window
    if (keyWindow == nil) {
        self.keyWindow = UIApplication.shared.keyWindow
    }
    // 2.遮罩view
    overlayView = UIControl.init(frame: UIScreen.main.bounds)
    overlayView?.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.5)
    overlayView?.addTarget(self, action: #selector(hide), for: .touchUpInside)
    overlayView?.alpha = 0
    // 3.创建工具条toolView
    let toolView:UIView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: Int(self.bounds.size.width), height: Int(toolBarH)))
    toolView.backgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
    addSubview(toolView)
    
    cancelButton = UIButton.init(frame: CGRect.init(x: btnMargin, y: 0, width: 44, height: toolView.bounds.size.height))
    cancelButton?.setTitle("取消", for: .normal)
    cancelButton?.setTitleColor(cancelTextNormalColor, for: .normal)
    cancelButton?.setTitleColor(cancelTextSelectedColor, for: .selected)
    cancelButton?.titleLabel?.font = UIFont.systemFont(ofSize: 17.5)
    cancelButton?.contentHorizontalAlignment = .left
    cancelButton?.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
    toolView.addSubview(cancelButton!)
    
    confirmButton = UIButton.init(frame: CGRect.init(x: (toolView.bounds.size.width - 44.0 - btnMargin), y: 0, width: 44, height: toolView.bounds.size.height))
    confirmButton?.setTitle("确定", for: .normal)
    confirmButton?.setTitleColor(confirmTextNormalColor, for: .normal)
    confirmButton?.setTitleColor(confirmTextSelectedColor, for: .selected)
    confirmButton?.titleLabel?.font = UIFont.systemFont(ofSize: 17.5)
    confirmButton?.contentHorizontalAlignment = .left
    confirmButton?.addTarget(self, action: #selector(confirmAction), for: .touchUpInside)
    toolView.addSubview(confirmButton!)
}
时间选择模式便利方法:(可设置dateFormat格式化字符串;选择器datePickerMode;时间范围控制:可选最小、最大时间及默认时间)
/// 时间选择便利构造器
///
/// - Parameters:
///   - frame: frame
///   - dateFormat: 时间格式化字符串,可空
///   - datePickerMode: 选择器的时间模式,可空
///   - minAndMaxAndCurrentDateArr: 可选最小、最大时间及当前时间,可空
convenience init(frame: CGRect,dateFormat:NSString?,datePickerMode:UIDatePickerMode?,minAndMaxAndCurrentDateArr:[NSDate]?) {
    self.init(frame: frame)
    pickerViewType = HHPickerViewType.time
    
    let picker = HHDatePicker.init(frame: CGRect.init(x: (confirmButton?.superview?.frame.minX)!, y: (confirmButton?.superview?.frame.maxY)!, width: UIScreen.main.bounds.size.width, height: CGFloat(pickerViewH)), dateFormat: dateFormat,datePickerMode:datePickerMode, minAndMaxAndCurrentDateArr: nil, resultCallBack: {[weak self] (resultStr) in
        self?.blockContent = resultStr
    })
    picker.getSelectedResult({[weak self] (resultStr) in
        self?.blockContent = resultStr
    })
    addSubview(picker)
}
单选、多选模式便利构造方法:(可设置单选、多选模式;可设置默认选中的选项)
/// 单选/多选便利构造器
///
/// - Parameters:
///   - frame: frame
///   - pickerType: 选择类型(单选或多选)
///   - dataSource: 数据源
///   - defaultIntegerArr:  默认选中项的索引数组
convenience init(frame: CGRect,dataSource:NSArray,defaultIntegerArr:NSArray?,pickerType:HHPickerViewType) {
    self.init(frame: frame)
    pickerViewType = pickerType
    if (dataSource.count != 0) {
        let picker = HHCollectionView.init(frame: CGRect.init(x: (confirmButton?.superview?.frame.minX)!, y: (confirmButton?.superview?.frame.maxY)!, width: UIScreen.main.bounds.size.width, height: CGFloat(pickerViewH)), collectionViewLayout: HHWaterfallLayout(), dataSource: dataSource, defaultIntegerArr: defaultIntegerArr, contentCallBack: { [weak self] (resultStr, selectedArr) in
            self?.blockContent = resultStr
            self?.selectedArr = selectedArr
        })
        picker.rowAndComponentCallBack = {[weak self](resultStr,selectedArr) in
            self?.blockContent = resultStr
            self?.selectedArr = selectedArr
        }
        addSubview(picker)
    }else{
        assert(dataSource.count != 0, "dataSource is not allowed to be nil")
    }
}
主要事件处理

//显示

func show(){
    keyWindow?.addSubview(overlayView!)
    keyWindow?.addSubview(self)
    UIView.animate(withDuration: 0.25, animations: {
        self.overlayView?.alpha = 1.0
        var frame = self.frame
        frame.origin.y = UIScreen.main.bounds.size.height - self.bounds.size.height
        self.frame = frame
    }) { (isFinished) in
        //
    }
}

//隐藏

func hide() {
    self.dismissCallBack()
    UIView.animate(withDuration: 0.25, animations: {
        self.overlayView?.alpha = 0
        var frame = self.frame
        frame.origin.y = UIScreen.main.bounds.size.height
        self.frame = frame
    }) { (isFinished) in
        self.overlayView?.removeFromSuperview()
        self.removeFromSuperview()
    }
}

//取消选择

func cancelAction() {
    hide()
}

//确定选择

func confirmAction() {
    if blockContent == "" {
        showAlert(withTitle: "提示", message: "未选择任何一项!")
    }else if pickerViewType != HHPickerViewType.time && (selectedArr?.count)! > 1 && pickerViewType == HHPickerViewType.single {
        showAlert(withTitle: "提示", message: "此项仅支持单选!")
    }else{
        self.rowAndComponentCallBack!(blockContent,selectedArr)
    }
    hide()
}

//异常提示

@objc private func showAlert(withTitle title: String?, message: String?) {
    let alertVc = UIAlertController.init(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
    alertVc.addAction(UIAlertAction.init(title: "我知道了", style: UIAlertActionStyle.cancel, handler: nil))
    UIApplication.shared.keyWindow?.rootViewController?.present(alertVc, animated: true, completion: nil)
}

以上就是HHPickerView的主要实现,代码简单,就不再详细说明了,可查看Demo:https://github.com/wanghhh/HHPickerViewDemo

HHDatePicker实现:

//结果回调闭包

typealias HHDatePickerCallBackClosure = (_ resultStr:NSString?) -> ()

var dateChangeCallBack:HHDatePickerCallBackClosure? //时间改变回调
便利构造方法:
/// 时间选择器便利构造方法
///
/// - Parameters:
///   - frame: frame
///   - dateFormat: 时间格式化字符串
///   - datePickerMode: 选择器的时间模式
///   - minAndMaxAndCurrentDateArr: 可选最小、最大时间及当前时间
///   - resultCallBack: 选择结果
convenience init(frame: CGRect,dateFormat:NSString?,datePickerMode:UIDatePickerMode?,minAndMaxAndCurrentDateArr:[NSDate]?,resultCallBack:((_ resultStr:NSString) -> Void)?) {
    self.init(frame: frame)
    self.backgroundColor = UIColor.white;
    if datePickerMode != nil {
        self.datePickerMode = datePickerMode!
    }else{
        self.datePickerMode = .dateAndTime //默认显示月、日、时间
    }
    if dateFormat?.range(of: "yy").location != NSNotFound {
        self.datePickerMode = .dateAndTime
    }else{
        self.datePickerMode = .date
    }
    //可以设置时间范围
    var minDateTem = NSDate.init()
    var maxDateTem = NSDate.init(timeIntervalSinceNow: 90*365*24*60*60)
    var currentDateTem = NSDate.init()
    if minAndMaxAndCurrentDateArr != nil && minAndMaxAndCurrentDateArr?.count == 2 {
        minDateTem = (minAndMaxAndCurrentDateArr?[0])!
        maxDateTem = (minAndMaxAndCurrentDateArr?[1])!
        currentDateTem = (minAndMaxAndCurrentDateArr?[2])!
    }
    self.minimumDate = minDateTem as Date
    self.maximumDate = maxDateTem as Date
    self.setDate(currentDateTem as Date, animated: false)
    self.locale = Locale.init(identifier: "zh_CN")
    
    self.addTarget(self, action: #selector(dateChange(datePicker:)), for: UIControlEvents.valueChanged)
    
    //默认回调当前时间
    let theDate = self.date
    let dateFormatter = DateFormatter.init()
    if (dateFormat != nil) {
        dateFormatter.dateFormat = dateFormat! as String
        self.dateFormat = dateFormat
    }else{
        dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
        self.dateFormat = dateFormatter.dateFormat! as NSString
    }
    let nowDate = dateFormatter.string(from: theDate)
    resultCallBack!(nowDate as NSString)
}

//时间改变结果回调

fileprivate func getSelectedResult(_ callBack: @escaping(HHDatePickerCallBackClosure)) {
    dateChangeCallBack = callBack
}

//时间改变监听

func dateChange(datePicker:UIDatePicker) {
    let theDate = datePicker.date
    print("\(theDate.description(with: Locale.current))")
    
    let dateFormatter = DateFormatter.init()
    dateFormatter.dateFormat = self.dateFormat! as String
    let nowDate = dateFormatter.string(from: theDate)

    dateChangeCallBack!(nowDate as NSString)
}
HHCollectionView主要实现:

//回调

fileprivate var rowAndComponentCallBack:HHPickerViewCallBackClosure?//选择内容回调
lazy var dataSourceArr = NSMutableArray() //数据源
lazy var selectedArr = NSMutableArray() //被选中的数据
便利方法:
/// 便利构造器
///
/// - Parameters:
///   - frame: frame
///   - collectionViewLayout: collectionViewLayout
///   - dataSource: 选择项数据源
///   - defaultIntegerArr: 默认选中的项索引数组
///   - contentCallBack: 选择结果回调
convenience init(frame:CGRect,collectionViewLayout:UICollectionViewLayout,dataSource:NSArray,defaultIntegerArr:NSArray?,contentCallBack:HHPickerViewCallBackClosure?) {
    self.init(frame: frame, collectionViewLayout: collectionViewLayout)
    self.delegate = self
    self.dataSource = self
    self.backgroundColor = UIColor.white
    self.dataSourceArr = NSMutableArray.init(array: dataSource)
    if (defaultIntegerArr != nil) {
        self.selectedArr = NSMutableArray.init(array: defaultIntegerArr!)
    }
    
    self.register(HHCollectionCell.self, forCellWithReuseIdentifier: HHCollectionViewCellId)
    
    if (contentCallBack != nil) {
        //默认选中数据
        var resultStr = "" //选中的结果的拼接字符串,多选用“;”号隔开(按需要自定义)
        
        if self.selectedArr.count > 0 {
            for (idx,obj) in self.selectedArr.enumerated() {
                if idx == 0 {
                    resultStr = self.dataSourceArr[(obj as? Int)!] as! String
                }else{
                    resultStr = "\(resultStr);\(self.dataSourceArr[(obj as? Int)!])"
                }
            }
        }
        contentCallBack!(resultStr as NSString,selectedArr)
    }
}
在UICollectionViewDelegate中回调结果:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let cell:HHCollectionCell = collectionView.cellForItem(at: indexPath) as! HHCollectionCell        
    if (self.selectedArr.count>0) {
        var isExited = false//是否已经被选中,即存在selectedArr中
        for (_,obj) in self.selectedArr.enumerated() {
            if obj as? NSInteger == indexPath.row{
                cell.isSelected = false //取消选中
                isExited = true
                selectedArr.remove(indexPath.row)
                break
            }
        }
        if isExited == false {
            selectedArr.add(indexPath.row)
        }
    }else{
        cell.isSelected = true
        selectedArr.add(indexPath.row)
    }
    reloadItems(at: [indexPath])
    
    //组装回调结果***
    //默认选中数据
    var resultStr = "" //选中的结果的拼接字符串,多选用“;”号隔开(按需要自定义)
    
    if self.selectedArr.count > 0 {
        for (idx,obj) in self.selectedArr.enumerated() {
            if idx == 0 {
                resultStr = self.dataSourceArr[(obj as? Int)!] as! String
            }else{
                resultStr = "\(resultStr);\(self.dataSourceArr[(obj as? Int)!])"
            }
        }
    }
    self.rowAndComponentCallBack!(resultStr as NSString,selectedArr)
}
HHCollectionView的布局及自定义cell的代码比较简单就不在bia了,详细代码可以下载demo:https://github.com/wanghhh/HHPickerViewDemo查看。
本文所涉及到代码都比较简单,主要提供一个思路:通过自定义view+自定义datePicker+自定义文本选择view实现时间选择、单选及多选。大家可以根据项目需要比如UI需求等,予以扩展、完善。

欢迎大家指出错误、相互交流,共同学习!

推荐阅读更多精彩内容