Variables in UICollectionViewCells change to different cells - ios

I have a shop menu for my game in swift. I am using a UICollectionView to hold the views for the items. There is a black glass covering over them before they are bought, and it turns clear when they buy it. I am storing data for owning the certain items in the cells' classes. When I scroll down in the scrollView and then come back up after clicking a cell. A different cell than collected has the clear class and the one I previously selected is black again.
import UIKit
class Shop1CollectionViewCell: UICollectionViewCell {
var owned = Bool(false)
var price = Int()
var texture = String()
#IBOutlet weak var glass: UIImageView!
#IBOutlet weak var ball: UIImageView!
func initiate(texture: String, price: Int){//called to set up the cell
ball.image = UIImage(named: texture)
if owned{//change the glass color if it is owned or not
glass.image = UIImage(named: "glass")
}else{
glass.image = UIImage(named: "test")
}
}
func clickedOn(){
owned = true//when selected, change the glass color
glass.image = UIImage(named: "glass")
}
}
Then I have the UICollectionView class
import UIKit
class ShopViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
struct ball{
var price = Int()
var texture = String()
var owned = Bool()
}
var balls = Array<ball>()//This is assigned values, just taken off of the code because it is really long
override func viewDidLoad() {
super.viewDidLoad()
balls = makeBalls(
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return (balls.count - 1)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Shop1CollectionViewCell
cell.initiate(texture: balls[indexPath.item].texture, price: 1)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! Shop1CollectionViewCell
cell.clickedOn()
}
}
I am wondering why the variables stored in one class of a cell are being switched to another.
Please ask if you need me to add any additional information or code.

You should definitely implement prepareForReuse, and set the cell back to its default state - which in this case would be with the black glass, and unowned.
You can still change the glass in the same way as you're doing in didSelectItemAt, but I'd recommend having the view controller track the state of each ball.
For example, when didSelectItemAt gets called - the view controller would update the ball stored in self.balls[indexPath.row] to have owned = true.
That way, the next time cellForItemAt gets called, you'll know what colour the glass should be by checking the value in self.balls[indexPath.row].owned.
Lastly, you're returning balls.count - 1 in numberOfItems, is this intentional?
In the case where have 10 balls, you're only going to have 9 cells. If you want a cell for each of your objects you should always return the count as is.

Sometimes, you may have to override the following function
override func prepareForReuse(){
super.prepareForReuse()
// reset to default value.
}
in class Shop1CollectionViewCell to guarantee cells will behave correctly. Hope you got it.

Related

Check Boxes in CollectionView are changing positions when scrolling

