Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update all calendarView cells when new date is selected. #554

Closed
santiagoprieto opened this issue Sep 6, 2017 · 14 comments
Closed

Update all calendarView cells when new date is selected. #554

santiagoprieto opened this issue Sep 6, 2017 · 14 comments

Comments

@santiagoprieto
Copy link

santiagoprieto commented Sep 6, 2017

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:

img_4744

Screenshot of how that tail looks when a new date is selected:
img_4745

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)
        }
    }
@patchthecode
Copy link
Owner

OK. I'm @ work. I'll read it in more after im done.
We'll find you a solution 👍

@patchthecode
Copy link
Owner

patchthecode commented Sep 7, 2017

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?

@santiagoprieto
Copy link
Author

santiagoprieto commented Sep 7, 2017

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

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
                }
            }
        }

@patchthecode
Copy link
Owner

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?

@santiagoprieto
Copy link
Author

Yes!!! That would solve this issue perfectly!
Is this something already in your code?

@patchthecode
Copy link
Owner

patchthecode commented Sep 8, 2017

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'

@santiagoprieto
Copy link
Author

santiagoprieto commented Sep 8, 2017

I just added the pod. Next steps: applying changes and testing it. I'll surely let you know how it goes soon.

@santiagoprieto
Copy link
Author

santiagoprieto commented Sep 8, 2017

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:

  • keep using the dateRange but simplify my views so selectedPosition doesn't matter.
  • use only one date as 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
img_4775

@patchthecode
Copy link
Owner

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 -> #553

@patchthecode
Copy link
Owner

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

@santiagoprieto
Copy link
Author

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!

@patchthecode
Copy link
Owner

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

@santiagoprieto
Copy link
Author

santiagoprieto commented Sep 8, 2017

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

@patchthecode
Copy link
Owner

patchthecode commented Sep 8, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants