swift collectionview cell didSelectItemAtIndexPath shows wrong cell if you scroll - ios

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
}

Related

making cells in collection view dynamic and add images to them

I want to upload pictures for different user types. So for user type 1..I want to create a collection view displaying 3 cells containing 3 options to add and display pic and for other users, 2 cells each. Like in above pic, 2 cells were created for userType = 3. So far Im able to create required number of cells according to users but haven't been able to upload pictures in any of them. I already have separate imagePicker delegate method to upload pic somewhere else but that's a static pic. I want to upload pics in these dynamically created cells. So if user = 3, Return 2 cells containing image view and button (Document) to add pic. Here's my code so far:
let reuseIdentifier = "cell"
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let userType = typeOfUser //typeOfUser = 1,2,3
var cellNumber = 0
if userType == "2" {
cellNumber = 3 //to return 3 cells for userType 2
} else {
cellNumber = 2 //to return 2 cells for userType 1 and 3
}
return cellNumber
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! DocumentCollectionViewCell
if userType == "2" {
for index in 0 ..< 3 { // for user = "2", 3 cells required so I used forloop < 3
let imageData:NSData = cell.userPic.image!.jpegData(compressionQuality: 60)! as NSData //compress image
let base64 = imageData.base64EncodedString(options: .lineLength64Characters) //store image in base64
//code below to pass base64 to image view and give it some id(stuck at creating logic for that)
}
}
return cell
}
This code does return correct number of cells with 3 empty image views with add picture button below each of them for typeOfUser = "2" as required but how to make button below each imageView add 1 picture at a time to one cell? Make each button use that imagePicker delegate and open photo library to add pic in "n" cells for respective user types?
Like how can I add picture in above image by clicking Document button below 1st image and then second image and if some other user, add as much images I want to add by clicking button below each image view created in no matter how many cells are returned?
This is all I want in short:
For user = 3
return 2 cells like above image
add pics by clicking document button below each image
for other users do same for different number of cells.
You can achieve this through delegate.
Create Protocol:
protocol ButtonDelegate {
// Define expected delegate functions
func cellButtonClicked(tag: Int, indexPath: IndexPath)
}
Confirm this Protocol in class where you want to take action:
extension ViewController: ButtonDelegate {
func cellButtonClicked(tag: Int, indexPath: IndexPath) {
//implement Logic to open ImageLibrary, you have indexPath available here
}
}
Inside PhotoCell create delegate instance and info needed for delegate method:
class PhotoCell... {
var cellButtonDelegate: ButtonDelegate?
var indexPath: IndexPath = IndexPath()
}
In ViewController:func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell add these lines:
cell.indexPath = indexPath
cell.ImageButton.tag = 0 // tag to identify userNumber
cell.cellButtonDelegate = self
In button action inside PhotoCell, inform delegate function:
cellButtonDelegate.cellButtonClicked(tag: sender.tag, indexPath: self.indexPath)

Variables in UICollectionViewCells change to different cells

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.

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

Cells insight UICollectionView do not load until there are visible?

