I am trying to make countdown timer app with collectionView.
Features & Functions:
Each cell has own label and timer function.
CountDown timer will run if user touches a cell.
Time string have to updated as timer run.
I successfully build a timer in each cell but I'm stuck updating timeLabel (reload selected cell).
Please check the codes below and give me some hint.
class ListViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var recipeList: TimeRecipeList
var timer = Timer()
required init?(coder aDecoder: NSCoder) {
recipeList = TimeRecipeList()
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
let width = (view.frame.size.width - 10) / 2
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: width, height: width)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "AddItemSegue" {
if let addItemVC = segue.destination as? AddRecipeViewController {
addItemVC.delegate = self
}
}
}
}
extension ListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return recipeList.item.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeListCell", for: indexPath)
let item = recipeList.item[indexPath.row]
configureText(for: cell, with: item)
return cell
}
func configureText(for cell: UICollectionViewCell, with item: TimeRecipe) {
if let label = cell.viewWithTag(1) as? UILabel {
label.text = item.name
}
if let label = cell.viewWithTag(2) as? UILabel {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
let timeString = formatter.string(from: TimeInterval(item.time))
label.text = timeString
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeListCell", for: indexPath)
let item = recipeList.item[indexPath.row]
let cellTimer = TimerControl()
cellTimer.second = item.time
cellTimer.timerRun()
configureText(for: cell, with: item)
}
class TimerControl {
var timer = Timer()
var second: Int = 0
func timerRun() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(countDown), userInfo: nil, repeats: true)
}
#objc func countDown() {
//let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeListCell", for: indexPath)
if second <= 1 {
timer.invalidate()
} else {
second -= 1
//collectionView.reloadItems(at: [indexPath])
//MARK: Reload selected cell
}
}
}
}
extension ListViewController: AddRecipeViewControllerDelegate {
func addRecipeViewControllerDidCancel(_ controller: AddRecipeViewController) {
dismiss(animated: true)
}
func addRecipeViewControllerDisSave(_ controller: AddRecipeViewController, didFinishAdding item: TimeRecipe) {
dismiss(animated: true)
let rowIndex = recipeList.item.count
recipeList.item.append(item)
let indexPath = IndexPath(row: rowIndex, section: 0)
let indexPaths = [indexPath]
collectionView.insertItems(at: indexPaths)
}
}
#objc func countDown(indexPath: IndexPath) {
if second <= 1 {
timer.invalidate()
} else {
second -= 1
//MARK: Reload selected cell
yourarray[indexPath.row] = newvalues
collectionView.reloadItems(at: [indexPath])
}
}
Related
I want to change value of temperature in UIView (blue square), when I tapping on cell in collection view (date). I am trying to use delegate for it, but i can't do it. I want to change value of selectedIndex and pass this value to UIView in viewModel.weather[selectedIndex]
ViewController with collectionView
protocol WeekCityWeatherViewControllerDelegate {
func reloadWeatherData()
}
class WeekCityWeatherViewController: UIViewController {
var delegate: WeekCityWeatherViewControllerDelegate?
...
extension WeekCityWeatherViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "dateCell", for: indexPath) as! DateWeekWeatherScreenCell
cell.layer.cornerRadius = 5
if selectedIndex == indexPath.row {
cell.backgroundColor = UIColor.blue
cell.dateLabel.textColor = UIColor.white
} else {
cell.backgroundColor = UIColor.white
cell.dateLabel.textColor = UIColor.black
}
if let dateInt = viewModel.weather.first?.week.daily[indexPath.item].dt {
let timeInterval = TimeInterval(dateInt)
let myNSDate = Date(timeIntervalSince1970: timeInterval)
let dateFormatter2 = DateFormatter()
dateFormatter2.dateFormat = "dd/MM E"
dateFormatter2.locale = Locale(identifier: "ru_RU")
let dateString = dateFormatter2.string(from: myNSDate)
cell.dateLabel.text = dateString
}
return cell
}
}
extension WeekCityWeatherViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width / 5, height: collectionView.frame.height)
}
}
extension WeekCityWeatherViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndex = indexPath.row
self.dateCollectionView.reloadData()
delegate?.reloadWeatherData()
}
}
UIView
class DayView: UIView, WeekCityWeatherViewControllerDelegate {
var viewModel: GeneralViewModel
var selectedIndex: Int
var currentIndex: Int
init(frame: CGRect, viewModel: GeneralViewModel, selectedIndex: Int, currentIndex: Int) {
self.viewModel = viewModel
self.selectedIndex = selectedIndex
self.currentIndex = currentIndex
super.init(frame: frame)
createSubviews()
reloadWeatherData()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reloadWeatherData() {
self.reloadInputViews()
}
private func createSubviews() {
let weekCityWeatherViewController = WeekCityWeatherViewController(viewModel: viewModel, currentIndex: currentIndex)
weekCityWeatherViewController.delegate = self
let tempLabel = UILabel()
tempLabel.textColor = .black
tempLabel.translatesAutoresizingMaskIntoConstraints = false
tempLabel.font = UIFont(name: "Rubik-Regular", size: 30)
if let maxTemp = viewModel.weather[selectedIndex].week.daily[0].temp.max {
tempLabel.text = String(format: "%.0f", maxTemp) + " " + "°"
}
addSubview(tempLabel)
NSLayoutConstraint.activate([
tempLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
tempLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10),
tempLabel.heightAnchor.constraint(equalToConstant: 50),
tempLabel.widthAnchor.constraint(equalToConstant: 50),
])
}
}
create a protocol and send the target cell as a parameter in the trigger method.
Here is the steps;
// 1. protocol and trigger method
protocol ItemTableViewCellDelegate: AnyObject {
func doneButtonTapped(_ cell: ItemTableViewCell)
}
// 2. implement delegate and send the trigger method in your case
class ItemTableViewCell: UITableViewCell {
weak var delegate: ItemTableViewCellDelegate?
//...
}
// MARK: - Implementation
extension ViewController: ItemTableViewCellDelegate {
func doneButtonTapped(_ cell: ItemTableViewCell) {
// 3. find the index of item you tapped the button, via cell as a parameter
guard let indexPath = tableView.indexPath(for: cell) else { return }
var item = MainScreenModel.dummyModel[indexPath.row]
// 4. reload the cell by indexpath
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
I've been working on a feature to detect when a user sees a post and when he doesn't. When the user does see the post I turn the cell's background into green, when it doesn't then it stays red. Now after doing that I notice that I turn on all the cells into green even tho the user only scroll-down the page, so I added a timer but I couldn't understand how to use it right so I thought myself maybe you guys have a suggestion to me cause I'm kinda stuck with it for like two days :(
Edit: Forgot to mention that a cell marks as seen if it passes the minimum length which is 2 seconds.
Here's my Code:
My VC(CollectionView):
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
var impressionEventStalker: ImpressionStalker?
var impressionTracker: ImpressionTracker?
var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"
#IBOutlet weak var collectionView: UICollectionView!{
didSet{
collectionView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
impressionEventStalker = ImpressionStalker(minimumPercentageOfCell: 0.70, collectionView: collectionView, delegate: self)
}
}
func registerCollectionViewCells(){
let cellNib = UINib(nibName: CustomCollectionViewCell.nibName, bundle: nil)
collectionView.register(cellNib, forCellWithReuseIdentifier: CustomCollectionViewCell.reuseIdentifier)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
collectionView.delegate = self
collectionView.dataSource = self
registerCollectionViewCells()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
impressionEventStalker?.stalkCells()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
impressionEventStalker?.stalkCells()
}
}
// MARK: CollectionView Delegate + DataSource Methods
extension ViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.reuseIdentifier, for: indexPath) as? CustomCollectionViewCell else {
fatalError()
}
customCell.textLabel.text = "\(indexPath.row)"
if indexPathsOfCellsTurnedGreen.contains(indexPath){
customCell.cellBackground.backgroundColor = .green
}else{
customCell.cellBackground.backgroundColor = .red
}
return customCell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 150, height: 225)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) // Setting up the padding
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
//Start The Clock:
if let trackableCell = cell as? TrackableView {
trackableCell.tracker = ImpressionTracker(delegate: trackableCell)
trackableCell.tracker?.start()
}
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
//Stop The Clock:
(cell as? TrackableView)?.tracker?.stop()
}
}
// MARK: - Delegate Method:
extension ViewController:ImpressionStalkerDelegate{
func sendEventForCell(atIndexPath indexPath: IndexPath) {
guard let customCell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewCell else {
return
}
customCell.cellBackground.backgroundColor = .green
indexPathsOfCellsTurnedGreen.append(indexPath) // We append all the visable Cells into an array
}
}
my ImpressionStalker:
import Foundation
import UIKit
protocol ImpressionStalkerDelegate:NSObjectProtocol {
func sendEventForCell(atIndexPath indexPath:IndexPath)
}
protocol ImpressionItem {
func getUniqueId()->String
}
class ImpressionStalker: NSObject {
//MARK: Variables & Constants
let minimumPercentageOfCell: CGFloat
weak var collectionView: UICollectionView?
static var alreadySentIdentifiers = [String]()
weak var delegate: ImpressionStalkerDelegate?
//MARK: Initializer
init(minimumPercentageOfCell: CGFloat, collectionView: UICollectionView, delegate:ImpressionStalkerDelegate ) {
self.minimumPercentageOfCell = minimumPercentageOfCell
self.collectionView = collectionView
self.delegate = delegate
}
// Checks which cell is visible:
func stalkCells() {
for cell in collectionView!.visibleCells {
if let visibleCell = cell as? UICollectionViewCell & ImpressionItem {
let visiblePercentOfCell = percentOfVisiblePart(ofCell: visibleCell, inCollectionView: collectionView!)
if visiblePercentOfCell >= minimumPercentageOfCell,!ImpressionStalker.alreadySentIdentifiers.contains(visibleCell.getUniqueId()){ // >0.70 and not seen yet then...
guard let indexPath = collectionView!.indexPath(for: visibleCell), let delegate = delegate else {
continue
}
delegate.sendEventForCell(atIndexPath: indexPath) // send the cell's index since its visible.
ImpressionStalker.alreadySentIdentifiers.append(visibleCell.getUniqueId()) // to avoid double events to show up.
}
}
}
}
// Func Which Calculate the % Of Visible of each Cell:
private func percentOfVisiblePart(ofCell cell:UICollectionViewCell, inCollectionView collectionView:UICollectionView) -> CGFloat{
guard let indexPathForCell = collectionView.indexPath(for: cell),
let layoutAttributes = collectionView.layoutAttributesForItem(at: indexPathForCell) else {
return CGFloat.leastNonzeroMagnitude
}
let cellFrameInSuper = collectionView.convert(layoutAttributes.frame, to: collectionView.superview)
let interSectionRect = cellFrameInSuper.intersection(collectionView.frame)
let percentOfIntersection: CGFloat = interSectionRect.height/cellFrameInSuper.height
return percentOfIntersection
}
}
ImpressionTracker:
import Foundation
import UIKit
protocol ViewTracker {
init(delegate: TrackableView)
func start()
func pause()
func stop()
}
final class ImpressionTracker: ViewTracker {
private weak var viewToTrack: TrackableView?
private var timer: CADisplayLink?
private var startedTimeStamp: CFTimeInterval = 0
private var endTimeStamp: CFTimeInterval = 0
init(delegate: TrackableView) {
viewToTrack = delegate
setupTimer()
}
func setupTimer() {
timer = (viewToTrack as? UIView)?.window?.screen.displayLink(withTarget: self, selector: #selector(update))
timer?.add(to: RunLoop.main, forMode: .default)
timer?.isPaused = true
}
func start() {
guard viewToTrack != nil else { return }
timer?.isPaused = false
startedTimeStamp = CACurrentMediaTime() // Current Time in seconds.
}
func pause() {
guard viewToTrack != nil else { return }
timer?.isPaused = true
endTimeStamp = CACurrentMediaTime()
print("Im paused!")
}
func stop() {
timer?.isPaused = true
timer?.invalidate()
}
#objc func update() {
guard let viewToTrack = viewToTrack else {
stop()
return
}
guard viewToTrack.precondition() else {
startedTimeStamp = 0
endTimeStamp = 0
return
}
endTimeStamp = CACurrentMediaTime()
trackIfThresholdCrossed()
}
private func trackIfThresholdCrossed() {
guard let viewToTrack = viewToTrack else { return }
let elapsedTime = endTimeStamp - startedTimeStamp
if elapsedTime >= viewToTrack.thresholdTimeInSeconds() {
viewToTrack.viewDidStayOnViewPortForARound()
startedTimeStamp = endTimeStamp
}
}
}
my customCell:
import UIKit
protocol TrackableView: NSObject {
var tracker: ViewTracker? { get set }
func thresholdTimeInSeconds() -> Double //Takes care of the screen's time, how much "second" counts.
func viewDidStayOnViewPortForARound() // Counter for how long the "Post" stays on screen.
func precondition() -> Bool // Checks if the View is full displayed so the counter can go on fire.
}
class CustomCollectionViewCell: UICollectionViewCell {
var tracker: ViewTracker?
static let nibName = "CustomCollectionViewCell"
static let reuseIdentifier = "customCell"
#IBOutlet weak var cellBackground: UIView!
#IBOutlet weak var textLabel: UILabel!
var numberOfTimesTracked : Int = 0 {
didSet {
self.textLabel.text = "\(numberOfTimesTracked)"
}
}
override func awakeFromNib() {
super.awakeFromNib()
cellBackground.backgroundColor = .red
layer.borderWidth = 0.5
layer.borderColor = UIColor.lightGray.cgColor
}
override func prepareForReuse() {
super.prepareForReuse()
print("Hello")
tracker?.stop()
tracker = nil
}
}
extension CustomCollectionViewCell: ImpressionItem{
func getUniqueId() -> String {
return self.textLabel.text!
}
}
extension CustomCollectionViewCell: TrackableView {
func thresholdTimeInSeconds() -> Double { // every 2 seconds counts as a view.
return 2
}
func viewDidStayOnViewPortForARound() {
numberOfTimesTracked += 1 // counts for how long the view stays on screen.
}
func precondition() -> Bool {
let screenRect = UIScreen.main.bounds
let viewRect = convert(bounds, to: nil)
let intersection = screenRect.intersection(viewRect)
return intersection.height == bounds.height && intersection.width == bounds.width
}
}
The approach you probably want to use...
In you posted code, you've created an array of "read posts":
var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"
Assuming your real data will have multiple properties, such as:
struct TrackPost {
var message: String = ""
var postAuthor: String = ""
var postDate: Date = Date()
// ... other stuff
}
add another property to track whether or not it has been "seen":
struct TrackPost {
var message: String = ""
var postAuthor: String = ""
var postDate: Date = Date()
// ... other stuff
var hasBeenSeen: Bool = false
}
Move all of your "tracking" code out of the controller, and instead add a Timer to your cell class.
When the cell appears:
if hasBeenSeen for that cell's Data is false
start a 2-second timer
if the timer elapses, the cell has been visible for 2 seconds, so set hasBeenSeen to true (use a closure or protocol / delegate pattern to tell the controller to update the data source) and change the backgroundColor
if the cell is scrolled off-screen before the timer elapses, stop the timer and don't tell the controller anything
if hasBeenSeen is true to begin with, don't start the 2-second timer
Now, your cellForItemAt code will look something like this:
let p: TrackPost = myData[indexPath.row]
customCell.authorLabel.text = p.postAuthor
customCell.dateLabel.text = myDateFormat(p.postDate) // formatted as a string
customCell.textLabel.text = p.message
// setting hasBeenSeen in your cell should also set the backgroundColor
// and will be used so the cell knows whether or not to start the timer
customCell.hasBeenSeen = p.hasBeenSeen
// this will be called by the cell if the timer elapsed
customCell.wasSeenCallback = { [weak self] in
guard let self = self else { return }
self.myData[indexPath.item].hasBeenSeen = true
}
What about a much simpler approach like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for subview in collectionView!.visibleCells {
if /* check visible percentage */ {
if !(subview as! TrackableCollectionViewCell).timerRunning {
(subview as! TrackableCollectionViewCell).startTimer()
}
} else {
if (subview as! TrackableCollectionViewCell).timerRunning {
(subview as! TrackableCollectionViewCell).stopTimer()
}
}
}
}
With a Cell-Class extended by:
class TrackableCollectionViewCell {
static let minimumVisibleTime: TimeInterval = 2.0
var timerRunning: Bool = true
private var timer: Timer = Timer()
func startTimer() {
if timerRunning {
return
}
timerRunning = true
timer = Timer.scheduledTimer(withTimeInterval: minimumVisibleTime, repeats: false) { (_) in
// mark cell as seen
}
}
func stopTimer() {
timerRunning = false
timer.invalidate()
}
}
I added the repo I am working here:
https://github.com/AlexMarshall12/singleDayTimeline/tree/master/singleDayTimeline
Basically I have 900 collectionView cells (with a custom XIB layout).
let cellIdentifier = "DayCollectionViewCell"
class ViewController: UIViewController, UICollectionViewDataSource,UICollectionViewDelegate {
#IBOutlet weak var button: UIButton!
var dates = [Date?]()
var startDate: Date?
#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
})
startDate = self.dates.first! ?? Date()
daysCollectionView.delegate = self
daysCollectionView.dataSource = self
// Do any additional setup after loading the view, typically from a nib.
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 900
}
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 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
} else {
print("not me")
//cell.backgroundColor = UIColor.lightGray
}
return cell
}
// func collectionView(_ collectionView: UICollectionView,
// layout collectionViewLayout: UICollectionViewLayout,
// sizeForItemAt indexPath: IndexPath) -> CGSize {
// return CGSize(width: 2, height: daysCollectionView.bounds.size.height/2 )
// }
#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)
// if let cell = daysCollectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as DayCollectionViewCell? {
// print("found it")
// } else {
// print("didn't find it")
// }
daysCollectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredHorizontally)
daysCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Then here is the cell:
class DayCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var arrowImage: UIImageView!
override var isSelected: Bool{
didSet{
arrowImage.isHidden = !isSelected
}
}
override func awakeFromNib() {
super.awakeFromNib()
arrowImage.isHidden = true
}
override func prepareForReuse() {
self.backgroundColor = UIColor.clear
}
func drawMonth(month: String){
}
func drawYear(year: Int){
}
}
It looks like this:
So the plan is that when that button is pressed, you can see in the #IBAction func buttonPressed that a random date is chosen and scrolled to, then that cell is selected in the collectionView. Then that makes the cell's arrow drawn with arrowImage.isHidden = !isSelected in the override var isSelected function.
Currently, this works almost perfectly. The arrow is redrawn under the selected cell EXCEPT when the new index which gets randomly selected is far enough away from the current index. My theory is that if the index difference is big enough, the next cell hasn't been loaded/dequeued yet and thus isSelected is never called. However I am not sure still why its not working properly
1- Add a reloadCell function to change ui of cell. Then you should remove override var isSelected and arrowImage.isHidden = true from awakeFromNib function.
func reloadCell(_ isSelected:Bool){
arrowImage.isHidden = !isSelected
}
2- You should define a variable on ViewController.swift class private var selectedIndexPath: IndexPath? and then you should add this code for to check if arrow is hidden or not.
if let selectedRow = selectedIndexPath {
cell.reloadCell(selectedRow == indexPath)
} else {
cell.reloadCell(false)
}
3- And if you change your button action function like this below, it would be worked.
#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.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
daysCollectionView.reloadData()
}
All codes here.
ViewController.swift
import UIKit
let cellIdentifier = "DayCollectionViewCell"
class ViewController: UIViewController, UICollectionViewDataSource,UICollectionViewDelegate {
#IBOutlet weak var button: UIButton!
var dates = [Date?]()
var startDate: 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
})
startDate = self.dates.first! ?? Date()
daysCollectionView.delegate = self
daysCollectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 900
}
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
} else {
print("not me")
//cell.backgroundColor = UIColor.lightGray
}
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()
}
}
DayCollectionViewCell.swift
import UIKit
class DayCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var arrowImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
}
override func prepareForReuse() {
self.backgroundColor = UIColor.clear
}
func drawMonth(month: String){
}
func drawYear(year: Int){
}
func reloadCell(_ isSelected:Bool){
arrowImage.isHidden = !isSelected
}
}
I am trying to trigger a certain function in a collection view when I scroll. Basically Im trying to implement pagination so that when it scrolls to the bottom more items are loaded.
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item >= allEvents.count - 1 {
// print("paginating for post")
paginationHelper.paginate(completion: { [unowned self] (events) in
self.allEvents.append(contentsOf: events)
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
})
}else{
print("Not paginating")
}
}
This is the entire Function
import Foundation
import UIKit
import Alamofire
import AlamofireNetworkActivityIndicator
import SwiftLocation
import CoreLocation
import AMScrollingNavbar
class HomeFeedController: UICollectionViewController, UICollectionViewDelegateFlowLayout,UIGestureRecognizerDelegate {
// let dropDownLauncher = DropDownLauncher()
var isFinishedPaging = false
let detailView = EventDetailViewController()
let refreshControl = UIRefreshControl()
var emptyLabel: UILabel?
var allEvents = [Event]()
//will containt array of event keys
var eventKeys = [String]()
let customCellIdentifier = "customCellIdentifier"
var grideLayout = GridLayout(numberOfColumns: 2)
lazy var dropDownLauncer : DropDownLauncher = {
let launcer = DropDownLauncher()
launcer.homeFeed = self
return launcer
}()
let paginationHelper = PaginationHelper<Event>(serviceMethod: PostService.showEvent)
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.white
collectionView?.collectionViewLayout = grideLayout
collectionView?.reloadData()
self.collectionView?.contentInset = UIEdgeInsetsMake(15, 0, 0, 0)
navigationItem.title = "Home"
collectionView?.register(CustomCell.self, forCellWithReuseIdentifier: customCellIdentifier)
// self.navigationItem.hidesBackButton = true
let backButton = UIBarButtonItem(image: UIImage(named: "menu"), style: .plain, target: self, action: #selector(handleDropDownMenu))
self.navigationItem.leftBarButtonItem = backButton
// PostService.showEvent(location: User.current.location!) { (event) in
// self.allEvents = event
// print(self.allEvents)
//
// DispatchQueue.main.async {
// self.collectionView?.reloadData()
// }
// }
configureCollectionView()
reloadHomeFeed()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let navigationController = self.navigationController as? ScrollingNavigationController {
navigationController.followScrollView(self.collectionView!, delay: 50.0)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let navigationController = navigationController as? ScrollingNavigationController {
navigationController.stopFollowingScrollView()
}
}
override func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
if let navigationController = navigationController as? ScrollingNavigationController {
navigationController.showNavbar(animated: true)
}
return true
}
func handleDropDownMenu(){
dropDownLauncer.showDropDown()
}
//will query by selected category
func categoryFetch(dropDown: DropDown){
navigationItem.title = dropDown.name
paginationHelper.category = dropDown.name
configureCollectionView()
reloadHomeFeed()
}
func reloadHomeFeed() {
self.paginationHelper.reloadData(completion: { [unowned self] (events) in
self.allEvents = events
if self.refreshControl.isRefreshing {
self.refreshControl.endRefreshing()
}
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
})
}
func configureCollectionView() {
// add pull to refresh
refreshControl.addTarget(self, action: #selector(reloadHomeFeed), for: .valueChanged)
collectionView?.addSubview(refreshControl)
}
// need to tell it how many cells to have
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allEvents.count
}
// need to tell the collection view controller what type of cell we want to return
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: customCellIdentifier, for: indexPath) as! CustomCell
let imageURL = URL(string: allEvents[indexPath.item].currentEventImage)
print(imageURL ?? "")
customCell.sampleImage.af_setImage(withURL: imageURL!)
return customCell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//let selectedEvent = self.imageArray[indexPath.row]
//let eventDetailVC
if let cell = collectionView.cellForItem(at: indexPath){
// print("Look here for event name")
// print(detailView.eventName)
detailView.eventKey = allEvents[indexPath.row].key!
detailView.eventPromo = allEvents[indexPath.row].currentEventPromo!
detailView.currentEvent = allEvents[indexPath.row]
present(detailView, animated: true, completion: nil)
//self.navigationController?.pushViewController(detailView, animated: true)
}
print("Cell \(indexPath.row) selected")
}
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item >= allEvents.count - 1 {
// print("paginating for post")
paginationHelper.paginate(completion: { [unowned self] (events) in
self.allEvents.append(contentsOf: events)
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
})
}else{
print("Not paginating")
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
//will make surepictures keep same orientation even if you flip screen
// will most likely lock into portrait mode but still good to have
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
grideLayout.invalidateLayout()
}
//Will allow the first two cells that are displayed to be of varying widths
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 || indexPath.item == 1 {
return CGSize(width: view.frame.width, height: grideLayout.itemSize.height)
}else{
return grideLayout.itemSize
}
}
func showLeftView(sender: AnyObject?){
print("Button Pressed")
// sideMenuController?.leftViewController = LeftViewController()
//sideMenuController?.showLeftView(animated: true, completionHandler: nil)
}
}
//responsible for populating each cell with content
class CustomCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
let sampleImage: UIImageView = {
let firstImage = UIImageView()
firstImage.clipsToBounds = true
firstImage.translatesAutoresizingMaskIntoConstraints = false
firstImage.contentMode = .scaleToFill
firstImage.layer.masksToBounds = true
return firstImage
}()
let nameLabel: UILabel = {
let name = UILabel()
name.text = "Custom Text"
name.translatesAutoresizingMaskIntoConstraints = false
return name
}()
func setupViews() {
addSubview(sampleImage)
backgroundColor = UIColor.white
//addSubview(nameLabel)
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": sampleImage]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": sampleImage]))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// responsible for creating the grid layout that you see in the home view feed
class GridLayout: UICollectionViewFlowLayout {
var numberOfColumns:Int = 2
init(numberOfColumns: Int) {
super.init()
// controlls spacing inbetween them as well as spacing below them to next item
self.numberOfColumns = numberOfColumns
self.minimumInteritemSpacing = 3
self.minimumLineSpacing = 5
}
// just needs to be here because swift tells us to
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var itemSize: CGSize{
get{
if collectionView != nil {
let collectionVieweWidth = collectionView?.frame.width
let itemWidth = (collectionVieweWidth!/CGFloat(self.numberOfColumns)) - self.minimumInteritemSpacing
let itemHeight: CGFloat = 200
return CGSize(width: itemWidth, height: itemHeight)
}
return CGSize(width: 100, height: 100)
}set{
super.itemSize = newValue
}
}
}
This is the function that is responsible for triggering the event but it seems to go to the else statement literally every time. Any insight would be helpful
This is the code form the 'Opgeslagen Rooster' Viewcontroller
class vcSavedTimetable: UIViewController {
#IBOutlet weak var noTimetableSavedLabel: UILabel!
var timetable: Timetable!
override func viewWillAppear(_ animated: Bool) {
prepareView()
}
override func viewWillDisappear(_ animated: Bool) {
noTimetableSavedLabel.isHidden = true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showSavedTimetable"{
let viewController = segue.destination as! vcTimetableShow
viewController.timetable = self.timetable
viewController.willSave = false
}
}
func prepareView(){
let savedTimetable = getTimetableFromUserDefaults()
guard (savedTimetable != nil) else {
noTimetableSavedLabel.isHidden = false
return
}
self.timetable = savedTimetable!
performSegue(withIdentifier: "showSavedTimetable", sender: nil)
}
func getTimetableFromUserDefaults()->Timetable?{
let timetableID = UserDefaults.standard.string(forKey: "savedTimetableID")
if isConnectedToNetwork(){
if let id = timetableID{
let table = Timetable(timetableID: id)
return table
}
}
let val = UserDefaults.standard.data(forKey: "savedTimetable")
if let data = val{
let table = NSKeyedUnarchiver.unarchiveObject(with: data) as! Timetable
return table
}
return nil
}
}
This is the code of the destination viewcontroller 'Rooster Bekijken'. The viewdidload() function should be executed, which doesn't happen. I tested it using breakpoints
//
// vcTimetableShowCollectionViewController.swift
// Ostrea Rooster App
//
// Created by Giel-Jan Looij on 14-12-16.
// Copyright © 2016 GJ-Computers. All rights reserved.
//
import UIKit
private let reuseIdentifier = "Cell"
class vcTimetableShow: UICollectionViewController, UICollectionViewDelegateFlowLayout {
weak var timetable: Timetable?
var willSave = false
//UIVIEWCONTROLLER
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.delegate = self
collectionView?.register( UINib(nibName: "timetableViewCell", bundle: nil), forCellWithReuseIdentifier: "lessonEntry")
NotificationCenter.default.addObserver(self, selector: #selector(self.dataDidArrive(_:)), name: .didFetchTimetable, object: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
deinit {
NotificationCenter.default.removeObserver(self)
}
//METHODES
func dataDidArrive(_ notification: NSNotification){
collectionView?.reloadData()
if let cv = self.collectionView {
cv.layoutIfNeeded()
let indexPath = IndexPath(item: 0, section: getDayOfWeek()-1)
if let attributes = cv.layoutAttributesForSupplementaryElement(ofKind: UICollectionElementKindSectionHeader, at: indexPath) {
let topOfHeader = CGPoint(x: 0, y: attributes.frame.origin.y - cv.contentInset.top)
cv.setContentOffset(topOfHeader, animated:true)
}
}
if willSave {
UserDefaults.standard.set(timetable!.timetableID, forKey: "savedTimetableID")
let encodedTimetable = NSKeyedArchiver.archivedData(withRootObject: self.timetable!)
UserDefaults.standard.set(encodedTimetable, forKey: "savedTimetable")
UserDefaults.standard.synchronize()
}
}
func getDayOfWeek()->Int {
let myCalendar = Calendar(identifier: .gregorian)
let weekDay = myCalendar.component(.weekday, from: Date())
if weekDay > 6 || weekDay < 2{
return 5
}
return weekDay-1
}
//UICOLLECTIONVIEW DATASOURCE
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return timetable!.days.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return timetable!.days[section+1]!.hour.count * 2
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item % 2 == 0{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hourIndicator", for: indexPath)
let dayLabel = cell.viewWithTag(1) as! UILabel
let dayIndex = indexPath.section + 1
let hourIndex = indexPath.item/2 + 1
let cellHour = timetable!.days[dayIndex]!.hour[hourIndex]!
dayLabel.text = String(cellHour.hourID)
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 2
return cell
}else{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "lessonEntry", for: indexPath) as! timetableViewCell
let dayIndex = indexPath.section + 1
let hourIndex = indexPath.item/2 + 1
let cellHour = timetable!.days[dayIndex]!.hour[hourIndex]!
cell.cellHour = cellHour
cell.prepareCell()
return cell
}
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let cell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "dayHeaderCell", for: indexPath as IndexPath)
let dayIndex = indexPath.section + 1
let dayName = timetable!.days[dayIndex]!.dayName
let day = cell.viewWithTag(1) as! UILabel
day.text = dayName
if timetable!.days[dayIndex]!.dayID == getDayOfWeek(){
cell.backgroundColor = UIColor.lightGray
}
return cell
}
//UICOLLECTIONVIEW DELEGATE
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item % 2 == 0{
let dayIndex = indexPath.section + 1
let hourIndex = indexPath.item/2 + 1
let cellHour = timetable!.days[dayIndex]!.hour[hourIndex]!
var cellHeight: CGFloat = 0
for _ in cellHour.lessons{
cellHeight += 23
}
if cellHour.notice != ""{
cellHeight += 23
}
return CGSize(width: 50, height: cellHeight)
}else{
let dayIndex = indexPath.section + 1
let hourIndex = indexPath.item/2 + 1
let cellHour = timetable!.days[dayIndex]!.hour[hourIndex]!
var cellHeight: CGFloat = 0
for _ in cellHour.lessons{
cellHeight += 23
}
if cellHour.notice != ""{
cellHeight += 23
}
return CGSize(width: 226.0, height: cellHeight)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let edgeInset: CGFloat = (collectionView.frame.width - (226.0 + 50.0))/2
return UIEdgeInsets.init(top: 0, left: edgeInset, bottom: 30, right: edgeInset)
}
}
I'm trying to segue from 'Opgeslagen Rooster' to 'Rooster Bekijken', but it doesn't work. It is running in the main thread (thread 1), so that isn't the problem. It gets to prepare(for segue...), does everything. After that the view should change right? But that doesn't happen. The viewdidload method of the 'Rooster Bekijken' ViewController doesn't get called. It hangs after the last line in the pepare(for segue ....) function.
Why doesn't it change to the other viewcontroller? It does work when im accessing the 'Rooster Bekijken' viewcontroller form the other viewcontroller which segue's to it.