I have a view with a grid movies. There's three properties: title Label, image view for poster movie and a image to show if a movie is or not favored.
When click on a cell, go on to a detail view with show others infos about the movie and there's a button action to favor a movie. I wish update the button icon in grid collection view. So I create a delegate to listen when this event occurs and then reload the collection view.
Screenshots:
grid movies screen
detail movie screen
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! MoviesCollectionViewCell
cell.titleLabel.text = popularMovies[indexPath.row].title
getImageMovies(imageURLString: popularMovies[indexPath.row].poster, imageView: cell.movieImage)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let detailMovieViewController = DetailMovieViewController()
detailMovieViewController.titleMovieLabel.text = popularMovies[indexPath.row].title
detailMovieViewController.releaseDateMovieLabel.text = popularMovies[indexPath.row].date
detailMovieViewController.overviewMovieLabel.text = popularMovies[indexPath.row].overview
getImageMovies(imageURLString: popularMovies[indexPath.row].poster, imageView: detailMovieViewController.movieImage)
getGenresMovies(genresMoviesID: popularMovies[indexPath.row].genre, genreMovieLabel: detailMovieViewController.genreMovieLabel)
collectionView.deselectItem(at: indexPath, animated: true)
detailMovieViewController.delegate = self
selectedIndexPath = indexPath
self.navigationController?.pushViewController(detailMovieViewController, animated: true)
}
protocol favoriteMovieDelegate: class {
func updateFavoriteImage ()
}
#objc func markFavoriteButtom (buttom: UIButton){
if buttom.isSelected == false {
buttom.isSelected = true
}else {
buttom.isSelected = false
}
delegate?.updateFavoriteImage()
}
func updateFavoriteImage() {
if let indexPath = selectedIndexPath {
let cell = collectionView.cellForItem(at: indexPath) as! MoviesCollectionViewCell
cell.favoriteIconImage.image = UIImage(named: "favorite_full_icon")
collectionView.reloadData()
}
}
struct Films: Codable {
let id: Int
let title: String
let poster: String
let genre: [Int]
let date: String
let overview: String
enum CodingKeys: String, CodingKey {
case id
case title
case poster = "poster_path"
case genre = "genre_ids"
case date = "release_date"
case overview
}
}
Instead of updating directly your cell.favoriteIconImage.image you will have to update your object directly and then reload CollectionView. you need to set fav image on cellForItem Also you need to post that to server once user make it fav ... and fetch isFav from server if you want to persist data. And if your server did not support isFav then you need to store it locally against film Id ... in user default
Related
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)
}
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'm having a collectionview setup, with local data and multiselect enabled. When I run this on an actual device I'm having some rendering issues. When I scroll superfast it takes a while before the active cells show up. They remain inactive for a little while when I stop and then suddenly pop up.
On didSelect I'm doing the following;
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! FRMenuCollectionViewCell
let selectedCells:[IndexPath] = collectionView.indexPathsForSelectedItems!
cell.isSelected = true
switch indexPath.section {
case 0:
let item = fixedData[indexPath.row]
itemSelection.append(item)
break
case 1:
handleContinentSelection(with: indexPath)
break
default:
print("An unexpected case occured")
}
}
func handleContinentSelection(with indexPath: IndexPath) {
let continent = continentData[indexPath.row]
for country in countryData {
if (country.continent?.continentName)! == continent {
itemSelection.append(country.countryName!)
if let index = countryData.lastIndex(of: country) {
collectionView?.selectItem(at: IndexPath(row: index, section: 2), animated: false, scrollPosition: [])
}
}
}
}
As illustrated above, all collectionview selections have a different way of handling the selection. If I select something in section 1, I also select cells in section 2.
The selected and deselected state are being handled in the subclass like this;
class FRMenuCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
override var isSelected: Bool {
didSet {
if super.isSelected == true {
self.label.attributedText = NSMutableAttributedString(string: self.label.text!, attributes: FRSharedStyles.filledTextAttributes)
} else {
self.label.attributedText = NSMutableAttributedString(string: self.label.text!, attributes: FRSharedStyles.opaqueStrokeTextAttributes)
}
}
}
}
So the selected state is filled text, the deselected state is outlined. Everything works fine, it just renders slowly on my apple TV when scrolling fast (with the side scroll it's most noticable). Any ideas on how to optimise this would be appreciated.
I am not sure if this is correct way to say this but I have been trying to get data onto ColectionViewCell but it doesn't seem to show all of them. This question follows up from Cards are not displaying different data. Koloda/Yalantis. It appears that the framework requires a different method of applying data to the index but I'm not sure how to achieve this.
Here is how my class looks:
import SwiftyJSON
class Person {
var firstName: String?
init(json: JSON) {
self.firstName = json["first_name"].stringValue
}
}
and here is my singleton that manages the Person class:
class PersonManager {
static let shared = PersonManager()
private init() {}
var persons = [Person]()
func removeAll() {
persons.removeAll()
}
func addPerson(_ person: Person) {
persons.append(person)
}
}
And here is how I try to call the data after it has been fetched and initialised and appended:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let person = PersonManager.shared.persons[kolodaView.currentCardIndex]
//Returns the first persons first name as it is supposed to
cell.textLabel?.text = person.firstName
}
The data exists as it counts the number of person in my database. But it keeps on returning the first person for all the cards.
Update:
Using Firebase I am fetching the user data and then appending it like this:
func fetchUser() {
let databaseRef = Database.database().reference(withPath: "users")
databaseRef.observe( .value, with: { (snapshot) in
for child in snapshot.children {
guard let snapshot = child as? DataSnapshot else { continue }
let person = Person(from: snapshot)
self.person = person
PersonManager.shared.addPerson(person)
}
self.kolodaView.reloadData()
})
}
Try changing your function as follows:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let person = PersonManager.shared.persons[indexPath.row]
//Returns the first persons first name as it is supposed to
cell.textLabel?.text = person.firstName
}
The problem is that your variable kolodaView.currentCardIndex is always 0, it will never show other results but the first in the set.
indexPath.row is meant to be used to control this index you need.
May be you should try indexPath.item, not row(That's for UITableView)
And make sure
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:
"cell", for: indexPath)
cell is not nil. You should print every cells.If it's nil, you can't put your data in a nil UICollectionViewCell.
So add code
if(cell == nil) cell = [UICollectionViewCell new];
I suppose in:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return PersonManager.shared.persons.count
}
And in cellForItem you should use indexPath.row instead of kolodaView.currentCardIndex unless this is really what you need(very weird).
If so, check if PersonManager append same Person every time, which gives you only your first item every time. Double check it, because there is no another reason
Problem: TableView is very slow when scrolling. Looks like my code is not efficient at all.
So I have a UICollectionView embedded inside a tableViewCell like so (I used this tutorial to accomplish it.)
I am using Firebase to populate data into the UICollectionViewCells. I have 3 class folders:
TableViewCtrl: Responsible for downloading section titles and then passing some logic to tableViewCell. Here is partial code of the main TableViewCtrl:
// 1. DOWNLOAD SECTION TITLES AND THEN CALL RELOADTABLE
// 2. TableView:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return featuredCollection.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "browseCell", for: indexPath) as! BrowseCell
cell.configureCell(of: featuredCollection[indexPath.row].typeOfItem, title: featuredCollection[indexPath.row].categoryTitle, bookReference: featuredCollection[indexPath.row].bookReference, spotlightTests: featuredCollection[indexPath.row].spotlightTests, bookUniqueIDsToDownload: featuredCollection[indexPath.row].bookUniqueIDsToDownload)
return cell
}
TableViewCell:
func configureCell(of type: FeaturedItem, title: String, bookReference: BookReferenceTest?, spotlightTests: [BookReferenceTest]?, bookUniqueIDsToDownload: [String]?) {
setCollectionViewDataSourceDelegate(delegate: self, dataSource: self)
// DOWNLOAD THE BOOK ITEMS (eg. IMAGES, TITLES, ETC) then call self.collectionView.reloadData()
}
internal func setCollectionViewDataSourceDelegate <D: UICollectionViewDelegate, S: UICollectionViewDataSource>(delegate: D, dataSource: S) {
collectionView.delegate = delegate
collectionView.dataSource = dataSource
let collectionViewFlowLayout = UICollectionViewFlowLayout()
let myCollectionView = UICollectionView(frame: self.collectionView.bounds, collectionViewLayout: collectionViewFlowLayout)
myCollectionView.delegate = self
myCollectionView.dataSource = self
collectionView.reloadData()
}
// Snipit of code that's responsible for downloading book assets:
func downloadBrowsingBooks(bookUniqueKeys: [String]) {
let databaseReference = FIRDatabase.database().reference()
databaseReference.child("Kutub/Books/").observeSingleEvent(of: .value, with: {
(snapshot) in
var books = [BrowsingBook]()
for book in (snapshot.children.allObjects as! [FIRDataSnapshot]) {
if bookUniqueKeys.contains((book.key)) {
let browsingBookValues = book.value as! [String : AnyObject]
let browsingBook = self.createBrowsingBookObject(data: browsingBookValues, uniqueKey: book.key)
books.append(browsingBook)
}
}
self.storedBooks = books
self.collectionView.reloadData()
})
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "booksCollectionCell", for: indexPath) as! BooksCollectionCell
let bookTitle = storedBooks[indexPath.item].title
let authors = storedBooks[indexPath.item].authors
cell.configureCell(title: bookTitle, authorNames: authors)
return cell
}
UICollectionViewCell:
func configureCell(title: String, authorNames: [String]? = nil, imageCover: UIImage? = nil) {
var authorName = ""
if let authors = authorNames {
authorName = authors[0]
for index in 1..<authors.count {
authorName += ", \(authors[index])"
}
}
// ....
}
From my understanding, here's step-by-step of what's happening:
Section titles are downloaded
TableView.reload() configures the tableViewCells
Inside tableViewCells, firebase downloads images and other book assets (eg. titles, authors, publishers names in text from Firebase database) and calls on collectionView
CollectionView configures it's cells.
Again, my main problems is that scrolling is very slow and laggy with the way that I'm doing this. When I tried different methods (eg. downloading the data and passing it on to tableviewCell) it works but when I add items to Firebase database only section titles show up and not the content inside the collectionViewCells.