美文网首页iOS相关技术
我写一个IOS日历应用的过程-swift

我写一个IOS日历应用的过程-swift

作者: 鹤鹤 | 来源:发表于2016-04-12 23:38 被阅读2274次

需求

在项目中要用到一个日历控件,它需要支持单选、多选、节选(选择首尾)日期的功能,在网上找了一下,我不喜欢项目中出现OC,而swift代码里没找到太合适的控件,就琢磨着自己写一个。

地址

https://github.com/xiaohepan/Calendar.git

结构

我想使用collectionView或者tableView来实现我的日历控件,现在已经写好了使用collectionView实现的控件,就以collectionView来讲这个过程。

  1. 以一个月为一个section,一天为一个cell,而collectionView的sectionHeader正好可以作为一个月的标题。在设置了startDate和endDate之后,要计算出有几个月,每个月有多少天。如果能够计算出这两个值collectionView的dataSource 要实现的两个方法就完成了:
numberOfSectionsInCollectionView(collectionView:     UICollectionView) -> Int;
collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
  1. 接下来就要获得每天的信息,比如是否可以选择(selectable),是否已经选中(selected),是否是今天(isToday),是否在本月(isInMonth,因为要按星期排,所以在月初月末要用临近月份占坑)。所以我定义了一个结构体来表达这些信息。
public struct DayStateOptions:OptionSetType{
    
    public var rawValue:UInt
    
    public init(rawValue: UInt){
        self.rawValue = rawValue
    }
    public static var NotThisMonth:DayStateOptions{
        return DayStateOptions(rawValue: 1<<0)
    }
    public static var Today:DayStateOptions{
        return DayStateOptions(rawValue: 1<<1)
    }
    public static var UnSelectable:DayStateOptions{
        return DayStateOptions(rawValue: 1<<2)
    }
    public static var Selected:DayStateOptions{
        return DayStateOptions(rawValue: 1<<3)
    }
}
  1. 再下来,就要考虑不可选、单选、多选、节选(选择首尾)这四种选择模式。collectionView 有allowsSelection、allowsMultipleSelection两个属性,不可选的时候allowsSelection = false,单选的时候 allowsSelection = true ,allowsMultipleSelection = false,多选和节选的时候 allowsSelection = true,allowsMultipleSelection = true。节选的情况要我们自己做处理。我定义了一个枚举来表示这四种模式
public enum SelectionType:Int{
    case None = 0,Single,Mutable,Section
}

具体实现

我把这些计算放在了一个CalenadrDataSource类里,先看他的属性
//默认的开始日期

  1. public static let defaultFirstDate = NSDate(timeIntervalSince1970: -28800)
    //默认的结束日期
  2. public static let defaultLastDate = NSDate(timeIntervalSince1970:2145801599)
    //保存用户设置的开始日期
  3. private var firstDate:NSDate!
    //保存用户设置的结束日期
  4. private var lastDate:NSDate!
    //用户设置的开始日期的月初那一秒
  5. private var firstDateMonthBegainDate:NSDate!
    //用户设置的结束日期的月末那一秒
  6. private var lastDateMonthEndDate:NSDate!
    //日历类
  7. lazy var calendar:NSCalendar
    //当天所在的NSIndexPath
  8. private var todayIndex:NSIndexPath?
    //选中的日子所在的位置的数组,单选保持0-1个值,多选 0-N个值,节选 0-2个值
  9. private var selectedDates = NSIndexPath
    //格式化
  10. public let formatter:NSDateFormatter
    //选择模式
  11. public var selectionType:SelectionType = .None
    //日历开始的日期,计算属性
  12. public var startDate:NSDate
    //日历结束的日期,计算属性
  13. public var endDate:NSDate
    //有几个月
  14. public var monthCount:Int

只有calendar,formatter,selectionType,startDate,endDate是公开的,startDate,endDate是计算属性,根据这两个值,计算出firstDate,lastDate,firstDateMonthBegainDate,lastDateMonthEndDate,todayIndex,monthCount。

再看CalenadrDataSource的构造方法

//使用默认的开始和结束时间
    public convenience init(){
        self.init(startDate:CalenadrDataSource.defaultFirstDate,endDate:CalenadrDataSource.defaultLastDate)
    }
    //使用自定义的开始和结束时间
    public init(startDate:NSDate,endDate:NSDate){
        let result = startDate.compare(endDate)
        if result == .OrderedDescending {
            fatalError("startDate must smaller than endDate")
        }
        self.formatter = NSDateFormatter()
        self.startDate = startDate
        self.endDate = endDate
        formatter.dateFormat = "yyyy年MM月"
    }

