Current Index associating with wrong data - ios

I have a collection view that scrolls horizontally and each cell pushes to a detail view upon a tap. When I load the app, I have it print the object at index. At first it will load the one cell it is supposed to. But when I scroll over one space it prints off two new ids, and then begins to associate the data of the last loaded cell with the one currently on the screen, which is off by one spot now. I have no clue how to resolve this, my best guess is there is some way to better keep up with the current index or there is something in the viewDidAppear maybe I am missing. Here is some code:
open var currentIndex: Int {
guard (self.collectionView) != nil else { return 0 }
return Int()
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExpandCell", for: indexPath) as! ExpandingCollectionViewCell
let object = self.imageFilesArray[(indexPath).row] as! PFObject
cell.nameLabel.text = object["Name"] as! String
whatObject = String(describing: object.objectId)
print(whatObject)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let object = self.imageFilesArray[(indexPath as NSIndexPath).row]
guard let cell = collectionView.cellForItem(at: indexPath) as? ExpandingCollectionViewCell else { return }
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "EventDetailViewController") as! EventDetailViewController
nextViewController.lblName = nameData
nextViewController.whatObj = self.whatObject
self.present(nextViewController, animated:false, completion:nil)
}
Does anyone know a better way to set the current index so I am always pushing the correct data to the next page?

The data source that is setting the elements of the cell can keep count of index that can also be set as a property and can be used to retrieve back from the cell to get correct current index.

EventDetailViewController is your UICollectionViewController subclass I assume.
If this is correct then you need to implement on it:
A method that tells the collection how many items there are in the datasource.
//1
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1 // you only have 1 section
}
//2
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return array.count// this is the number of models you have to display... they will all live in one section
}
A method that tells the collection which cellView subclass to use for that given row.
-A method that populates the cell with it's datasource.
Follow this tutorial from which I extracted part of the code, they convey this core concepts pretty clearly.
Reusing, as per Apple's docs happens for a number of cells decided by UIKit, when you scroll up a little bit 2 or N cells can be dequed for reusing.
In summary, with this three methods you can offset your collection into skipping the one record that you want to avoid.
Happy coding!

Related

Stored selected indexPath of UICollectionView inside UITableView

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.

Is there any way to write concise code for below case

I have an app which has two tabs. In the first BookVC tab, I use UICollectionViewController to show books and in didSelectItemAtIndexPath i call a function that push the BookDetailVC.
And in Bookmark tab, i want to show all books which was bookmarked and when user select certain book, i want to push BookDetailVC. I know it can be achieved by writing the same code as in BookVC. But i don't want to repeat the same code.
I'd tried to make BookmarkVC subclass of BookVC and ended up as showing the same book in both BookVC and BookmarkVC since i'm using the same one instance of UICollectionView from BookVC. Is there any way to override UICollectionView of BookVC or any other approach to solve. Sorry for my bad english. Thanks.
You are taking the wrong approach. The way you describe your bookmarks and your books View controller, it seems to me that they are identical, the only thing that changes is the content.
So, since collection views use data sources all you have to do is change the Data source based on whether you wanna show all books, or only the bookmarked ones.
Added code:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier :"secondViewController") as! UIViewController
self.present(viewController, animated: true)
I think you are doing wrong use just need to reload collection view based on which button is clicked take boolean
isBookMarkCliked:Bool
For Better Readablity create Model For Book
like
class Book {
var title: String
var author: String
var isBookMarked:Bool
init(title: String, author: String, isBookMarked:Bool) {
self.title = title
self.author = author
self.isBookMarked = isBookMarked
}
}
and declare two array globally with Book Model
arrForBooks:[Book] = []
arrForBookMarkedBooks:[Book] = []
Create CollectionViewDelegate methods using extension
extension YourClassVC: UICollectionViewDataSource,UICollectionViewDelegate
{
//MARK: UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if isBookMarkClicked
{
return arrForBookMarkedBooks.count
}
return arrForBooks.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellIdentifier", for: indexPath) as! YourCellClass
var currentBook:Book = nil
if isBookMarkClicked
currentBook = arrForStoreDetails[indexPath.row]
else
currentBook = arrForBookMarkedBooks[indexPath.row]
//Set data to cell from currentBook
return cell
}
//MARK: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: false)
//Your code to push BookDetailVC
}
}

How to pass a collection view cell's indexPath to another view controller

