Stored selected indexPath of UICollectionView inside UITableView - ios

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.

Related

Present ViewController by Clicking CollectionViewCell inside TableView(with several sections)

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

Auto Select Middle Visible Cell Of Collection View

I'm trying to select and highlight the middle cell of the visible cells in a collection view at any given time. The collection view in question displays days for six months forwards and back.
I've tried using the scroll view delegates and the collection view delegates. But all that works is select and highlight code in didSelectItem() collection view delegate.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("delegate called")
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
collectionView.cellForItem(at: indexPath)?.backgroundColor = UIColor.highlightCellGreen()
if let cell = collectionView.cellForItem(at: indexPath) as? ClientListDateCollectionViewCell{
monthLabel.text = cell.monthName
monthLabel.text = monthLabel.text?.capitalized
}
I tried to select the middle cell while scrolling using the viewDidScroll() delegate. But, I wasn't able to get the output I wanted.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleCellCount = dateCollectionView.indexPathsForVisibleItems.count
let cellCount = dateCollectionView.visibleCells.count
let visibleCells = dateCollectionView.indexPathsForVisibleItems[visibleCellCount-1/2]
if visibleCellCount>0{
let middle = visibleCellCount/2
let midValue = dateCollectionView.indexPathsForVisibleItems[middle]
dateCollectionView.selectItem(at: midValue, animated: true, scrollPosition: .centeredHorizontally)
}
How do I go about selecting the middle cell?
edit 1: The collection view starts on the leftmost point and then scrolls to the middle i.e, today's date
You can use delegate of UICollectionView (i.e: didHighlightItemAtIndexPath). just make sure to call collection view delegates on your desired time by calling reload function
self.collectionView.reloadData()
and in you collection view delegate just do this
func collectionView(collectionView: UICollectionView, didHighlightItemAtIndexPath indexPath: NSIndexPath){
var cell : UICollectionViewCell = UICollectionViewCell()
self.collectionView.cellForItemAtIndexPath = indexPath
//change highlighted color as of your need
cell.view.backgroundColor = UIColor.init(red: 25, green: 118, blue: 210).cgColor
}
This will highlight you selected item
Disable multiple selection (or selection entirely?) to make things easier.
collectionView.allowsMultipleSelection = false
On scrollViewDidScroll(_:) get the center point of the screen as CGpoint.
let center = collectionView.center
Use that information to get the index path of the center item
let indexPath = collectionView.indexPathForItem(at: center)
Select the item
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .top)
Suppose that you have the horizontal of displaying, and you want to have the auto scroll to the center of your item in datasource.
Creating a method and calling it immediately after your collection view is completely configured:
func scrollToCenterIndex() {
let centerIndex = LIST_OF_YOUR_DATA_SOURCE.count / 2
let indexPath = IndexPath(item: centerIndex, section: 0)
self.collectionView.scrollToItem(at: indexPath,
at: .right,
animated: false)
}
Inside the method:
public func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CELL,
for: indexPath) as? CustomCell else {
fatalError("Cannot create cell")
}
If indexPath.row == LIST_OF_YOUR_DATA_SOURCE.count / 2 {
// perform your hight light color to the cell
} else {
// reset your hight light color to default color
}
let model = LIST_OF_YOUR_DATA_SOURCE[indexPath.row]
cell.configure(model)
return cell
}
I think you can use a method to get the center point of collection view, and use this value to get the the middle of visible cell.
let centerPoint = self.view.convert(collectionView.center, to: collection)
Here is an example I did it with a tableView. You can apply it to your collection view with the same approach.
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var dataSource = Array(1...31)
var centerIndex: IndexPath?
func setCellSelected(cell: UITableViewCell, _ selected: Bool) {
cell.contentView.backgroundColor = selected ? .green : .white
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL")
cell?.textLabel?.text = String(dataSource[indexPath.row])
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = cell {
setCellSelected(cell: cell, indexPath.row == index.row)
}
return cell!
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// reset the previous hight light cell
if let centerIndex = centerIndex, let cell = tableView.cellForRow(at: centerIndex) {
setCellSelected(cell: cell, false)
}
// set hight light to a new center cell
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = tableView.cellForRow(at: index) {
setCellSelected(cell: cell, true)
centerIndex = index
}
}
}
I was also trying to do the auto-selection of the middle visible cell of the collection view, and I got the solution, here is the solution:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Reload Collection View
collectionView.reloadData()
// Find centre point of collection view
let visiblePoint = CGPoint(x: collectionView.center.x + collectionView.contentOffset.x, y: collectionView.center.y + collectionView.contentOffset.y)
// Find index path using centre point
guard let newIndexPath = collectionView.indexPathForItem(at: visiblePoint) else { return }
// Select the new centre item
collectionView.selectItem(at: newIndexPath, animated: true, scrollPosition: .centeredHorizontally) }
You need to use the Scroll view delegate function, scrollViewDidEndDecelerating. Reload the collection view first. Second, find the center visible point of the collection view. Third, using the center visible point, find the indexPath of collection view and finally use the index to select the item in the collection view.
I know I answered this question a little late, still thinking that it will be helpful for someone.
Cheers!

