UICollectionView Reloading cells backwards - ios

I am using a UICollectionView as a square grid layout for a crossword. The user will 'create' the puzzle by first tapping each cell to change the colour, press the 'insert numbers button' then add the numbers for each square and finally press the 'get clues button'. When the 'insert numbers button' is pressed, the cells are reloaded to 'fix' the colour and a textfield for each square's number is unhidden (for white cells only). The problem arises when I reload the cells to make the square's number un-editable, the cells reload in reverse. I.e. if the first cell (index 0) has a number, the last cell is updated (index 63). Pressing the button twice puts the cells in the correct position.
I have printed the indexPath to see which cell has the text and it is always the reverse...
Code:
import UIKit
class gridSetup: UIViewController, UICollectionViewDataSource,
UICollectionViewDelegate {
var setColours: Bool = true
var setNumbers: Bool = false
var numbers: Array = [Int]()
var whiteSquares:Array = [IndexPath]()
var gridSizer: Int? //value passed in from previous screen
#IBOutlet weak var getCluesButton: UIButton!
#IBOutlet weak var insertNumbersButton: UIButton!
#IBOutlet weak var grid: UICollectionView!
let reuseIdentifier = "cell"
override func viewDidLoad() {
super.viewDidLoad()
setColours = true
setNumbers = false
getCluesButton.isHidden = true
self.navigationController?.isNavigationBarHidden = false
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - UICollectionViewDataSource protocol
// tell the collection view how many cells to make
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//return (self.gridSizer * self.gridSizer)
return (self.gridSizer! * self.gridSizer!)
}
// make a cell for each cell index path
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! MyCollectionViewCell
cell.backgroundColor = UIColor.white
if setColours == true {
whiteSquares.append(indexPath) //initalise whiteSquares by adding all the squares
cell.cellGuess.isHidden = true //hide the letter guesses
cell.squareNumber.isHidden = true //hide the sqaure numbers
}
//redraw cells to fix the colours
// if we are on a white cell and are done setting colours
if setColours == false && whiteSquares.contains(indexPath) == true {
if setNumbers == true {
cell.squareNumber.isHidden = false
}
else {
if cell.squareNumber.text != "" {
print("I have a number inside, Index: ", indexPath.row)
}
cell.squareNumber.isUserInteractionEnabled = false
cell.squareNumber.placeholder = ""
cell.cellGuess.isHidden = false
return cell
}
}
// on a black square
else if setColours == false && whiteSquares.contains(indexPath) == false {
cell.squareNumber.isHidden = true
//print("on a black square", indexPath)
cell.backgroundColor = UIColor.black
cell.squareNumber.isHidden = true
cell.cellGuess.isHidden = true
}
//initial grid setup
else {
cell.backgroundColor = UIColor.white
cell.layer.borderColor = UIColor.black.cgColor
cell.squareNumber.isHidden = true
}
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8
return cell
}
// MARK: - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
//print("You selected cell #\(indexPath.item)!")
}
// change background color back when user releases touch
func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
if setColours == true {
if cell?.backgroundColor == UIColor.black {
//print("add", indexPath.row)
cell?.backgroundColor = UIColor.white
var k: Int = 0
for i in whiteSquares {
if indexPath.row < i.row {
whiteSquares.insert(indexPath, at: k)
break
}
else {
k += 1
}
}
}
else {
//print("remove", indexPath.row)
cell?.backgroundColor = UIColor.black
var k: Int = 0
for i in whiteSquares {
if indexPath.row == i.row {
whiteSquares.remove(at: k)
}
else {
k += 1
}
}
}
}
}
#IBAction func getClues(_ sender: Any) {
setNumbers = false
grid.reloadData() //problem comes from/after calling this function
}
#IBAction func insertNumbers(_ sender: Any) {
setColours = false
insertNumbersButton.isHidden = true
setNumbers = true
getCluesButton.isHidden = false
grid.reloadData()
}
}
// some layout stuff, (not important)
extension gridSetup: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.size.width / (CGFloat(gridSizer! + 1))
//let width = collectionView.frame.size.width / (CGFloat(gridSizer! + 1))
let height = width
return CGSize(width: width, height: height)
}
}
The '1' should be in the 1st row, second column