I have collectionView (3*3) with Images I am loading from server and I placed a checkBox in the top left corner of each cell so that I can select the cells and based on the selected cells I will get ids for the respective cells images(ids coming from server) and I am able do everything right. But, the problem is if there is are 20 images and if I check the 5 random cells which are loaded for the first time and when I scroll down to select other cells 5 other random checkBoxes are already checked and if I scroll up again some other 5 random cells are checked. It appears that the checked checkBoxes are changing positions because of the dequeue reusable property in the cellForItemAtIndexPath of UICollectionView DataSource method..
I have no Idea how to overcome this problem. Please help me If any one knows how to do this. I am posting below the code I wrote so far and some simulator screenshots for better understanding of the problem...
EditCertificatesViewController:
import UIKit
import Alamofire
protocol CheckBoxState {
func saveCheckBoxState(cell: EditCertificateCell)
}
class EditCertificatesViewController: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
#IBOutlet weak var certificatesCollectionView: UICollectionView!
var certificatesArray = [Certificates]()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Delete Certificates"
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return certificatesArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "editCertificate", for: indexPath) as! EditCertificateCell
if let certificateURL = URL(string: certificatesArray[indexPath.item].imagePath) {
cell.certificateImage.af_setImage(withURL: certificateURL)
}
cell.certificateId.text = "\(certificatesArray[indexPath.item].imageId)"
cell.selectCertificate.customBox()
if selectedCellIndex.contains(indexPath.item) {
cell.selectCertificate.on = true
}
else {
cell.selectCertificate.on = false
}
cell.selectCertificate.tag = indexPath.item
cell.checkState = self
return cell
}
}
extension EditCertificatesViewController: CheckBoxState {
func saveCheckBoxState(cell: EditCertificateCell) {
if cell.selectCertificate.on == true {
cell.selectCertificate.on = false
}
else {
cell.selectCertificate.on = true
}
if selectedCellIndex.contains(cell.selectCertificate.tag) {
selectedCellIndex = selectedCellIndex.filter{$0 != cell.selectCertificate.tag}
}
else {
selectedCellIndex.append(cell.selectCertificate.tag)
}
print("Status1 \(selectedCellIndex.sorted { $0 < $1 })")
// certificatesCollectionView.reloadData()
}
}
EditCertificateCell:
import UIKit
class EditCertificateCell: UICollectionViewCell {
#IBOutlet weak var certificateImage: UIImageView!
#IBOutlet weak var selectCertificate: BEMCheckBox!
#IBOutlet weak var certificateId: UILabel!
#IBOutlet weak var selectCertificateBtn: UIButton!
var checkState: CheckBoxState?
override func awakeFromNib() {
super.awakeFromNib()
self.selectCertificateBtn.addTarget(self, action: #selector(btnTapped(_:event:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: UIButton,event: UIEvent) {
self.checkState?.saveCheckBoxState(cell: self)
}
}
CollectionView dequeue's your cell. To rid of this you need to maintain array of selected certificates. Follow below procedure.
Create an array arrSelectedIndex : [Int] = []
In cellForRow,
First check either current index in available in arrSelectedIndex or not? If yes, then make your cell as selected otherwise keep it uncheck.
Give tag to your check button as like this buttonCheck.tag = indexPath.item
If you wanted to select images on check button action, do below.
Get the button tag let aTag = sender.tag
Now check wther this index is available in arrSelectedIndex or not? If yes then remove that index from from the arrSelectedIndex otherwise append that array.
reload your cell now.
If you wanted to select images on didSelectItem instaead check button action, do below.
Now check wther this selected index (indexPath.item) is available in arrSelectedIndex or not? If yes then remove that index from from the arrSelectedIndex otherwise append that array.
reload your cell now.
As this procedure is lengthy so I can only explain you how to do this. If need further help then you can ask.
This is expected. Because you are reusing the cells.
Consider this. You select the first 2 cells, and now scroll down. This function of yours will be called func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {. Now this might get the views from the first 2 cells, that you had selected, and their checkboxes are already selected too.
You need to unset them, and set them, depending upon their last state.
I would recommend adding another property isCertificateSelected to your Certificate model. Each time the user taps on a cell, you retrieve the model, and set/unset this bool. When collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) is called, you retrieve the isCertificateSelected again, and set the checkbox accordingly.
Create an array var Status1CheckList = [Int]()
And in cellForItemAt indexPath check the condition like
if Status1CheckList.contains(indexPath.row) {
cellOfCollection.CheckBtn.setImage(UIImage(named: "check"), for: .normal)
} else {
cellOfCollection.CheckBtn.setImage(UIImage(named: "uncheck"), for: .normal)
}
cellOfCollection.CheckBtn.tag = indexPath.row
cellOfCollection.CheckBtn.addTarget(self, action: #selector(self.checkList), for: .touchUpInside)
And checklist method, After selecting button reload the collectionview
#objc func checkList(_ sender: UIButton) {
if Status1CheckList.contains(sender.tag) {
Status1CheckList = Status1CheckList.filter{ $0 != sender.tag}
} else {
Status1CheckList.append(sender.tag)
}
print("Status1 \(Status1CheckList.sorted { $0 < $1 })")
self.collectionviewObj.reloadData()
}

How to access off-screen but existing cells in UICollectionView in Swift?

The title might be a little hard to understand, but this situation might help you with it.
I'm writing a multiple image picker. Say the limit is 3 pictures, after the user has selected 3, all other images will have alpha = 0.3 to indicate that this image is not selectable. (Scroll all the way down to see a demo)
First of all, this is the code I have:
PickerPhotoCell (a custom collection view cell):
class PickerPhotoCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var selectable: Bool {
didSet {
self.alpha = selectable ? 1 : 0.3
}
}
}
PhotoPickerViewController:
class PhotoPickerViewController: UICollectionViewController {
...
var photos: [PHAsset]() // Holds all photo assets
var selected: [PHAsset]() // Holds all selected photos
var limit: Int = 3
override func viewDidLoad() {
super.viewDidLoad()
// Suppose I have a func that grabs all photos from photo library
photos = grabAllPhotos()
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell ...
let asset = photos[indexPath.row]
...
// An image is selectable if:
// 1. It's already selected, then user can deselect it, or
// 2. Number of selected images are < limit
cell.selectable = cell.isSelected || selected.count < limit
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! PickerPhotoCell
if cell.isSelected {
// Remove the corresponding PHAsset in 'selected' array
} else {
// Append the corresponding PhAsset to 'selected' array
}
// Since an image is selected/deselected, I need to update
// which images are selectable/unselectable now
for visibleCell in collectionView.visibleCells {
let visiblePhoto = visibleCell as! PickerPhotoCell
visiblePhoto.selectable = visiblePhoto.isSelected || selected.count < limit
}
}
}
This works almost perfectly, except for one thing, look at the GIF:
The problem is
After I've selected 3 photos, all other visible photos have alpha = 0.3, but when I scroll down a little more, there are some photos that still have alpha = 1. I know why this is happening - Because they were off-screen, calling collectionView.visibleCells wouldn't affect them & unlike other non-existing cells, they did exist even though they were off-screen. So I wonder how I could access them and therefore make them unselectable?
The problem is that you are trying to store your state in the cell itself, by doing this: if cell.isSelected.... There are no off screen cells in the collection view, it reuses cells all the time, and you should actually reset cell's state in prepareForReuse method. Which means you need to store your data outside of the UICollectionViewCell.
What you can do is store selected IndexPath in your view controller's property, and use that data to mark your cells selected or not.
pseudocode:
class MyViewController {
var selectedIndexes = [IndexPath]()
func cellForItem(indexPath) {
cell.isSelected = selectedIndexes.contains(indexPath)
}
func didSelectCell(indexPath) {
if selectedIndexes.contains(indexPath) {
selectedIndexes.remove(indexPath)
} else if selectedIndexes.count < limiit {
selectedIndexes.append(indexPath)
}
}
}