Getting IndexPath Item in TableView Embedded in CollectionView

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.

UICollectionView nested in UITableViewCell does not update using DispatchQueue after receiving new data

I have a UICollectionView nested inside a UITableViewCell:
The number inside a collection view cell gets updated on a different view, so when I return back to this screen, I want to be able to refresh the view and the new numbers are reflected in their cells. I have a model called topUserModel in my collection view that I populate with data from my firebase database. When I pull down to refresh, the following function is run from inside my main table view:
#objc func refreshView(refreshControl: UIRefreshControl) {
DispatchQueue.main.async {
//this is the row that the collection view is in
if let index = IndexPath(row: 1, section: 0) as? IndexPath {
if let cell = self.homeTableView.cellForRow(at: index) as? TopUserContainerViewController {
cell.userCollectionView.reloadData()
}
}
}
refreshControl.endRefreshing()
}
Which then runs my awakeFromNib() in collection view triggering:
func fetchTopUsers() {
topUserModel.removeAll()
let queryRef = Database.database().reference().child("users").queryOrdered(byChild: "ranking").queryLimited(toLast: 10)
queryRef.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String : AnyObject] {
let topUser = TopUser(dictionary: dictionary)
self.topUserModel.append(topUser)
}
DispatchQueue.main.async {
self.userCollectionView.reloadData()
}
})
}
note that the first thing I do is remove all data from the topUserModel. After storing the new data and appending it (see above), I can print out the value of that integer in that block of code to screen and it displays as the updated value.
However in my collection view (see below), if I were to print out the integer value at any point here (it's called watchTime), it still displays the old value even though the topUserModel has been wiped clean and new data has been added?:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topUserCell", for: indexPath) as! TopUsersViewController
if topUserModel.count > indexPath.row {
//image stuff redacted
Nuke.loadImage(
with: ImageRequest(url: url).processed(with: _ProgressiveBlurImageProcessor()),
options: options,
into: cell.topUserImage
)
cell.topUserName.text = topUserModel[indexPath.row].username
cell.topUserMinutes.text = "\(String(describing: topUserModel[indexPath.row].watchTime!))"
}
return cell
}
You shouldn't call dequeueReusableCell anywhere but in cellForRowAt.
In order to get the currently displayed cell (if any) you use cellForRowAt:; this may return nil if the row isn't currently onscreen.
Once you have the cell you can reload it's data and refresh its collection view.
Your codes:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topUserCell", for: indexPath) as! TopUsersViewController
if topUserModel.count > indexPath.row {
//image stuff redacted
Nuke.loadImage(
with: ImageRequest(url: url).processed(with: _ProgressiveBlurImageProcessor()),
options: options,
into: cell.topUserImage
)
cell.topUserName.text = topUserModel[indexPath.row].username
cell.topUserMinutes.text = "\(String(describing: topUserModel[indexPath.row].watchTime!))"
}
return cell
}
For example, if current indexPath is (1, 0), but the cell is reused from an indexPath (100, 0). Because it is out of screen, and is reused to display new content.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topUserCell", for: indexPath) as! TopUsersViewController
if topUserModel.count > indexPath.row {
// ... Update to new content
}
// If topUserModel.count <= indexPath.row, then the cell content is undefined.
// Use the original content of reuse cell.
// Ex, it may be come from (100, 0), but current indexPath is (1, 0).
// ...
// You can add a property to the cell to observe the behavior.
if (cell.indexPath != indexPath) {
// Ops ....
}
// Update to current indexPath
cell.indexPath = indexPath
return cell

CollectionView Cell Moving when Selected

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

Resources