需求
在项目中要用到一个日历控件,它需要支持单选、多选、节选(选择首尾)日期的功能,在网上找了一下,我不喜欢项目中出现OC,而swift代码里没找到太合适的控件,就琢磨着自己写一个。
地址
https://github.com/xiaohepan/Calendar.git
结构
我想使用collectionView或者tableView来实现我的日历控件,现在已经写好了使用collectionView实现的控件,就以collectionView来讲这个过程。
- 以一个月为一个section,一天为一个cell,而collectionView的sectionHeader正好可以作为一个月的标题。在设置了startDate和endDate之后,要计算出有几个月,每个月有多少天。如果能够计算出这两个值collectionView的dataSource 要实现的两个方法就完成了:
numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int;
collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
- 接下来就要获得每天的信息,比如是否可以选择(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)
}
}
- 再下来,就要考虑不可选、单选、多选、节选(选择首尾)这四种选择模式。collectionView 有allowsSelection、allowsMultipleSelection两个属性,不可选的时候allowsSelection = false,单选的时候 allowsSelection = true ,allowsMultipleSelection = false,多选和节选的时候 allowsSelection = true,allowsMultipleSelection = true。节选的情况要我们自己做处理。我定义了一个枚举来表示这四种模式
public enum SelectionType:Int{
case None = 0,Single,Mutable,Section
}
具体实现
我把这些计算放在了一个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!
//日历类 - lazy var calendar:NSCalendar
//当天所在的NSIndexPath - private var todayIndex:NSIndexPath?
//选中的日子所在的位置的数组,单选保持0-1个值,多选 0-N个值,节选 0-2个值 - private var selectedDates = NSIndexPath
//格式化 - public let formatter:NSDateFormatter
//选择模式 - public var selectionType:SelectionType = .None
//日历开始的日期,计算属性 - public var startDate:NSDate
//日历结束的日期,计算属性 - public var endDate:NSDate
//有几个月 - 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()
}
}
}
网友评论