iOS-swift-自定义View之时间选择器

1 需求分析

当我们选择购买一个东西时,有时需要预约发货,这时候需要用户手动选择某一天某一个时间,ui给的方案是这样的:
预约日期:

预约时间:

这里是间隔了2个小时一个时间。

可以看到这个日期是以日历形式展现,可以选择未来某个日期;时间是以滚轮的方式展现,可以上下滑动到目标时间点。

对于这个需求,我们要如何实现呢?‘

答案当然是自定义View了。

2 结构分析

显而易见,这个因为设计到分页,我们可以考虑分页区域使用一个UIScrollView,水平滑动,宽度设置为屏幕的2倍,一半用来展示日历,一半用来展示预约时间的UIPickerView。

所以需要我们自定义一个UIView,这里面先绘制顶部分割线,再绘制标题,“预约日期”和“预约时间”和“取消”的文案。

然后是一个显示日历操作栏,包括左右箭头和中间的月份显示,这个要放到UIScrollView的左半边,包括星期视图和日历的UICollectionView的集合View,这里日历我们采用这个集合View很容易实现的。

然后右侧就是一个UIPickerView,这个系统封装好了,使用很方便,我们主要负责填充数据。

那这个结构大体出来了,一个是整个自定义View,暂且把它叫做CalenderView,还有一个就是日历内部的Cell,暂且叫做CalenderCell。

3 撸下CalenderView

3.1 全局变量

class CalendarView: UIView {
    
    /// 传出去给调用者
    typealias DoneBlock = (TimeModel)->()
    var doneHandle: DoneBlock!
    
    // 当月日期Model
    var modelArr: [TimeModel] = []
    
    // 动态调整上下月的时间变量
    var dynamicDate = Date()
    
    //默认选中的日期
    var timeModel: TimeModel = TimeModel()
    
