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.)

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
Fahim Parkar
  • 30,974
  • 45
  • 160
  • 276

3 Answers3

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

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
  • thank you for the answer. I will look into it and get back to you. – Fahim Parkar Apr 17 '18 at 10:23
  • do you have any idea what I can do to show 2 calendar in 1 screen? right now I have 1 month only showing... – Fahim Parkar Apr 22 '18 at 05:49
  • @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` – Ahmad F Apr 22 '18 at 06:50
  • @FahimParkar glad to hear that – Ahmad F Apr 24 '18 at 06:18
  • @AhmadF, can you please help me on this : https://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. – Kuldeep Jun 19 '18 at 11:15
  • @AhmadF can your please help me regarding this : https://github.com/WenchaoD/FSCalendar/issues/904 – Kuldeep Jul 25 '18 at 10:46
  • how to select range @AhmadF could u help me i m able to select only one date help me – Dilip Tiwari Apr 15 '19 at 11:28
  • @DilipTiwari have you followed my answer? if yes, what is the problem? – Ahmad F Apr 15 '19 at 16:11
  • i tried i m getting error in let range = datesRange(from: firstDate!, to: date) error :- Cannot call value of non-function type '[Date]' – Dilip Tiwari Apr 16 '19 at 03:44
  • 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` – Marty Jun 22 '19 at 17:51
  • @AhmadF this works great. But i want to color the range of dates dates as shown in the output. How to achieve that?? – TheTravloper Oct 09 '19 at 12:11
  • Thankyou So Much Bro save my life – Jealian Jan 20 '22 at 06:53
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
    }
}
lepsch
  • 8,927
  • 5
  • 24
  • 44
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];
    }
}
GNChishti
  • 39
  • 10