前言

日常开发中经常会遇到日期选择,为了方便使用,简单封装了一个日历控件,在此抛砖引玉供大家参考。

效果

效果图

功能

原理

层次结构使用UIStackView布局,UICollectionView复用,背景使用DecorationView

核心代码

日历核心代码在于计算每个月的日期,主要代码如下:

func month() -> [CLCalendarMonthModel] {
    func day(with date: Date, section: Int) -> [CLCalendarDayModel] {
        var newDate = date
        let tatalDay = newDate.daysInMonth
        let firstDay = max(0, newDate.weekday - 1)
        let columns = Int(ceil(CGFloat(tatalDay + firstDay) / CGFloat(weekArray.count)))
        var resultArray = [CLCalendarDayModel]()
        for column in 0 ..< columns {
            for weekDay in 0 ..< weekArray.count {
                if column == 0,
                   weekDay <= firstDay - 1
                {
                    resultArray.append(CLCalendarDayModel())
                } else {
                    let subtitle: String? = {
                        guard !newDate.isToday else { return "今天" }
                        guard config.isShowLunarCalendar else { return nil }
                        guard let index = chinese.dateComponents([.day], from: newDate).day else { return nil }
                        return chineseDayArray[index - 1]
                    }()
                    let type: CLCalendarDayModel.CLCalendarDayType = {
                        guard !newDate.isToday else { return .today }
                        guard newDate.compare(todayDate) == .orderedDescending else { return .future }
                        return .past
                    }()
                    
                    let dayModel = CLCalendarDayModel(title: "\(newDate.day)", date: newDate, subtitle: subtitle, type: type)
                    resultArray.append(dayModel)
                    newDate = newDate + 1.days
                    if beginDate?.year == dayModel.date?.year,
                       beginDate?.month == dayModel.date?.month,
                       beginDate?.day == dayModel.date?.day
                    {
                        startIndexPath = .init(row: 0, section: section)
                    }
                    guard (resultArray.count - firstDay) != tatalDay else { break }
                }
            }
        }
        return resultArray
    }
    
    var resultArray = [CLCalendarMonthModel]()
    let month: Int = {
        var value = 0
        if config.type == .past {
            value = config.limitMonth - 1
        } else if config.type == .today {
            value = config.limitMonth / 2
        }
        return value
    }()
    
    let start = todayDate - month.months
    for i in 0 ..< config.limitMonth {
        let date = start + i.months
        let headerModel = CLCalendarMonthModel(headerText: date.format(with: "yyyy年MM月"),
                                               month: date.format(with: "MM"),
                                               daysArray: day(with: Date(year: date.year, month: date.month, day: 1), section: i))
        resultArray.append(headerModel)
    }
    return resultArray
}

基础配置

struct CLCalendarConfig {
    enum CLCalendarType {
        case past
        case today
        case future
    }

    enum CLSelectType {
        case single
        case area
    }

    struct CLTouchType: OptionSet {
        static let past = CLTouchType(rawValue: 1)
        static let today = CLTouchType(rawValue: 1 << 1)
        static let future = CLTouchType(rawValue: 1 << 2)
        let rawValue: Int64
        init(rawValue: Int64) {
            self.rawValue = rawValue
        }
    }

    struct CLColor {
        var background = "#ffffff".uiColor
        var topToolBackground = "#F4F4F4".uiColor
        var topToolText = "#444444".uiColor
        var topToolTextWeekend = "#3CCA79".uiColor
        var sectionBackgroundText = "f2f2f2".uiColor
        var selectStartBackground = "#4bce817f".uiColor
        var selectBackground = "#afe9c77f".uiColor
        var selectEndBackground = "#4bce817f".uiColor
        var todayText = "#32cd32".uiColor
        var titleText = "#555555".uiColor
        var subtitleText = "#555555".uiColor
        var selectTodayText = "#32cd32".uiColor
        var selectTitleText = "#ffffff".uiColor
        var selectSubtitleText = "#ffffff".uiColor
        var failureTitleText = "#a9a9a9".uiColor
        var failureSubtitleText = "#a9a9a9".uiColor
        var failureBackground = "#dcdcdc32".uiColor
    }

    var color = CLColor()
    var selectBegin: Date?
    var selectEnd: Date?
    var limitMonth = 12
    var type = CLCalendarType.today
    var selectType = CLSelectType.area
    var touchType: CLTouchType = [.today, .past]
    var isShowLunarCalendar = true
    var insetsLayoutMarginsFromSafeArea = true
    var headerHight = 50.0
}

总结

定制化开发请自行参考CLDemo修改 , 如果喜欢,欢迎star。

参考资料

  1. DateToolsSwift

  2. UICollectionView: 装饰视图 Decoration View

  3. 使用UIStackView来简化iOS的界面布局