    //时间时短数据
    static let range: String = "                      "
    private var timeArr: [String] = ["08:00\(range)10:00",
                                   "10:00\(range)12:00",
                                   "12:00\(range)14:00",
                                   "14:00\(range)16:00",
                                   "16:00\(range)18:00",
                                   "18:00\(range)20:00",
                                   "20:00\(range)22:00"]
    private var selectIndex = 0
    

这里定义了一个闭包返回,主要是用户选择后,回调给外部。

然后有一个TimeModel的数组, 也就是填充我们的日历视图。

timeModel是我们选择的日期,会高亮显示。

timeArr是我们右侧使用的UIPickerView要用的数据。

3.2 UI定义

这里我们大致会用到这些视图。

//白色背景底 整个半屏的视图
private lazy var acView: UIView = {
    let view = UIView()
    view.backgroundColor = .white
    return view
}()

//选中条
private lazy var lineView: UIView = {
    let view = UIView()
    view.backgroundColor = UIColor.init(hex: "#409EFF")
    return view
}()

/// 预约日期按钮
private lazy var dateBtn: UIButton = {
    let btn = UIButton()
    btn.setTitle("预约日期", for: .normal)
    btn.titleFont = .pingFangMedium(size: 16)
    btn.setTitleColor(UIColor.lightGray, for: .normal)
    btn.setTitleColor(UIColor.black, for: .selected)
    btn.addTarget(self, action: #selector(actionForChooseDate), for: .touchUpInside)
    btn.isSelected = true
    return btn
}()

/// 预约时间按钮
private lazy var timeBtn: UIButton = {
    let btn = UIButton()
    btn.setTitle("预约时间", for: .normal)
    btn.titleFont = .pingFangMedium(size: 16)
    btn.setTitleColor(UIColor.lightGray, for: .normal)
    btn.setTitleColor(UIColor.black, for: .selected)
    btn.addTarget(self, action: #selector(actionForChooseTime), for: .touchUpInside)
    return btn
}()

/// 取消按钮
private lazy var cancelBtn: UIButton = {
    let btn = UIButton()
    btn.setTitle("取消", for: .normal)
    btn.setTitleColor(UIColor.init(hex: "#409EFF"), for: .normal)
    btn.titleFont = .pingFangRegular(size: 16)
    btn.addTarget(self, action: #selector(actionForCancel), for: .touchUpInside)
    return btn
}()

//日历工具条
private lazy var dateToolView: UIView = {
    let view = UIView()
    return view
}()

//日历工具条显示当前月份标签
private lazy var monthLb: UILabel = {
    let lab = UILabel()
    lab.font = .pingFangMedium(size: 16)
    lab.textColor = UIColor.init(hex: "#3B4058")
    lab.text = ""
    lab.textAlignment = .center
    return lab
}()

//日历工具条左箭头
private lazy var toolLeftBtn: UIButton = {
    let btn = UIButton()
    btn.setImage(UIImage.init(named: "日历_左箭头"), for: .normal)
    btn.addTarget(self, action: #selector(actionForLeftEvent), for: .touchUpInside)
    btn.isHidden = true
    return btn
}()

//日历工具条右箭头
private lazy var toolRightBtn: UIButton =  {
    let btn = UIButton()
    btn.setImage(UIImage.init(named: "日历_右箭头"), for: .normal)
    btn.addTarget(self, action: #selector(actionForRightEvent), for: .touchUpInside)
    return btn
}()

//星期X显示条
private lazy var weeksView: UIView = {
    let view = UIView()
    view.backgroundColor = .white
    view.layer.shadowColor = UIColor.init(hex: "#7D7E80").cgColor
    view.layer.shadowOffset = CGSize.init(width: 0, height: 2)
    view.layer.shadowRadius = 10
    view.layer.shadowOpacity = 0.16
    return view
}()

//滚动视图 左右滚动
private lazy var scrollView: UIScrollView = {
    let _scrollView = UIScrollView()
    _scrollView.contentSize = CGSize.init(width: ScreenWidth*2, height: _scrollView.size.height)
    _scrollView.bounces = false
    _scrollView.isScrollEnabled = true
    _scrollView.isPagingEnabled = true
    _scrollView.showsVerticalScrollIndicator = false
    _scrollView.showsHorizontalScrollIndicator = false
    return _scrollView
}()

//月份水印背景
private lazy var watermarkView: CalendarWatermark = {
    let view = CalendarWatermark()
    view.backgroundColor = .white
    view.isHidden = true  //当前版本隐藏水印
    return view
}()

//日历展示表
    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        let collection = UICollectionView(frame: CGRect.init(x: 0, y: 34+40, width: ScreenWidth, height: ScreenHeight*0.57*0.57), collectionViewLayout: layout)
        collection.backgroundColor = .clear
        collection.delegate = self
        collection.dataSource = self
        collection.isPagingEnabled = true
        collection.showsHorizontalScrollIndicator = false
        collection.bounces = false
        collection.register(CalendarCell.self, forCellWithReuseIdentifier: CalendarCell.identifier)
        return collection
}()

//时间选择器 x轴以 屏幕宽度为起点,刚好在第二屏了
private lazy var pickerView: UIPickerView = {
    let picker = UIPickerView.init(frame: CGRect.init(x: ScreenWidth, y: 0, width: ScreenWidth, height: ScreenHeight*0.57*0.57+34+40))
    picker.delegate = self
    picker.dataSource = self
    return picker
}()

这些需要用代理和数据源的都设置self,后面再具体实现。

这里可以认为是我们搭建房子用到的一些素材。先写好,后期再拼接下。

3.3 生命周期函数

初始化看下,应该要先设置一个蒙层。

override init(frame: CGRect){
        super.init(frame: frame)
        /// 这里应该是蒙层
        self.backgroundColor = UIColor.init(r: 100, g: 1, b: 1, a: 0.3)
        self.alpha = 0
        
        let lastDyaDate = YSDateTool.lastDay()
        let com = YSDateTool.currentDateCom(date: lastDyaDate)
        watermarkView.monthStr = "\(com.month!)"
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

确实如此,这里就只加了个颜色。

3.4 定义方法,展示和隐藏选择器

extension CalendarView{
    /// 显示弹窗
    public func showAlertView() {
        let str = "\(timeModel.startTime)\(CalendarView.range)\(timeModel.endTime)"
        selectIndex = timeArr.firstIndex(of:str) ?? 0
        let timeStr = "\(timeModel.year)-\(timeModel.month)-\(timeModel.day)"
        let date = YSDateTool.dateStringToDate(timeStr)
        self.setDate(date: date)
        self.setUI()
        pickerView.selectRow(selectIndex, inComponent: 0, animated: false)
        UIView.animate(withDuration: 0.25) {
            self.alpha = 1
        }
    }

    /// 隐藏弹窗
    public func dismissAlertView() {
        self.animate(duration: 0.25) {
            self.alpha = 0
        } completion: { finish in
            self.removeFromSuperview()
        }
    }
}

这个方法很重要,主要提供给外部使用,这里帮助外部设置数据,因为外部调用者可能不知道内部细节,所以我们最好在这个类里面实现展示弹框的方法。

3.5 设置初始化日期

外部可能有选择好的日期,所以我们必须提供一个方法,设置数据。

extension CalendarView{
    /// 按照这个date获取这个月的数据
    func setDate(date: Date = Date()){
        modelArr.removeAll()
        let count = YSDateTool.countOfDaysInCurrentMonth(date: date)    //当月天数
        for i in 0..<count{
            let model = self.initializeModel(date: date, day: i+1)
            modelArr.append(model)
        }
        let firstWeekDay = YSDateTool.firstWeekDayInCurrentMonth(date: date)    //当月第一天周几
        
        //头部空缺数据填充 插入一个空的model
        if firstWeekDay != 7 {
            for _ in 0 ..< firstWeekDay{
                let model = self.initializeModel(date: date, day: 0)
                modelArr.insert(model, at: 0)
            }
        }
        //尾部空缺数据填充,插入一个空的model
        if modelArr.count < 35{
            for _ in modelArr.count ..< 35{
                let model = self.initializeModel(date: date, day: 0)
                modelArr.append(model)
            }
        }
        
        collectionView.reloadData()
    }
    
    /// 初始化数据,返回
    func initializeModel(date: Date, day: Int)->TimeModel{
        let calendar = NSCalendar.current
        let com = calendar.dateComponents([.year, .month, .day], from: date)
        let model = TimeModel()
        model.year = com.year!
        model.month = com.month!
        model.day = day
        
        // model指代当前时间
        let monthStr = com.month! < 10 ? "0\(model.month)" : "\(model.month)"
        let dayStr = com.day! < 10 ? "0\(model.day)" : "\(model.day)"
        model.dateStr = "\(com.year!)-\(monthStr)-\(dayStr)"
        return model
    }
}

这里也搞了2个扩展函数,会根据目标日期,获取到该日期的一个月的数据,然后会填充第一天非星期天的情况的数据,也会填最后几天空白区域的数据。

3.6 设置UI

有了日期就可以设置UI了,所以我们看下如何填充视图的。

extension CalendarView {
    func setUI() {
        
        //日历顶部按钮布局
        self.addSubview(acView)
        acView.snp.makeConstraints{make in
            make.leading.trailing.bottom.equalTo(0)
            make.height.equalTo(ScreenHeight*0.57)
        }
        
        // 加顶部横线 左上角默认位置
        acView.addSubview(lineView)
        self.lineViewLayout()
        
        // 加预约日期
        acView.addSubview(dateBtn)
        dateBtn.snp.makeConstraints{make in
            make.top.equalTo(11)
            make.leading.equalTo(28)
            make.width.equalTo(66)
            make.height.equalTo(22)
        }
        
        // 预约时间
        acView.addSubview(timeBtn)
        timeBtn.snp.makeConstraints{make in
            make.top.equalTo(11)
            make.centerX.equalToSuperview()
            make.width.equalTo(66)
            make.height.equalTo(22)
        }
        
        // 加取消按钮
        acView.addSubview(cancelBtn)
        cancelBtn.snp.makeConstraints{make in
            make.top.equalTo(11)
            make.trailing.equalTo(-16)
            make.width.equalTo(66)
            make.height.equalTo(22)
        }
        
        
        // 继续加滚动视图
        acView.addSubview(scrollView)
        scrollView.snp.makeConstraints{make in
            make.top.equalTo(dateBtn.snp.bottom).offset(10)
            make.leading.trailing.equalToSuperview()
            make.bottom.equalToSuperview()
        }
        
        // acView添加完毕,现在到内部ScrollView层级了--------------
    
        // 加个集合View 集合View因为初始化确定了y轴高度,这里无需重复布局
        scrollView.addSubview(collectionView)
        
        // 再加个pickerView 时间滚轮,这里直接加到ScrollView里面了,也无需布局,直接覆盖到上层的
        scrollView.addSubview(pickerView)
        
        // 先加工具栏
        scrollView.addSubview(dateToolView)
        dateToolView.snp.makeConstraints{make in
            make.top.leading.equalTo(0)
            make.width.equalTo(ScreenWidth)
            make.height.equalTo(40)
        }
        
        // 滚动视图再加星期视图
        scrollView.addSubview(weeksView)
        weeksView.snp.makeConstraints{make in
            make.top.equalTo(dateToolView.snp.bottom).offset(4)
            make.leading.equalTo(0)
            make.width.equalTo(ScreenWidth)
            make.height.equalTo(30)
        }
        
        
        // 处理下工具栏内部布局
        dateToolView.addSubview(monthLb)
        monthLb.snp.makeConstraints{make in
            make.centerY.equalToSuperview()
            make.centerX.equalToSuperview()
            make.width.equalTo(100)
            make.height.equalTo(20)
        }
        
        // 日历工具条加左按钮
        dateToolView.addSubview(toolLeftBtn)
        toolLeftBtn.snp.makeConstraints{make in
            make.centerY.equalToSuperview()
            make.width.height.equalTo(40)
            make.leading.equalTo(16)
        }

        // 日历工具条加右侧箭头
        dateToolView.addSubview(toolRightBtn)
        toolRightBtn.snp.makeConstraints{make in
            make.centerY.equalToSuperview()
            make.width.height.equalTo(40)
            make.trailing.equalTo(-16)
        }
        
        // 设置星期几内部布局
        setWeekUiInner()
        
        // 地址底部按钮
        addBottomUI()
    
    }

中规中矩,老老实实从上到下,从左到右布局。

内部星期几看下如何布局的:

func setWeekUiInner() {
        let arr = ["日","一","二","三","四","五","六"]

        for i in 0 ..< arr.count {
            let width = ScreenWidth/7
            let lab = UILabel()
            lab.text = arr[i]
            lab.textColor = UIColor.init(hex: "#323233")
            lab.font = .pingFangRegular(size: 12)
            lab.textAlignment = .center
            weeksView.addSubview(lab)
            lab.snp.makeConstraints{make in
                make.leading.equalTo(width*CGFloat(i))
                make.centerY.equalToSuperview()
                make.width.equalTo(width)
                make.height.equalTo(20)
            }
        }
    }

还有底部的下一步和完成:

func addBottomUI() {
        let nextBtn: UIButton = UIButton.init(type: .custom)
        nextBtn.frame = CGRect.init(x: 16, y: ScreenHeight*0.57*0.57+8+34+40, width: (ScreenWidth-32), height: 44)
        nextBtn.titleFont = .pingFangMedium(size: 16)
        nextBtn.setTitleColor(.white, for: .normal)
        nextBtn.backgroundColor = UIColor.init(hex: "#409EFF")
        nextBtn.setTitle("下一步", for: .normal)
        nextBtn.layer.cornerRadius = 22
        nextBtn.addTarget(self, action: #selector(actionForNext), for: .touchUpInside)
        scrollView.addSubview(nextBtn)
        
        let doneBtn: UIButton = UIButton.init(type: .custom)
        doneBtn.frame = CGRect.init(x: ScreenWidth+16, y: ScreenHeight*0.57*0.57+8+34+40, width: (ScreenWidth-32), height: 44)
        doneBtn.titleFont = .pingFangMedium(size: 16)
        doneBtn.setTitleColor(.white, for: .normal)
        doneBtn.backgroundColor = UIColor.init(hex: "#409EFF")
        doneBtn.setTitle("完成", for: .normal)
        doneBtn.layer.cornerRadius = 22
        doneBtn.addTarget(self, action: #selector(actionForDone), for: .touchUpInside)
        scrollView.addSubview(doneBtn)
    }

3.7 填充数据

这里先看下日历数据如何使用代理和数据源的:

extension CalendarView: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout{
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return modelArr.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarCell.identifier, for: indexPath) as! CalendarCell
        cell.model = modelArr[indexPath.item]
        cell.selectedModel = timeModel
        return cell
    }

    // 定义每个Cell的大小
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = ScreenWidth/7
        let height = modelArr.count > 35 ? (ScreenHeight*0.57*0.57)/6 : (ScreenHeight*0.57*0.57)/5
        return CGSize(width: width-2, height: height-1)
    }
    
    // 定义每个Section的四边间距
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 3.5, bottom: 0, right: 0)
    }
    
    // 这个是两行cell之间的间距(上下行cell的间距)
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 1.0
    }
    
    // 两个cell之间的间距(同一行的cell的间距)
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 1.0
    }
    
    // 选中某个ietm
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let model = modelArr[indexPath.item]
        let com = YSDateTool.currentDateCom()
        if com.year == model.year && com.month == model.month && model.day <= com.day! || model.day == 0{
            return
        }

        timeModel.year = model.year
        timeModel.month = model.month
        timeModel.day = model.day
        collectionView.reloadData()
    }
}

主要逻辑委托给Cell来实现了。后面再看下。

3.8 UIPickerView数据填充

然后看下预约时间如何填充数据的。

extension CalendarView:UIPickerViewDelegate,UIPickerViewDataSource {

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return timeArr.count
    }