Button inside CollectionView not clickable

I have a button in a custom cell of a collectionview. The collectionview is on a scrollview. For some reason, I am not able to click on the button. I've checked that all my elements have User Interaction enabled.
Here is my layout of the collection (I've hidden some sensitive data)
Here is my custom collection view cell:
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var connectButton: UIButton!
var onConnectTap: (MyCollectionViewCell) -> Void)?
#IBAction func connectButton(_ sender: Any) {
onConnectTap?(self)
}
func populate(_ user: User) {
nameLabel.text = user.name
}
}
I have a xib file where a Touch Up Inside event of a button has been hooked up to the connectButton IBAction.
And in my ViewController:
MyCollectionView.register(UINib(nibName: "MyCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "cell")
Here's my collection view function in my ViewController:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = myCollectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionViewCell
let user = users.values[indexPath.row]
cell.populate(user)
cell.onConnectTap = { (cell) in
//do something
}
return cell
}
Nothing happens when I click on the button. Am I missing something here? Is the scroll view interfering? Do I need to specifiy a addTarget? Or something else?
After searching the entire web pretty much, I finally found the solution that was in the comment of this SO answer: https://stackoverflow.com/a/44908916/406322
I needed to add this in MyCollectionViewCell:
self.contentView.isUserInteractionEnabled = false
I think the cell selection was hijacking the touch event.
I'm facing the same issue and found the best solution after spending much time.
cell.contentView.isUserInteractionEnabled = false
But its the perfect solution, and adds only one line in the cell for item method
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = myCollectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionViewCell
cell.contentView.isUserInteractionEnabled = false
return cell
}
Double-check the structure of the XIB file. I lost time dealing with this issue (where the button in the XIB did not seem to respond), as the structure had a second embedded cell, rather than just one (AboutCell in my case).