Related

TableView cells becomes inactive

I am using a tableView to take some surveys.
Header I use for a question. Footer for «back» and «next» buttons. And tableView cells for answer options.
Now I started to have a problem, with some user interaction: when you simultaneously click on the “next” button and select an answer, the answer options cease to be active, nothing can be selected. Although the buttons remain active.
Tell me in what direction to look for the problem and how you can debug this problem in order to understand what's wrong.
It all started after fixing bugs, when the application crashed when simultaneously (or almost) pressing the "next" button and choosing an answer. Because the didSelectRowAt method worked after I changed the current array of answer options, and the selected index in the previous question turned out to be larger than the size of the array with the answers to the new question.
class AssessmentVC: UIViewController {
#IBOutlet weak var tableView: UITableView!
var footer: FooterTableView?
var header: UIView?
var arrayAssessmnet = [AssessmentDM]()
var assessment: AssessmentDM!
var question: QuestionDM!
var viewSeperationHeader = UIView()
var arrayOptions: [Option]?
var countAssessment = 0
var numberAssessment = 0
var numberQuestion = 0
var countQuestion = 0
var numberQusttionForLabel = 1
var arrayQuestion = [QuestionDM]()
var arrayAnswers = [AnswerDM]()
var arrayEvents = [EventDM]()
override func viewDidLoad() {
super.viewDidLoad()
settingAssessment()
}
//MARK: - settingAssessment()
private func settingAssessment() {
let id = self.assessment.serverId
arrayQuestion = QuestionDM.getQuestions(id: id)
assessmentName.text = assessment.name
countQuestion = arrayQuestion.count
let day = self.assessment.day
arrayAnswers = AnswerDM.getAnswers(idAssessment: id, day: day)
settingQuestion(eventType: .start)
}
//MARK: - settingQuestion()
private func settingQuestion(eventType: EventType? = nil) {
let prevQuestion = question
question = arrayQuestion[numberQuestion]
timeQuestion = 0
footer!.grayNextButton()
//first question
if numberQuestion == 0 && numberAssessment == 0 {
footer!.previousButton.isHidden = true
} else {
footer!.previousButton.isHidden = false
}
arrayOptions = [Option]()
let sortOption = question.options!.sorted {$0.numberOption < $1.numberOption}
for option in sortOption {
arrayOptions?.append(Option(label: option.label, value: option.value))
}
tableView.rowHeight = UITableView.automaticDimension
tableView.reloadData()
heightTableView()
tableView.setContentOffset(.zero, animated: false)
}
//MARK: - heightTableView()
func heightTableView() {
}
//MARK: - UITableViewDataSource
extension AssessmentVC: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewSeperationHeader.isHidden = false
footer?.viewSeperationFooter.isHidden = false
tableView.separatorStyle = .singleLine
return question.options?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath as IndexPath) as AnswerAssessmentCell
cell.initCell(text: arrayOptions![indexPath.row].label, value: arrayOptions![indexPath.row].value, arrayValue: arrayAnswers[numberQuestion].response, isCheckbox: true)
return cell
}
}
//MARK: - UITableViewDelegate
extension AssessmentVC: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isChangAnswerInAssessment = true
if question.answerType == "Radio" || question.answerType == "Checkbox"{
selectRadioOrChekbox(indexPath: indexPath)
}
}
}
//MARK: - selectRadioOrChekbox
extension AssessmentVC {
private func selectRadioOrChekbox(indexPath: IndexPath) {
if question.answerType == "Radio" {
let cells = tableView.visibleCells as! Array<AnswerAssessmentCell>
for cell in cells {
cell.select = false
cell.isSelected = false
}
let cell = tableView.cellForRow(at: indexPath) as! AnswerAssessmentCell
cell.select = true
cell.isSelected = true
if arrayOptions?.count ?? 0 > indexPath.row {
arrayAnswers[numberQuestion].response = arrayOptions![indexPath.row].value
footer?.greenNextButton()
}
}
if question.answerType == "Checkbox" {
if arrayOptions?.count ?? 0 > indexPath.row {
//если нажато что-то, что должно сбросить "None"
// question.options![0].isSelect = false
let cells = tableView.visibleCells as! Array<AnswerAssessmentCell>
if cells[0].answerLabel.text == "None" {
cells[0].select = false
cells[0].isSelected = false
}
var array = arrayAnswers[numberQuestion].response?.components(separatedBy: ";")
array?.removeAll { $0 == "0"}
if array?.count == 0 {
arrayAnswers[numberQuestion].response = nil
} else {
arrayAnswers[numberQuestion].response = array?.joined(separator: ";")
}
let cell = tableView.cellForRow(at: indexPath) as! AnswerAssessmentCell
cell.select = !cell.select
cell.isSelected = cell.select
arrayAnswers[numberQuestion].response = array.joined(separator: ";")
if array.count == 0 {
arrayAnswers[numberQuestion].response = nil
footer?.grayNextButton()
} else {
footer?.greenNextButton()
}
}
}
}
}
//MARK: - Navigation between questions
extension AssessmentVC {
func nextQuestion() {
footer!.grayNextButton()
numberQuestion += 1
numberQusttionForLabel += 1
settingQuestion(eventType: .next)
} else {
}
func previousQuestion() {
numberQusttionForLabel -= 1
settingQuestion(eventType: .previous)
}
}
Some snippets that can help you :
// Answer type : use enum . Here the Strong/Codable is if you want to
// save using JSON encoding/decoding
enum AnswerType: String, Codable {
case checkBox = "CheckBox"
case radio = "Radio"
}
Setup of your cell :
class AnswerAssessmentCell: UITableViewCell {
...
// work with Option type
func initCell(option: Option, response: String?, answerType: AnswerType) {
// setup cell contents (labels)
// check for selected status
switch answerType {
case .checkBox:
// check if option is in response
// set isSelected according
break
case .radio:
// check if option is response
// set isSelected according
break
}
}
}
In table view data source :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! AnswerAssessmentCell
// Use the option to init the cell
// this will also set the selected state
let optionNumber = indexPath.row
cell.initCell(option: arrayOptions![optionNumber], response: arrayAnswers[numberQuestion].response, answerType: question.answerType)
return cell
}
In Table view delegate :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isChangAnswerInAssessment = true
let optionNumber = indexPath.row
switch question.answerType {
case .radio:
selectRadio(optionNumber: optionNumber)
case .checkBox:
selectCheckBox(optionNumber: optionNumber)
}
// Reload tableview to show changes
tableView.reloadData()
}
// Separate in 2 function for smaller functions
// in this function work only with model data, the reload data will do
// cell update
// only the footer view button cooler may need to be changed
private func selectRadio(optionNumber: Int) {
// Reset current response
// set response to optionNumber
// update footer button cooler if necessary
}
private func selectCheckBox(optionNumber: Int) {
// if option is in response
// remove option from response
// else
// add response to option
// update footer button cooler if necessary
}
Hope this can help you

