'How to customise cell in FSCalendar?
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
}
}
}
Solution 1:[1]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |


