How to Select Range - FSCalendar in Swift?
Asked Answered
S

4

9

I am trying to use FSCalendar in one of my project to selecting range of dates.

What I found is this library have Swift version however range selection version is only available with Objective-C. So what I tried to make is using bridging, however I am unable to use the RangePickerViewController in Swift.

Did anyone implemented this library for Swift for using date range? (e.x. I want to select 2 dates as range for flight app where I am select Departure & Return flight dates.)

Sparker answered 16/4, 2018 at 11:37 Comment(0)
K
34

Although FSCalendar does not directly supports range selection, it does allow multiple selections, which means that at some point you would be able to handle the range selection by yourself.

So, in the viewDidLoad() you have to make sure that you set the calendar allowsMultipleSelection property to true:

private weak var calendar: FSCalendar!

override func viewDidLoad() {
    super.viewDidLoad()
    
    calendar.allowsMultipleSelection = true
}

The view controller should conform to FSCalendarDelegate protocol for handling the logic of selecting/deselecting a range.

What we need so far is to declare two dates for the range (the starting date and the ending date):

// first date in the range
private var firstDate: Date?
// last date in the range
private var lastDate: Date?

also an array of dates to hold value dates between firstDate and lastDate:

private var datesRange: [Date]?

and then implement the didSelect date and the didDeselect date delegate methods as:

func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
    // nothing selected:
    if firstDate == nil {
        firstDate = date
        datesRange = [firstDate!]
        
        print("datesRange contains: \(datesRange!)")
        
        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!)")
            
            return
        }
        
        let range = datesRange(from: firstDate!, to: date)

        lastDate = range.last
        
        for d in range {
            calendar.select(d)
        }
        
        datesRange = range
        
        print("datesRange contains: \(datesRange!)")
        
        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!)")
    }
}

func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
    // both are selected:
    
    // NOTE: the is a REDUANDENT CODE:
    if firstDate != nil && lastDate != nil {
        for d in calendar.selectedDates {
            calendar.deselect(d)
        }
        
        lastDate = nil
        firstDate = nil
        
        datesRange = []
        print("datesRange contains: \(datesRange!)")
    }
}

What about datesRange method?

I did not mention it in my answer for the purpose of making it shorter, all you have to do is to copy-paste it from this answer.

Output:

enter image description here

Kendrickkendricks answered 17/4, 2018 at 7:9 Comment(12)
thank you for the answer. I will look into it and get back to you. – Sparker
do you have any idea what I can do to show 2 calendar in 1 screen? right now I have 1 month only showing... – Sparker
@FahimParkar if you are aiming to add two calendars in the same view controller, you should check which one are you talking to in the delegate methods, by saying if calendar === firstCalendar – Kendrickkendricks
@FahimParkar glad to hear that πŸ‘ – Kendrickkendricks
@AhmadF, can you please help me on this : github.com/WenchaoD/FSCalendar/issues/904 I am able to implement Range Picker by converting Objective C code to Swift but Start Date selection color is not changing until end date selection. – Ashly
@AhmadF can your please help me regarding this : github.com/WenchaoD/FSCalendar/issues/904 – Ashly
how to select range @AhmadF could u help me i m able to select only one date help me – Mignonne
@DilipTiwari have you followed my answer? if yes, what is the problem? – Kendrickkendricks
i tried i m getting error in let range = datesRange(from: firstDate!, to: date) error :- Cannot call value of non-function type '[Date]' – Mignonne
Instead of keeping an array of dates in the range, why not just check each date on the fly like return date <= endDate && date >= startDate – Helpmeet
@AhmadF this works great. But i want to color the range of dates dates as shown in the output. How to achieve that?? – Snider
Thankyou So Much Bro save my life – Karinkarina
L
3
class CalendarDelegate: NSObject, FSCalendarDelegate {
  
    func calendar(_ calendar: FSCalendar, shouldDeselect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
        performDateDeselect(calendar, date: date)
        return true
    }
    
    func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
        performDateSelection(calendar)
    }
    
    private func performDateDeselect(_ calendar: FSCalendar, date: Date) {
        let sorted = calendar.selectedDates.sorted { $0 < $1 }
        if let index = sorted.firstIndex(of: date)  {
            let deselectDates = Array(sorted[index...])
            calendar.deselectDates(deselectDates)
        }
    }
    
    private func performDateSelection(_ calendar: FSCalendar) {
        let sorted = calendar.selectedDates.sorted { $0 < $1 }
        if let firstDate = sorted.first, let lastDate = sorted.last {
            let ranges = datesRange(from: firstDate, to: lastDate)
            calendar.selectDates(ranges)
        }
    }
    
    func datesRange(from: Date, to: Date) -> [Date] {
        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
    }
}
Lebensraum answered 27/1, 2020 at 18:7 Comment(0)
H
0

I converted @Ahmed F's code to Objective C.

Create following variables

NSDate *firstDate;
NSDate *lastDate;
NSMutableArray *datesRange;

add following line in viewDidLoad

datesRange = [NSMutableArray array];
calendar.allowsMultipleSelection = YES;

and functions for FSCalendar

- (NSArray <NSDate *> *)datesRange:(NSDate *)from andTo:(NSDate *)to {
    if([from isLaterThan:to]) {
        return [NSArray array];
    }
    NSDate *tempDate = from;
    NSMutableArray <NSDate *> *arrDates = [NSMutableArray array];
    [arrDates addObject:tempDate];
    NSDateComponents *component = [[NSDateComponents  alloc] init];
    component.day = 1;
    while ([tempDate isEarlierThan:to]) {
        tempDate  = [[NSCalendar currentCalendar] dateByAddingComponents:component toDate:tempDate options:0];
        [arrDates addObject:tempDate];
    }
    return [NSArray arrayWithArray:arrDates];
}

- (void)calendar:(FSCalendar *)calendar didSelectDate:(NSDate *)date atMonthPosition:(FSCalendarMonthPosition)monthPosition {
    if (firstDate == nil) {
        firstDate = date;
        datesRange = [NSMutableArray arrayWithObject:firstDate];
        return;
    }
    
    if(firstDate != nil && lastDate == nil) {
        if ([date isEarlierThan:firstDate]) {
            [calendar deselectDate:firstDate];
            firstDate = date;
            datesRange = [NSMutableArray arrayWithObject:firstDate];
            return;
        }
        
        NSArray<NSDate *> *range = [self datesRange:firstDate andTo:date];
        lastDate = [range lastObject];
        
        for (NSDate *d in range) {
            [calendar selectDate:d];
        }
        datesRange = [NSMutableArray arrayWithArray:range];
        return;
    }
    
    if (firstDate != nil && lastDate != nil) {
        for (NSDate *d in calendar.selectedDates) {
            [calendar deselectDate:d];
        }
        lastDate = nil;
        firstDate = nil;
        [datesRange removeAllObjects];
    }
}

- (void)calendar:(FSCalendar *)calendar didDeselectDate:(NSDate *)date atMonthPosition:(FSCalendarMonthPosition)monthPosition {
    if (firstDate != nil && lastDate != nil) {
        for (NSDate *d in calendar.selectedDates) {
            [calendar deselectDate:d];
        }
        lastDate = nil;
        firstDate = nil;
        [datesRange removeAllObjects];
    }
}
Henke answered 10/8, 2021 at 13:29 Comment(0)
F
0

Please check here: Video GIF

import UIKit
import FSCalendar

class AppointmentCalenderVC: UIViewController, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance {
    
    // MARK: - Outlets
    @IBOutlet weak var calendarFS: FSCalendar!
    
    // MARK: - Variables
    var callBack: ((_ days: [String]) -> ())?
    var selectedCategory = [String]()
    var multipleDates: [String] = []
    var categorySelect = String()
    var date: String?
    var dateDoctor = Int()
    var currentDate = ""
    var arrEventDate = [Mycallender]()
    var datesArray = [DateRanges]()
    var checkID = [Int]()
    var isSelectRange = false
    private var firstDate: Date?
    private var lastDate: Date?
    private var datesRange: [Date]?
    private var datesRangeNew: [Date]?
    
    fileprivate lazy var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter
    }()
    
    // MARK: - ViewDidLoad
    override func viewDidLoad() {
        super.viewDidLoad()
        
        calendarFS.delegate = self
        calendarFS.dataSource = self
        calendarFS.appearance.todayColor = #colorLiteral(red: 0.9826683402, green: 0.3910637498, blue: 0.3135554194, alpha: 0.5971775429)
        calendarFS.accessibilityIdentifier = "calendar"
        calendarFS.allowsMultipleSelection = true
        
        for event in arrEventDate {
            let dateString = event.dates
            let dateFormatter = DateFormatter()
            dateFormatter.locale = Locale(identifier: "en_US_POSIX")
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.000Z"
            if let date = dateFormatter.date(from: dateString) {
                let formattedDate = DateFormatter()
                formattedDate.dateFormat = "yyyy-MM-dd"
                let drawDate = formattedDate.string(from: date)
                selectedCategory.append(drawDate)
            }
        }
        
        datesRangeNew = convertStrToDateArr(strArr: multipleDates)
        datesRange = convertStrToDateArr(strArr: multipleDates)
        
        for date in datesRange ?? [] {
            calendarFS.select(date)
        }
        
        calendarFS.reloadData()
    }
    
    func convertStrToDateArr(strArr: [String]) -> [Date] {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let dates = strArr.compactMap { dateFormatter.date(from: $0) }
        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date())
        let futureDates = dates.filter { $0 >= today }
        return futureDates
    }
    
    func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
        if firstDate == nil {
            firstDate = date
            datesRange = [firstDate!]
            return
        }
        
        if firstDate != nil && lastDate == nil {
            if date <= firstDate! {
                calendar.deselect(firstDate!)
                firstDate = date
                datesRange = [firstDate!]
                return
            }
            let range = datesRanges(from: firstDate!, to: date)
            lastDate = range.last
            for d in range {
                calendar.select(d)
            }
            datesRange = range + (datesRangeNew ?? [])
            let randomNumber = arc4random()
            datesArray.append(DateRanges(rangeID: Int(randomNumber), firstDate: firstDate ?? Date(), lastDate: lastDate ?? Date(), dates: datesRange ?? []))
            multipleDates = isSelectRange ? (multipleDates + covertDatesToStrArray(dateArr: datesRange!)) : covertDatesToStrArray(dateArr: datesRange!)
            isSelectRange = true
            firstDate = nil
            lastDate = nil
            return
        }
        
        if firstDate != nil && lastDate != nil {
            datesRange?.append(date)
        }
    }
    
    func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
        datesRange = (datesRange ?? []) + (datesRangeNew ?? [])
        datesRange = unique(source: datesRange ?? [])
        for (i, d) in (datesRange ?? []).enumerated() {
            if date == d {
                datesRange?.remove(at: i)
                calendar.deselect(date)
                break
            }
        }
        multipleDates = covertDatesToStrArray(dateArr: calendarFS.selectedDates)
        calendarFS.reloadData()
        print(calendarFS.selectedDates, "selectedDates")
    }
    
    func minimumDate(for calendar: FSCalendar) -> Date {
        return Date()
    }
    
    func covertDatesToStrArray(dateArr: [Date]) -> [String] {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        dateFormatter.locale = Locale.current
        dateFormatter.timeZone = TimeZone.current
        let stringDates = dateArr.map { dateFormatter.string(from: $0) }
        return stringDates
    }
    
    func datesRanges(from: Date, to: Date) -> [Date] {
        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
    }
    
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, fillDefaultColorFor date: Date) -> UIColor? {
        let dateString = dateFormatter.string(from: date)
        if multipleDates.contains(dateString) {
            return nil
        } else {
            let weekday = Calendar.current.component(.weekday, from: date)
            if weekday == 7 {
                return UIColor.white
            }
        }
        return nil
    }
    
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, titleDefaultColorFor date: Date) -> UIColor? {
        let dateString = dateFormatter.string(from: date)
        let weekday = Calendar.current.component(.weekday, from: date)
        if multipleDates.contains(dateString) {
            return UIColor.white
        } else if weekday == 7 {
            return UIColor.black
        } else {
            return UIColor.white
        }
    }
    
    // DOT ON EVENTS
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, eventDefaultColorsFor date: Date) -> [UIColor]? {
        let key = dateFormatter.string(from: date)
        if selectedCategory.contains(key) {
            return [UIColor.systemYellow, UIColor.red, UIColor.systemYellow]
        }
        return nil
    }
    
    @IBAction func crossBtn(_ sender: Any) {
        dismiss(animated: true)
    }
    
    @IBAction func doneBtn(_ sender: UIButton) {
        dismiss(animated: true)
        var allDates = multipleDates // selectedCategory + multipleDates
        allDates = unique(source: allDates)
        categorySelect = allDates.map { String($0) }.joined(separator: ", ")
        print(categorySelect)
        callBack?(allDates)
    }
}

struct DateRanges {
    var rangeID: Int
    var firstDate: Date
    var lastDate: Date
    var dates: [Date]
    
    init(rangeID: Int, firstDate: Date, lastDate: Date, dates: [Date]) {
        self.rangeID = rangeID
        self.firstDate = firstDate
        self.lastDate = lastDate
        self.dates = dates
    }
}
Firer answered 21/6, 2024 at 9:21 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center. – Kielce

© 2022 - 2025 β€” McMap. All rights reserved.