    func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        return ScreenWidth
    }
    
    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        44
    }
    
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        
        //设置分割线 pickerView的subvews的第二个一般用来做分割线 subviews会有2个view
        for view in pickerView.subviews {
            print("view.height=\(view.size.height) 长度为:\(pickerView.subviews.count)")
            if view.size.height <= 50 {
                view.backgroundColor = .clear
                //自定义分割线
                let toplayer = CALayer()
                toplayer.frame = CGRect.init(x: 0, y: 0, width: ScreenWidth, height: 1)
                toplayer.backgroundColor = UIColor.init(hex: "#EBEDF0").cgColor
                view.layer.addSublayer(toplayer)

                let bottomlayer = CALayer()
                bottomlayer.frame = CGRect.init(x: 0, y: 44-1, width: ScreenWidth, height: 1)
                bottomlayer.backgroundColor = UIColor.init(hex: "#EBEDF0").cgColor
                view.layer.addSublayer(bottomlayer)
               
            }else{
                view.backgroundColor = .clear
            }
        }
        
        //设置内容样式
        var label = view as? UILabel
        if label == nil {
            label = UILabel.init()
            label?.backgroundColor = .clear
            label?.frame = CGRect.init(x: 0, y: 0, width:ScreenWidth, height:44)
        }
        if row == selectIndex {
            label?.textColor = UIColor.init(hex: "#409EFF")
        }else{
            label?.textColor = UIColor.init(hex: "#3B4058")
        }
        label?.textAlignment = .center
        label?.font = .pingFangRegular(size: 16)
        //label?.minimumScaleFactor = 0.5
        //label?.adjustsFontSizeToFitWidth = true
        label?.text = timeArr[row]
        return label!
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectIndex = row
        pickerView.reloadAllComponents()
    }
}