Transferring Data from one View to another in Swift 3

Following Situation:
I have 2 Controllers, a ViewController and a CollectionViewController. The normal ViewController is supposed to collect data from the user and when the start button is clicked, an algorithm solves the problem and returns the result. The Result is supposed to be transferred to the CollectionViewController, and based on the solution the CollectionView will be built.
Below is the code I used for the start button. As you can see, the Algorithm is called, the results are stored in several variables and now I was trying to transfer the matrixArray to my CollectionViewController (its a first test). The CollectionViewController should use the data stored in this array to present some form of tableau
#IBAction func startButton(_ sender: Any) {
...
let solution = PrimalSimplex(problem: problem, currentSolution: currentSolution)
matrixArray = solution.0
basicArray = solution.1
maxArray = solution.2
currentSolutionArray = solution.3
isOptimal = solution.4
isCyceling = solution.5
let CollectionVC = storyboard?.instantiateViewController(withIdentifier: "CollectionView") as! CollectionViewController
CollectionVC.testMatrix = matrixArray
}
So far so good, the data arrives is available in the CollectionViewController after the start button is pushed. But when I try to use the data to build the CollectionView, I get an error message.
This is the code that I used in the collectionViewController to build the CollectionView (It worked before, with static values... the problems occur when I try to use values that the algorithm returns):
class CollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var myCollectionView: UICollectionView!
// Creates an empty array for the values
var testMatrix = Array<Matrix>()
//Setup CollectionView: Table to display LPs
let reuseIdentifier = "cell"
var items = testMatrix[0] <----ERROR
// MARK: - UICollectionViewDataSource protocol
// tell the collection view how many cells to make
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
// 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
// Use the outlet in our custom class to get a reference to the UILabel in the cell
cell.myLabel.text = items[indexPath.item]
cell.backgroundColor = UIColor(red:0.94, green:0.94, blue:0.94, alpha:1.0) // make cell more visible in our example project
// Change shape of cells
cell.layer.cornerRadius = 8
return cell
}
....
The error is displayed at var items = testMatrix[0]:
Cannot use instance member 'testMatrix' within property initializer; property initializers run before 'self' is available
I can understand that Xcode has a problem here, because it can't be sure that the testMatrix has values stored.... I think that the problem. I tried to use if let/ guard statement, but that didnt solve the problem.
Any advice on how to fix it or whats actually wrong here?
Maybe there is a better way to transfer the data from the first VC to the other one?
You can't initialize property from other dependent property at the class level.
You should try to initialized in viewDidLoad.
var items: Matrix?
override func viewDidLoad() {
super.viewDidLoad()
if testMatrix.count>0{
items = testMatrix[0]
}
}

swift collectionview cell didSelectItemAtIndexPath shows wrong cell if you scroll