I have a UITableView with UICollectionView insight every table view cell. I use the UICollectionView view as a gallery (collection view with paging). My logic is like this:
Insight the method
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// This is a dictionary with an index (for the table view row),
// and an array with the url's of the images
self.allImagesSlideshow[indexPath.row] = allImages
// Calling reloadData so all the collection view cells insight
// this table view cell start downloading there images
myCell.collectionView.reloadData()
}
I call collectionView.reloadData() and in the
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// This method is called from the cellForRowAtIndexPath of the Table
// view but only once for the visible cell, not for the all cells,
// so I cannot start downloading the images
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoCollectionCell
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
if indexPath.item < arr.count {
var imageName:String? = arr[indexPath.item]
if let imageName = imageName {
var escapedAddress:String? = imageName.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
if let escapedAddress = escapedAddress {
var url:NSURL? = NSURL(string: escapedAddress)
if let url = url {
cell.imageOutlet.contentMode = UIViewContentMode.ScaleAspectFill
cell.imageOutlet.hnk_setImageFromURL(url, placeholder: UIImage(named: "placeholderImage.png"), format: nil, failure: nil, success: nil)
}
}
}
}
}
}
return cell
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
println("collection row: \(collectionView.tag), items:\(arr.count)")
return arr.count
}
}
return 0
}
I set the right image for the cell. The problem is that the above method is called only for the first collection view cell. So when the user swipe to the next collection view cell the above method is called again but and there is a delay while the image is downloaded. I would like all the collection view cells to be loaded insight every visible table view cell, not only the first one.
Using the image I have posted, "Collection View Cell (number 0)" is loaded every time but "Collection View Cell (number 1)" is loaded only when the user swipe to it. How I can force calling the above method for every cell of the collection view, not only for the visible one? I would like to start the downloading process before swiping of the user.
Thank you!
you're right. the function func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell will be called only when cell start to appear. that's a solution of apple called "Lazy loading". imagine your table / collection view have thousand of row, and all of those init at the same time, that's very terrible with both memory and processor. so apple decide to init only view need to be displayed.
and for loading image, you can use some asynchronous loader like
https://github.com/rs/SDWebImage
it's powerful and useful too :D

iOS CollectionView Help (saving current state)

I am making an iOS app in Swift and I am running into a obstacle I seem to be stuck on. I have a collectionview populated by an string array, which are the names of the images I am populating the image within the collectionview cells:
var tableData: [String] = ["cricket1.png", "cricket1.png", "cricket1.png"]
I've linked up the images to the collectionview with the following code:
//How many cells there are is equal to the amount of items in tableData (.count property)
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tableData.count
}
//Linking up collectionView with tableData
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: CricketCell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CricketCell
cell.imgCell.image = UIImage(named: tableData[indexPath.row])
return cell
}
When I have the user tap the cell, the image goes from cricket1.png to cricket2.png:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("Cell selected")
var cell = collectionView.cellForItemAtIndexPath(indexPath) as! CricketCell
if cell.imgCell.image == UIImage(named:"cricket1.png"){
var cell = collectionView.cellForItemAtIndexPath(indexPath) as! CricketCell
cell.imgCell.image = UIImage(named:"cricket2.png")
}
Now.. here is where I am having trouble. I am currently trying to save data in tableData, however when I do, it always saves it as ["cricket1.png", "cricket1.png", "cricket1.png"]. Even if the image has been tapped and changed to "cricket2.png". Even if all the images on the screen is cricket2.png, when I save tableData, it saves it as ["cricket1.png", "cricket1.png", "cricket1.png"]. I am aware it is because I am storing the variable tableData I declared earlier, but is there any way I can grab a string array of what is on the screen/the current state of the collectionview?
Any help would be appreciated!
Thank you!
You need to update the data in the array yourself. It's independent from image.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("Cell selected")
//1: Get the index for which data in array you need to update
let index = indexPath.row
//I don't think this comparison may server your purpose. When you new an UIImage object, it's a different one than the original image. You may want to just compare the data
let unSelectedImage = "cricket1.png"
if self.tableData[index] != unselectedImage {
//2: Update data in array
var cell = collectionView.cellForItemAtIndexPath(indexPath) as! CricketCell
let selectedImageName = "cricket2.png"
self.tableData[index] = selectedImageName
cell.imgCell.image = UIImage(named: selectedImageName)
}
}
So in this case, even you refresh the table, image will loading according to the new tableData.
I would recommend that u use another property to hold the state of the cell. For example,
var selectedState = [true, false, true, true, true]
Upon tapping the image, update the image and selectedState array accordingly.
For example,
if selectedState[indexPath.row] {
cell.imgCell.image == UIImage(named:"cricket1.png")
else {
cell.imgCell.image = UIImage(named:"cricket2.png")
}
Once you are ready to grab the state of the tapped cells, you can use the selectedState array variable.
You can't use the cell to persist any state as the cell can get dequeued by collectionView and may lost its state as you scroll.
Hope this helps.

Resources