Swift CollectionView ReloadData changes order for a moment - ios

I have a very simple CollectionView which shows 4 buttons, each with the name of the Turtles. When I click any button, I want to refresh my CollectionView in case my array has increased and a 5th name was added, which should then appear as a 5th button. This basically works, but the moment I press and release any button, the button titles show my array from the last to first and then switch back to first to last.
Does anyone know why it does that and how I can stop it?
I just want to update my CollectionView every time a button was pressed. If the array should increase, let's say "Splinter", I want the Button titles to stay the same on the first four buttons and just a 5th button to be added.
Thank you very much in advance!
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var meineCV: UICollectionView!
let cv = CollectionViewCell()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cv.turtles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
cell.meinButton.setTitle(cv.turtles[indexPath.row], for: .normal)
cell.meinButton.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
return cell
}
#IBAction func buttonAction(_ sender: UIButton) {
print(sender.currentTitle!)
meineCV.reloadData()
}
}
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var meinButton: UIButton!
let turtles: [String] = ["Leonardo", "Donatello", "Michelangelo", "Raphael"]
}

As I understood you see that the button labels are being changed at the moment of press gestures and you want to get rid of it.
Please try this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
let turtle = cv.turtles[indexPath.row]
cell.meinButton.titleLabel?.text = turtle
cell.meinButton.setTitle(turtle, for: .normal)
cell.meinButton.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
return cell
}

Related

new view created with every collectionView cell tap instead of updating the existing one

I want to have this ring progress view (cocoa pod) inside collectionView Cells. When you tap on a cell, the progress should increase by 0.1 (0.0 = 0% Progress, 1.0 = 100% progress).
Unfortunately, it seems like it always creates a new progress view above the old one, because the shade of red is getting darker and you can see a second/third/.. ring. I have no idea how I can fix this.
This is my code: (Cell class and CollectionViewController Class)
import UIKit
private let reuseIdentifier = "Cell"
class myCollectionViewController: UICollectionViewController {
#IBOutlet var myCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
myCollectionView.reloadData()
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! myCollectionViewCell
if(indexPath.item != 0){
cell.setProgress(progress: 1 / Double(indexPath.item))
}
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! myCollectionViewCell
cell.setProgress(progress: cell.getProgress() + 0.1)
myCollectionView.reloadData()
}
}
import UIKit
import MKRingProgressView
class myCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var ringView: UIView!
let ringsize = 150
var ringProgressView = RingProgressView()
override func layoutSubviews() {
ringProgressView = RingProgressView(frame: CGRect(x: 0, y: 0, width: ringsize, height: ringsize))
ringProgressView.startColor = .red
ringProgressView.endColor = .magenta
ringProgressView.ringWidth = CGFloat(ringsize) * 0.15
ringProgressView.progress = 0.0
ringProgressView.shadowOpacity = 0.5
ringView.addSubview(ringProgressView)
}
}
extension myCollectionViewCell{
func setProgress(progress: Double){
UIView.animate(withDuration: 0.5){
self.ringProgressView.progress = progress
}
}
func getProgress() -> Double{
return self.ringProgressView.progress
}
}
This is what it looks like when launch (the progress it for testing at (1/indexpath.item):
And this is what it looks like when I TAP THE BOTTOM LEFT CICLE 1 TIME:
I can see with the animation, that a second ring overlays the first ring on the bottom left circle with same progress. Somehow the other rings also changed... why? You can even see the second ring on some other circles. You can also see that the shade of the red went a little bit darker. When I tap multiple times, everything becomes completely (intensive) red.
Thanks for any help!
Simple usage:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! myCollectionViewCell
cell.setProgress(progress: cell.getProgress() + 0.1)
}
you should not call like this:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! myCollectionViewCell
cell.setProgress(progress: cell.getProgress() + 0.1)
myCollectionView.reloadData()
}
because of cell reuse mechanism,
you call
collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! myCollectionViewCell
you get a cell of objects pool, namely a cell of newly created , or a cell created and not visible
myCollectionView.reloadData()
that's worse, call this, just do resetting,
because you do not maintain the data.
You can use pattern mark & config
in method didSelectItemAt , update the data
in method cellForItemAt , update the UI, use myCollectionView.reloadData() to trigger
var progressData = [IndexPath: Double]()
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! myCollectionViewCell
if let val = progressData[indexPath]{
cell.setProgress(progress: val)
}
else{
var value = Double(0)
if(indexPath.item != 0){
value = 1 / Double(indexPath.item)
}
progressData[indexPath] = value
cell.setProgress(progress: value)
}
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
progressData[indexPath]! += 0.1
myCollectionView.reloadData()
}

How to set my UIButton tittle inside of uicollection view

