Trouble understanding resuming cells in collection views - ios

I'm following a site to help learn swift and I'm getting confused about this part right here. Basically we added the if cell.imageview.image == nil statement so hat when the collection view loads and you scroll the image doesn't reload the filters. What I don't understand is if you scroll down a cell is reused for the bottom row, now why if I scroll back up doesn't it have to reload the filter? is that data saved somewhere so when I scroll up the properties don't have to repopulate? and If thats the case why would I have to use that if statement at all?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as! FilterCell
if cell.imageView.image == nil {
cell.imageView.image = placeholder
let filterQueue: dispatch_queue_t = dispatch_queue_create("filter queue", nil)
dispatch_async(filterQueue, { () -> Void in
let filterImage = self.filteredImageFromImage(self.thisFeeditem.thumbNail, filter: self.filters[indexPath.row])
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.imageView.image = filterImage
})
})
}
return cell
}

When a cell is reused, a cell object that was already allocated is just used again. Any properties or data that was set to it will remain.
When you scroll back up, the cell has already had it's image set, and so it won't reload the new filtered image.

Cells can be reused whether you scroll up or down. You should assume the cell returned is a cached version for a different item. Therefore it may already be bound with another cell's data and you'd want to always rebind the cell with the proper items' data.

Related

Show hints for new users in collectionView in iOS

I'm trying to create hints for new users in iOS application, like to open profile click here, etc. I have a collection view and inside it I have elements that I fetch from server.
I need to show hint near the first element. My code:
var tipView = EasyTipView(
text: "Выбирай предмет и начинай обучение",
preferences: EasyTipView.globalPreferences
)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let section = sections[indexPath.section]
switch section {
case .fav_subjects:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FavSubjectsCollectionViewCell", for: indexPath) as! FavSubjectsCollectionViewCell
let favSubjects = model[.fav_subjects] as! [Subjects.FavSubject]
let cellModel = favSubjects[indexPath.item]
cell.configure(with: cellModel)
//here I'm trying to render hint near first element of collectionView
if indexPath.section == 0 && indexPath.item == 0 {
let view = cell.contentView
tipView.show(forView: view)
}
return cell
case .all_subjects:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AllSubjectsCollectionViewCell", for: indexPath) as! AllSubjectsCollectionViewCell
let allSubjects = model[.all_subjects] as! [Subjects.Subject]
let cellModel = allSubjects[indexPath.item]
cell.configure(with: cellModel)
return cell
}
}
}
My problem is when I open screen for the first time my hint displays on the top of the screen and not near the first element, but when I switch to another screen and then go back, hint displays exactly where expected.
I suggest that my problem is screen rendered for the first time before all items fetched from server so tried to use layoutIfNeeded() to rerender when the size is changed but it doesn't help:
tipView.layoutIfNeeded()
if indexPath.section == 0 && indexPath.item == 0 {
let view = cell.contentView
self.tipView.layoutIfNeeded()
self.tipView.show(forView: view)
}
Also I have tried to use viewDidAppear, because I thought that if screen is already rendered than I will have element rendered and I will show hint in the right place for the first render but it turns out that in viewDidAppear cell is nil:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let cell = collectionView.cellForItem(at: IndexPath(item: 0, section: 0))
}
Incorrect(when screen opened first time):
Correct:
I'm new to iOS so I think I missed something important, please help.
The method cellForItem(at:) gets called before the cells gets displayed on screen. In fact, at this point the final frame of the cell is not yet determined, it will happen later when the cell gets added to the view hierarchy and the layout pass happens.
In some cases even if cellForItem(at:) gets called, the cell may never appear on screen.
If you always display the hint near the first element, perhaps it will be easier to use the collection view's frame as reference instead of the first cell unless you need the hint to scroll along with the cell.
Alternatively, add the hint as a subview to the cell's contentView and use Auto Layout to define its position inside the cell. Don't forget to remove the hint from the view hierarchy when appropriate because the same cell may end up being dequeued for another index path during scroll.

Showing and hiding a view only on a specific cell of a table view

I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.

Asynchronous loading of image in UICollectionViewCell causes labels to disappear