主要逻辑是viewForRow覆写的方法里面。
交代了如何绘制分割线和选中高亮显示。
这里有个点要注意,这个分割线用到了 UIPickerView的subViews,这里通常设置分割线都要用到这个东西,具体细节我不是很清楚,预测这个subViews第二个就是居中的item。第一个item很长,不能用来分割线。

然后就是didSelectRow用来响应用户点击事件,需要刷新下components。

3.9 交互事件

这里交互事件统一用一个扩展类来实现。

extension CalendarView {
    
    @objc func actionForChooseDate(){
        self.lineViewLayout()
        self.scrollDate()
    }
    
    @objc func actionForChooseTime(){
        self.lineViewLayout(leading: 1)
        self.scrollTime()
    }
    
    @objc func actionForCancel(){
        self.dismissAlertView()
    }
    
    @objc func actionForLeftEvent(){
        dynamicDate = YSDateTool.lastMonth(date: dynamicDate)
        self.isRefrenshMonth()
    }
    
    @objc func actionForRightEvent(){
        dynamicDate = YSDateTool.nextMonth(date: dynamicDate)
        self.isRefrenshMonth()
    }
    
    @objc func actionForNext(){
        self.lineViewLayout(leading: 1)
        self.scrollTime()
    }
    
    @objc func actionForDone(){
        if doneHandle != nil {
            let str = timeArr[selectIndex]
            let arr = str.components(separatedBy: CalendarView.range)
            timeModel.startTime = arr[0]
            timeModel.endTime = arr[1]
            
            let monthStr = timeModel.month < 10 ? "0\(timeModel.month)" : "\(timeModel.month)"
            let dayStr = timeModel.day < 10 ? "0\(timeModel.day)" : "\(timeModel.day)"
            timeModel.dateStr = "\(timeModel.year)-\(monthStr)-\(dayStr)"
            
            doneHandle(timeModel)
        }
        self.dismissAlertView()
    }
    
    // 滑动到日期
    func scrollDate(){
        dateBtn.isSelected = true
        timeBtn.isSelected = false
        scrollView.setContentOffset(CGPoint.init(x: 0, y: 0), animated: true)
    }
    
    // 滑动到时间
    func scrollTime(){
        dateBtn.isSelected = false
        timeBtn.isSelected = true
        scrollView.setContentOffset(CGPoint.init(x: ScreenWidth, y: 0), animated: true)
    }
    
    func isRefrenshMonth(){
        self.setDate(date: dynamicDate)
        let com = YSDateTool.currentDateCom(date: dynamicDate)
        monthLb.text = "\(com.year!)年\(com.month!)月"
        watermarkView.monthStr = "\(com.month!)"
        
        let currentCom = YSDateTool.currentDateCom()
        if (com.year! == currentCom.year!  && com.month! > currentCom.month!) || (com.year! > currentCom.year!){
            toolLeftBtn.isHidden = false
        }else{
            toolLeftBtn.isHidden = true
        }
    }
    
    func lineViewLayout(leading: Int = 0){
        lineView.snp.remakeConstraints{make in
            make.top.equalTo(0)
            if leading == 0 {
                make.leading.equalTo(leading)
            }else{
                make.centerX.equalToSuperview() // 居中了
            }
            make.width.equalTo(66+56) // 宽度
            make.height.equalTo(2)  // 高度
        }
    }
}

4 撸下时间工具

这个只是个单纯工具,不必重复造轮子。

class YSDateTool {
    // MARK: - 当前时间组件
    static func currentDateCom(date: Date = Date()) -> DateComponents{
        let calendar = Calendar.current
        let com = calendar.dateComponents([.year, .month, .day], from: date)
        return com
    }
    // MARK: - 今年
    static func currentYear(date: Date = Date()) -> Int {
        let calendar = NSCalendar.current
        let com = calendar.dateComponents([.year, .month, .day], from: date)
        return com.year!
    }
  
    // MARK: - 今月
    static func currentMonth(date: Date = Date()) -> Int {
        let calendar = NSCalendar.current
        let com = calendar.dateComponents([.year, .month, .day], from: date)
        return com.month!
    }
  
    // MARK: - 今日
    static func currentDay(date: Date = Date()) -> Int {
        let calendar = NSCalendar.current
        let com = calendar.dateComponents([.year, .month, .day], from: date)
        return com.day!
    }
    
    // MARK: - 今天星期几
    static func currentWeekDay(date: Date = Date()) -> Int {
        let calendar = NSCalendar.current
        let com = calendar.dateComponents([.weekday], from: date)
        return com.weekday!
    }
    
    // MARK: - 农历今年
    static func currentChineseYear(date: Date = Date()) -> String {
        let calendar = NSCalendar.init(calendarIdentifier: .chinese)
        let com = calendar?.components([.year], from: date)
        return numberToChina(yearNum: (com?.year)!) + "年"
    }
    
    // MARK: - 农历今月
    static func currentChineseMonth(date: Date = Date()) -> String {
        let calendar = NSCalendar.init(calendarIdentifier: .chinese)
        let com = calendar?.components([.month], from: date)
        return numberToChina(monthNum: (com?.month)!)
    }

    // MARK: - 农历今日
    static func currentChineseDay(date: Date = Date()) -> String {
        let calendar = NSCalendar.init(calendarIdentifier: .chinese)
        let com = calendar?.components([.day], from: date)
        return numberToChina(dayNum: (com?.day)!)
    }
    
    // MARK: - 农历今日
    static func currentChineseDayInt(date: Date = Date()) -> Int {
        let calendar = NSCalendar.init(calendarIdentifier: .chinese)
        let com = calendar?.components([.day], from: date)
        return (com?.day)!
    }
    
    // MARK: - 农历星期
    static func currentChineseWeekYear(date: Date = Date()) -> String {
        let calendar = NSCalendar.init(calendarIdentifier: .chinese)
        let com = calendar?.components([.weekday], from: date)
        return numberToChina(weekNum: (com?.weekday)!)
    }
  
    // MARK: - 下个月
    static func nextMonth(date: Date = Date()) -> Date {
        let calendar = NSCalendar.current
        let nDate = calendar.date(byAdding: DateComponents(month: 1), to: date)
        return nDate!
    }
    
    // MARK: - 上个月
    static func lastMonth(date: Date = Date()) -> Date {
        let calendar = NSCalendar.current
        let lDate = calendar.date(byAdding: DateComponents(month: -1), to: date)
        return lDate!
    }
    
    // MARK: - 下一天
    static func lastDay(date: Date = Date()) -> Date{
        let calendar = NSCalendar.current
        let lDate = calendar.date(byAdding: DateComponents(day: 1), to: date)
        return lDate!
    }
  
    // MARK: - 本月天数
    static func countOfDaysInCurrentMonth(date: Date = Date()) -> Int {
        let calendar = Calendar(identifier: Calendar.Identifier.gregorian)
        let range = (calendar as NSCalendar?)?.range(of: NSCalendar.Unit.day, in: NSCalendar.Unit.month, for: date)
        return (range?.length)!
    }
 
    // MARK: - 当月第一天是星期几
    static func firstWeekDayInCurrentMonth(date: Date = Date()) -> Int {
        // 星期和数字一一对应 星期日:7
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM"
        let date = dateFormatter.date(from: String(date.xj.year)+"-"+String(date.xj.month))
        let calender = Calendar(identifier: Calendar.Identifier.gregorian)
        let comps = (calender as NSCalendar?)?.components(NSCalendar.Unit.weekday, from: date!)
        var week = comps?.weekday
        if week == 1 {
            week = 8
        }
        return week! - 1
    }