How to track a CollectionView cell by time in Swift

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

UIButton affecting UICollectionViewCell Appearance

Ok, I have a ViewController that contains a UICollectionView and a UIButton. The Button is Not inside the collection view. When a user taps on an item in the UICollectionView I have some code that changes the background color of a UIView in the cell to show that it is selected. I have similar code for the UIButton which changes its background color when a user taps it, so it is acting like a styled checkbox.
The issue I am seeing is when I tap an item in the collection view, it highlights as it should. But then if I tap on the button, the button highlights normally but the collection view changes which item is highlighted. Even though the button that was tapped is not inside the collection view and has no ties to the collection view.
See following images:
first image: I have tapped on an item in the collection view, see the item is highlighted as normal.
second image: Notice, here, when I tap the Im not sure button, the collectionview changes and the highlight goes to a different cell. But no code in the DidSelectCellForItemAt is running
import Foundation
class SchedulingVisitForViewController: SchedulingViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var continueButton: RoundedButton!
#IBOutlet weak var notSureButton: RoundedButton!
#IBOutlet weak var notSureButtonTop: NSLayoutConstraint!
var collectionData = [[String: String]]()
override func viewDidLoad() {
super.viewDidLoad()
self.appointment = fetchAppointment()
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionData.append([
"image": "regularExam",
"text": "Regular Exam",
"data": "regular_exam"])
self.collectionData.append([
"image": "vaccines",
"text": "Essential Vaccines",
"data": "vaccines"])
self.collectionData.append([
"image": "meds",
"text": "Parasite Meds",
"data": "preventitive_meds"])
self.collectionData.append([
"image": "dog",
"text": "My pet is sick",
"data": "sick_pet"])
self.notSureButton.layer.shadowColor = Constants.appColor.gray.light.cgColor
self.notSureButton.layer.shadowOffset = CGSize(width: 0, height: 2.0)
self.notSureButton.layer.shadowRadius = 3.0
self.notSureButton.layer.shadowOpacity = 0.8
self.notSureButton.layer.masksToBounds = false
}
override func viewDidLayoutSubviews(){
self.collectionView.frame = CGRect(x: self.collectionView.frame.origin.x, y: self.collectionView.frame.origin.y, width: self.collectionView.frame.size.width, height: CGFloat(2 * 170))
self.notSureButtonTop.constant = CGFloat(2 * 170) + 20.0
self.view.layoutIfNeeded()
self.collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.collectionData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SelectVisitForCollectionViewCell", for: indexPath) as! SelectVisitForCollectionViewCell
let item = indexPath.item
cell.displayContent(image: UIImage(named: self.collectionData[item]["image"]!)!, text: self.collectionData[item]["text"]!)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? SelectVisitForCollectionViewCell {
var categories = self.appointment?.categories
if let index = categories!.index(of: self.collectionData[indexPath.item]["data"]!) {
categories?.remove(at: index)
cell.showSelected(false)
} else {
categories?.append(self.collectionData[indexPath.item]["data"]!)
cell.showSelected(true)
}
self.appointment?.categories = categories!
if (self.appointment?.categories.count)! > 0 {
self.continueButton.isHidden = false
} else {
self.continueButton.isHidden = true
}
saveAppointment(data: self.appointment!)
}
}
#IBAction func onNotSureButtonPressed(_ sender: Any) {
var categories = self.appointment?.categories
if let index = categories!.index(of: "not_sure") {
categories?.remove(at: index)
self.notSureButton.backgroundColor = UIColor.white
self.notSureButton.setTitleColor(Constants.appColor.gray.dark, for: .normal)
} else {
categories?.append("not_sure")
self.notSureButton.backgroundColor = Constants.appColor.yellow.main
self.notSureButton.setTitleColor(UIColor.white, for: .normal)
}
self.appointment?.categories = categories!
if (self.appointment?.categories.count)! > 0 {
self.continueButton.isHidden = false
} else {
self.continueButton.isHidden = true
}
print(self.appointment?.categories)
saveAppointment(data: self.appointment!)
}
#IBAction func onContinueButtonPressed(_ sender: Any) {
self.parentPageboy?.scrollToPage(.next, animated: true)
}
}
The issue pointed out by caninster_exister is that I was reloading the collection view