Let's say you set up a bunch of image views inside a UICollectionView's cells (from an array of image names) and make their alpha 0.5 by default when you set up the items.
Then you make the image view's alpha to 1.0 in the didSelectItemAtIndexPath func, so it becomes alpha 1 when the user taps.
This works when the user taps a cell, but it does not persist if the user scrolls, because the cell is being re-used by the UI on some other level.
The result is another cell farther down the way (when scrolling) becomes alpha 1.0 and the original cell you selected reverts back to its previous alpha 0.5 appearance.
I understand that this is all done to make things more efficient on the device, but I still have not figured out how to make it work properly where the selected item persists.
ANSWER
Apple does provide a selectedBackgroundView for cells that you can use to change the background color, shadow effect, or outline etc. They also allow you to use an image inside the cell with a "default" and "highlighted" state.
Both of those methods will persist with the selection properly.
However, if you wish to use attributes or different elements than one of those provided for indicating your selected state, then you must use a separate data model element that includes a reference to the currently selected item. Then you must reload the viewcontroller data when the user selects an item, resulting in the cells all being redrawn with your selected state applied to one of the cells.
Below is the jist of the code I used to solve my problem, with thanks to Matt for his patience and help.
All of this can be located inside your main UICollectionView Controller class file, or the data array and struct can be located inside their own swift file if you need to use it elsewhere in the project.
Data and data model:
let imagesArray=["image1", "image2", "image3", ...]
struct Model {
var imageName : String
var selectedState : Bool
init(imageName : String, selectedState : Bool = false){
self.imageName = imageName
self.selectedState = selectedState
}
}
Code for the UICollectionView Controller
// create an instance of the data model for images and their status
var model = [Model]()
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// build out a data model instance based on the images array
for i in 0..<imagesArray.count {
model.append(Model(imageName: imagesArray[i]))
// the initial selectedState for all items is false unless otherwise set
}
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imagesArray.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// when the collectionview is loaded or reloaded...
let cell:myCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! myCollectionViewCell
// populate cells inside the collectionview with images
cell.imageView.image = UIImage(named: model[indexPath.item].imageName)
// set the currently selected cell (if one exists) to show its indicator styling
if(model[indexPath.item].selectedState == true){
cell.imageView.alpha = 1.0
} else {
cell.imageView.alpha = 0.5
}
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// when a cell is tapped...
// reset all the selectedStates to false in the data model
for i in 0..<imagesArray.count {
model[i].selectedState = false
}
// set the selectedState for the tapped item to true in the data model
model[indexPath.item].selectedState = true
// refresh the collectionView (triggering cellForItemAtIndexPath above)
self.collectionView.reloadData()
}
but it does not persist if the user scrolls, because the cell is being re-used by the UI on some other level
Because you're doing it wrong. In didSelect, make no change to any cells. Instead, make a change to the underlying data model, and reload the collection view. It's all about your data model and your implementation of cellForItemAtIndexPath:; that is where cells and slots (item and section) meet.
Here's a simple example. We have just one section, so our model can be an array of model objects. I will assume 100 rows. Our model object consists of just an image name to go into this item, along with the knowledge of whether to fade this image view or not:
struct Model {
var imageName : String
var fade : Bool
}
var model = [Model]()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<100 {
// ... configure a Model object and append it to the array
}
}
override func collectionView(
collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return 100
}
Now, what should happen when an item is selected? I will assume single selection. So that item and no others should be marked for fading in our model. Then we reload the data:
override func collectionView(cv: UICollectionView,
didSelectItemAtIndexPath indexPath: NSIndexPath) {
for i in 0..<100 {model[i].fade = false}
model[indexPath.item].fade = true
cv.reloadData()
}
All the actual work is done in cellForItemAtIndexPath:. And that work is based on the model:
override func collectionView(cv: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let c = self.collectionView!.dequeueReusableCellWithReuseIdentifier(
"Cell", forIndexPath: indexPath) as! MyCell
let model = self.model[indexPath.item]
c.iv.image = UIImage(named:model.imageName)
c.iv.alpha = model.fade ? 0.5 : 1.0
return c
}
You logic is incorrect. didSelectItemAtIndexPath is used to trigger something when a cell is selected. All this function should contain is this:
let cell:stkCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! stkCollectionViewCell
cell.imageView.alpha = 1.0
selectedIndex = indexPath.item
Then in your cellForItemAtIndexPath function you should have the logic to set the cell because this is where the cells are reused. So this logic should be in there:
if (indexPath.item == selectedIndex){
print(selectedIndex)
cell.imageView.alpha = 1.0
}
else {
cell.imageView.alpha = 0.5
}

Resources