    // MARK: - - 获取指定日期各种值
    // 根据年月得到某月天数
    static func getCountOfDaysInMonth(year: Int, month: Int) -> Int{
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM"
        let date = dateFormatter.date(from: String(year)+"-"+String(month))
        let calendar = Calendar(identifier: Calendar.Identifier.gregorian)
        let range = (calendar as NSCalendar?)?.range(of: NSCalendar.Unit.day, in: NSCalendar.Unit.month, for: date!)
        return (range?.length)!
    }
    
    // MARK: - 根据年月得到某月第一天是周几
    static func getfirstWeekDayInMonth(year: Int, month: Int) -> Int{
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM"
        let date = dateFormatter.date(from: String(year)+"-"+String(month))
        let calendar = Calendar(identifier: Calendar.Identifier.gregorian)
        let comps = (calendar as NSCalendar?)?.components(NSCalendar.Unit.weekday, from: date!)
        let week = comps?.weekday
        return week! - 1
    }

    // MARK: - date转日期字符串
    static func dateToDateString(_ date: Date, dateFormat: String) -> String {
        let timeZone = NSTimeZone.default
        let formatter = DateFormatter()
        formatter.timeZone = timeZone
        formatter.dateFormat = dateFormat
        let date = formatter.string(from: date)
        return date
    }

    // MARK: - 日期字符串转date
    static func dateStringToDate(_ dateStr: String) -> Date {
        let dateFormatter = DateFormatter()
        dateFormatter.timeZone = TimeZone(abbreviation: "UTC+8")
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let date = dateFormatter.date(from: dateStr)
        return date!
    }
    
    // MARK: - 时间字符串转date
    static func timeStringToDate(_ dateStr: String) -> Date {
        let dateFormatter = DateFormatter()
        dateFormatter.timeZone = TimeZone(abbreviation: "UTC+8")
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        let date = dateFormatter.date(from: dateStr)
        return date!
    }

    // MARK: - 计算天数差
    static func dateDifference(_ dateA: Date, from dateB: Date) -> Double {
        let interval = dateA.timeIntervalSince(dateB)
        return interval/86400
    }

    // MARK: - 比较时间先后
    static func compareOneDay(oneDay: Date, withAnotherDay anotherDay: Date) -> Int {
        let dateFormatter: DateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let oneDayStr: String = dateFormatter.string(from: oneDay)
        let anotherDayStr: String = dateFormatter.string(from: anotherDay)
        let dateA = dateFormatter.date(from: oneDayStr)
        let dateB = dateFormatter.date(from: anotherDayStr)
        let result: ComparisonResult = (dateA?.compare(dateB!))!
        // Date1 is in the future
        if(result == ComparisonResult.orderedDescending ) {
            return 1
        }
        // Date1 is in the past
        else if(result == ComparisonResult.orderedAscending) {
            return 2
        }
            // Both dates are the same
        else {
            return 0
        }
    }

    // MARK: - 时间与时间戳之间的转化
    // 将时间转换为时间戳
    static func stringToTimeStamp(_ stringTime: String) -> Int {
        let dfmatter = DateFormatter()
        dfmatter.dateFormat = "yyyy-MM-dd HH: mm: ss"
        dfmatter.locale = Locale.current
        let date = dfmatter.date(from: stringTime)
        let dateStamp: TimeInterval = date!.timeIntervalSince1970
        let dateSt: Int = Int(dateStamp)
        return dateSt
    }
    
    // 将时间戳转换为年月日
    static func timeStampToString(_ timeStamp: String) -> String {
        let string = NSString(string: timeStamp)
        let timeSta: TimeInterval = string.doubleValue
        let dfmatter = DateFormatter()
        dfmatter.dateFormat="yyyy年MM月dd日"
        let date = Date(timeIntervalSince1970: timeSta)
        return dfmatter.string(from: date)
    }
    
    // 将时间戳转换为具体时间
    static func timeStampToStringDetail(_ timeStamp: String) -> String {
        let string = NSString(string: timeStamp)
        let timeSta: TimeInterval = string.doubleValue
        let dfmatter = DateFormatter()
        dfmatter.dateFormat="yyyy年MM月dd日HH: mm: ss"
        let date = Date(timeIntervalSince1970: timeSta)
        return dfmatter.string(from: date)
    }
    
    // 将时间戳转换为时分秒
    static func timeStampToHHMMSS(_ timeStamp: String) -> String {
        let string = NSString(string: timeStamp)
        let timeSta: TimeInterval = string.doubleValue
        let dfmatter = DateFormatter()
        dfmatter.dateFormat="HH: mm: ss"
        let date = Date(timeIntervalSince1970: timeSta)
        return dfmatter.string(from: date)
    }
    
    // 将时间戳转换为时分
    static func timeStampToHHMM(_ timeStamp: String) -> String {
        let string = NSString(string: timeStamp)
        let timeSta: TimeInterval = string.doubleValue
        let dfmatter = DateFormatter()
        dfmatter.dateFormat="HH: mm"
        let date = Date(timeIntervalSince1970: timeSta)
        return dfmatter.string(from: date)
    }
    