再看其它业务方法:

    //多少年
    public func yearCount() -> Int
    {
    }
    //一星期多少天
    public func daysInWeek() -> Int{
        return calendar.maximumRangeOfUnit(.Weekday).length
    }
    //一个月多少天
    public func daysInMonth(monthIndex:Int) -> Int{
        return weeksInMonth(monthIndex) * daysInWeek()
    }
    //每天的信息
    public func dayState(indexPath:NSIndexPath) -> (NSDate,DayStateOptions){
    }
   //格式化日期
    public func StringDayFromDate(date:NSDate) -> String{
        return String(self.calendar.component(.Day, fromDate: date))
    }
   //格式化日期
    public func StringDateFromDate(date:NSDate) -> String{
        return formatter.stringFromDate(date)
    }
    //某个section的对应月份有几个星期
    public func weeksInMonth(monthIndex:Int) -> Int{
    }
    //把时时分分秒秒的信息改成0
    private func clampDate(date:NSDate, toComponents unitFlags:NSCalendarUnit) -> NSDate{
    }
    //某个section的对应月份开始那天
    private func firstOfMonthForSection(monthIndex:Int) -> NSDate
    {
    }
     //某个section的对应月份的信息 目前和firstOfMonthForSection差不多
    public func monthState(monthIndex:Int) -> NSDate
    {
    }
     //某日期所在的section
    func sectionForDate(date:NSDate) -> Int
    {
    }
     //某日期所在的NSIndexPath
    func indexPathForRowAtDate(date:NSDate) -> NSIndexPath
    {
    }
     //选中某天,返回true表示修改了selectedDates,collectionView需要刷新
    func didSelectItemAtIndexPath(indexPath:NSIndexPath) -> Bool{
    }
   //选中某天,返回true表示修改了selectedDates,collectionView需要刷新
    func didDeselectItemAtIndexPath(indexPath:NSIndexPath) -> Bool{
    }

有了这个类就能很方便的用collectionView写出日历控件了。但现在的实现有些差,还有很多要改进的地方。希望多指教。完整代码如下:

public class CalenadrDataSource
{
    
    public static let defaultFirstDate = NSDate(timeIntervalSince1970: -28800)
    public static let defaultLastDate = NSDate(timeIntervalSince1970:2145801599)
    //保存用户设置的开始日期
    private var firstDate:NSDate!
    //保存用户设置的结束日期
    private var lastDate:NSDate!
    //用户设置的开始日期的开始那一秒
    private var firstDateMonthBegainDate:NSDate!
    //用户设置的结束日期的结束那一秒
    private var lastDateMonthEndDate:NSDate!
    
    private lazy var calendar:NSCalendar = {
        //            let calendar =
        return NSCalendar.currentCalendar()
    }()
    
    private var todayIndex:NSIndexPath?
    
    var selectedDates = [NSIndexPath]()
    
    let formatter:NSDateFormatter
    
    var selectionType:SelectionType = .None{
        
        didSet{
            if selectionType == .None{
                selectedDates.removeAll()
                return
            }
            if selectedDates.count < 2 {
                return
            }
            switch (oldValue,selectionType) {
            case (.Mutable,.Single):
                selectedDates = Array(selectedDates.prefix(1))
            case (.Section,.Single):
                selectedDates = Array(selectedDates.prefix(1))
            case (.Mutable,.Section):
                selectedDates = [selectedDates.first!,selectedDates.last!]
            case (.Section,.Mutable):
               sectionToMutable()
            default:
                break
            }
        }
    }
    //日历开始的日期
    public var startDate:NSDate{
        set{
            if firstDate != nil && newValue.isEqualToDate(firstDate){
                return
            }
            if lastDate != nil{
                let result = newValue.compare(lastDate)
                if result == .OrderedDescending {
                    fatalError("startDate must smaller than endDate")
                }
                
            }
            
            firstDate =  clampDate(newValue, toComponents: [.Year,.Month,.Day])
            firstDateMonthBegainDate =  clampDate(newValue, toComponents: [.Month,.Year])
            if lastDate != nil{
                let today = NSDate()
                let result1 = today.compare(newValue)
                let result2 = today.compare(lastDate)
                if result1 != .OrderedAscending && result2 != .OrderedDescending{
                    todayIndex = indexPathForRowAtDate(today)
                }else{
                    todayIndex = nil
                }
            }
        }
        get{
            return firstDate
        }
    }
    
