I'm making a music app recently, and I'd like to know how to pass data from CollectionView to TableView which has several sections. Here is the home page of my project, and what I want to do is when user tap the image, it will precent another ViewController with the information about the picture. I already know how to present a ViewController by clicking CollectionViewCell inside TableView by delegate, but only if there's only one section.
The thing is that I have 5 sections in this page, and I also have 5 different models for encoding the JSON from API. So how I show the pictures is to send the image urls(from 5 models) to TableViewCell in each section like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.identifier, for: indexPath) as? HomeTableViewCell else { return UITableViewCell() }
cell.delegate = self
switch homeSections[indexPath.section] {
case .newReleases:
if let newReleases = self.newReleases?.albums.items.map({$0.images[0].url}) {
cell.getPictures = newReleases
}
return cell
case .followSingers:
if let currentlyFollowing = self.currentlyFollowing?.artists.items.map({$0.images[0].url}) {
cell.getPictures = currentlyFollowing
cell.isCircle = true
}
return cell
case .catrgories:
if let categories = self.musicCategory?.playlists.items.map({$0.images[0].url}) {
cell.getPictures = categories
}
return cell
case .artists:
if let playlist = self.relatedArtists?.artists.map({$0.images[0].url}){
cell.getPictures = playlist
}
return cell
case .recentlyPlayed:
if let recentlyPlayed = self.recentlyPlayed?.items?.map({$0.track.album.images[0].url}) {
cell.getPictures = recentlyPlayed
}
return cell
}
}
However, when I want to pass the information which the user tap, there's nothing I can pass but indexPath. I've tried to declare the 5 different model types in TableViewCell, but I still don't know which section did the user tap. Does anyone can help? Thanks a lot!
Update:
To make the question more clearly, please refer to the information below.
In this page, I have a TableView and embed a CollectionView in the TableViewCell, and there's only one ImageView in the CollectionViewCell.
The "New Releases", "Currently Following" and "Categories" are the header of TableView. The "New Releases" is the first section, and the "Currently Following" is the second section, and so on.
Here is how I set cellForItem in CollectionView Delegate. It basically converts String to URL, and display the picture on the screen.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeCollectionViewCell.identifier, for: indexPath) as? HomeCollectionViewCell else { return UICollectionViewCell() }
guard let url = URL(string: getPictures[indexPath.row]) else { return UICollectionViewCell() }
cell.myImageView.getImages(url: url)
if isCircle == true {
cell.myImageView.layer.cornerRadius = cell.myImageView.frame.width/2
}
return cell
}
When the user taps the image, it will only trigger the didSelectItemAt in CollectionView. And I can only pass indexPath.row so far.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.sendIndexPath(index: indexPath.row)
}
Related
I am using the collection view to show the gif's on the list.
Now facing the cell reusable issue while scrolling the cells up or down of collection view.
Like itemA is on first place in the list and itemB is on the second place in the list.
but when I scroll the data in the collection view. the places of items got misplaced. like some time itemA gone on 5th place or sometimes anywhere in the list.
i know i think this is the use with reusable cell, but don't know how to salve this.
Plss help.
Collection view cellForItemAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GifCell", for: indexPath as IndexPath) as? GifCell else {
fatalError()
}
if gifArr.count > 0 {
let urlString = self.gifArr[indexPath.row]
let url = URL(string: urlString)!
DispatchQueue.global().async {
let imageData = try? Data(contentsOf: url)
let imageData3 = FLAnimatedImage(animatedGIFData: imageData) // this is the 3rd pary library to show the gifs on UIimageview's
DispatchQueue.main.async {
cell.imageView.animatedImage = imageData3
cell.textLabel.text = String(indexPath.row)
}
}
}
return cell
}
In GifCell you could implement prepareForReuse() method:
Performs any clean up necessary to prepare the view for use again.
override func prepareForReuse() {
super.prepareForReuse()
imageView.animatedImage = nil
textLabel.text = ""
}
Note:
at this point, each time cellForItemAt method gets called, the url will be reloaded, so later, you might want find a way to cache the images instead of keep reloading them.
First solution: You can cache data and every time check if there is, use your cache.
you can use this link, but replace UIImage with gift type!
or
try this, I did not test it
if let giftAny = UserDefaults.standard.value(forKey: "giftUrl") {
//cast giftAny to Data
// use cached gift
} else {
// cache gift
let giftData = try? Data(contentsOf: url)
UserDefaults.standard.setValue(giftData, forKeyPath: "giftUrl")
//use gift
}
Second Solution: Don't reuse cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = UICollectionViewCell(style: .default, reuseIdentifier:"Cell")
return cell
}
but in this case, if you have many cells, memory leak is unavoidable.
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 TableView that is embedded into a CollectionView, and I am trying to show relevant data in the TableView that corresponds to the correct CollectionViewCell or IndexPath Item. I tried assigning tag as such: cell.tableView.tag = indexPath.item but it seems to be problematic.
I tried print(tableView.tag) in my collectionViewCell and it printed
2 1 0 3 4 5
but I have 7 collectionViewCells in total so the last tag isn't printing for some reason.
My collectionView is embedded in another TableView already, below is the code in the MasterTableViewCell.swift:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if diningIndexPath.section == 0 {
let cell: FoodCourtCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "foodCourtCell", for: indexPath) as! FoodCourtCollectionViewCell
cell.tableView?.register(UINib(nibName: "RestaurantTableViewCell", bundle: nil), forCellReuseIdentifier: "restaurantCell")
cell.tableView.tag = indexPath.item
//...
return cell
}
}
In the customCollectionViewCell.swift, I have this code for my tableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: RestaurantTableViewCell = tableView.dequeueReusableCell(withIdentifier: "restaurantCell", for: indexPath) as! RestaurantTableViewCell
print(tableView.tag)
let currentRestaurant = foodCourts[tableView.tag].childLocations[indexPath.row]
cell.textLabel.text = currentRestaurant.name
//...
return cell
}
Is there any way to fix this, or are there other ways to achieve what I want to do? Any help is appreciated, thanks!
Nesting these entities is always a pain especially when you need to access indexPaths of items later. If if get your problem correctly. One of the solutions I suggest is to store a map (dictionary) of your paths. For a fast access to them. Here's an example of how I managed this in a similar situation:
typealias CollectionIndexPath = IndexPath
typealias TableIndexPath = IndexPath
var indexMap: [CollectionIndexPath: TableIndexPath] = [:]
Now when you need to access some of the items or configure it.
func cellForItemAtIndexPath { ... } {
let cell = { ... }
let cellPath = indexPath
let tablePath = indexMap[cellPath]
let foodCourtForCell = foodCourts[cellPath.item]
let childLocationsForTableView = foodCourtForCell.childLocations
cell.configureWith(court: foodCourtForCell, locations: childLocations)
Now you can manage all the data related to this nested monster from the outside.
I have a CollectionView issue, I have a video showing the problem detailed below. When I click one cell it moves in a weird manner.
Here is my code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedFilter = indexPath.row
if filters[indexPath.row] != "Todo" {
filteredNews = news.filter { $0.category == filters[indexPath.row] }
} else {
filteredNews = news
}
tableView.reloadData()
collectionView.reloadData()
}
My Cell is moving, (Just the last cell, don't know why).
I think it might be related to collectionView.reloadData() But I need to do that for updating the green bar you can see on this Video when I select a Cell.
How can I make it not move? Someone had had a similar problem?
I noticed you reloaded a tableView during collectionView didSelectItemAt. If that tableView is a superView of your collectionView that will be the exact reason why you are having this abnormal behaviour.
If it were not, I can offer 3 solutions:
This library have a view controller subclass that can create the effect you want to show.
Manually create a UIView/UIImageView that is not inside the collectionView but update it's position during the collectionView's didSelectItemAt delegate method to but visually over the cell instead - this would require some calculation, but your collectionView will not need to reload.
You can attempt to only reload the two affected cells using the collectionView's reloadItem(at: IndexPath) method.
Know that when you reload a table/collection view, it will not change the current visible cell. However any content in each cell will be affected.
Finally I Solve it! I removed collectionView.reloadData() and added my code to change colors inside didSelectItemAt changing current selected item and old selected item (I created a Variable to see which one was the old selected item).
If someone interested, here is my code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let oldSelectedFilter = selectedFilter
if selectedFilter != indexPath.row {
let oldIndexPath = IndexPath(item: oldSelectedFilter, section: 0)
selectedFilter = indexPath.row
if filters[indexPath.row] != "Todo" {
filteredNews = news.filter { $0.category == filters[indexPath.row] }
} else {
filteredNews = news
}
if let cell = collectionView.cellForItem(at: indexPath) as? FiltersCollectionViewCell {
cell.selectedView.backgroundColor = MainColor
}
if let cell = collectionView.cellForItem(at: oldIndexPath) as? FiltersCollectionViewCell {
cell.selectedView.backgroundColor = UIColor(red:0.31, green:0.33, blue:0.35, alpha:1.0)
}
tableView.reloadData()
}
}
I'm using a unclickable tableView to display different information of one object.
For this informations I have different custom cell types one where I placed a map, if my object have locations, one have a list with links, and another a multiple line label for a little description...for example.
I manage this cells with:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! MapCell
return cell
} else if indexPath.row == 1 {
let cell: textCell = tableView.dequeueReusableCellWithIdentifier("textCell") as! TextCell
return cell
} else if indexPath.row == 2 {
let cell: listCell = tableView.dequeueReusableCellWithIdentifier("listCell") as! ListCell
return cell
}
}
So far so good, everything working fine. My problem is, not every object needs a map, some of them just need some text and a list, other objects need a map and a list, other all of them. I want my tableView to skip some cells if there is a condition.
I know, I can make an symbolic array for changing the number of cells of my tableView, but that deleting just from the end of my tableView, not specific cells.
One of my ideas is to generate a empty cell, maybe with a height of 0 or 1 so that I can do something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
if mapCellNeeded {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
} else {
let cell: emptyCell = tableView.dequeueReusableCellWithIdentifier("emptyCell") as! EmptyCell
}
return cell
} else if indexPath.row == 1 {
...
}...
}
put I don't know if there isn't an efficient way. Hope you guys can help me.
Your solution would work. Another approach (very nice and swifty) would be not to hardcode row numbers, but rather use enum instead:
enum InfoCellType {
case Map
case Text
case Links
}
...
var rows = [InfoCellType]()
...
// when you know what should be there or not
func constructRows() {
if (mapCellNeeded) {
rows.append(InfoCellType.Map)
}
rows.append(InfoCellType.Text)
... etc
}
Then in the table view methods just see what's the type for current indexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellType: InfoCellType = self.rows[indexPath.row]
switch cellType {
case .Map:
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
return cell
case .Text:
...
case.Links:
...
}
}
This solution also allows to easily change order of rows - just change the order of items in rows array.