    // 获取系统的当前时间戳
    static func getStamp(date: Date = Date()) -> Int{
        // 获取当前时间戳
        let timeInterval: Int = Int(date.timeIntervalSince1970)
        return timeInterval
    }
    
    // 日数字转汉字
    static func numberToChina(yearNum: Int) -> String {
        // 以一个天干和一个地支相配,排列起来,天干在前,地支在后,天干由甲起,地支由子起,阳干对阳支,阴干对阴支(阳干不配阴支,阴干不配阳支)
        // 就会得到六十年一周期的甲子回圈,一般称为“六十甲子”或“花甲子”
        // 十天干: 甲(jiǎ)、乙(yǐ)、丙(bǐng)、丁(dīng)、戊(wù)、己(jǐ)、庚(gēng)、辛(xīn)、壬(rén)、癸(guǐ)
        // 十二地支: 子(zǐ)、丑(chǒu)、寅(yín)、卯(mǎo)、辰(chén)、巳(sì)、午(wǔ)、未(wèi)、申(shēn)、酉(yǒu)、戌(xū)、亥(hài)
        let ChinaArray = [ "甲子", "乙丑", "丙寅", "丁卯", "戊辰", "己巳", "庚午", "辛未", "壬申", "癸酉",
                         "甲戌", "乙亥", "丙子", "丁丑", "戊寅", "己卯", "庚辰", "辛己", "壬午", "癸未",
                         "甲申", "乙酉", "丙戌", "丁亥", "戊子", "己丑", "庚寅", "辛卯", "壬辰", "癸巳",
                         "甲午", "乙未", "丙申", "丁酉", "戊戌", "己亥", "庚子", "辛丑", "壬寅", "癸丑",
                         "甲辰", "乙巳", "丙午", "丁未", "戊申", "己酉", "庚戌", "辛亥", "壬子", "癸丑",
                         "甲寅", "乙卯", "丙辰", "丁巳", "戊午", "己未", "庚申", "辛酉", "壬戌", "癸亥"]
        let ChinaStr: String = ChinaArray[yearNum - 1]
        return ChinaStr
    }
    
    // 月份数字转汉字
    static func numberToChina(monthNum: Int) -> String {
        let ChinaArray = ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
        let ChinaStr: String = ChinaArray[monthNum - 1]
        return ChinaStr
    }
    
    // 日数字转汉字
    static func numberToChina(dayNum: Int) -> String {
        let ChinaArray = [ "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
                         "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
                         "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"]
        let ChinaStr: String = ChinaArray[dayNum - 1]
        return ChinaStr
    }
      
    // 星期数字转汉字
    static func numberToChina(weekNum: Int) -> String {
        let ChinaArray = ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
        let ChinaStr: String = ChinaArray[weekNum - 1]
        return ChinaStr
    }
    
    // MARK: - 数字前补0
    static func add0BeforeNumber(_ number: Int) -> String {
        if number >= 10 {
            return String(number)
        }else{
            return "0" + String(number)
        }
    }
    
    static func getCurrentSystemDate() -> Date{
        let date = Date() // 获得时间对象
        let zone = NSTimeZone.system // 获得系统的时区
        let time = zone.secondsFromGMT(for: date)// 以秒为单位返回当前时间与系统格林尼治时间的差
        return date.addingTimeInterval(TimeInterval(time))// 然后把差的时间加上,就是当前系统准确的时间
    }
    
    static func date(_ date: String?, dateFormat: String = "yyyy-MM-dd") -> Date? {
        guard let date = date else {
            return nil
        }
        let dateformatter = DateFormatter()
        dateformatter.timeZone = TimeZone.init(secondsFromGMT: 0)
        dateformatter.dateFormat = dateFormat
        return dateformatter.date(from: date)
    }

    // MARK: - 将时间显示为(几分钟前,几小时前,几天前)
    static func compareCurrentTime(str: String) -> String {
        //let currentDateStr = dateToDateString(Date(), dateFormat: "yyyy-MM-dd hh:mm:ss")
        let currentDate = getCurrentSystemDate()
        let sendTimeDate = date(str,dateFormat: "yyyy-MM-dd HH:mm:ss")!//self.timeStringToDate(str)
        let sendTimeInterval = Double(getStamp(date: sendTimeDate))
        let timeInterval = currentDate.timeIntervalSince(sendTimeDate)

        var todayStr = dateToDateString(Date(), dateFormat: "yyyy-MM-dd")
        todayStr = todayStr+" 00:00"
        let todayTimeInterval = Double(Date().stringToSecondTimeStamp(todayStr)) ?? 0
        let yestDayTimeInterval = Double(todayTimeInterval - 86400)
        let nextDayTimeInterval = Double(todayTimeInterval + 86400)

        var temp: Double = 0
        var result: String = ""
        if timeInterval/60 < 1 {
            result = "刚刚"
        }else if (timeInterval/60) < 60{
            temp = timeInterval/60
            result = "\(Int(temp))分钟前"
        }else if sendTimeInterval > todayTimeInterval && sendTimeInterval < nextDayTimeInterval{

            //let interval = nextDayTimeInterval - sendTimeInterval
            temp = timeInterval/60/60
            result = "\(Int(temp))小时前"
        }else if sendTimeInterval > yestDayTimeInterval && sendTimeInterval < todayTimeInterval{
            //let str = timeStampToHHMM(String.init(format: "%d", sendTimeInterval))
            let timeStr = str.components(separatedBy: " ").last ?? ""
            let str = timeStr.prefix(5)
            result = "昨天\(str)"
        }else{
            let timeStr = str.components(separatedBy: " ").first ?? ""
            let timeArr = timeStr.components(separatedBy: "-")
            if !timeArr.isEmpty {
                result = "\(timeArr[0])年\(timeArr[1])月\(timeArr[2])日"
            }
            //result = timeStampToString(String.init(format: "%d", sendTimeInterval))
        }
        
//        else if timeInterval/(30 * 24 * 60 * 60) < 12 {
//            temp = timeInterval/(30 * 24 * 60 * 60)
//            result = "\(Int(temp))个月前"
//        }else{
//            temp = timeInterval/(12 * 30 * 24 * 60 * 60)
//            result = "\(Int(temp))年前"
//        }
        return result
    }
}

