Hey Jay, I currently added JTAppleCalendar v 7.0 to my app and I'm loving it! Thanks for the great work and saving the dev community a ton of time. I feel lame asking this but haven't found a way to solve my issue, which seems to be like something that's super simple that I'm not doing. Any pointers or support would be hugely appreciated!
A) What's happening
In the didSelectDate method, I'm adding a function with the hope of triggering multiple date calculations, but as I added this function here, it created an infinite loop when loading the viewController. I believe this is happening because the calendarView.selectDates([date]) calls the didSelectDate method. So I changed it to calendarView.selectDates([date], triggerSelectionDelegate: false) which solved the infinite loop issue! However, now as you select a new date it won't update the rest of the cells.
B) What I would want to happen
This is important to my app because the selected date has a "tail of dates". As it's a mailing service, the selected date is the "Deliver By" date, and the tail of dates shows the user the shipping time required. My hope is that by pressing the selected date, the rest of the cells update to show this tail.
Screenshot of how that tail looks:
Screenshot of how that tail looks when a new date is selected:
The latest code I'm using:
extension PostSchedulerViewController: JTAppleCalendarViewDataSource {
func setupCalendarView() {
calendarView?.ibCalendarDelegate = self
calendarView?.ibCalendarDataSource = self
calendarView?.minimumLineSpacing = 0
calendarView?.minimumInteritemSpacing = 0
calendarView?.allowsMultipleSelection = false
calendarView?.isRangeSelectionUsed = false
calendarView?.sectionInset = UIEdgeInsetsMake(20, 20, 20, 20)
dateStringFormatter.dateFormat = DateFormatType.yearMonthDay
monthFormatter.dateFormat = DateFormatType.monthYear
dateFormatter.timeZone = Calendar.current.timeZone
dateFormatter.locale = Calendar.current.locale
calendarView?.visibleDates { [unowned self] (visibleDates:DateSegmentInfo) in
self.setupCalendarView(from: visibleDates)
}
}
func configureCalendar(_ calendar:JTAppleCalendarView) -> ConfigurationParameters {
dateFormatter.dateFormat = DateFormatType.monthDay
dateFormatter.timeZone = Calendar.current.timeZone
dateFormatter.locale = Calendar.current.locale
if let post = MyObjects.sharedInstance.workingPostViewModel?.post,
let createdAt = post.createdAt,
let endDate = Calendar.current.date(byAdding: .year, value: 2, to: createdAt) {
let parameters = ConfigurationParameters(startDate: createdAt, endDate: endDate, calendar: Calendar.current)
return parameters
} else {
let parameters = ConfigurationParameters(startDate: Date(), endDate: Date(),calendar: Calendar.current)
return parameters
}
}
func setInitialPostDatesOnCalendarView() {
guard let post = MyObjects.sharedInstance.workingPostViewModel?.post,
let deliverDate = post.deliverDate else {
return
}
calendarView?.deselectAllDates()
calendarView?.selectDates([deliverDate])
calendarView?.scrollToDate(deliverDate, animateScroll:false)
}
}
extension PostSchedulerViewController: JTAppleCalendarViewDelegate {
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date:Date, cellState: CellState, indexPath:IndexPath) ->JTAppleCell {
let cell = calendar.dequeueReusableJTAppleCell(withReuseIdentifier: "DayCell", for: indexPath) as! DayCell
cell.label.text = cellState.text
cell.handleSelected(cellState: cellState)
return cell
}
func calendar(_ calendar: JTAppleCalendarView, didScrollToDateSegmentWith visibleDates: DateSegmentInfo) {
setupCalendarView(from: visibleDates)
}
func calendar(_ calendar: JTAppleCalendarView, didSelectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
print(cellState.date)
print(GlobalConstants.arrowMark, "selected date: ", date)
updateFromNewDeliverDate(from: date, cell:cell, cellState:cellState)
}
func updateFromNewDeliverDate(from date:Date, cell:JTAppleCell?, cellState:CellState) {
// guard let cell = cell as? DayCell else {return}
guard cellState.day != .sunday else {
// TODO: showPopLabel()
return
}
guard let post = MyObjects.sharedInstance.workingPostViewModel?.post else {
// TODO: showPopLabel()
return
}
post.calculateDatesFromDeliver(date) { (success:Bool, error:Error?) in
guard success else {
// TODO: showPopLabel()
return
}
self.calendarView?.deselectAllDates()
self.calendarView?.selectDates([date], triggerSelectionDelegate: false)
self.calendarView?.reloadData(withanchor: date, completionHandler: {
self.setPostDatesOnLabels()
})
}
}
func calendar(_ calendar: JTAppleCalendarView, didDeselectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
func calendar(_ calendar: JTAppleCalendarView, willDisplayCell cell: JTAppleCell?, date: Date, cellState: CellState) {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
}
OK. I'm @ work. I'll read it in more after im done.
We'll find you a solution 👍
ok, whenever you call
calendarView.selectDates([date], triggerSelectionDelegate: false)
This will cause the didSelect delegate to not be triggered as you have found out. But you cells still need to be updated. So, instead of calling the didSelect function, it instead calls the cellforItemWIthIndex function with all the cells that needs to be updated.
In the cellForItem function, put the stuff in there the configure the cell.
Let me know if this helped?
Jay! You rock!! Thanks for your response! You're right, the cell needed to be handled on all methods didSelect as well as cellForItemWithIndex. This helped!
I also realized that I should change my strategy and use calendarView?.generateDateRange(from: prep, to: deliver) which made it much smoother. I love this function, as well as being able to adjust the cell via a switch:
switch cellState.selectedPosition() {
case .left:
case .middle:
case .right:
}
I do have one thing to figure out still.
Any pointers as you did yesterday would be of grand support. I'm so close to getting it just right!
If a user taps on a cell with cellState.isSelected, it currently deselects it. However, the desired behavior is to recalculate the dateRange just as it would as when the user taps on a didSelect cell.
If add the "recalculate" in didDeselect then it goes back to infinite loop. Maybe there's a way to tell the didDeselect that its past state used to be cellState.isSelected? Or maybe there's something more obvious that I'm not doing?
Where would I put the "recalculate" function to ensure it always happens when a user taps on any cell?
Screenshot of what happens when a user taps on a cell that already is cellState.isSelected:
(where ideally it would not allow for "cutting" dates).
Below is current code just in case:
DATA SOURCE
extension PostSchedulerViewController: JTAppleCalendarViewDataSource {
func setupCalendarView() {
calendarView?.ibCalendarDelegate = self
calendarView?.ibCalendarDataSource = self
calendarView?.minimumLineSpacing = 0
calendarView?.minimumInteritemSpacing = 0
calendarView?.allowsMultipleSelection = true
calendarView?.isRangeSelectionUsed = true
calendarView?.sectionInset = UIEdgeInsetsMake(22, 22, 22, 22)
dateStringFormatter.dateFormat = DateFormatType.yearMonthDay
monthFormatter.dateFormat = DateFormatType.monthYear
dateFormatter.timeZone = Calendar.current.timeZone
dateFormatter.locale = Calendar.current.locale
calendarView?.visibleDates { [unowned self] (visibleDates:DateSegmentInfo) in
self.setupCalendarView(from: visibleDates)
}
}
func configureCalendar(_ calendar:JTAppleCalendarView) -> ConfigurationParameters {
dateFormatter.dateFormat = DateFormatType.monthDay
dateFormatter.timeZone = Calendar.current.timeZone
dateFormatter.locale = Calendar.current.locale
if let post = MyObjects.sharedInstance.workingPostViewModel?.post,
let createdAt = post.createdAt,
let endDate = Calendar.current.date(byAdding: .year, value: 2, to: createdAt) {
let parameters = ConfigurationParameters(startDate: createdAt, endDate: endDate, calendar: Calendar.current)
return parameters
} else {
let parameters = ConfigurationParameters(startDate: Date(), endDate: Date(),calendar: Calendar.current)
return parameters
}
}
func setInitialPostDatesOnCalendarView() {
guard let post = MyObjects.sharedInstance.workingPostViewModel?.post,
let prep = post.prepDate,
let deliver = post.deliverDate,
let dateRange = self.calendarView?.generateDateRange(from: prep, to: deliver) else {
return
}
calendarView?.scrollToDate(deliver, animateScroll:false)
calendarView?.selectDates(dateRange)
}
func setNewSelectedDates(deliverDate:Date) {
guard let post = MyObjects.sharedInstance.workingPostViewModel?.post else {
showPopLabel(NewPostError.noPost)
return
}
post.calculateDatesFromDeliver(deliverDate) { (success:Bool, error:Error?) in
if success == false {
self.showPopLabel(error?.localizedDescription ?? PostError.errorCalculatingDates.localizedDescription)
}
guard let prep = post.prepDate,
let deliver = post.deliverDate,
let dateRange = self.calendarView?.generateDateRange(from: prep, to: deliver) else {
return
}
self.calendarView?.deselectAllDates()
self.calendarView?.selectDates(dateRange, triggerSelectionDelegate: false)
self.setPostDatesOnLabels()
}
}
}
DELEGATE
extension PostSchedulerViewController: JTAppleCalendarViewDelegate {
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date:Date, cellState: CellState, indexPath:IndexPath) ->JTAppleCell {
let cell = calendar.dequeueReusableJTAppleCell(withReuseIdentifier: "DayCell", for: indexPath) as! DayCell
cell.label.text = cellState.text
cell.handleSelected(cellState: cellState)
return cell
}
func calendar(_ calendar: JTAppleCalendarView, didScrollToDateSegmentWith visibleDates: DateSegmentInfo) {
setupCalendarView(from: visibleDates)
}
func calendar(_ calendar: JTAppleCalendarView, didSelectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
updateDatesWithSelected(deliverDate: date, cell:cell, cellState:cellState)
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
func calendar(_ calendar: JTAppleCalendarView, didDeselectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
// TODO: if date == within current selected date range {
// update dates with user's tapped deselected date as delivery date
// }
}
func updateDatesWithSelected(deliverDate:Date, cell:JTAppleCell?, cellState:CellState) {
if cellState.selectedPosition() == .full {
guard cellState.day != .sunday else {
showPopLabel(PostError.deliverDateIsSunday.localizedDescription)
setInitialPostDatesOnCalendarView()
return
}
}
setNewSelectedDates(deliverDate: deliverDate)
}
func calendar(_ calendar: JTAppleCalendarView, willDisplayCell cell: JTAppleCell?, date: Date, cellState: CellState) {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
func calendar(_ calendar: JTAppleCalendarView, shouldSelectDate date: Date, cell: JTAppleCell?, cellState: CellState) -> Bool {
guard date > Date() else {
return false
}
return true
}
}
CELL EXTENSION
extension DayCell {
func handleSelected(cellState:CellState) {
switch cellState.selectedPosition() {
case .left:
// Prep Date
label.textColor = Colors.lightBlue
label.font = UIFont.systemFont(ofSize: 15, weight: UIFontWeightRegular)
selectedView.isHidden = true
travelView.isHidden = false
travelView.roundLeftCorners(radius: prepView.frame.height/2)
prepView.setRound()
prepView.isHidden = false
return
case .right, .full:
// Deliver Date
selectedView.setRound()
selectedView.isHidden = false
label.textColor = UIColor.white
label.font = UIFont.systemFont(ofSize: 15, weight: UIFontWeightHeavy)
travelView.isHidden = false
travelView.roundRightCorners(radius: prepView.frame.height/2)
prepView.isHidden = true
return
case .middle:
// Travel Time
selectedView.isHidden = true
travelView.isHidden = false
travelView.roundRightCorners(radius: 0)
label.textColor = UIColor.white
label.font = UIFont.systemFont(ofSize: 15, weight: UIFontWeightRegular)
prepView.isHidden = true
return
case .none:
// Other Dates
selectedView.isHidden = true
travelView.isHidden = true
label.textColor = UIColor.black
label.font = UIFont.systemFont(ofSize: 15, weight: UIFontWeightRegular)
prepView.isHidden = true
// Excemption Dates
if cellState.day == DaysOfWeek.sunday {
label.textColor = UIColor.lightGray
} else if cellState.date < Date() {
label.textColor = UIColor.lightGray
} else if cellState.dateBelongsTo != .thisMonth {
label.textColor = UIColor.lightGray
}
return
}
}
}
I havent looked at you new question thoroughly yet, but
would it help if you knew when a cell was selected programatically vs userSelected ?
The you might be able to tell when you are doing something vs when a user is doing something?
Yes!!! That would solve this issue perfectly!
Is this something already in your code?
Yes, the code on master branch has this update.
cellState now has a property which allows you to know if a cell was selected/deselected programitically.
But I am still working on it.
There are some changes on master branch though.
Devs will have to implement the willDisplayCell function.
The willDisplayCell function should have the exact same code as the cellForItem function. The the code is both functions a 99% the same, then devs should avoid code duplication by sharing code in a function.
I did not want devs to implement this function by because synchronization issues due to Apple cell pre-fetching, the apple documents recommended that we implement it. I am currently doing more research on this to come up with a more elegant solution.
To test master branch, put this in your pod file, then do a pod update
pod 'JTAppleCalendar', :git => 'https://github.com/patchthecode/JTAppleCalendar.git', :branch => 'master'
I just added the pod. Next steps: applying changes and testing it. I'll surely let you know how it goes soon.
Hey Jay,
The behavior is now much better. Exactly what I needed, plus it was very easy to implement!
🙌👌🤘
All I had to do was add this to didSelect and didDeselect:
if cellState.selectionType == SelectionType.userInitiated {
updateDatesWithSelected(deliverDate: date, cell:cell, cellState:cellState)
}
One thing that did change.
When using a dateRange selected cells now come back with cellState.selectedPosition() == .right with one exception: .left is now .full.
It was not happening before, so I thought it was worth sharing with you as a possible bug or something that needs to be updated in the delegate implementation.
Now I'm wondering if I should:
dateRange but simplify my views so selectedPosition doesn't matter.cellState.isSelected and handle secondary date comparisons within cell(either way this is manageable so thank you!)
What the calendar looks like with the selectedPosition issue
ouch. thats a bad issue.
Let me look into that.
@santiagoprieto by the way, here are the changes I was talking about earlier about master branch -> https://github.com/patchthecode/JTAppleCalendar/issues/553
@santiagoprieto if youre still online can you join me here? https://gitter.im/patchthecode/JTAppleCalendar
I do not understand how you got so many right selections.
Hahaha no worries, man. For now I can move forward with this and later improve it.
Let me know if I can do anything to help figure it out!
OK i misunderstood. I thought the screen shot you showed above was a bug i introduced accidentally. If it is not, then I guess I can close this issue?
But if it is a bug, then once you let me know how you got this, then I will fix it.
cheers man. and thanks 🍻.
Oh! Sorry. I just saw your message about joining you in gitter. I did some extra tests to see what functions were being called to create the array of selected dates and I figured out what was going on. It was something very lame on my side.
When I read the below:
The willDisplayCell function should have the exact same code as the cellForItem function.
I forgot to remove the deque part:
let cell = calendar.dequeueReusableJTAppleCell(withReuseIdentifier: "DayCell", for: indexPath) as! DayCell
So basically I was dequeuing new cells every time. 😲
Anyway, I thought you'd like to know that now it's working like a charm. It feels soooo good. Thanks for your patience and sorry for freaking you out about a possible bug. Awesome work with these new features!!!
Here's the final code
func calendar(_ calendar: JTAppleCalendarView, didScrollToDateSegmentWith visibleDates: DateSegmentInfo) {
setupCalendarView(from: visibleDates)
}
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date:Date, cellState: CellState, indexPath:IndexPath) ->JTAppleCell {
let cell = calendar.dequeueReusableJTAppleCell(withReuseIdentifier: "DayCell", for: indexPath) as! DayCell
cell.label.text = cellState.text
cell.handleSelected(cellState: cellState)
return cell
}
func calendar(_ calendar: JTAppleCalendarView, willDisplay cell: JTAppleCell, forItemAt date: Date, cellState: CellState, indexPath: IndexPath) {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
func calendar(_ calendar: JTAppleCalendarView, didSelectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
if cellState.selectionType == SelectionType.userInitiated {
updateDatesWithSelected(deliverDate: date, cell: cell, cellState:cellState)
} else {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
}
func calendar(_ calendar: JTAppleCalendarView, didDeselectDate date: Date, cell: JTAppleCell?, cellState: CellState) {
if cellState.selectionType == SelectionType.userInitiated {
updateDatesWithSelected(deliverDate: date, cell:cell, cellState:cellState)
} else {
guard let cell = cell as? DayCell else {return}
cell.handleSelected(cellState: cellState)
}
}
Awesome man. Good stuff.
If you find any new issues, let me know. i'll close this one now. 🍻
I'll keep on testing it, and if all looks sounds, then i'll release version 7.1.0 soon.
I also updated the statement to alert others to remove the dequeued code.
Most helpful comment
Oh! Sorry. I just saw your message about joining you in gitter. I did some extra tests to see what functions were being called to create the array of selected dates and I figured out what was going on. It was something very lame on my side.
When I read the below:
I forgot to remove the deque part:
So basically I was dequeuing new cells every time. 😲
Anyway, I thought you'd like to know that now it's working like a charm. It feels soooo good. Thanks for your patience and sorry for freaking you out about a possible bug. Awesome work with these new features!!!
Here's the final code