I am trying to achieve Date Range selection using FSCalendar , and i am achieving that but the view i want that is not proper . so can anyone help for that ? I want output like below screenshot , but i am getting right now like this
My Current Output:
Desired output:
Viewcontroller.swift
class ViewController: UIViewController {
@IBOutlet weak var calendar: FSCalendar!
var firstDate: Date?
var lastDate: Date?
var datesRange: [Date]?
fileprivate let gregorian = Calendar(identifier: .gregorian)
fileprivate let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
let highlightedColorForRange = UIColor.init(red: 2/255, green: 138/255, blue: 75/238, alpha: 0.2)
override func viewDidLoad() {
super.viewDidLoad()
calendar.delegate = self
calendar.dataSource = self
calendar.today = nil
calendar.calendarHeaderView.backgroundColor = UIColor.lightGray
calendar.calendarWeekdayView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.1)
calendar.register(DIYCalendarCell.self, forCellReuseIdentifier: "cell")
calendar.allowsMultipleSelection = true
calendar.clipsToBounds = true
}
}
extension ViewController {
func configureVisibleCells() {
self.calendar.visibleCells().forEach { (cell) in
let date = self.calendar.date(for: cell)
let position = self.calendar.monthPosition(for: cell)
self.configureCell(cell, for: date, at: position)
}
}
func configureCell(_ cell: FSCalendarCell?, for date: Date?, at position: FSCalendarMonthPosition) {
let diyCell = (cell as! DIYCalendarCell)
// Configure selection layer
if position == .current {
var selectionType = SelectionType.none
if calendar.selectedDates.contains(date!) {
let previousDate = self.gregorian.date(byAdding: .day, value: -1, to: date!)!
let nextDate = self.gregorian.date(byAdding: .day, value: 1, to: date!)!
if calendar.selectedDates.contains(date!) {
if calendar.selectedDates.contains(previousDate) && calendar.selectedDates.contains(nextDate) {
diyCell.selectionLayer.fillColor = highlightedColorForRange.cgColor
selectionType = .middle
}
else if calendar.selectedDates.contains(previousDate) && calendar.selectedDates.contains(date!) {
selectionType = .single // .rightBorder
}
else if calendar.selectedDates.contains(nextDate) {
selectionType = .single // .leftBorder
}
else {
selectionType = .middle //.single
}
}
}
else {
selectionType = .none
}
if selectionType == .none {
diyCell.selectionLayer.isHidden = true
return
}
diyCell.selectionLayer.isHidden = false
diyCell.selectionType = selectionType
} else {
diyCell.selectionLayer.isHidden = true
}
}
func datesRange(from: Date, to: Date) -> [Date] {
// in case of the "from" date is more than "to" date,
// it should returns an empty array:
if from > to { return [Date]() }
var tempDate = from
var array = [tempDate]
while tempDate < to {
tempDate = Calendar.current.date(byAdding: .day, value: 1, to: tempDate)!
array.append(tempDate)
}
return array
}
}
extension ViewController:FSCalendarDelegate,FSCalendarDataSource,FSCalendarDelegateAppearance {
func calendar(_ calendar: FSCalendar, boundingRectWillChange bounds: CGRect, animated: Bool) {
self.calendar.frame.size.height = bounds.height
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
// nothing selected:
if firstDate == nil {
firstDate = date
datesRange = [firstDate!]
print("datesRange contains: \(datesRange!)")
configureVisibleCells()
return
}
// only first date is selected:
if firstDate != nil && lastDate == nil {
// handle the case of if the last date is less than the first date:
if date <= firstDate! {
calendar.deselect(firstDate!)
firstDate = date
datesRange = [firstDate!]
print("datesRange contains: \(datesRange!)")
configureVisibleCells()
return
}
let range = datesRange(from: firstDate!, to: date)
lastDate = range.last
for d in range {
calendar.select(d)
}
datesRange = range
print("datesRange contains: \(datesRange!)")
configureVisibleCells()
return
}
// both are selected:
if firstDate != nil && lastDate != nil {
for d in calendar.selectedDates {
calendar.deselect(d)
}
lastDate = nil
firstDate = nil
datesRange = []
print("datesRange contains: \(datesRange!)")
}
configureVisibleCells()
}
func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
}
func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell {
let cell = calendar.dequeueReusableCell(withIdentifier: "cell", for: date, at: position)
return cell
}
func calendar(_ calendar: FSCalendar, willDisplay cell: FSCalendarCell, for date: Date, at monthPosition: FSCalendarMonthPosition) {
self.configureCell(cell, for: date, at: monthPosition)
}
func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
return monthPosition == FSCalendarMonthPosition.current
}
func calendar(_ calendar: FSCalendar, shouldDeselect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
return false
}
func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
print("did deselect date \(self.formatter.string(from: date))")
configureVisibleCells()
}
}
DIYCalendarCell.swift
public var selectedColor = UIColor.init(red: 2/255, green: 138/255, blue: 75/255, alpha: 1)
enum SelectionType : Int {
case none
case single
case leftBorder
case middle
case rightBorder
}
class DIYCalendarCell: FSCalendarCell {
weak var circleImageView: UIImageView!
weak var selectionLayer: CAShapeLayer!
var selectionType: SelectionType = .none {
didSet {
setNeedsLayout()
}
}
required init!(coder aDecoder: NSCoder!) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
let selectionLayer = CAShapeLayer()
selectionLayer.fillColor = selectedColor.cgColor
selectionLayer.actions = ["hidden": NSNull()]
self.contentView.layer.insertSublayer(selectionLayer, below: self.titleLabel!.layer)
self.selectionLayer = selectionLayer
self.shapeLayer.isHidden = true
let view = UIView(frame: self.bounds)
self.backgroundView = view;
}
override func layoutSubviews() {
super.layoutSubviews()
// self.circleImageView.frame = self.contentView.bounds
self.backgroundView?.frame = self.bounds.insetBy(dx: 1, dy: 1)
self.selectionLayer.frame = self.contentView.bounds
if selectionType == .middle {
self.selectionLayer.path = UIBezierPath(rect: self.selectionLayer.bounds).cgPath
}
else if selectionType == .leftBorder {
self.selectionLayer.path = UIBezierPath(roundedRect: self.selectionLayer.bounds, byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: self.selectionLayer.frame.width / 2, height: self.selectionLayer.frame.width / 2)).cgPath
}
else if selectionType == .rightBorder {
self.selectionLayer.path = UIBezierPath(roundedRect: self.selectionLayer.bounds, byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSize(width: self.selectionLayer.frame.width / 2, height: self.selectionLayer.frame.width / 2)).cgPath
}
else if selectionType == .single {
let diameter: CGFloat = min(self.selectionLayer.frame.height, self.selectionLayer.frame.width)
self.selectionLayer.path = UIBezierPath(ovalIn: CGRect(x: self.contentView.frame.width / 2 - diameter / 2, y: self.contentView.frame.height / 2 - diameter / 2, width: diameter, height: diameter)).cgPath
}
}
override func configureAppearance() {
super.configureAppearance()
// Override the build-in appearance configuration
if self.isPlaceholder {
self.eventIndicator.isHidden = true
self.titleLabel.textColor = UIColor.lightGray
}
}
}
I just changed your code to support 3 (edited) shape layers:
weak var selectionLayer: CAShapeLayer?
weak var roundedLayer: CAShapeLayer?
weak var todayLayer: CAShapeLayer?
One for the range selection , one for the rounded layout and one for today.
import FSCalendar
import UIKit
enum SelectionType {
case none
case today
case single
case leftBorder
case middle
case rightBorder
}
class CalendarCell: FSCalendarCell {
private weak var circleImageView: UIImageView?
private weak var selectionLayer: CAShapeLayer?
private weak var roundedLayer: CAShapeLayer?
private weak var todayLayer: CAShapeLayer?
var selectionType: SelectionType = .none {
didSet {
setNeedsLayout()
}
}
required init!(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
let selectionLayer = CAShapeLayer()
selectionLayer.fillColor = UIColor.lightGray.cgColor
selectionLayer.actions = ["hidden": NSNull()]
self.contentView.layer.insertSublayer(selectionLayer, below: self.titleLabel?.layer)
self.selectionLayer = selectionLayer
let roundedLayer = CAShapeLayer()
roundedLayer.fillColor = UIColor.blue.cgColor
roundedLayer.actions = ["hidden": NSNull()]
self.contentView.layer.insertSublayer(roundedLayer, below: self.titleLabel?.layer)
self.roundedLayer = roundedLayer
let todayLayer = CAShapeLayer()
todayLayer.fillColor = UIColor.clear.cgColor
todayLayer.strokeColor = UIColor.orange.cgColor
todayLayer.actions = ["hidden": NSNull()]
self.contentView.layer.insertSublayer(todayLayer, below: self.titleLabel?.layer)
self.todayLayer = todayLayer
self.shapeLayer.isHidden = true
let view = UIView(frame: self.bounds)
self.backgroundView = view
}
override func layoutSubviews() {
super.layoutSubviews()
self.selectionLayer?.frame = self.contentView.bounds
self.roundedLayer?.frame = self.contentView.bounds
self.todayLayer?.frame = self.contentView.bounds
let contentHeight = self.contentView.frame.height
let contentWidth = self.contentView.frame.width
let selectionLayerBounds = selectionLayer?.bounds ?? .zero
let selectionLayerWidth = selectionLayer?.bounds.width ?? .zero
let roundedLayerHeight = roundedLayer?.frame.height ?? .zero
let roundedLayerWidth = roundedLayer?.frame.width ?? .zero
switch selectionType {
case .middle:
self.selectionLayer?.isHidden = false
self.roundedLayer?.isHidden = true
self.todayLayer?.isHidden = true
let selectionRect = selectionLayerBounds
.insetBy(dx: 0.0, dy: 4.0)
self.selectionLayer?.path = UIBezierPath(rect: selectionRect).cgPath
case .leftBorder:
self.selectionLayer?.isHidden = false
self.roundedLayer?.isHidden = false
self.todayLayer?.isHidden = true
let selectionRect = selectionLayerBounds
.insetBy(dx: selectionLayerWidth / 4, dy: 4)
.offsetBy(dx: selectionLayerWidth / 4, dy: 0.0)
self.selectionLayer?.path = UIBezierPath(rect: selectionRect).cgPath
let diameter: CGFloat = min(roundedLayerHeight, roundedLayerWidth)
let rect = CGRect(x: contentWidth / 2 - diameter / 2,
y: contentHeight / 2 - diameter / 2,
width: diameter,
height: diameter)
.insetBy(dx: 2.5, dy: 2.5)
self.roundedLayer?.path = UIBezierPath(ovalIn: rect).cgPath
case .rightBorder:
self.selectionLayer?.isHidden = false
self.roundedLayer?.isHidden = false
self.todayLayer?.isHidden = true
let selectionRect = selectionLayerBounds
.insetBy(dx: selectionLayerWidth / 4, dy: 4)
.offsetBy(dx: -selectionLayerWidth / 4, dy: 0.0)
self.selectionLayer?.path = UIBezierPath(rect: selectionRect).cgPath
let diameter: CGFloat = min(roundedLayerHeight, roundedLayerWidth)
let rect = CGRect(x: contentWidth / 2 - diameter / 2,
y: contentHeight / 2 - diameter / 2,
width: diameter,
height: diameter)
.insetBy(dx: 2.5, dy: 2.5)
self.roundedLayer?.path = UIBezierPath(ovalIn: rect).cgPath
case .single:
self.selectionLayer?.isHidden = true
self.roundedLayer?.isHidden = false
self.todayLayer?.isHidden = true
let diameter: CGFloat = min(roundedLayerHeight, roundedLayerWidth)
let rect = CGRect(x: contentWidth / 2 - diameter / 2,
y: contentHeight / 2 - diameter / 2,
width: diameter,
height: diameter)
.insetBy(dx: 2.5, dy: 2.5)
self.roundedLayer?.path = UIBezierPath(ovalIn: rect).cgPath
case .today:
self.selectionLayer?.isHidden = true
self.roundedLayer?.isHidden = true
self.todayLayer?.isHidden = false
let diameter: CGFloat = min(roundedLayerHeight, roundedLayerWidth)
let rect = CGRect(x: contentWidth / 2 - diameter / 2,
y: contentHeight / 2 - diameter / 2,
width: diameter,
height: diameter)
.insetBy(dx: 2.5, dy: 2.5)
self.todayLayer?.path = UIBezierPath(ovalIn: rect).cgPath
case .none:
self.selectionLayer?.isHidden = true
self.roundedLayer?.isHidden = true
self.todayLayer?.isHidden = true
}
}
}
And that's the final result.
You can check this Github project and see how it works.
Important: You need to handle the day select/deselect states to make sure everything is working, the snipped code is just about the UI.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With