5 撸下最后的日历Cell

这个就是日历里面的item,还是相当简单的。

5.1 全局变量

class CalendarCell: UICollectionViewCell {
    
    static var identifier = "UICollectionViewCell"
    
    var isGreaterThan: Bool = false     //是否取大于今天的值
    
    var isShowPassTime: Int = 0         //是否设定禁选
    
    var model:TimeModel?{
        didSet{
            guard let _model = model else {return}
            if _model.day != 0 {
                titleLab.text = "\(_model.day)"
            }else{
                titleLab.text = "" // 为0,啥也不显示
            }
            
            let com = YSDateTool.currentDateCom()
            
            // 是否是选中的日子
            if com.year == _model.year && com.month == _model.month && com.day == _model.day {
                titleLab.textColor = UIColor.init(hex: "#409EFF")
            }else{
                titleLab.textColor = UIColor.init(hex: "#323233")
            }
            
            // 禁用还是可用
            if isShowPassTime == 0 {
                if isGreaterThan {
                    if (com.year == _model.year && com.month == _model.month && _model.day > com.day!) || (_model.year == com.year! && _model.month > com.month!) || (_model.year > com.year!){
                        titleLab.textColor = UIColor.init(hex: "#C8C9CC")
                    }
                }else{
                    // 如果要大于今天,那么前面的时间就的置灰了 前一个月的点不过去,不考虑了
                    if com.year == _model.year && com.month == _model.month && _model.day < com.day!{
                        titleLab.textColor = UIColor.init(hex: "#C8C9CC")
                    }
                }
            }
        }
    }
    
    // 选中的item,背景颜色高亮
    var selectedModel: TimeModel?{
        didSet{
            guard let _model = selectedModel else{return}
            if _model.year == model?.year && _model.month == model?.month && _model.day == model?.day{
                titleLab.backgroundColor = UIColor.init(hex: "#409EFF")
                titleLab.textColor = UIColor.white
            }else{
                titleLab.backgroundColor = UIColor.clear
            }
        }
    }

比较重要的是model,表示当前时间实体类。这里监听变量改变,利用didSet实现动态改变文案效果。

另外一个是selectedModel,这个是当前选中的item,会比对item是否为选中,选中样式会有所不同。

5.2 UI子View

这里只用到一个子View。

var titleLab: UILabel = {
        let lan = UILabel()
        lan.font = .pingFangRegular(size: 16)
        lan.textAlignment = .center
        lan.textColor = UIColor.init(hex: "#323233")
        
        lan.layer.cornerRadius = 4
        lan.layer.masksToBounds = true
        return lan
    }()

5.3 生命周期函数

override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .clear
        contentView.addSubview(titleLab)
        titleLab.snp.makeConstraints{make in
            make.top.bottom.leading.trailing.equalToSuperview()
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

把那个子View添加到Cell里面去了。

大概就是这样子。

6 总结

  • 如果UI图有那种Tab页,就可以考虑使用多种方案来实现,iOS直接用简单的Button也是可以实现的,分割线也是可以用最简单的UIView来实现,不一定要用封装好的框架,谁用挖掘机来搬小石头呢,大材小用了。

  • 分页效果可以使用UIScrollView来实现,把它宽度设置为屏幕的n倍,这样最有滑动可以很好控制的,不要动画也可以设置的,办法总是就很多的。

  • 日历效果可以用UICollectionView来实现,看着很复杂,其实很简单,主要逻辑也不复杂,控制好数据刷新就好了。UICollectionView可以实现类似网格的效果,我们只需要在Cell里面绘制一个一个Cell,空的部分,我们啥不展示就好了。

  • 系统的UIPickerView设置分割线很简单,利用UIPickerView下有一个subViews,这里寻找高度为item高度大小的,然后给view设置一个Layer即可。


   转载规则


《iOS-swift-自定义View之时间选择器》 Jason 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
iOS swift 如何实现扫码功能 iOS swift 如何实现扫码功能
1 需求定义这里有个需求,就是继承扫码能力,可以识别到条形码里面的内容。首先我们需要下载一个离线库,这个库里面会包含很多条码Code,相机通过识别到条码跟离线库中的匹配,如果匹配上了,会提示用户。所以首先我们肯定要有一个识别能力,数据库先不
2023-01-29
下一篇 
iOS-swift-自定义View之四级地址 iOS-swift-自定义View之四级地址
1 需求分析目标是可以选择四级地址,什么是四级地址呢?省市区街道。类似淘宝买东西时,支付订单时需要填写收货地址,这里就需要构造一个四级地址弹框,让用户去选择。具体效果如下: 这个我们可以单独将这个地址弹框单独封装一下,本身就是一个工具,很多
2023-01-27
  目录