JTAppleCalendarView是一个基于CollectionView可定制化的日历控件。虽然在实现效率上低的惊人--它是预先生成一个指定起始终止日期区间的有限长度的日期集合,再一并渲染到View上。相比于很多Android日历控件,这种设计思路还是很原始的。但是好在轮子造的还不错,可定制化也很友好,该有的功能都有了。所以不是追求性能的情况下还是可以拿来一用。
下图是一个简单的月模式的日历控件效果图。
image.png
这个控件实现了几个基本的日历效果:
- 显示一个月前后连续周的日期
- 非本月日期用暗色标注
- 一个日期选择标示(白色圆形)
- 日期标注(日期下面的白色圆点)
- 左右滑动
- 如果选择非本月日期则自动滑动到目标月份
- 一键返回当天
- 日历表头
JTAppleCalendarView的自定义化很简单,只需要设计一个CellView,这个例子使用了Xib生成的方式,目标代码如下:
import UIKit
import JTAppleCalendar
class UTKUICalendarCellView: JTAppleCell{
@IBOutlet weak var selectNoter: UTKUICircleBgView!
@IBOutlet weak var dayLabel: UILabel!
@IBOutlet weak var marker: UTKUICircleBgView!
var date:Date? //当前cell的日期
var isMarked = false //当前cell是否包含标记
var isChosen = false //cell是否被选中
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
//使用一个通用的init来加载xib
func commonInit() {
let nib = UINib(nibName: "UTKUICalendarCell", bundle: Bundle.main);
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
//自动匹配父控件的长宽
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addSubview(view)
marker.alpha = 0
}
func setDate(date:Date) {
self.date = date
}
//选中时的动画效果
func setSelected() {
self.isChosen = true
UIView.animate(withDuration: 0.2) {
self.selectNoter.transform = CGAffineTransform(scaleX: 1, y: 1)
let colorSelected = UIColor(red: 118/255.0, green: 204/255.0, blue: 211/255.0, alpha: 1)
self.dayLabel.textColor = colorSelected
}
}
//未选中时的动画效果
func setUnSelected(isThisMonth:Bool) {
self.isChosen = false
// UIView.animate(withDuration: 0.8) {
self.selectNoter.transform = CGAffineTransform(scaleX: 0, y: 0)
if isThisMonth {
self.dayLabel.textColor = UIColor.white
}
else {
self.dayLabel.textColor = UIColor(red: 102/255.0, green: 176/255.0, blue: 170/255.0, alpha: 1)
}
// }
}
//标记的动画效果
func setMarked() {
self.isMarked = true
UIView.animate(withDuration: 0.4) {
self.marker.alpha = 1
}
}
//无标记的动画效果
func setUnmarked() {
self.marker.alpha = 0
self.isMarked = false
}
}
整体上和普通的CollectionCellView一致
本例采用了一个简单的UIView拓展组件,大多数文章内谈论的是calendardelegate和datesource的设置,这里我们重点注释一下它的其他设置。
import UIKit
import JTAppleCalendar
//回调
protocol UTKUICalendarViewDelegate {
func onYearChanged(year:Int)
func onMonthChanged(month:Int)
func onToMarkDate(date:Date)
func onToDate(date:Date)
}
class UTKUICalendarView: UIView {
var delegate:UTKUICalendarViewDelegate?
let cellReuseId = "UTKUICalendar"
var markedDate:[Date] = []
var selectedDate = Date()
var today = Date()
var inDay = 1
var inMonth = 1
var inYear = 2014
let dateFormatter = DateFormatter()
var calendar:JTAppleCalendarView?
var calendarHeader:UIView?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
dateFormatter.dateFormat = "yyyy MM dd"
dateFormatter.timeZone = Calendar.current.timeZone
dateFormatter.locale = Calendar.current.locale
let strs = dateFormatter.string(from: today).split(separator: " ")
inMonth = Int(strs[1])!
inYear = Int(strs[0])!
if delegate != nil {
delegate?.onYearChanged(year: inYear)
delegate?.onMonthChanged(month: inMonth)
}
setup()
}
var isInited = false
//initiating calendar and header components
func setup() {
let headerXib = UINib(nibName: "UTKUICalendarHeader", bundle: Bundle.main)
calendarHeader = headerXib.instantiate(withOwner: self, options: nil)[0] as? UIView
calendar = JTAppleCalendarView(frame: CGRect(x: 0, y: 40, width: frame.width, height: frame.height-40))
//设置JTAppleCalendar的属性
setupCalendar()
//设置完毕之后默认初始选中日期是当天
backToday()
}
/*不要在这里设置calendar的属性,多次调用会引起错误*/
override func layoutSubviews() {
super.layoutSubviews()
calendarHeader?.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: 40)
self.addSubview(calendarHeader!)
calendar?.frame = CGRect(x: 0, y: 40, width: frame.width, height: frame.height-40)
self.addSubview(calendar!)
}
func setupCalendar() {
//首先注册cellview
calendar?.register(UTKUICalendarCellView.self, forCellWithReuseIdentifier: self.cellReuseId)
//然后注册delegate和datasource
calendar?.calendarDataSource = self
calendar?.calendarDelegate = self
//设置日历的一些基本格式,这里我们把所有的空全部去除
calendar?.minimumLineSpacing = 0
calendar?.minimumInteritemSpacing = 0
//这里设置为按月份滑动,另一种滑动模式是连续滑动,不按月份停留
calendar?.scrollingMode = ScrollingMode.stopAtEachCalendarFrame
//选择左右滑动
calendar?.scrollDirection = UICollectionViewScrollDirection.horizontal
//隐藏所有的滑动条
calendar?.showsVerticalScrollIndicator = false
calendar?.showsHorizontalScrollIndicator = false
calendar?.backgroundColor = UIColor(red: 118/255.0, green: 204/255.0, blue: 211/255.0, alpha: 1)
}
//返回当天
func backToday() -> Bool {
today = Date()
var ret = false
if dateFormatter.string(from: today) == dateFormatter.string(from: selectedDate) {
ret = false
}
else {
ret = true
}
//在这里只需要设置selectDates就会自动调用后面的didSelectedDate的delegate,建议在完成滚动之后再设置,否则容易出现cell被回收的问题
calendar?.scrollToDate(today, triggerScrollToDateDelegate: true, animateScroll: true, completionHandler: {
self.calendar?.selectDates([self.today])
self.selectedDate = self.today
//如果不放心可以调用下面这个函数重置所有的格子状态
// self.calendar?.reloadData() //failsafe solution to avoid nil cell deselect
})
return ret
}
func isThisMonth(date:Date) -> Int64 {
let formatter = DateFormatter()
formatter.dateFormat = "MM"
let month = Int(formatter.string(from: date))
formatter.dateFormat = "yyyy"
let year = Int(formatter.string(from: date))
if month! < inMonth && year! > inYear {
return 1 //next month
}
else if month! > inMonth && year! < inYear{
return -1 //previous month
}
else {
if month! < inMonth {
return -1
}
else if month! > inMonth {
return 1
}
else {
return 0
}
}
}
}
extension UTKUICalendarView:JTAppleCalendarViewDelegate {
//7.1版本之后作者建议调用WillDisplay,否则容易出现问题
func calendar(_ calendar: JTAppleCalendarView, willDisplay cell: JTAppleCell, forItemAt date: Date, cellState: CellState, indexPath: IndexPath) {
let dcell = cell as! UTKUICalendarCellView
//configCell配置cell的状态,这个与下面一个函数调用的过程是一样的
configCell(dateCell: dcell, date: date, label: cellState.text, state:cellState)
}
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date: Date, cellState: CellState, indexPath: IndexPath) -> JTAppleCell {
let cell = calendar.dequeueReusableJTAppleCell(withReuseIdentifier: self.cellReuseId, for: indexPath) as! UTKUICalendarCellView
configCell(dateCell: cell, date:date, label:cellState.text, state:cellState)
return cell
}
func configCell(dateCell:UTKUICalendarCellView, date:Date, label:String, state:CellState) {
dateCell.dayLabel.text = label
dateCell.setDate(date: date)
if markedDate.contains(date) {
dateCell.setMarked()
}
else {
dateCell.setUnmarked()
}
if (calendar?.selectedDates.count)! > 0 {
//判断当前日期是否被选中,可以采用下面两种方法
// if state.isSelected {
if dateFormatter.string(from: date) == dateFormatter.string(from:selectedDate) {
dateCell.setSelected()
}
else {
dateCell.setUnSelected(isThisMonth:state.dateBelongsTo == .thisMonth)
if state.dateBelongsTo != .thisMonth {
}
}
}
else {
dateCell.setUnSelected(isThisMonth: state.dateBelongsTo == .thisMonth)
}
}
func calendar(_ calendar: JTAppleCalendarView, didSelectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
if dateFormatter.string(from:date) != dateFormatter.string(from: selectedDate) {
selectedDate = date
//只有设置了selectDates后续的刷新才会正常
self.calendar?.selectDates([date])
let dateCell = cell as? UTKUICalendarCellView
if dateCell != nil {
//这里需要手动更新cell的view状态
dateCell?.setSelected()
if (dateCell?.isMarked)! && delegate != nil {
delegate?.onToMarkDate(date: date)
}
}
//下面的代码判断是否选中了其他月份的日期,并自动滑动
let res = isThisMonth(date: date)
if res == 1 {
self.calendar?.scrollToSegment(SegmentDestination.next)
}
else if res == -1 {
self.calendar?.scrollToSegment(SegmentDestination.previous)
}
if delegate != nil {
delegate?.onToDate(date: date)
}
}
}
func calendar(_ calendar: JTAppleCalendarView, didDeselectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
let dateCell = cell as? UTKUICalendarCellView
dateCell?.setUnSelected(isThisMonth: cellState.dateBelongsTo == .thisMonth)
}
//当滑动到新的月份时触发这个delegate,可以通过回调对外刷新年份与月份
func calendar(_ calendar: JTAppleCalendarView, didScrollToDateSegmentWith visibleDates: DateSegmentInfo) {
let inDate = visibleDates.monthDates[0].date
let str = dateFormatter.string(from: inDate)
let strs = str.split(separator: " ")
let year = Int(strs[0])!
let month = Int(strs[1])!
if delegate != nil {
if (month != inMonth) {
inMonth = month
delegate?.onMonthChanged(month: inMonth)
}
if (year != inYear) {
inYear = year
delegate?.onYearChanged(year: inYear)
}
}
}
}
extension UTKUICalendarView:JTAppleCalendarViewDataSource {
func configureCalendar(_ calendar: JTAppleCalendarView) -> ConfigurationParameters {
//在这里设置起始终止时间,时间长度越短加载越快
let startTime = dateFormatter.date(from: "2014 01 01")
let stopTime = dateFormatter.date(from: "2030 12 31")
let params = ConfigurationParameters(startDate: startTime!, endDate: stopTime!, numberOfRows: 6, calendar: Calendar.current, generateInDates: InDateCellGeneration.forAllMonths, generateOutDates: OutDateCellGeneration.tillEndOfGrid, firstDayOfWeek: .monday, hasStrictBoundaries: false)
return params
}
}
总体而言,JTAppleCalendarView的配置还是很简单的。CollectionView大部分的问题都会出现在当Cell被回收的时候带来的状态错乱。因此尽量使用控件自有的selectDates,CellState。此外,Calendar的高度会根据配置的参数调用layoutsubviews进行多次调整,因此大部分的设置与状态初始化需要避免在这个方法内进行。
网友评论