ios - How to load more object in UICollectionView

I have a collection view and data to load. My goal is when the collection view loaded it's load only first 5 items. And when I scroll to bottom of collection view it's load more 5 items and again until load all the data.
// The data to load (It already have 20 item loaded from server)
static var productInfo: [ProductInfo] = []
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MainProductCell.reuseIdentifier, for: indexPath) as? MainProductCell else {
fatalError("Expected `\(MainProductCell.self)` type for reuseIdentifier \(MainProductCell.reuseIdentifier). Check the configuration in Main.storyboard.")
}
// Set data for collection cell
cell.modelId = String(ProductCollectionViewController.productInfo[indexPath.row].productId)
cell.modelName = String(ProductCollectionViewController.productInfo[indexPath.row].name)
cell.modelPrice = String(ProductCollectionViewController.productInfo[indexPath.row].price)
cell.modelImage = ProductCollectionViewController.productInfo[indexPath.row].image
cell.objectImageView.downloadedFrom(link: ProductCollectionViewController.productInfo[indexPath.row].image, contentMode: UIViewContentMode.scaleAspectFit)
cell.modelLink = String(ProductCollectionViewController.productInfo[indexPath.row].remoteStorage)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let lastItem = ProductCollectionViewController.productInfo.count - 1
if indexPath.row == lastItem {
loadMore()
}
}
func loadMore() {
print("load more...")
//How to handle load more here?
}
How to handle the loadMore() function? Thank you.
Till that it's going well plus
if indexPath.row == lastItem && !loading {
loading = true
loadMore()
}
Using itemAtIndexIndexPath to determine the last cell is not the best way, you should check when the scrollView is going to scroll to bottom.
var isScrolledToBottomWithBuffer: Bool {
let buffer = tableView.bounds.height - tableView.contentInset.top - tableView.contentInset.bottom
let maxVisibleY = tableView.contentOffset.y + self.tableView.bounds.size.height
let actualMaxY = tableView.contentSize.height + tableView.contentInset.bottom
return maxVisibleY + buffer >= actualMaxY
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if( scrollView.contentSize.height == 0 ) {
return;
}
if isScrolledToBottomWithBuffer {
self.loadMore()
}
}
Improving upon #duytph answer
var isAllowedToFetchNewDataFromBottom = false
...
var products: [Products] = []
...
collectionView.delegate = self
...
extension AuthorProductsListViewController: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentSize.height == 0 {
return
}
if isScrolledToBottomWithBuffer, isAllowedToFetchNewDataFromBottom {
fetchNewData() {
self.isAllowedToFetchNewDataFromBottom = false
}
}
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
lastItemCount = pendingProducts.count
if indexPath.row == lastItemCount - 1 {
isAllowedToFetchNewDataFromBottom = true
}
}
var isScrolledToBottomWithBuffer: Bool {
let buffer = collectionView.bounds.height - collectionView.contentInset.top - collectionView.contentInset.bottom
let maxVisibleY = collectionView.contentOffset.y + self.collectionView.bounds.size.height
let actualMaxY = collectionView.contentSize.height + collectionView.contentInset.bottom
return maxVisibleY + buffer >= actualMaxY
}
}