    //日历结束的日期
    public var endDate:NSDate{
        set{
            if lastDate != nil && newValue.isEqualToDate(lastDate){
                return
            }
            if firstDate != nil{
                let result = firstDate.compare(newValue)
                if result == .OrderedDescending {
                    fatalError("startDate must smaller than endDate")
                }
            }
            let components =  self.calendar.components([.Year,.Month,.Day],fromDate:newValue)
            components.hour = 23
            components.minute = 59
            components.second = 59
            lastDate = self.calendar.dateFromComponents(components)
            let firstOfMonth = self.clampDate(newValue, toComponents: [.Month,.Year])
            let offsetComponents = NSDateComponents()
            offsetComponents.month = 1
            let temp = self.calendar.dateByAddingComponents(offsetComponents, toDate: firstOfMonth, options: .WrapComponents)!
            lastDateMonthEndDate = NSDate(timeIntervalSince1970: temp.timeIntervalSince1970 - 1)
            if firstDate != nil{
                let today = NSDate()
                let result1 = today.compare(firstDate)
                let result2 = today.compare(newValue)
                if result1 != .OrderedAscending && result2 != .OrderedDescending{
                    todayIndex = indexPathForRowAtDate(today)
                    todayIndex?.section
                    todayIndex?.row
                }else{
                    todayIndex = nil
                }
            }
        }
        get{
            return lastDate
        }
    }
    
    
    
    
    func sectionToMutable(){
//        let length = self.calendar.components(.Day, fromDate: selectedDates[0], toDate: selectedDates[1], options: NSCalendarOptions(rawValue: 0)).day
//        selectedDates = Array<NSIndexPath>()
//        var date = selectedDates[0]
//        let offset = NSDateComponents()
//        offset.day = 1
//        for _ in 0 ..< length {
//            selectedDates.append(date)
//            date = self.calendar.dateByAddingComponents(offset, toDate: date, options: NSCalendarOptions(rawValue: 0))!
//        }
    }
    
    
    //使用默认的开始和结束时间
    public convenience init(){
        self.init(startDate:CalenadrDataSource.defaultFirstDate,endDate:CalenadrDataSource.defaultLastDate)
    }
    //使用自定义的开始和结束时间
    public init(startDate:NSDate,endDate:NSDate){
        let result = startDate.compare(endDate)
        if result == .OrderedDescending {
            fatalError("startDate must smaller than endDate")
        }
        self.formatter = NSDateFormatter()
        self.startDate = startDate
        self.endDate = endDate
        formatter.dateFormat = "yyyy年MM月"
    }
    
    //多少年
    public func yearCount() -> Int
    {
        let startYear = calendar.components(.Year,fromDate: CalenadrDataSource.defaultFirstDate, toDate: firstDateMonthBegainDate,options: .WrapComponents).year
        
        let endYear = calendar.components(.Year,fromDate: CalenadrDataSource.defaultFirstDate, toDate: lastDateMonthEndDate,options: .WrapComponents).year
        
        return endYear - startYear + 1
    }
    
    
    
    //一星期多少天
    public func daysInWeek() -> Int{
        return calendar.maximumRangeOfUnit(.Weekday).length
    }
    
    
    
    //多少月
    public var monthCount:Int{
        get{
            return calendar.components(.Month, fromDate: firstDateMonthBegainDate, toDate: lastDateMonthEndDate, options: .WrapComponents).month + 1
        }
    }
    
    //一个月多少天
    public func daysInMonth(monthIndex:Int) -> Int{
        return weeksInMonth(monthIndex) * daysInWeek()
    }
    //每天的信息
    public func dayState(indexPath:NSIndexPath) -> (NSDate,DayStateOptions){
        var options = DayStateOptions(rawValue:0)
        
        if todayIndex != nil{
            if indexPath.isEqual(todayIndex){
                options = [options,.Today]
            }
        }
        
        let firstOfMonth =  self.firstOfMonthForSection(indexPath.section)
        
        let ordinalityOfFirstDay =  1 - self.calendar.component(.Weekday, fromDate: firstOfMonth) + indexPath.row
        
        if ordinalityOfFirstDay < 0{
            options = [options,.NotThisMonth]
        } else{
            let maxRangeDay =  calendar.rangeOfUnit(.Day, inUnit: .Month, forDate: firstOfMonth).length
            if ordinalityOfFirstDay >= maxRangeDay{
                options = [options,.NotThisMonth]
            }else{
                let count = selectedDates.count
                switch count{
                case 0:
                    break
                case 2:
                    if selectionType == .Section{
                        let a = selectedDates[0].compare(indexPath)
                        NSLog("a = \(a)")
                        if a == .OrderedAscending {
                            let b = indexPath.compare(selectedDates[1])
                              NSLog("b = \(b)")
                            if b != .OrderedDescending{
                                options = [options,.Selected]
                            }
                        }else if a == .OrderedSame{
                             options = [options,.Selected]
                        }
                    }else{
                        fallthrough
                    }
                default:
                    if selectedDates.contains(indexPath){
                        options = [options,.Selected]
                    }
                }
            }
        }
        let  dateComponents = NSDateComponents()
        dateComponents.day = ordinalityOfFirstDay
        let date = self.calendar.dateByAddingComponents(dateComponents, toDate: firstOfMonth, options: NSCalendarOptions(rawValue: 0))!
        return (date,options)

    }

