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
}
}
Related
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 have a UITableView in which i have placed a UICollectionView and then reused this scenario for 10 or more cells.
What i want is when i scroll my UICollectionView each of the Cells of other collection view must also be scrolled at the same time synchronously.
MyViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet private weak var menuButton: UIButton!
#IBOutlet var dottedView: UIView!
var array1 = ["Indian Standard Time","Adelaide Standard Time"
,"Auckland Standard Time","Melbourne Standard Time",
"Manchester Standard Time","Paris Standard Time",
"Alaska Standard Time","Greenland Standard Time"
,"Sutherland Standard Time","Russia Standard Time"]
var array2 = ["desc","desc","desc","desc","desc"]
var array3 = ["Mon","Tue","Wed","Thu", "Fri","Sat","Sun"]
var array4 = ["12 2021","12 2021","12 2021","12 2021", "12 2021","12 2021","12 2021"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
#IBOutlet var maintableview: UITableView!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath) as! TableViewCell
cell.tag = indexPath.row
cell.collectionview.tag = (100 * indexPath.section) + indexPath.row
cell.label1.text = array1[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 130
}
#IBAction func datetimeclick(_ sender: Any) {
let vc = storyboard?.instantiateViewController(identifier: "dateViewController") as! DateViewController
present(vc, animated: true)
}
#IBAction func addbtnclick(_ sender: Any) {
let vc = storyboard?.instantiateViewController(identifier: "timezonecontroller") as! TimeZoneViewController
present(vc, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
self.dottedView.addDashedBorder()
let nextdate = Date().addDate()
let prevdate = Date().subtractDate()
let dayofweek = String().dayOfWeek(fDate: nextdate)
let tomorrowDate = String().dateOfWeek(fDate: nextdate)
let yesterdayDate = String().dateOfWeek(fDate: prevdate)
let year = String().getYear(fDate: Date())
let splitstr = dayofweek?.prefix(3)
print("tomdate",tomorrowDate!)
print("prevdate",yesterdayDate!)
print("year",year!)
print("dayofWeekCap", splitstr!)
menuButton.isUserInteractionEnabled = true
let interaction = UIContextMenuInteraction(delegate :self)
menuButton.backgroundColor = UIColor(hexString: "#1361E5")
menuButton.addInteraction(interaction)
}
override func viewDidLayoutSubviews() {
}
}
MyTableViewCell.swift
import UIKit
class TableViewCell: UITableViewCell {
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
#IBOutlet weak var collectionview: UICollectionView!
#IBOutlet weak var collectioncell: UICollectionViewCell!
#IBOutlet weak var collectionText: UILabel!
var scrollToPosition = 0
var indexPosition = 0
extension TableViewCell : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 48
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let arrayString = timeSlot[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectioncell", for: indexPath)
let title = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 33))
indexPosition = indexPath.row
if indexPath.row >= scrollToPosition
{
title.textColor = UIColor.white
cell.contentView.backgroundColor = UIColor.systemBlue
}
else
{
title.textColor = UIColor.black
cell.layer.borderWidth = 1.0
cell.layer.borderColor = UIColor.darkGray.cgColor
cell.contentView.backgroundColor = UIColor.white
}
title.text = arrayString
title.textAlignment = NSTextAlignment.center
for subView in cell.contentView.subviews {
subView.removeFromSuperview()
}
cell.contentView.addSubview(title)
let now = Date()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_IN")
formatter.dateFormat = "HH:mm" //for complete format EEEE dd MMMM YYYY HH:mm
let datetime = formatter.string(from: now)
let currentDate = createDateFromString(string: datetime)
for i in timeSlot.indices {
let arrayDate = createDateFromString(string: timeSlot[i])
if currentDate > arrayDate && flagDate == false {
flagDate = true
countToScroll = i
}
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 33)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
for cell in self.collectionview.visibleCells {
let indexpath = self.collectionview.indexPath(for: cell)
self.indexPosition = indexpath!.row
self.collectionview.selectItem(at: IndexPath(row: self.indexPosition, section: 0), animated: true, scrollPosition: .centeredHorizontally)
}
}
}
}
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])
}
}
So I am making a kind of timeline where every day is represented by 1 cell in a really wide horizontal collectionView. Here you can see/try the full code https://github.com/AlexMarshall12/singleDayTimeline.git. Its also copied below
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.suffix(from: 8))
print(self.dates)
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.reloadData() //LINE IN QUESTION
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()
}
}
So there are 2 randomly generated dates at the tail end of 900 days prior to the current date. In the very beginning I use the willDisplay cell callback to scroll to the very last date (the most recent) which happens to be the last cell in entire collectionView. Then at the very end of that function I do daysCollectionView.reloadData(). When this happens, no matter where you scroll, the collectionView stops dequeuing cells, cellForItemAt is never called again.
Notably you can tell because only that original far right block of the entire View is colored blue, if you scroll to the left, its just clear because nothing is coloring the cells anymore. Why would this reloadData function stop this process?
I put UICollectionView inside table cell in Home page. Each category row has their contents image. But, my problem is that when I run the app, the images under second row/second category do not show whenever I run the app.
The images will show after I click more button and it will goes to details UI. Then, I click on Back button and reach the Home page. At this time, the images shows properly. I tried many codes but I don't know where it is wrong.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
tableView.reloadData()
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
}
override func viewDidAppear(_ animated: Bool) {
tableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//getSearchWords()
DispatchQueue.main.async {
self.tableView.reloadData()
}
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
}
override func viewDidLoad() {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// debugPrint("HOme Table View Cell")
let cell = tableView.dequeueReusableCell(withIdentifier: "homecell") as! HomeCategoryRowCell
let list = sections[(indexPath as NSIndexPath).row]
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
cell.collectionViewOffset = self.storedOffsets[indexPath.row] ?? 0
DispatchQueue.main.async {
cell.categoryTitle.text = list.package_name
cell.mainAssociatedURL.text = list.package_url
}
cell.categoryTitle.font = UIFont.boldSystemFont(ofSize: 17.0)
cell.collectionView.tag = indexPath.row
cell.parentVC = self
cell.collectionView.reloadData()
return cell
}
extension Home : UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sections[collectionView.tag].packageTable.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! HomeVideoCell
let list = sections[collectionView.tag].packageTable[indexPath.row]
if(list.poster_image_url != StringResource().posterURL){
let url = NSURL(string: list.poster_image_url)
do{
if let url = url {
_ = try Data(contentsOf:url as URL)
poster_url = list.poster_image_url
}
}catch let error {
//debugPrint("ERRor ::\(error)")
poster_url = StringResource().posterURL
}
}else{
poster_url = StringResource().posterURL
}
AsyncImageLoader.sharedLoader.imageForUrl(urlString: poster_url) { (image, url) -> () in
DispatchQueue.main.async(){
cell.movieTitle.text = list.name
if(url == StringResource().posterURL){
cell.imageView.image = UIImage(named: "sample_cover")
}else{
cell.imageView.image = image
}
}
}
// cell.layer.shouldRasterize = true
// cell.layer.rasterizationScale = UIScreen.main.scale
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Home Collection view at row \(collectionView.tag) selected index path \(indexPath)")
let list = sections[collectionView.tag].packageTable[indexPath.row]
self.prefs.set(list.poster_image_url, forKey: "poster_image_url")
self.prefs.set(list.name, forKey: "name")
self.prefs.set(list.assets_id, forKey: "VEDIO_ID")
debugPrint(list.name)
debugPrint(list.assets_id)
}
}
These above codes are written in Home.swift.
In HomeCell.swift,
class HomeCell : UITableViewCell {
var parentVC: UIViewController?
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var categoryTitle: UILabel!
#IBOutlet weak var SeeAll: UIButton!
#IBOutlet weak var mainAssociatedURL: UILabel!
let prefs:UserDefaults = UserDefaults.standard
var catId: String! = ""
var associated_url: String!
override func awakeFromNib() {
super.awakeFromNib()
SeeAll.setTitle(NSLocalizedString("See All >", comment: ""), for: .normal)
SeeAll.titleLabel!.font = UIFont (name: "Tharlon", size: 14)
}
#IBAction func btnSeeAll(_ sender: Any) {
catId = categoryTitle.text!
associated_url = mainAssociatedURL.text!
self.prefs.setValue(1, forKey: "PROVIDER_ID")
self.prefs.set(catId, forKey: "PROVIDER_NAME")
self.prefs.set(associated_url, forKey: "associated_url")
}
override func layoutSubviews() {
super.layoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
}
extension HomeCategoryRowCell {
func setCollectionViewDataSourceDelegate<D: UICollectionViewDataSource & UICollectionViewDelegate>(_ dataSourceDelegate: D, forRow row: Int) {
self.collectionView.delegate = dataSourceDelegate
self.collectionView.dataSource = dataSourceDelegate
self.collectionView.tag = row
self.collectionView.setContentOffset(self.collectionView.contentOffset, animated:false) // Stops collection view if it was scrolling.
self.collectionView.reloadData()
}
var collectionViewOffset: CGFloat {
get { return collectionView.contentOffset.x }
set { collectionView.contentOffset.x = newValue }
}
}
Can anyone help me please?
Edit controller like below, you are calling too many reloadData.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tableView.layoutIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//getSearchWords()
DispatchQueue.main.async {
self.tableView.reloadData()
}
self.tableView.layoutIfNeeded()
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
and add:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView .deselectRow(at: indexPath, animated: false)
}