so I want to set my button tittle like this with my array, but the result the button title didn't show my array
//viewcontroller
let menus = ["Topokki","Sundubu","Galbitang"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return menus.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
cell.menusButton.titleLabel?.text = menus[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath.item)
}
//collectionviewcell class
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var menusButton: UIButton!
}
Try:
cell.menusButton.setTitle(menus[indexPath.item], forState: .normal)
From docs about titleLabel of UIButton
To set the actual text of the label, use setTitle(_:for:) (button.titleLabel.text does not let you set the text)
So... you need to set title of your button by calling setTitle(_:for:)
cell.menusButton.setTitle(menus[indexPath.item], for: .normal)

Make sure only 1 cell has an active state in a UICollectionView

I have an UICollectionView in which I want only want 1 cell to be active. With active I mean: the last cell that has been clicked (or the very first cell when to collection view lays out). When a user clicks a non-active cell, I want to reset the old active cell to a non-active state. I am having trouble doing this. This is because visibleCells, a property of collection view, only returns the cells on screen but not the cells in memory. This is my current way to locate an active cell and reset the state to non active.
This scenario can happen, causing multiple active cells: A user scroll slightly down so that the current active cell is not visible anymore, taps on a random cell and scroll up. The problem is that the old active cell stays in memory, although it is not visible: cellForItemAt(_:) does not gets called for that cell. Bad news is that visibleCells also do not find the old active cell. How can I find it? The function willDisplay cell also does not work.
An example project can be cloned directly into xCode: https://github.com/Jasperav/CollectionViewActiveIndex.
This is the code in the example project:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CollectionView!
static var activeIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
collectionView.go()
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var button: MyButton!
}
class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
func go() {
delegate = self
dataSource = self
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
if indexPath.row == ViewController.activeIndex {
cell.button.setTitle("active", for: .normal)
} else {
cell.button.setTitle("not active", for: .normal)
}
cell.button.addTarget(self, action: #selector(touchUpInside(_:)), for: .touchUpInside)
return cell
}
#objc private func touchUpInside(_ sender: UIButton){
let hitPoint = sender.convert(CGPoint.zero, to: self)
guard let indexPath = indexPathForItem(at: hitPoint), let cell = cellForItem(at: indexPath) as? Cell else { return }
// This is the problem. It does not finds the current active cell
// if it is just out of bounds. Because it is in memory, cellForItemAt: does not gets called
if let oldCell = (visibleCells as! [Cell]).first(where: { $0.button.titleLabel!.text == "active" }) {
oldCell.button.setTitle("not active", for: .normal)
}
cell.button.setTitle("active", for: .normal)
ViewController.activeIndex = indexPath.row
}
}
To recover from this glitch you can try in cellForRowAt
cell.button.tag = indexPath.row
when the button is clicked set
ViewController.activeIndex = sender.tag
self.reloadData()
You can use the isSelected property of the UIColectionViewCell. You can set an active layout to your cell if it is selected. The selection mechanism is implemented by default in the UIColectionViewCell. If you want to select/activate more than one cell you can set the property allowsMultipleSelection to true.
Basically this approach will look like this:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.go()
}
func activeIndex()->Int?{
if let selectedItems = self.collectionView.indexPathsForSelectedItems {
if selectedItems.count > 0{
return selectedItems[0].row
}
}
return nil
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var myLabel: UILabel!
override var isSelected: Bool{
didSet{
if self.isSelected
{
myLabel.text = "active"
}
else
{
myLabel.text = "not active"
}
}
}
}
class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
func go() {
delegate = self
dataSource = self
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
return cell
}
}

Singleton Class not updating properly

I've got a collectionViewController and a normal viewcontroller. When a cell is tapped it goes to the VC and sets the label to the cell tapped. The variable for this is in a singleton class.
The issue I'm having is that the first time you tap a cell and go to the VC the label doesn't say anything (console prints correct data though). Then you go back to the collectionView and tap a different cell, the label in the view now shows the cell you tapped previously.
I tried cleaning the build folder etc. but didn't do anything. I also tried another method - let CVC = CollectionViewController() then lbl.text = CVC.cellTapped (create var first) but that didnt work either.
SharingManager.swift
class SharingManager {
var cellChoose = String()
static let sharedInstance = SharingManager()
}
CollectionViewController.swift
class CollectionViewController: UICollectionViewController {
let sm = SharingManager.sharedInstance
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
// Configure the cell
let lbl = cell.viewWithTag(1) as! UILabel
lbl.text = String(indexPath.row)
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
sm.cellChoose = "cell\(indexPath.row)"
print(sm.cellChoose)
}
}
VC2.swift (the viewcontroller tapping a cell takes you to)
class VC2: UIViewController {
let sm2 = SharingManager.sharedInstance
#IBOutlet weak var lbl2: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.lbl2.text = sm2.cellChoose
//print(sm2.cellChoose)
}
Since you're navigating via a segue, it's possible your new ViewController is loaded before didSelectItem is called. Moving lbl2.text = sm2.cellChoose to viewWillAppear will fix your issue.
The "correct" way to do this with segues is handle this in prepareForSegue, but what you have now will work.

