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.
Related
I am trying to create an array of images/videos that are selected from a collection view and store them using Core Data. I have an array of the selected cells and it is dynamic because it allows for the user to add or subtract images to the array by selecting or deselecting the cell. My code currently uses IndexPath to accomplish this but I believe I need to convert it to an UIImage array so that I can add it to Core Data based on another solution I saw. So how do I convert the selectedCells array below to a UIImage array?
var selectedCells: [IndexPath] = []
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? ImageCell {
selectedCells.append(indexPath)
cell.index = selectedCells.count
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
guard let idx = selectedCells.firstIndex(of: indexPath) else { return }
selectedCells.remove(at: idx)
let curSelected: [IndexPath] = collectionView.indexPathsForSelectedItems ?? []
collectionView.reloadData()
let saveY = collectionView.contentOffset.y
collectionView.performBatchUpdates({
curSelected.forEach { pth in
collectionView.selectItem(at: pth, animated: false, scrollPosition: .centeredVertically)
}
}, completion: { _ in
collectionView.contentOffset.y = saveY
})
}
UPDATE:
The reason I decided to use Core Data is because the users will need to be able to edit the images but if they stop in the middle of it and come back the users progress needs to be saved. If they close the app the progress will be lost. I no longer think I need to store the image itself, just a reference to the the image in the library. Please correct me if I am wrong.
You dont even need to hold your own selectedCells, there is a property called "indexPathsForSelectedItems" for this purpose. You are overcomplicating things with your reload strategy.
Then use those indexpaths to access your datasource like you do in cellForRow(at: indexPath).
So lets say you have a helper called func object(at indexPath: IndexPath) -> YourObjectModel
then you can "convert" the indexpath array using array's map functionality.
Additionally, an image should not end up being stored in coredata ... only a reference to it.
I'm currently making a screen that has an UITableView with many sections that have the content of cells is UICollectionView. Now I'm saving the selected indexPath of the collection into an array then save to UserDefaults (because the requirement is showing all cells has selected before when reopening view controller).
But I have the issues is when I reopen view controller all items in all sections with the same selected indexPath show the same state.
I know it occurs because I just save the only indexPath of the selected item without the section of UITableview which is holding the collection view. But I don't know how to check the sections. Can someone please help me to solve this problem? Thank in advance.
I'm following this solution How do I got Multiple Selections in UICollection View using Swift 4
And here is what I do in my code:
var usrDefault = UserDefaults.standard
var encodedData: Data?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
if let act = usrDefault.data(forKey: "selected") {
let outData = NSKeyedUnarchiver.unarchiveObject(with: act)
arrSelectedIndex = outData as! [IndexPath]
}else {
arrSelectedData = []
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let optionItemCell = collectionView.dequeueReusableCell(withReuseIdentifier: "optionCell", for: indexPath) as! SDFilterCollectionCell
let title = itemFilter[indexPath.section].value[indexPath.item].option_name
if arrSelectedIndex.contains(indexPath) {
optionItemCell.filterSelectionComponent?.bind(title: title!, style: .select)
optionItemCell.backgroundColor = UIColor(hexaString: SDDSColor.color_red_50.rawValue)
optionItemCell.layer.borderColor = UIColor(hexaString: SDDSColor.color_red_300.rawValue).cgColor
}else {
optionItemCell.backgroundColor = UIColor(hexaString: SDDSColor.color_white.rawValue)
optionItemCell.layer.borderColor = UIColor(hexaString: SDDSColor.color_grey_100.rawValue).cgColor
optionItemCell.filterSelectionComponent?.bind(title: title!, style: .unselect)
}
optionItemCell.layoutSubviews()
return optionItemCell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let strData = itemFilter[indexPath.section].value[indexPath.item]
let cell = collectionView.cellForItem(at: indexPath) as? SDFilterCollectionCell
cell?.filterSelectionComponent?.bind(title: strData.option_name!, style: .select)
cell?.backgroundColor = UIColor(hexaString: SDDSColor.color_red_50.rawValue)
cell?.layer.borderColor = UIColor(hexaString: SDDSColor.color_red_300.rawValue).cgColor
if arrSelectedIndex.contains(indexPath) {
arrSelectedIndex = arrSelectedIndex.filter{($0 != indexPath)}
arrSelectedData = arrSelectedData.filter{($0 != strData)}
}else {
arrSelectedIndex.append(indexPath)
arrSelectedData.append(strData)
encodedData = NSKeyedArchiver.archivedData(withRootObject: arrSelectedIndex)
usrDefault.set(encodedData, forKey: "selected")
}
if let delegate = delegate {
if itemFilter[indexPath.section].search_key.count > 0 {
if (strData.option_id != "") {
input.add(strData.option_id!)
let output = input.componentsJoined(by: ",")
data["search_key"] = itemFilter[indexPath.section].search_key.count > 0 ? itemFilter[indexPath.section].search_key : strData.search_key;
data["option_id"] = output
}
}else {
data["search_key"] = itemFilter[indexPath.section].search_key.count > 0 ? itemFilter[indexPath.section].search_key : strData.search_key;
data["option_id"] = strData.option_id
}
delegate.filterTableCellDidSelectItem(item: data, indexPath: indexPath)
}
}
This will only work based on the assumption that both your parent table view and child collection views both are not using multiple sections with multiple rows and you only need to store one value for each to represent where an item is located in each respective view.
If I am understanding correctly, you have a collection view for each table view cell. You are storing the selection of each collection view, but you need to also know the position of the collection view in the parent table? A way to do this would be to add a property to your UICollectionView class or use the tag property and set it corresponding section it is positioned in the parent table. Then when you save the selected IndexPath, you can set the section to be that collection view's property you created(or tag in the example) so that each selected indexPath.section represents the table view section, and the indexPath.row represents the collection view's row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...
let collectionView = UICollectionView()
collectionView.tag = indexPath.section
//...
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
indexPath.section = collectionView.tag
let strData = itemFilter[indexPath.section].value[indexPath.item]
//...
}
Basically each selected index path you save will correspond to the following:
indexPath.section = table view section
indexPath.row = collection view row
IndexPath(row: 5, section: 9) would correlate to:
--table view cell at IndexPath(row: 0, section: 9) .
----collection view cell at IndexPath(row: 5, section: 0)
Edit: This is how you can use the saved index paths in your current code
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//...
let tempIndexPath = IndexPath(row: indexPath.row, section: collectionView.tag)
if arrSelectedIndex.contains(tempIndexPath) {
//...
} else {
//...
}
//...
}
Your statement arrSelectedIndex.contains(indexPath) in the cellForItemAt method is not correct.
Each time a UICollectionView in a UITableView's section is loaded, this will called the cellForItemAt for ALL cells.
Here is the error :
In your GIF example the first cell is selected in the first collectionView, you will store (0, 0) in the array.
But when the second collectionView will loads its cells, it will check if the indexPath (0, 0) is contained into your array. It is the case, so the backgroundColor will be selected.
This error will be reproduced on every collectionView stored in your tableView sections.
You should probably also store the sectionIndex of your UITableView into your array of IndexPath.
I have a collection view that scrolls horizontally and each cell pushes to a detail view upon a tap. When I load the app, I have it print the object at index. At first it will load the one cell it is supposed to. But when I scroll over one space it prints off two new ids, and then begins to associate the data of the last loaded cell with the one currently on the screen, which is off by one spot now. I have no clue how to resolve this, my best guess is there is some way to better keep up with the current index or there is something in the viewDidAppear maybe I am missing. Here is some code:
open var currentIndex: Int {
guard (self.collectionView) != nil else { return 0 }
return Int()
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExpandCell", for: indexPath) as! ExpandingCollectionViewCell
let object = self.imageFilesArray[(indexPath).row] as! PFObject
cell.nameLabel.text = object["Name"] as! String
whatObject = String(describing: object.objectId)
print(whatObject)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let object = self.imageFilesArray[(indexPath as NSIndexPath).row]
guard let cell = collectionView.cellForItem(at: indexPath) as? ExpandingCollectionViewCell else { return }
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "EventDetailViewController") as! EventDetailViewController
nextViewController.lblName = nameData
nextViewController.whatObj = self.whatObject
self.present(nextViewController, animated:false, completion:nil)
}
Does anyone know a better way to set the current index so I am always pushing the correct data to the next page?
The data source that is setting the elements of the cell can keep count of index that can also be set as a property and can be used to retrieve back from the cell to get correct current index.
EventDetailViewController is your UICollectionViewController subclass I assume.
If this is correct then you need to implement on it:
A method that tells the collection how many items there are in the datasource.
//1
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1 // you only have 1 section
}
//2
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return array.count// this is the number of models you have to display... they will all live in one section
}
A method that tells the collection which cellView subclass to use for that given row.
-A method that populates the cell with it's datasource.
Follow this tutorial from which I extracted part of the code, they convey this core concepts pretty clearly.
Reusing, as per Apple's docs happens for a number of cells decided by UIKit, when you scroll up a little bit 2 or N cells can be dequed for reusing.
In summary, with this three methods you can offset your collection into skipping the one record that you want to avoid.
Happy coding!
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
}
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