I’m working on a photo gallery app that consists essentially of a navigation controller on to which is loaded instances of a collection view. Each of the collection view cells contains a UIImageView, and a label for the name of the image. The cells load the images in asynchronously.
This all works fine the first time each of the collection views are loaded, but if you step back to a previous collection view and forward again the labels disappear.
I think it must have something to do with loading the images asynchronously, as if I remove the image load the labels are fine, have the correct text and don’t disappear.
I’m not sure where I’m going wrong with the image loading...
The various components are:
Images are loaded using this extension method:
extension UIImageView {
public func imageFromServerURL(url: URL) {
if ImageData.realPhotoCache.keys.contains(url.absoluteString){
self.image = ImageData.realPhotoCache[url.absoluteString];
}else{
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error)
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
ImageData.realPhotoCache[url.absoluteString] = image;
self.image = image
})
}).resume()
}
}}
And the cells for the collection view are contucted like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell: UICollectionViewCell;
cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellReuseIdentifier, for: indexPath);
let imageProvider = self.imageProviderForIndexPath(indexPath: indexPath);
(cell as! tsPhotoCollectionViewCell).photoView.imageFromServerURL(url: imageProvider.GetThumbImageUrl());
(cell as! tsPhotoCollectionViewCell).titleLabel.text = imageProvider.GetImageTitle()!;
return cell
}
with the collection view being reset when it is returned to like so:
func reset(){
photoProviders = [ImageDataProvider]();
self.collectionView?.reloadSections(IndexSet(integer: 0))
startLoad()
}
override func viewWillAppear(_ animated: Bool) {
if(hasNavigatedAway){
hasNavigatedAway = false;
self.reset();
}
}
The first load of a collection will generally be fine (cells in green, labels in red):
https://imgur.com/g3p03yF
but on naving away and coming back to a collection view, the labels are gone:
https://imgur.com/If2FQZS
The labels also seem to come and go with scrolling sometimes. I've tried everything I can think of, but haven't had any luck..
Judging by your screenshots, it seems like UILabels are getting their heights compressed to 0 by expanded UIImageViews. Try increasing UILabel's vertical content compression resistance priority, say, to 1000, so that it becomes non-compressible (and, in turn, makes auto layout engine compress either UIImageView or spacings - depends on your constraints).

How to Draw The View Controller with the Dyanamic Value

I have to make a view as image is below. I am confused and not able to darw Screen as Screen will have more Image on Bottom of Image as User will scroll the View and data on the Each time will come up Dynamically and Will on every time View Come up. Thanks any Help Appricated
after a long Research for this kind of view Finally i got a Solution Which is
take a Table View
take multiple Prototype Cell as much you Want
Set the Identifier Individually of each cell
create a Subcalss of UITableViewCell
Method like callForRowatindexPath you have to call like That with Diffrent Cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
var cell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as? specificTopicTableViewCell`cell.textlabel.text = "Hello"
return cell!
}
else {
let cell2 = tableView.dequeueReusableCellWithIdentifier(kSecCellIdentifier) as? scondTopicTableViewCell
cell2.textlabel.text = "Hello"
return cell2!
}
}

Using PINRemoteImage to populate CollectionViewCell Images

I make a network call in ViewDidLoad to get objects (first 25), then I make another call in willDisplayCell to get the rest of the objects. I'm Using PINReMoteImage in the code below. It works, but the problem is that as you scroll through the collection view, a cell will have one picture then another picture will appear over it. How can I improve this UI? I thought updateWithProgress was supposed to deal with this by using a blur until the image is loaded but it doesn't seem to be working?
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PinCollectionViewCell
if let pinImageURL = self.pins[indexPath.row].largestImage().url {
cell.pinImage?.pin_updateWithProgress = true
cell.pinImage?.pin_setImageFromURL(pinImageURL, completion: ({ (result : PINRemoteImageManagerResult) -> Void in
if let image = result.image {
self.imageArray.append(image)
}
}))
}
The problem is that I'm reusing cells, but I wasn't resetting the image. So I simply added cell.pinImage?.image = nil to the above. Once I found out that was the problem I added an UIActivityViewIndicatorViewto the cell and stop it when the image comes in.

Resources