    public func StringDayFromDate(date:NSDate) -> String{
        return String(self.calendar.component(.Day, fromDate: date))
    }
    public func StringDateFromDate(date:NSDate) -> String{
        return formatter.stringFromDate(date)
    }
    
   
    
    //一个月多少星期
    public func weeksInMonth(monthIndex:Int) -> Int{
        let firstOfMonth = self.firstOfMonthForSection(monthIndex)
        let rangeOfWeeks = self.calendar.rangeOfUnit(.WeekOfMonth,inUnit: .Month,forDate: firstOfMonth).length
        return rangeOfWeeks
    }
    
    
    
    private func clampDate(date:NSDate, toComponents unitFlags:NSCalendarUnit) -> NSDate{
        let components = self.calendar.components(unitFlags,fromDate:date)
        return self.calendar.dateFromComponents(components)!
    }
    //一个月开始的时间
    private func firstOfMonthForSection(monthIndex:Int) -> NSDate
    {
        let offset = NSDateComponents()
        offset.month = monthIndex
        return self.calendar.dateByAddingComponents(offset, toDate: firstDateMonthBegainDate, options: NSCalendarOptions(rawValue: 0))!
    }
    
    
    public func monthState(monthIndex:Int) -> NSDate
    {
        let offset = NSDateComponents()
        offset.month = monthIndex
        return self.calendar.dateByAddingComponents(offset, toDate: firstDateMonthBegainDate, options: NSCalendarOptions(rawValue: 0))!
    }
    
    
    func sectionForDate(date:NSDate) -> Int
    {
        return self.calendar.components(.Month,fromDate:self.firstDateMonthBegainDate,toDate:date,options:.WrapComponents).month
    }
    
    func indexPathForRowAtDate(date:NSDate) -> NSIndexPath
    {
        let section = self.sectionForDate(date)
        let firstOfMonth = self.firstOfMonthForSection(section)
        
        let ordinalityOfFirstDay =  1 - self.calendar.component(.Weekday, fromDate: firstOfMonth)
        
        let  dateComponents = NSDateComponents()
        dateComponents.day = ordinalityOfFirstDay
        let startDateInSection = self.calendar.dateByAddingComponents(dateComponents, toDate: firstOfMonth, options: NSCalendarOptions(rawValue: 0))!
        
        let row = self.calendar.components(.Day, fromDate: startDateInSection, toDate: date, options: NSCalendarOptions(rawValue: 0)).day
        return NSIndexPath(forRow:row,inSection:section)
    }
    
    func didSelectItemAtIndexPath(indexPath:NSIndexPath) -> Bool{
        
        if selectedDates.contains(indexPath){
            return false
        }
        switch selectionType {
        case .None:
            return false
        case .Single:
            if selectedDates.count == 0{
                selectedDates.append(indexPath)
            }else{
                selectedDates[0] = indexPath
            }
        case .Mutable:
            selectedDates.append(indexPath)
        case .Section:
            if selectedDates.count == 0{
                selectedDates.append(indexPath)
            }else if selectedDates.count == 1{
                let result = selectedDates[0].compare(indexPath)
                switch result {
                case .OrderedSame:
                    return false
                case .OrderedAscending:
                    selectedDates.append(indexPath)
                case .OrderedDescending:
                   selectedDates.insert(indexPath, atIndex: 0)
                }
            }else{
                selectedDates.removeAll()
                selectedDates.append(indexPath)
            }
        }
        NSLog("didSelectItemAtIndexPath \(selectedDates)")
        return true
    }
    func didDeselectItemAtIndexPath(indexPath:NSIndexPath) -> Bool{
        if let index = selectedDates.indexOf(indexPath){
            selectedDates.removeAtIndex(index)
             NSLog("didDeselectItemAtIndexPath \(selectedDates)")
            return true
        }
        return false
    }
    
}

public struct DayStateOptions:OptionSetType{
    
    public var rawValue:UInt
    
