David Seek
David Seek

Reputation: 17132

Swift / TableView reusable Cell loads with different content every time it gets displayed

I'm having a UITableViewController. Inside section 1 there is a Cell displaying a JTAppleCalendar easily populated with:

    if indexPath.section == 1 {

        let cell = tableView.dequeueReusableCellWithIdentifier("calendarViewCell") as! CalendarViewCell

        print(eventDatesAsNSDates)
        cell.calendarView.selectDates(eventDatesAsNSDates)            
        return cell
    }

eventDatesAsNSDates gets populated within viewDidAppear.

In theory everything is working as I want it. But if I scroll the TableView down and up, following completely annoying behavior happens.

enter image description here

The print statement within cellForRowAtIndexPath print(eventDatesAsNSDates) proves that eventDatesAsNSDates does not change and yet the Calendar keeps being populated one time and not the other time and then being populated again...

Neither cell.calendarView.selectDates nor eventDatesAsNSDates gets set or called on another time in the App.

What am I missing? Help is very appreciated.

As requested, the selectDates function:

public func selectDates(dates: [NSDate], triggerSelectionDelegate: Bool = true, keepSelectionIfMultiSelectionAllowed: Bool = false) {
        var allIndexPathsToReload: [NSIndexPath] = []
        var validDatesToSelect = dates
        // If user is trying to select multiple dates with multiselection disabled, then only select the last object
        if !calendarView.allowsMultipleSelection && dates.count > 0 { validDatesToSelect = [dates.last!] }

        let addToIndexSetToReload = {(indexPath: NSIndexPath)->Void in
            if !allIndexPathsToReload.contains(indexPath) { allIndexPathsToReload.append(indexPath) } // To avoid adding the  same indexPath twice.
        }

        let selectTheDate = {(indexPath: NSIndexPath, date: NSDate) -> Void in
            self.calendarView.selectItemAtIndexPath(indexPath, animated: false, scrollPosition: .None)
            addToIndexSetToReload(indexPath)
            // If triggereing is enabled, then let their delegate handle the reloading of view, else we will reload the data
            if triggerSelectionDelegate {
                self.internalCollectionView(self.calendarView, didSelectItemAtIndexPath: indexPath)
            } else { // Although we do not want the delegate triggered, we still want counterpart cells to be selected

                // Because there is no triggering of the delegate, the cell will not be added to selection and it will not be reloaded. We need to do this here
                self.addCellToSelectedSetIfUnselected(indexPath, date: date)
                let cellState = self.cellStateFromIndexPath(indexPath, withDate: date)
                if let aSelectedCounterPartIndexPath = self.selectCounterPartCellIndexPathIfExists(indexPath, date: date, dateOwner: cellState.dateBelongsTo) {
                    // If there was a counterpart cell then it will also need to be reloaded
                    addToIndexSetToReload(aSelectedCounterPartIndexPath)
                }
            }
        }

        let deSelectTheDate = { (oldIndexPath: NSIndexPath) -> Void in
            addToIndexSetToReload(oldIndexPath)
            if let index = self.theSelectedIndexPaths.indexOf(oldIndexPath) {
                let oldDate = self.theSelectedDates[index]
                self.calendarView.deselectItemAtIndexPath(oldIndexPath, animated: false)
                self.theSelectedIndexPaths.removeAtIndex(index)
                self.theSelectedDates.removeAtIndex(index)

                // If delegate triggering is enabled, let the delegate function handle the cell
                if triggerSelectionDelegate {
                    self.internalCollectionView(self.calendarView, didDeselectItemAtIndexPath: oldIndexPath)
                } else { // Although we do not want the delegate triggered, we still want counterpart cells to be deselected
                    let cellState = self.cellStateFromIndexPath(oldIndexPath, withDate: oldDate)
                    if let anUnselectedCounterPartIndexPath = self.deselectCounterPartCellIndexPath(oldIndexPath, date: oldDate, dateOwner: cellState.dateBelongsTo) {
                        // If there was a counterpart cell then it will also need to be reloaded
                        addToIndexSetToReload(anUnselectedCounterPartIndexPath)
                    }
                }
            }
        }

        for date in validDatesToSelect {
            let components = self.calendar.components([.Year, .Month, .Day],  fromDate: date)
            let firstDayOfDate = self.calendar.dateFromComponents(components)!

            // If the date is not within valid boundaries, then exit
            if !(firstDayOfDate >= self.startOfMonthCache && firstDayOfDate <= self.endOfMonthCache) { continue }
            let pathFromDates = self.pathsFromDates([date])

            // If the date path youre searching for, doesnt exist, then return
            if pathFromDates.count < 0 { continue }
            let sectionIndexPath = pathFromDates[0]

            // Remove old selections
            if self.calendarView.allowsMultipleSelection == false { // If single selection is ON
                let selectedIndexPaths = self.theSelectedIndexPaths // made a copy because the array is about to be mutated
                for indexPath in selectedIndexPaths {
                    if indexPath != sectionIndexPath { deSelectTheDate(indexPath) }
                }

                // Add new selections
                // Must be added here. If added in delegate didSelectItemAtIndexPath
                selectTheDate(sectionIndexPath, date)
            } else { // If multiple selection is on. Multiple selection behaves differently to singleselection. It behaves like a toggle. unless keepSelectionIfMultiSelectionAllowed is true.
                // If user wants to force selection if multiselection is enabled, then removed the selected dates from generated dates
                if keepSelectionIfMultiSelectionAllowed {
                    if selectedDates.contains(calendar.startOfDayForDate(date)) {
                        addToIndexSetToReload(sectionIndexPath)
                        continue // Do not deselect or select the cell. Just add it to be reloaded
                    }
                }
                if self.theSelectedIndexPaths.contains(sectionIndexPath) { // If this cell is already selected, then deselect it
                    deSelectTheDate(sectionIndexPath)
                } else {
                    // Add new selections
                    // Must be added here. If added in delegate didSelectItemAtIndexPath
                    selectTheDate(sectionIndexPath, date)
                }
            }
        }


        // If triggering was false, although the selectDelegates weren't called, we do want the cell refreshed. Reload to call itemAtIndexPath
        if /*triggerSelectionDelegate == false &&*/ allIndexPathsToReload.count > 0 {
            delayRunOnMainThread(0.0) {
                self.batchReloadIndexPaths(allIndexPathsToReload)
            }
        }
    }

Upvotes: 0

Views: 566

Answers (1)

pedrouan
pedrouan

Reputation: 12910

Well the cellForRow delegate method is attempting to update tableView rows/section(s) each time your cell is visible again.

I would definitely prepare the data (for calendar) somewhere else and I'd prefer to render data only. Reusability of UITableViewCell will handle content properly.

Don't call that method in the cellForRow method. Try to place it e.g. into viewDidAppear() method. If selectDates() method does all the work of updating your tableView, it could still work.

Upvotes: 1

Related Questions