I'm following a tutorial for making an Instagram-esque app, and the tutorial goes through how to display all data (image, author, likes, etc) all in one collection view. I'm trying to do it a little bit differently so only the images are displayed in the collection view, then if an image is tapped, the user is taken to a different view controller where the image plus all the other data is displayed.
So in my view controller with the collection view (FeedViewController), I have my array of posts declared outside the class (Post is the object with all the aforementioned data):
var posts = [Post]()
Then inside the FeedViewController class, my cellForItemAt indexPath looks like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath) as! PostCell
// creating the cell
cell.postImage.downloadImage(from: posts[indexPath.row].pathToImage)
photoDetailController.authorNameLabel.text = posts[indexPath.row].author
photoDetailController.likeLabel.text = "\(posts[indexPath.row].likes!) Likes"
photoDetailController.photoDetailImageView.downloadImage(from: posts[indexPath.row].pathToImage)
return cell
}
And then I also obviously have a function to fetch the data which I can post if necessary, but I think the problem is because PhotoDetailController doesn't know the indexPath (although I may be wrong).
When I run the app and try to view the FeedViewController I get a crash fatal error: unexpectedly found nil while unwrapping an Optional value, highlighting photoDetailController.authorNameLabel line.
Am I correct in assuming the problem is because the indexPath is available only in the collection view data sources within FeedViewController? If so, how can I pass that indexPath to PhotoDetailController so my code works correctly?
Thanks for any advice!
EDIT: Edited my didSelectItem method:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let photoDetailController = self.storyboard?.instantiateViewController(withIdentifier: "photoDetail") as! PhotoDetailController
photoDetailController.selectedPost = posts[indexPath.row]
self.navigationController?.pushViewController(photoDetailController, animated: true)
}
and cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath) as! PostCell
cell.postImage.downloadImage(from: posts[indexPath.row].pathToImage)
return cell
}
And in PhotoDetailController:
var selectedPost: Post! inside the class, then:
override func viewDidLoad() {
super.viewDidLoad()
self.authorNameLabel.text = selectedPost[indexPath.row].author
self.likeLabel.text = "\(selectedPost[indexPath.row].likes!) Likes"
self.photoDetailImageView.downloadImage(from: selectedPost[indexPath.row].pathToImage)
}
But still getting error use of unresolved identifier "indexPath
EDIT 2: Storyboard
You are getting this nil crash because this photoDetailController is not yet loaded so all its outlet are nil also the way currently you are doing is also wrong.
You need to add the controller code in didSelectItemAt method and perform the navigation.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let photoDetailController = self.storyboard?.instantiateViewController(withIdentifier: "YourIdentifier") as! PhotoDetailController
photoDetailController.selectedPost = posts[indexPath.row]
self.navigationController?.pushViewController(photoDetailController, animated: true)
}
Now simply create one instance property of type Post with name selectedPost in PhotoDetailController and set all the detail from this selectedPost object in viewDidLoad of PhotoDetailController.
Edit: Set your viewDidLoad like this in PhotoDetailController
override func viewDidLoad() {
super.viewDidLoad()
self.authorNameLabel.text = selectedPost.author
self.likeLabel.text = "\(selectedPost.likes!) Likes"
self.photoDetailImageView.downloadImage(from: selectedPost.pathToImage)
}

Realm/iOS: Bumpy scrolling performance for UICollectionView

I am using Realm as the alternative for coredata for the first time.
Sadly, I had this bumpy scrolling issue(It is not too bad, but quite obvious) for collectionView when I try Realm out. No data were downloaded blocking the main thread, I use local stored image instead.
Another issue is when I push to another collectionVC, if the current VC will pass data to the other one, the segue is also quite bumpy.
I am guessing it is because of the way I write this children property in the Realm Model. But I do not know what might be the good way to compute this array of array value (merging different types of list into one)
A big thank you in advance!!
Here is the main model I use for the collectionView
class STInstitution: STHierarchy, STContainer {
let boxes = List<STBox>()
let collections = List<STCollection>()
let volumes = List<STVolume>()
override dynamic var _type: ReamlEnum {
return ReamlEnum(value: ["rawValue": STHierarchyType.institution.rawValue])
}
var children: [[AnyObject]] {
var result = [[AnyObject]]()
var tempArr = [AnyObject]()
boxes.forEach{ tempArr.append($0) }
result.append(tempArr)
tempArr.removeAll()
collections.forEach{ tempArr.append($0) }
result.append(tempArr)
tempArr.removeAll()
volumes.forEach{ tempArr.append($0) }
result.append(tempArr)
return result
}
var hierarchyProperties: [String] {
return ["boxes", "collections", "volumes"]
}
}
Here is how I implement the UICollectionViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.alwaysBounceVertical = true
dataSource = STRealmDB.query(fromRealm: realm, ofType: STInstitution.self, query: "ownerId = '\(STUser.currentUserId)'")
}
// MARK: - datasource:
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
guard let dataSource = dataSource else { return 0 }
return dataSource.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! STArchiveCollectionViewCell
guard let dataSource = dataSource,
dataSource.count > indexPath.row else {
return cell
}
let item = dataSource[indexPath.row]
DispatchQueue.main.async {
cell.configureUI(withHierarchy: item)
}
return cell
}
// MARK: - Open Item
func pushToDetailView(dataSource: [[AnyObject]], titles: [String]) {
guard let vc = storyboard?.instantiateViewController(withIdentifier: STStoryboardIds.archiveDetailVC.rawValue) as? STArchiveDetailVC
else { return }
vc.dataSource = dataSource
vc.sectionTitles = titles
self.navigationController?.pushViewController(vc, animated: true)
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let dataSource = self.dataSource,
dataSource.count > indexPath.row else {
return
}
let item = dataSource[indexPath.row]
self.pushToDetailView(dataSource: item.children, titles: item.hierarchyProperties)
}
Modification(more codes on configureUI):
// configureUI
// data.type is an enum type
func configureUI<T: STHierarchy>(withHierarchy data: T) {
print("data", kHierarchyCoverImage + "\(data.type)")
titleLabel.text = data.title
let image = data.type.toUIImage()
self.imageView.image = image
}
// toUIImage of enum data.type
func toUIImage() -> UIImage {
let key = kHierarchyCoverImage + "\(self.rawValue)" as NSString
if let image = STCache.imageCache.object(forKey: key) {
return image
}else{
print("toUIImage")
let defaultImage = UIImage(named: "institution")
let image = UIImage(named: "\(self)") ?? defaultImage!
STCache.imageCache.setObject(image, forKey: key)
return image
}
}
If your UI is bumpy when you're scrolling, it simply means the operations you're performing in collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell are too heavy.
Realm itself is structured in such a way that reading data from objects is very fast, so you shouldn't be seeing substantial dropped frames if all you're doing is populating a cell with values from Realm.
A couple of considerations:
If you're calling item.children inside the cellForItem block method, since you're manually looping through and paging in every Realm object doing that, that will cause frame drops. If you are, it'd be best to either do that ahead of time, or re-desing the logic to only access those arrays when absolutely needed.
You mentioned you're including images. Even if the images are on disk, unless you force image decompression ahead of time, Core Animation will lazily decompress the image at draw time on the main thread which can severely kill scroll performance. See this question for more info.
The cellForItemAt method call should already be on the main thread, so configuring your cell in a DispatchQueue.main.async closure seems un-necessary, and given that it's not synchronous, may be causing additional issues by running out of order.
Collection views are notoriously hard for performance since entire rows of cells used to be created and configured in one run loop iteration. This behavior was changed in iOS 10 to spread cell creation out across multiple run loop iterations. See this WWDC video for tips on optimizing your collection view code to take advantage of this.
If you're still having trouble, please post up more of your sample code; most importantly, the contents of configureUI. Thanks!
Turned out I was focusing on the wrong side. My lack of experience with Realm made me feel that there must be something wrong I did with Realm. However, the true culprit was I forgot to define the path for shadow of my customed cell, which is really expensive to draw repeatedly. I did not find this until I used the time profile to check which methods are taking the most CPU, and I should have done it in the first place.

Cells insight UICollectionView do not load until there are visible?

I have a UITableView with UICollectionView insight every table view cell. I use the UICollectionView view as a gallery (collection view with paging). My logic is like this:
Insight the method
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// This is a dictionary with an index (for the table view row),
// and an array with the url's of the images
self.allImagesSlideshow[indexPath.row] = allImages
// Calling reloadData so all the collection view cells insight
// this table view cell start downloading there images
myCell.collectionView.reloadData()
}
I call collectionView.reloadData() and in the
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// This method is called from the cellForRowAtIndexPath of the Table
// view but only once for the visible cell, not for the all cells,
// so I cannot start downloading the images
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoCollectionCell
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
if indexPath.item < arr.count {
var imageName:String? = arr[indexPath.item]
if let imageName = imageName {
var escapedAddress:String? = imageName.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
if let escapedAddress = escapedAddress {
var url:NSURL? = NSURL(string: escapedAddress)
if let url = url {
cell.imageOutlet.contentMode = UIViewContentMode.ScaleAspectFill
cell.imageOutlet.hnk_setImageFromURL(url, placeholder: UIImage(named: "placeholderImage.png"), format: nil, failure: nil, success: nil)
}
}
}
}
}
}
return cell
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
println("collection row: \(collectionView.tag), items:\(arr.count)")
return arr.count
}
}
return 0
}
I set the right image for the cell. The problem is that the above method is called only for the first collection view cell. So when the user swipe to the next collection view cell the above method is called again but and there is a delay while the image is downloaded. I would like all the collection view cells to be loaded insight every visible table view cell, not only the first one.
Using the image I have posted, "Collection View Cell (number 0)" is loaded every time but "Collection View Cell (number 1)" is loaded only when the user swipe to it. How I can force calling the above method for every cell of the collection view, not only for the visible one? I would like to start the downloading process before swiping of the user.
Thank you!
you're right. the function func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell will be called only when cell start to appear. that's a solution of apple called "Lazy loading". imagine your table / collection view have thousand of row, and all of those init at the same time, that's very terrible with both memory and processor. so apple decide to init only view need to be displayed.
and for loading image, you can use some asynchronous loader like
https://github.com/rs/SDWebImage
it's powerful and useful too :D

Resources