    public init(rawValue: UInt){
        self.rawValue = rawValue
    }
    public static var NotThisMonth:DayStateOptions{
        return DayStateOptions(rawValue: 1<<0)
    }
    public static var Today:DayStateOptions{
        return DayStateOptions(rawValue: 1<<1)
    }
    public static var UnSelectable:DayStateOptions{
        return DayStateOptions(rawValue: 1<<2)
    }
    public static var Selected:DayStateOptions{
        return DayStateOptions(rawValue: 1<<3)
    }
}
public enum SelectionType:Int{
    case None = 0,Single,Mutable,Section
}
class TimeSelectorVC: UICollectionViewController
{
    
    private var  dataSourceManager:CalenadrDataSource
    
    var selectionType:SelectionType = .Section{
        didSet{
            guard oldValue  != selectionType else{
                return
            }
            initSelectionType()
        }
    }
    
    
    required init?(coder aDecoder: NSCoder) {
        dataSourceManager = CalenadrDataSource()
        super.init(coder: aDecoder)
    }
    
    override init(collectionViewLayout layout: UICollectionViewLayout) {
        dataSourceManager = CalenadrDataSource()
        super.init(collectionViewLayout: layout)
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        dataSourceManager = CalenadrDataSource()
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        initSelectionType()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
  
    }
    
    func initSelectionType(){
        switch selectionType {
        case .None:
            self.collectionView?.allowsSelection = false
            dataSourceManager.selectionType = .None
        case .Single:
            self.collectionView?.allowsSelection = true
            self.collectionView?.allowsMultipleSelection = false
            dataSourceManager.selectionType = .Single
        case .Mutable:
            self.collectionView?.allowsSelection = true
            self.collectionView?.allowsMultipleSelection = true
            dataSourceManager.selectionType = .Mutable
        case .Section:
            self.collectionView?.allowsSelection = true
            self.collectionView?.allowsMultipleSelection = true
            dataSourceManager.selectionType = .Section
        }

    }
    
    // MARK: UICollectionViewDataSource
    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return dataSourceManager.monthCount
    }


    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataSourceManager.daysInMonth(section)
    }

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("DayCell", forIndexPath: indexPath)
        let (date,dayState) = dataSourceManager.dayState(indexPath)
        if let label = cell.viewWithTag(1) as? UILabel
        {
            if dayState.contains(.NotThisMonth)
            {
                label.hidden = true
                cell.backgroundColor = UIColor.whiteColor()
            }else{
                label.hidden = false
                label.text = dataSourceManager.StringDayFromDate(date)
                if dayState.contains(.Selected){
                
                    if dayState.contains(.Today){
                       cell.backgroundColor = UIColor.redColor()
                    }else{
                       cell.backgroundColor = UIColor.blueColor()
                    }
                    label.textColor = UIColor.whiteColor()
                }else{
                    cell.backgroundColor = UIColor.whiteColor()
                    if dayState.contains(.Today){
                        label.textColor = UIColor.redColor()
                    }else{
                        label.textColor = UIColor.blackColor()
                    }
                }
            }
        }
        return cell
    }

    override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView
    {
        let cell = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "SectionCell", forIndexPath: indexPath)
        if let label = cell.viewWithTag(1) as? UILabel
        {
            label.text = dataSourceManager.StringDateFromDate(dataSourceManager.monthState(indexPath.section))
        }
        
        return cell
    }
    
    // MARK: UICollectionViewDelegate
    override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        let (_,dayState) = dataSourceManager.dayState(indexPath)
        if dayState.contains(.NotThisMonth) || dayState.contains(.UnSelectable){
            return false
        }
        return true
    }

    override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
        if dataSourceManager.didSelectItemAtIndexPath(indexPath){
            self.collectionView?.reloadData()
        }
    }
    
    override func collectionView(collectionView: UICollectionView, shouldDeselectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        return true
    }
    
    override func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
        if dataSourceManager.didDeselectItemAtIndexPath(indexPath){
            self.collectionView?.reloadData()
        }
    }
}

相关文章

网友评论

  • liekkas1026:楼主,看到能不能给个demo,麻烦了,我的邮箱是1034478678@qq.com,正好最近有这个需求
    bc2e0ef905d2:大神 请问怎么设置结束时间为当前时间加30天呢
    鹤鹤:demo在https://github.com/xiaohepan/Calendar.git。麻烦您自己去克隆下
  • loveric:就冲不喜欢加OC,关注下~~哈哈
  • Lawrenceo0:写的不错,传个demo上来吧
    鹤鹤:demo地址 https://github.com/xiaohepan/Calendar.git
  • 29f10adf0167:楼主怎么就贴代码啊

本文标题:我写一个IOS日历应用的过程-swift

本文链接:https://www.haomeiwen.com/subject/oxewlttx.html