4
votes

I have a collectionView that I am making a timeline of sorts out of. There are many thin cells in a horizontal collectionView - each cell represents 1 day. Here is the project code: https://github.com/AlexMarshall12/singleDayTimeline.git

Here is the viewController:

let cellIdentifier = "DayCollectionViewCell"
class ViewController: UIViewController, UICollectionViewDataSource,UICollectionViewDelegate {

    @IBOutlet weak var button: UIButton!
    var dates = [Date?]()
    var startDate: Date?
    var endDate: Date?
    private var selectedIndexPath: IndexPath?

    @IBOutlet weak var daysCollectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        daysCollectionView.register(UINib.init(nibName: "DayCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: cellIdentifier)

        let allDates = Helper.generateRandomDate(daysBack: 900, numberOf: 10)
        self.dates = allDates.sorted(by: {
            $0!.compare($1!) == .orderedAscending
        })
        self.startDate = Calendar.current.startOfDay(for: dates.first as! Date)

        self.endDate = dates.last!
        self.dates = Array(dates.prefix(upTo: 1))
        daysCollectionView.delegate = self
        daysCollectionView.dataSource = self
    }

    var onceOnly = false

    internal func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if !onceOnly {
            //let lastDateIndexPath = IndexPath(row: dates.count - 1,section: 0)
            let lastDate = dates.last
            let lastDayIndex = lastDate!?.interval(ofComponent: .day, fromDate: startDate!)
            let lastDayCellIndexPath = IndexPath(row: lastDayIndex!, section: 0)
            self.daysCollectionView.scrollToItem(at: lastDayCellIndexPath, at: .left, animated: false)
            self.selectedIndexPath = lastDayCellIndexPath
            self.daysCollectionView.reloadItems(at: [lastDayCellIndexPath])

            onceOnly = true
        }
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        let days = self.endDate!.days(from: self.startDate!)
        if days <= 150 {
            return 150
        } else {
            print(days,"days")
            return days + 1
        }
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = daysCollectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! DayCollectionViewCell

        let cellDate = Calendar.current.date(byAdding: .day, value: indexPath.item, to: self.startDate!)

        if let selectedRow = selectedIndexPath {
            cell.reloadCell(selectedRow==indexPath)
        } else {
            cell.reloadCell(false)
        }

        if Calendar.current.component(.day, from: cellDate!) == 15 {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "MMM"
            let monthString = dateFormatter.string(from: cellDate!)
            cell.drawMonth(month: monthString)
        }
        if Calendar.current.component(.day, from: cellDate!) == 1 && Calendar.current.component(.month, from: cellDate!) == 1 {
            print("drawYEAR")
            cell.drawYear(year:Calendar.current.component(.year, from: cellDate!))
        }
        if self.dates.contains(where: { Calendar.current.isDate(cellDate!, inSameDayAs: $0!) }) {
            print("same")
            cell.backgroundColor = UIColor.red
        }
        //cell.backgroundColor = UIColor.blue
        return cell
    }

    @IBAction func buttonPressed(_ sender: Any) {

        let randomIndex = Int(arc4random_uniform(UInt32(self.dates.count)))
        let randomDate = self.dates[randomIndex]
        let daysFrom = randomDate?.days(from: self.startDate!)
        let indexPath = IndexPath(row: daysFrom!, section: 0)
        self.selectedIndexPath = indexPath;

        //daysCollectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredHorizontally)
        daysCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
        daysCollectionView.reloadData()
    }

}

Note that when the buttonPressed function is Called, the "selectedIndexPath" changes. Then the next time cellForItemAt is called, it does cell.reloadCell and if the indexPath is this selectedIndexPath, it unhides the arrowImageView (see below)

class DayCollectionViewCell: UICollectionViewCell {

    ...
    func reloadCell(_ isSelected:Bool){
        arrowImage.isHidden = !isSelected
    }

}

This works when the button is pressed, however I am trying to make it work in the willDisplayCell callback. You can see that in this callback I basically just get the latest (most recent) date and scroll to it and then set SelectedIndexPath to that indexPath. Now I need a way to make the arrow draw on that cell.

Here is what it looks like now: enter image description here

As you can see it doesn't show the arrow UNTIL you scroll around. I think what is happening is that when you scroll around enough cellForItemAt gets called which we know works. But How do I get willDisplayCell to do the same thing successfully and thus have the arrow load from initial startup?

2
Please lookup this post. First, cellForRowAtIndexPath is called. Here, you setup what will be displayed. willDisplayCell will be called later and is used only to change the UI properties of the cell. Because you set selectedIndexPath here, it seems to me that you assume that this will change the behaviour of cellForRowAtIndexPath, but this would be too late.Reinhard Männer

2 Answers

2
votes

In your viewDidLoad, you seem to be initializing dates, startDate and endDate.

So, right after those variables are set up, you can set the selectedIndexPath using the available data. Then, when the cellForItem method is called, the code there would automatically handle the visibility of your arrow.

Place this code right after you initialize the 3 variables, inside viewDidLoad.

if let startDate = startDate,
    let lastDayIndex = dates?.last?.interval(ofComponent: .day, fromDate: startDate) {
    selectedIndexPath = IndexPath(row: lastDayIndex, section: 0)
}

You would not need to handle this in willDisplayCell, or use the onceOnly boolean.

1
votes

I add a new answer is because I noticed some incomplete details about the other answers.

In the viewDidLoad , there is a weird code:

 self.dates = Array(dates.prefix(upTo: 1))

I think this is a test code. So the result would be only one bar totally. And whatever the reason is, those codes for random one or last one should be added after this code.

    let randomIndex = Int(arc4random_uniform(UInt32(self.dates.count)))
    let randomDate = self.dates[randomIndex]
    let daysFrom = randomDate?.days(from: self.startDate!)
    let indexPath = IndexPath(row: daysFrom!, section: 0)
    self.selectedIndexPath = indexPath;
//     daysCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)

Please notice I comment the last one. If there is only one bar, it is good enough, but if there is more than one, the scrollToItem should be added somewhere else, because currently the collectionView has not been initialized.

One idea place is just as original post in the willDisplay, Which is easy to implement. The only issue is the time span of willDisplay is short, so reloading whole collectionData cannot be called there.

But it is good for scrolling to one indexPath there. Just as the following code.

 var onceOnly = false
 internal func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

            if (!onceOnly){
            self.daysCollectionView.scrollToItem(at: self.selectedIndexPath!, at: .left, animated: false)
            onceOnly = true
            }
}

There is another good place to call both functions (selectIndex and scrolling) as in the following place :

    override  func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    let randomIndex = Int(arc4random_uniform(UInt32(self.dates.count)))
    let randomDate = self.dates[randomIndex]
    let daysFrom = randomDate?.days(from: self.startDate!)
    let indexPath = IndexPath(row: daysFrom!, section: 0)
    if self.selectedIndexPath != nil {
      daysCollectionView.reloadItems(at: [self.selectedIndexPath!])
    }
    self.selectedIndexPath = indexPath;
     daysCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
     daysCollectionView.reloadItems(at: [indexPath])
}

Now I think it is a complete answer. Hope it is helpful.