UITableViewCell doesn't update height after adding a view to UIStackView

I have a UIStackView inside UITableViewCell's contentView. Based on user interaction, I add/remove items in the UIStackView. After modifying the items in UIStackView, I expect the cell to update it's height accordingly. But, it doesn't update it's height unless I call tableView.reloadData(). But, calling reloadData() in cellForRowAtIndexPath / willDisplayCell becomes recursive.
What is the proper way to adjust the cell height at run time based on items in UIStackView?
I use UITableViewAutomaticDimension
Updating the Problem:
Here is a simple prototype of what I am trying to do.
My actual problem is dequeuing the cell.
In the prototype, I have 2 reusable cells and 3 rows. For row 0 and 2, I dequeue cellA and for row 1, I dequeue cellB. Below is the overview on the condition I use.
if indexPath.row == 0 {
// dequeue cellA with 2 items in stackView
}
if indexPath.row == 1 {
// dequeue cellB with 25 items in stackView
}
if indexPath.row == 2 {
// dequeue cellA with 8 items in stackView
}
But the output is,
row 0 contains 2 items in stackView - expected
row 1 contains 25 items in stackView - expected
row 2 contains 2 items in stackView - unexpected, row 0 is dequeued
I also tried removing all arranged subViews of stackView in cellForRowAtIndexPath. But, doing so, flickers the UI when scrolling. How can I manage to get the desired output?
I believe the problem is when you are adding views to the stack view.
In general, adding elements should take place when the cell is initialized.
willDisplay cell: is where one handles modifying attributes of cell contents.
If you move your code from willDisplay cell: to cellForRowAt indexPath: you should see a big difference.
I just made that one change to the code you linked to, and the rows are now auto-sizing based on the stack view contents.
Edit: Looked at your updated code... the issue was still that you are adding your arrangedSubviews in the wrong place. And you compound it by calling reloadData().
Second Edit: Forgot to handle previously added subviews when the cells are reused.
Updated code... replace your ViewController code with:
//
// ViewController.swift
//
import UIKit
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
tableView.estimatedRowHeight = 56
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if indexPath.row == 0 || indexPath.row == 2 {
cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
if let stackView = cell.viewWithTag(999) as? UIStackView {
let numberOfItemsInStackView = (indexPath.row == 0) ? 2 : 8
let color = (indexPath.row == 0) ? UIColor.gray : UIColor.black
// cells are reused, so clear out any previously added subviews...
// but leave the first view that is part of the cell prototype
while stackView.arrangedSubviews.count > 1 {
stackView.arrangedSubviews[1].removeFromSuperview()
}
// use "i" so we can count
for i in 1...numberOfItemsInStackView {
// use label instead of view so we can number them for testing
let newView = UILabel()
newView.text = "\(i)"
newView.textColor = .yellow
// add a border, so we can see the frames
newView.layer.borderWidth = 1.0
newView.layer.borderColor = UIColor.red.cgColor
newView.backgroundColor = color
let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 54)
heightConstraint.priority = 999
heightConstraint.isActive = true
stackView.addArrangedSubview(newView)
}
}
}
if indexPath.row == 1 {
cell = tableView.dequeueReusableCell(withIdentifier: "lastCell")!
if let stackView = cell.viewWithTag(999) as? UIStackView {
let numberOfItemsInStackView = 25
// cells are reused, so clear out any previously added subviews...
// but leave the first view that is part of the cell prototype
while stackView.arrangedSubviews.count > 1 {
stackView.arrangedSubviews[1].removeFromSuperview()
}
// use "i" so we can count
for i in 1...numberOfItemsInStackView {
// use label instead of view so we can number them for testing
let newView = UILabel()
newView.text = "\(i)"
newView.textColor = .yellow
// add a border, so we can see the frames
newView.layer.borderWidth = 1.0
newView.layer.borderColor = UIColor.red.cgColor
newView.backgroundColor = UIColor.darkGray
let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 32)
heightConstraint.priority = 999
heightConstraint.isActive = true
stackView.addArrangedSubview(newView)
}
}
}
return cell
}
// override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// if cell.reuseIdentifier == "cell" {
// if let stackView = cell.viewWithTag(999) as? UIStackView {
// let numberOfItemsInStackView = (indexPath.row == 0) ? 2 : 8
// let color = (indexPath.row == 0) ? UIColor.gray : UIColor.black
// guard stackView.arrangedSubviews.count == 1 else { return }
// for _ in 1...numberOfItemsInStackView {
// let newView = UIView()
// newView.backgroundColor = color
// let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 54)
// heightConstraint.priority = 999
// heightConstraint.isActive = true
// stackView.addArrangedSubview(newView)
// }
// tableView.reloadData()
// }
// }
//
// if cell.reuseIdentifier == "lastCell" {
// if let stackView = cell.viewWithTag(999) as? UIStackView {
// let numberOfItemsInStackView = 25
// guard stackView.arrangedSubviews.count == 1 else { return }
// for _ in 1...numberOfItemsInStackView {
// let newView = UIView()
// newView.backgroundColor = UIColor.darkGray
// let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 32)
// heightConstraint.priority = 999
// heightConstraint.isActive = true
// stackView.addArrangedSubview(newView)
// }
// tableView.reloadData()
// }
// }
// }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
}
Try to reload only the cell using: https://developer.apple.com/documentation/uikit/uitableview/1614935-reloadrows
Example code
Here is an example. We have basic table view cells (TableViewCell) inside a view controller. The cells have 2 labels inside a stack view. We can hide or show the second label using the collapse/reveal methods.
class TableViewCell : UITableViewCell {
#IBOutlet private var stackView: UIStackView!
#IBOutlet private var firstLabel: UILabel!
#IBOutlet private var secondLabel: UILabel!
func collapse() {
secondLabel.isHidden = true
}
func reveal() {
secondLabel.isHidden = false
}
}
class ViewController : UIViewController {
#IBOutlet var tableView: UITableView!
fileprivate var collapsedCells: Set<IndexPath> = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 128
}
#IBAction private func buttonAction(_ sender: Any) {
collapseCell(at: IndexPath(row: 0, section: 0))
}
private func collapseCell(at indexPath: IndexPath) {
if collapsedCells.contains(indexPath) {
collapsedCells.remove(indexPath)
} else {
collapsedCells.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
extension ViewController : UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
if collapsedCells.contains(indexPath) {
cell.collapse()
} else {
cell.reveal()
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
}

Resources