currently I have this loadCharacters function
var characterArray = [Character]()
func loadCharacters(with request: NSFetchRequest<Character> = Character.fetchRequest()) {
do {
characterArray = try context.fetch(request)
} catch {
print("error loading data")
}
collectionView.reloadData()
}
My question is: How can I pass the fetched Data from there to my subclass CharacterCollectionViewCell and later on use this cell for my
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)
-> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "characterCell",
for: indexPath) as! CharacterCollectionViewCell {
...
}
any suggestion or any better way to make it works are really appreciated!!
You only need to get the characterArray element corresponding to the indexPath.item and use it to pass it into the cell.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)
-> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "characterCell",
for: indexPath) as! CharacterCollectionViewCell {
cell.someLabel.text = characterArray[indexPath.item]
//...
return cell
}
If you have too much data to be passed on it's better to create a model and use it's instance to pass data on to the cell. So, for that firstly create a model.
struct CharacterCellModel { // all properties... }
Then in your UIViewController sub-class.
var characterCellModels = [CharacterCellModel]() // append this model
And finally in cellForItemAt:
cell.characterCellModel = characterCellModels[indexPath.item]
Related
I have a UICollection view of things. these things can have 3 states:
- Active
- Neutral
- Inactive
Now, here is the code for the UICollectionViewCell:
class NGSelectStashCell: UICollectionViewCell {
var status: String = "Active"
#IBOutlet weak var statusImage: UIImageView!
#IBOutlet weak var bgImage: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
func changeStatus()
{
switch status {
case "Active":
status = "Neutral"
//change bgImage
case "Neutral":
status = "Inactive"
//change bgImage
case "Inactive":
status = "Active"
//change bgImage
default:
print("No Status")
}
}
}
Now, when I declare the UICollection View, I want to make it so that when the user "clicks" on the UICell it will call out the changeStatus() function. How can I do this in the Delegate/DataSource code?. Also, how do I save the "status" of the each cell (so that if I refresh the UICollectionView they don't all return to "active" ?
/*
////////// UICOLLECTIONVIEW FUNCTIONS ///////////
*/
extension NewOnlineGameVC: UICollectionViewDelegate, UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return availableStashes.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let stashCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ngStashCell", for: indexPath) as! NGSelectStashCell
stashCell.titleLabel.text = availableStashes[indexPath.row]
// stashCell.bgImage make image file with the same name as the name and change bg image to it.
return stashCell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// code to toggle between active/neutral/inactive
// do I re-declare stashCell as! NGSelectStashCell? or what do I do?
}
}
Unfortunately the solution is a bit more complicated then you think. Collection views may queue and reuse their cells for performance gains. That means that a single cell may and will be used for multiple objects when scrolling. What will happen is that when you will change the state on first cell and will scroll so it is reused then this cell will preserve its state and will look as if another cell has this changed state...
So your source of truth must always be your data source. Whatever availableStashes contains it needs to also contain its state. So for instance if you currently have var availableStashes: [MyObject] = [] you can change it like this:
typealias MySource = (status: String, object: MyObject)
var availableStashes: [MySource] = []
func setNewObjects(objects: [MyObject]) {
availableStashes = objects.map { ("Neutral", $0) }
}
Now on press you need to update the object in your data source for instance:
func changeStatusOfObjectAtIndex(_ index: Int, to newStatus: String) {
availableStashes[index] = (newStatus, availableStashes[index].object)
}
So on press you do:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
changeStatusOfObjectAtIndex(indexPath.row, to: <#Your new status here#>)
UICollectionView().reloadItems(at: [indexPath])
}
This will now trigger a reload for this specific cell which you can now update like
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let stashCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ngStashCell", for: indexPath) as! NGSelectStashCell
stashCell.dataObject = availableStashes[indexPath.row]
return stashCell
}
And inside the cell:
var dataObject: NewOnlineGameVC.MySource {
didSet {
titleLabel.text = dataObject.object
switch dataObject.status {
case "Active":
//change bgImage
case "Neutral":
//change bgImage
case "Inactive":
//change bgImage
default:
print("No Status")
}
}
}
I hope this clears your issue.
You can change the status of the cell once you get the reference of the selected cell.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? NGSelectStashCell else {return}
cell.status = "Active"
cell.changeStatus()
}
If you want to save the status of the cell then it must be model driven i.e anything happens to the cell must be saved to the model and the same model have to be reflecte in the cell when collection view tries to reuse the previously instantiated cells.
You already have a model AvailableStash, lets use it in proper way.
struct AvailableStash {
var statusImage: UIImage?
var backgroundImage: UIImage?
var title: String?
var status: String
//Initilize properties properly
init(with status: String) {
self.status = status
}
}
Your collection view must be model driven. For eg:
class DemoCollectionView: UICollectionViewController {
var availableStashes: [AvailableStash]?
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return availableStashes?.count ?? 0
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let stashCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ngStashCell", for: indexPath) as! NGSelectStashCell
let item = availableStashes[indexPath.row]
stashCell.titleLabel.text = item
// stashCell.bgImage make image file with the same name as the name and change bg image to it.
stashCell.statusImage = item.statusImage
return stashCell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? NGSelectStashCell else {return}
cell.status = availableStashes[indexPath.row].status
cell.changeStatus()
availableStashes[indexPath.row].status = cell.status
}
}
In my view controller I want to have two collection views. After trying this, I got a Sigabrt Error and my app crashes. I am predicting that the problem is because I am assigning the datasource of these collection views to self. I can be wrong, here is my code:
In view did load, i set the datasource of the collection views:
#IBOutlet var hashtagCollectionView: UICollectionView!
#IBOutlet var createCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
createCollectionView.dataSource = self
hashtagCollectionView.dataSource = self
}
Then I create an Extension for the UICollectionViewDataSource
extension CategoryViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var returnValue = 0
if collectionView == hashtagCollectionView {
// Return number of hashtags
returnValue = hashtags.count
}
if collectionView == createCollectionView {
// I only want 3 cells in the create collection view
returnValue = 3
}
return returnValue
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var return_cell: UICollectionViewCell
// Place content into hashtag cells
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hashtagCell", for: indexPath) as! TrendingTagsCollectionViewCell
cell.hashtagText = hashtags[indexPath.row]
// Place content in creators cell
let createCell = collectionView.dequeueReusableCell(withReuseIdentifier: "createCell", for: indexPath) as! CreateCollectionViewCell
createCell.text = creators[indexPath.row]
createCell.image = creatorImages[indexPath.row]
// Is this the right logic?
if collectionView == hashtagCollectionView {
return_cell = cell
} else {
return_cell = createCell
}
return return_cell
}
}
This is crashing because your if statement in collectionView(_:cellForItemAt:) doesn't quite cover enough ground.
While you're right that you need to return a different cell based on what collection view is asking, you can't call dequeueReusableCell(withReuseIdentifier:for:) twice with different identifiers like that. Since you ask the same collection view both times, it's very likely that one of the identifiers isn't registered with that collection view.
Instead, you should expand the if to cover just about the entirety of that method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == hashtagCollectionView {
// Place content into hashtag cells
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hashtagCell", for: indexPath) as! TrendingTagsCollectionViewCell
cell.hashtagText = hashtags[indexPath.row]
return cell
} else if collectionView == createCollectionView {
// Place content in creators cell
let createCell = collectionView.dequeueReusableCell(withReuseIdentifier: "createCell", for: indexPath) as! CreateCollectionViewCell
createCell.text = creators[indexPath.row]
createCell.image = creatorImages[indexPath.row]
return cell
} else {
preconditionFailure("Unknown collection view!")
}
}
That only tries to dequeue a cell once, depending on which collection view is asking, and casts the returned cell to the right class.
N.B. This kind of approach works for awhile, but in the long run, you can get very long UICollectionViewDataSource method implementations, all wrapped up in a series of if statements. It might be worth considering separating out your data sources into separate smaller classes.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hashtagCell", for: indexPath) as? TrendingTagsCollectionViewCell {
cell.hashtagText = hashtags[indexPath.row]
return cell
}
if let createCell = collectionView.dequeueReusableCell(withReuseIdentifier: "createCell", for: indexPath) as? CreateCollectionViewCell {
createCell.text = creators[indexPath.row]
createCell.image = creatorImages[indexPath.row]
return createCell
}
return UICollectionViewCell() // or throw error here
}
I think your code crash with a nil when reuse cell
You should test cellForItemAt like this
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == hashtagCollectionView {
// Place content into hashtag cells
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hashtagCell", for: indexPath) as! TrendingTagsCollectionViewCell
cell.hashtagText = hashtags[indexPath.row]
return cell
} else {
// Place content in creators cell
let createCell = collectionView.dequeueReusableCell(withReuseIdentifier: "createCell", for: indexPath) as! CreateCollectionViewCell
createCell.text = creators[indexPath.row]
createCell.image = creatorImages[indexPath.row]
return createCell
}
}
Trying to load data in collection view with this method
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if urls.count == 0 {
return UICollectionViewCell()
}
let url = urls[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
for: indexPath) as! ImageCollectionViewCell
storageRef.reference(withPath: url).getData(maxSize: 15 * 1024 * 1024, completion: { data, error in
if let data = data {
cell.imageView.image = UIImage(data: data)
}
})
return cell
}
The thing is - at first i can see both cells without data, then completion get called, an i'm getting a data, but the first image in both cells.
How can i do it right? And how can i get metadata in the same time too?
Screenshot:
UPD:
I wrote an array
var datas = ["first", "second", "third", "fourth"]
An changed cell code to
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
for: indexPath) as! ImageCollectionViewCell
cell.nameLabel.text = datas[indexPath.item]
return cell
}
And got this:
Still can't see what's wrong. My overrided methods:
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return datas.count / 2
}
SOLVED. Should return numberOfSections = 1
I think the problem is that you're downloading the information in the cellForItemAt indexPath: function and you should download the images in another function like this
//Variable to store the UIImages
var images = [UIImage]()
//function to download the images, run it in the viewdidload like self.getImages()
func getImages(){
for url in self.urls{
storageRef.reference(withPath: url).getData(maxSize: 15 * 1024 * 1024, completion: { data, error in
if let data = data {
self.images.append(UIImage(data: data))
}
})
}
self.YourCollectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if urls.count == 0 {
return UICollectionViewCell()
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
for: indexPath) as! ImageCollectionViewCell
cell.imageView.image = self.images[indexPath.row]
return cell
}
Should return 1 in numberOfSection func
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
We are currently establishing the architecture for a project and I have difficulty visualising a complete solution for this issue.
So we currently have a collection view with multiple dynamic prototypes and we subclass one of them for each cell. I was wondering if there's a way we could do something along the lines of
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifierForIndexPath(indexPath), for: indexPath)
//update cell here in a generic way based on the class of this cell (we have this information)
return cell
}
Basically, we want to avoid doing things like
if indexPath.row == 0 {
//do stuff for this specific cell
}
inside methods like collectionView(_:cellForItemAt:) / collectionView(_:didSelectItemAt:), while also complying to the MVVM pattern.
You can use the MVVM pattern for the cells themselves. So each cell is going to have its own viewModel. Then you can use something like the code below:
class ViewModel {
func reuseIdentifier(for indexPath: IndexPath) -> String {
//...
}
func cellViewModel(for indexPath: IndexPath) -> BaseCellViewModel {
//...
}
}
class BaseCellViewModel {
//...
}
class CellAViewModel: BaseCellViewModel {
//...
}
class CellBViewModel: BaseCellViewModel {
//...
}
class CellA: UICollectionViewCell {
var viewModel: CellAViewModel! {
didSet {
//update UI
}
}
}
class CellB: UICollectionViewCell {
var viewModel: CellBViewModel! {
didSet {
//update UI
}
}
}
class ViewController: UIViewController {
//...
var viewModel: ViewModel = ViewModel()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let reuseIdentifier = viewModel.reuseIdentifier(for: indexPath)
let cell =
collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
//update cell here in a generic way based on the class of this cell (we have this information)
configure(cell: cell, indexPath: indexPath)
return cell
}
func configure(cell: UICollectionViewCell, indexPath: IndexPath) {
switch cell {
case let cell as CellA:
cell.viewModel = viewModel.cellViewModel(for: indexPath) as! CellAViewModel
case let cell as CellB:
cell.viewModel = viewModel.cellViewModel(for: indexPath) as! CellBViewModel
default:
fatalError("Unkown cell type")
}
}
//...
}
In Table view we can put checkmark easily on cells.
But in Collection View how can we put check mark, when we select a cell (image)?
I just took a image view inside the cell and image view and put a tick mark image. My code is below.
But it's not working.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
// handle tap events
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! customCollectionViewCell
if(cell.checkMarkImage.hidden == true)
{
print("Hidden")
cell.checkMarkImage.hidden = false
}
else
{
cell.checkMarkImage.hidden = true
print("No Hidden")
}
}
//Delegate Method cellForItemAtIndexPath
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) ->
UICollectionViewCell
{
//Get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(
"pickSomecell",
forIndexPath: indexPath) as! pickSomeGridViewController
//Show Images in grid view
cell.cellImage.image = self.arrAllOriginalImages[indexPath.row]
as? UIImage
//Check Mark toggle.
cell.toggleSelected()
//return cell.
return cell
}
And in pickSomeGridViewController show checkMark image selected or not.
class pickSomeGridViewController: UICollectionViewCell{
//Outlet of cell image.
#IBOutlet var cellImage: UIImageView!
//Outlet of checkMark image.
#IBOutlet var cellCheckMarkImage: UIImageView!
//Function for select and deselect checkmark.
func toggleSelected ()
{
//If image is selected.
if (selected)
{
//Show check mark image.
self.cellCheckMarkImage.hidden = false
}
else
{
//Hide check mark image.
self.cellCheckMarkImage.hidden = true
}
}
}
I see two main problems with this code:
You use dequeueReusableCellWithReuseIdentifier method which obtains different cell from collection view cache, not the one on screen.
Use cellForItemAtIndexPath method of collection view instead.
You try to save cell's state (selected/not selected) in the cell itself. It's common mistake when working with UITableView/UICollectionView and this approach will not work. Instead, keep the state in some other place (in dictionary, for example) and restore it every time collection view calls your data source cellForItemAtIndexPath method.
var arrData = NSMutableArray()
// 1.Make a ModalClass.swift and NSArray with modal class objects like this
class CustomModal: NSObject {
//Declare bool variable for select and deselect login
var is_selected = Bool()
//you can declare other variable also
var id = Int32()
}
// 2. custom array with modal objects
override func viewDidLoad() {
super.viewDidLoad()
let arrTemp = NSArray()
arrTemp = [1,2,3,4,5,6,7,8,9,10]
for i in 0 ..< arrTemp.count{
let eventModal = CustomModal()
eventModal.is_selected = false
eventModal.id = arrTemp[i]
arrData.add(eventModal)
}
tblView.reloadData()
}
// 2. Use collection view delegate method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let modal = arrData[indexPath.row] as! CustomModal()
modal.is_selected = true
self.arrData.replaceObject(at: indexPath.row, with: modal)
tblView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let modal = arrData[indexPath.row] as! CustomModal()
modal.is_selected = false
self.arrData.replaceObject(at: indexPath.row, with: modal)
tblView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! YourCellClass
let modal = arrData[indexPath.row] as! CustomModal
if modal.is_selected == true{
cell.imgView.image = UIImage(named:"selected_image")
}else{
cell.imgView.image = UIImage(named:"deselected_image")
}
}
#Kishor, paintcode is the third party tool through which you can do that. I have provided the link too. since by default you don't have this facility, you should make your custom behavior to achiever this. Thanks.
Swift 4
In ViewController
// make a cell for each cell index path
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YourCollectionViewCellID", for: indexPath as IndexPath) as! YourCollectionViewCell
cell.someImageView.image = imgArr[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("You selected cell #\(indexPath.item)!")
let cell = collectionView.cellForItem(at: indexPath) as? YourCollectionViewCell
cell?.isSelected = true
cell?.toggleSelected()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as? YourCollectionViewCell
cell?.isSelected = false
cell?.toggleSelected()
}
In YourCollectionViewCell
class YourCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var someImageView: UIImageView!
#IBOutlet weak var checkImageView: UIImageView!
//Function for select and deselect checkmark.
public func toggleSelected() {
if (isSelected == false) {
//Hide check mark image.
self.checkImageView.image = UIImage(named: "unCheckImage")
isSelected = true
}else{
//Show check mark image.
self.checkImageView.image = UIImage(named: "CheckImage")
isSelected = false
}
}
}
Hope enjoy!!
var selectedCellIndex:Int?
take variable if you want to show selected Item after reloadData() : which is previously selected CellItem. {inspired by above answer }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCollectionCell", for: indexPath) as! ColorCollectionCell
cell.isSelected = false
if selectedCellIndex == indexPath.item {
cell.checkMarkImgView.image = UIImage(named: "icn_checkMark")
}else {
cell.toggleSelected()
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! ColorCollectionCell
cell.isSelected = true
selectedCellIndex = indexPath.item
cell.toggleSelected()
}
In CollectionViewCell u can use this method
class ColorCollectionCell: UICollectionViewCell {
#IBOutlet weak var cellimgView: UIImageView!
#IBOutlet weak var checkMarkImgView: UIImageView!
func toggleSelected() {
if (isSelected) {
self.checkMarkImgView.image = UIImage(named: "icn_checkMark")
}else{
self.checkMarkImgView.image = UIImage(named: "")
// here you can use uncheck img here i am not using any image for not selected.
}
}
}