美文网首页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()
            }
        }
    }
    

    相关文章

      网友评论

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

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

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