Perform Segue from UICollectionViewCell Button different from Cell Click

I have a UICollectionViewCell, with a UIButton. And I have two different actions. The first one, when the user presses the cell, it will segue to another view withdidSelectITemAt; the second one, when the users presses the UIButton inside the cell.
My problem is that on Swift Code of MyCollectionViewCell, I cant perform a segue, When I write the Code:
self.performSegue(withIdentifier: "toStoreFromMyDiscounts", sender: nil)
it says the error:
Value of Type MyCollectionViewCell has no member performSegue.
I also cannot write prepareForSegue, it doesn't auto complete.
How can I create a segue from a cell, that is different from click the cell itself?
You can not call performSegue from your UICollectionViewCell subclass, because there is no interface declared on UICollectionViewCell like that.
The reason why it is working didSelectItemAtIndexPath() is because i suppose the delegate of your UICollectionView is a UIViewController subclass, what has the function called performSegueWithIdentifier:()`.
You need to notify your UIViewController when the button was clicked in your UICollectionViewCell, for what you have various possibilities, like KVO or using delegate.
Here is a little code sniplet, how to use KVO. This solution is great, as long as you do not care, in which cell was the button pressed.
import UIKit
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var button: UIButton!
}
class CollectionViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
}
extension CollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: CollectionViewCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
// Add your `UIViewController` subclass, `CollectionViewController`, as the target of the button
// Check out the documentation of addTarget(:) https://developer.apple.com/reference/uikit/uicontrol/1618259-addtarget
cell.button.addTarget(self, action: #selector(buttonTappedInCollectionViewCell), for: .touchUpInside)
return cell
}
func buttonTappedInCollectionViewCell(sender: UIButton) {
self.performSegue(withIdentifier: "toStoreFromMyDiscounts", sender: nil)
}
}
EDIT:
If you care, in which cell the touch event has happend, use the delegate pattern.
import UIKit
protocol CollectionViewCellDelegate: class {
// Declare a delegate function holding a reference to `UICollectionViewCell` instance
func collectionViewCell(_ cell: UICollectionViewCell, buttonTapped: UIButton)
}
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var button: UIButton!
// Add a delegate property to your UICollectionViewCell subclass
weak var delegate: CollectionViewCellDelegate?
#IBAction func buttonTapped(sender: UIButton) {
// Add the resposibility of detecting the button touch to the cell, and call the delegate when it is tapped adding `self` as the `UICollectionViewCell`
self.delegate?.collectionViewCell(self, buttonTapped: button)
}
}
class CollectionViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
}
extension CollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: CollectionViewCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
// Asssign the delegate to the viewController
cell.delegate = self
return cell
}
}
// Make `CollectionViewController` confrom to the delegate
extension CollectionViewController: CollectionViewCellDelegate {
func collectionViewCell(_ cell: UICollectionViewCell, buttonTapped: UIButton) {
// You have the cell where the touch event happend, you can get the indexPath like the below
let indexPath = self.collectionView.indexPath(for: cell)
// Call `performSegue`
self.performSegue(withIdentifier: "toStoreFromMyDiscounts", sender: nil)
}
}
Here's an elegant solution that only requires a few lines of code:
Create a custom UICollectionViewCell subclass
Using storyboards, define an IBAction for the "Touch Up Inside" event of your button
Define a closure
Call the closure from the IBAction
Swift 4+ code
class MyCustomCell: UICollectionViewCell {
static let reuseIdentifier = "MyCustomCell"
#IBAction func onAddToCartPressed(_ sender: Any) {
addButtonTapAction?()
}
var addButtonTapAction : (()->())?
}
Next, implement the logic you want to execute inside the closure in your
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.reuseIdentifier, for: indexPath) as? MyCustomCell else {
fatalError("Unexpected Index Path")
}
// Configure the cell
// ...
cell.addButtonTapAction = {
// implement your logic here, e.g. call preformSegue()
self.performSegue(withIdentifier: "your segue", sender: self)
}
return cell
}
You can use this approach also with table view controllers.
Another solution that also works like a charm:
extension YOURViewController : UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YOURCell", for: indexPath) as! YOURCollectionViewCell
cell.butTapped = {
[weak self] (YOURCollectionViewCell) -> Void in
// do your actions when button tapped
}
}
return cell
}
class YOURCollectionViewCell: UICollectionViewCell
{
var butQRTapped: ((YOURCollectionViewCell) -> Void)?
#IBAction func deleteButtonTapped(_ sender: AnyObject) {
butTapped?(self)
}
}

Resources