break strong reference cycle between Managed Object and Collectionview Cell - ios

Update :
Had nothing to do with Core Data or the CollectionView. I never dismissed the ViewController holding the CollectionView. More in the answer below!
I am having a memory leak in Swift iOS. First I thought it was located in my fetch function, but then I tried something different. I broke the connection between my UICollectionViewCell and my Managed Object at various places. I am fetching images. So I replaced the result from my fetch with a random image asset that was added to an array for as many times as there were results. This always fixed my leak. So now I knew it wasn't a problem in my fetch function, any other function on the way to the Cell.
After some googling I found that Core Data has some tendency to create strong reference cycles on it's own. But I don't think that that is it. If it was, it shouldn't matter that I don't use the resulting array of images in a Cell. I should still have a leak, but when I don't connect the images from Core Data to the Cell I have no leak.
What I don't understand is why I have a leak in the first place. I thought that arrays work like values, not references. So putting images loaded from core data in an array should work like a copy and there shouldn't be any strong reference cycle...
I also found that refreshing the Managed Object that has the Binary Data atribute for the images doesn't fix the problem.
So what do I do now? Do I need to delete all the cells? Do I need to make UIImages out of the NSData from Core Data (glanced over something that said that Strings work like values but NSStrings don't, so maybe an NSData thingy works like a reference)? Do I need to find a way to refresh the attribute of an object?...
Thanks!
Fetch(also tried setting things as weak, doesn't work):
import UIKit
import CoreData
func getFilteredThumbImages (albumIdentifier: String, moc : NSManagedObjectContext?) -> [NSData]? {
var error: NSError?
let resultPredicate = NSPredicate(format: "iD = %#", albumIdentifier)
let albumfetchRequest = NSFetchRequest(entityName: "Album")
albumfetchRequest.predicate = resultPredicate
var results = moc!.executeFetchRequest(albumfetchRequest, error:&error)!
if error == nil {
weak var tempFThumbs = results.last!.mutableSetValueForKey("filteredThumbs")
weak var testFoundImages = tempFThumbs!.mutableSetValueForKey("imageData")
var foundImages = testFoundImages!.allObjects as [NSData]
moc!.refreshObject(results.last! as NSManagedObject, mergeChanges: false)
moc!.reset()
return foundImages
}
else {
moc!.refreshObject(results.last! as NSManagedObject, mergeChanges: false)
//appDel?.managedObjectContext?.reset()
moc!.reset()
return nil
}
}
UICollectionViewCell :
import UIKit
class CollectionViewCell: UICollectionViewCell {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let textLabel: UILabel!
let imageView: UIImageView!
override init(frame: CGRect) {
NSLog("MyObject init")
super.init(frame: frame)
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
imageView.backgroundColor = UIColor(red:1, green:0.992, blue:0.965, alpha:1)
imageView.contentMode = UIViewContentMode.ScaleAspectFit
imageView.clipsToBounds = true
contentView.backgroundColor = UIColor(red:1, green:0.992, blue:0.965, alpha:1)
contentView.addSubview(imageView)
}
}
Input images in Cell(thumbs is a variable array => [NSData]):
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
println("loading...")
weak var cell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as? CollectionViewCell
//cell!.imageView?.image = UIImage(named: "test")
if thumbs != nil {
cell!.imageView?.image = UIImage(data: thumbs![indexPath.row])
println("loading \(thumbs!.count) cells")
}
return cell!
}

The problem is that you've got a cycle in your storyboard. You are pushing from view controller A to view controller B, and then pushing from view controller B "back" to view controller A. And so on, every time. Thus, your navigation controller just piles up numerous copies of the same view controllers. And so you wind up with numerous copies of all those images.
It always surprises me that you don't get a notice when you do this sort of thing. It's a classic mistake, and all too easy to make. Surely Interface Builder could detect this kind of cycle and warn you. But it doesn't...

Related

Why isn't my data not transferring between my two View Controllers using UITableViews?

I am attempting to pass data from a the UITableView function cellForRowAt to a custom UITableViewCell. The cell constructs a UIStackView with n amount of UIViews inside of it. n is a count of items in an array and is dependent on the data that is suppose to be transferred (a count of items in that array). Something very confusing happens to me here. I have checked in the VC with the tableView that the data has successfully passed by using the following snippet of code
print("SELECTED EXERCISES: ", self.selectedExercises)
cell.randomSelectedExercises = self.selectedExercises
print("PRINTING FROM CELL: ", cell.randomSelectedExercise)
I can confirm that both of these print statements return non-empty arrays. So to me, this means that the custom cell has the data I require it to have. But, when I try to print out the very same array in the UITableViewCell swift file (randomSelectedExercises) , it returns empty to me. How is this possible? From what I understand, the cell works on creating the property initializers first, then 'self' becomes available. I had a previous error telling me this and to fix it, I turned my UIStackView initializer to lazy, but this is how I ended up with my current problem.
Here is the code in beginning that is relevant to the question that pertains to the table view. I have decided to present this code incase the issue is not in my cell but in my table view code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
let workout = selectedWorkouts[indexPath.row]
//get the information we need - just the name at this point
let name = workout["name"] as! String
var randomInts = [Int]()
//perform a query
let query = PFQuery(className: "Exercise")
query.includeKey("associatedWorkout")
//filter by associated workout
query.whereKey("associatedWorkout", equalTo: workout)
query.findObjectsInBackground{ (exercises, error) in
if exercises != nil {
//Created an array of random integers... this code is irrelevant to the question
//Picking items from parent array. selectedExercises is a subarray
for num in randomInts {
//allExercises just contains every possible item to pick from
self.selectedExercises.append(self.allExercises[num-1])
}
//now we have our selected workouts
//Both print statements successfully print out correct information
print("SELECTED EXERCISES: ", self.selectedExercises)
cell.randomSelectedExercises = self.selectedExercises
print("PRINTING FROM CELL: ", cell.randomSelectedExercises)
//clear the arrays so we have fresh ones through each iteration
self.selectedExercises.removeAll(keepingCapacity: false)
self.allExercises.removeAll(keepingCapacity: false)
} else {
print("COULD NOT FIND WORKOUT")
}
}
//***This works as expected - workoutName is visible in cell***
cell.workoutName.text = name
//clear the used arrays
self.allExercises.removeAll(keepingCapacity: false)
self.selectedExercises.removeAll(keepingCapacity: false)
return cell
}
Below is the code that gives me a problem in the cell swift file. the randomSelectedExercise does not have any data in it when I enter this area. This is an issue because in my for loop I am iterating from 1 to randomSelectedExercise.count. If this value is 0, I receive an error. The issue is focused in the UIStackView initializer:
import UIKit
import Parse
//Constants
let constantHeight = 50
//dynamic height number
var heightConstantConstraint: CGFloat = 10
class RoutineTableViewCell: UITableViewCell {
//will hold the randomly selected exercises that need to be displayed
//***This is where I thought the data would be saved, but it is not... Why???***
var randomSelectedExercises = [PFObject]()
static var reuseIdentifier: String {
return String(describing: self)
}
// MARK: Overrides
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentView.addSubview(containerView)
containerView.addSubview(workoutName)
containerView.addSubview(stackView)
NSLayoutConstraint.activate(staticConstraints(heightConstantConstraint: heightConstantConstraint))
//reset value
heightConstantConstraint = 10
}
//MARK: Elements
//Initializing workoutName UILabel...
let workoutName: UILabel = {...}()
//***I RECEIVE AN EMPTY ARRAY IN THE PRINT STATEMENT HERE SO NUM WILL BE 0 AND I WILL RECEIVE AN ERROR IN THE FOR LOOP***
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.backgroundColor = .gray
stackView.translatesAutoresizingMaskIntoConstraints = false
//not capturing the data here
print("rSE array:", randomSelectedExercises)
var num = randomSelectedExercises.count
for i in 1...num {
let newView = UIView(frame: CGRect(x: 0, y: (i*50)-50, width: 100, height: constantHeight))
heightConstantConstraint += CGFloat(constantHeight)
newView.backgroundColor = .purple
let newLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
newLabel.text = "Hello World"
newView.addSubview(newLabel)
stackView.addSubview(newView)
}
return stackView
}()
//initializing containerView UIView ...
let containerView: UIView = {...}()
//Setting the constraints for each component...
private func staticConstraints(heightConstantConstraint: CGFloat) -> [NSLayoutConstraint] {...}
}
Why is my data not properly transferring? How do I make my data transfer properly?
Here is what you're doing in your cellForRowAt func...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// get a cell instance
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
// ... misc stuff
// start a background process
query.findObjectsInBackground { (exercises, error) in
// nothing here will happen yet... it happens in the background
}
// ... misc stuff
return cell
// at this point, you have returned the cell
// and your background query is doing its work
}
So you instantiate the cell, set the text of a label, and return it.
The cell creates the stack view (with an empty randomSelectedExercises) and does its other init /setup tasks...
And then - after your background find objects task completes, you set the randomSelectedExercises.
What you most likely want to do is run the queries while you are generating your array of "workout" objects.
Then you will already have your "random exercises" array as part of the "workout" object in cellForRowAt.

Data From Label in CollectionViewCell Sometimes Refreshes on Reload other times it Doesn't

First let me say this seems to be a common question on SO and I've read through every post I could find from Swift to Obj-C. I tried a bunch of different things over the last 9 hrs but my problem still exists.
I have a vc (vc1) with a collectionView in it. Inside the collectionView I have a custom cell with a label and an imageView inside of it. Inside cellForItem I have a property that is also inside the the custom cell and when the property gets set from datasource[indePath.item] there is a property observer inside the cell that sets data for the label and imageView.
There is a button in vc1 that pushes on vc2, if a user chooses something from vc2 it gets passed back to vc1 via a delegate. vc2 gets popped.
The correct data always gets passed back (I checked multiple times in the debugger).
The problem is if vc1 has an existing cell in it, when the new data is added to the data source, after I reload the collectionView, the label data from that first cell now shows on the label in new cell and the data from the new cell now shows on the label from old cell.
I've tried everything from prepareToReuse to removing the label but for some reason only the cell's label data gets confused. The odd thing is sometimes the label updates correctly and other times it doesn't? The imageView ALWAYS shows the correct image and I never have any problems even when the label data is incorrect. The 2 model objects that are inside the datasource are always in their correct index position with the correct information.
What could be the problem?
vc1: UIViewController, CollectionViewDataSource & Delegate {
var datasource = [MyModel]() // has 1 item in it from viewDidLoad
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}
// delegate method from vc2
func appendNewDataFromVC2(myModel: MyModel) {
// show spinner
datasource.append(myModel) // now has 2 items in it
// now that new data is added I have to make a dip to fb for some additional information
firebaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let dict = snapshot.value as? [String: Any] else { }
for myModel in self.datasource {
myModel.someValue = dict["someValue"] as? String
}
// I added the gcd timer just to give the loop time to finish just to see if it made a difference
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.datasource.sort { return $0.postDate > $1.postDate } // Even though this sorts correctly I also tried commenting this out but no difference
self.collectionView.reloadData()
// I also tried to update the layout
self.collectionView.layoutIfNeeded()
// remove spinner
}
})
}
}
CustomCell Below. This is a much more simplified version of what's inside the myModel property observer. The data that shows in the label is dependent on other data and there are a few conditionals that determine it. Adding all of that inside cellForItem would create a bunch of code that's why I didn't update the data it in there (or add it here) and choose to do it inside the cell instead. But as I said earlier, when I check the data it is always 100% correct. The property observer always works correctly.
CustomCell: UICollectionViewCell {
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let priceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var someBoolProperty = false
var myModel: MyModel? {
didSet {
someBoolProperty = true
// I read an answer that said try to update the label on the main thread but no difference. I tried with and without the DispatchQueue
DispatchQueue.main.async { [weak self] in
self?.priceLabel.text = myModel.price!
self?.priceLabel.layoutIfNeeded() // tried with and without this
}
let url = URL(string: myModel.urlStr!)
imageView.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"))
// set imageView and priceLabel anchors
addSubview(imageView)
addSubview(priceLabel)
self.layoutIfNeeded() // tried with and without this
}
}
override func prepareForReuse() {
super.prepareForReuse()
// even though Apple recommends not to clean up ui elements in here, I still tried it to no success
priceLabel.text = ""
priceLabel.layoutIfNeeded() // tried with and without this
self.layoutIfNeeded() // tried with and without this
// I also tried removing the label with and without the 3 lines above
for view in self.subviews {
if view.isKind(of: UILabel.self) {
view.removeFromSuperview()
}
}
}
func cleanUpElements() {
priceLabel.text = ""
imageView.image = nil
}
}
I added 1 breakpoint for everywhere I added priceLabel.text = "" (3 total) and once the collectionView reloads the break points always get hit 6 times (3 times for the 2 objects in the datasource).The 1st time in prepareForReuse, the 2nd time in cellForItem, and the 3rd time in cleanUpElements()
Turns out I had to reset a property inside the cell. Even though the cells were being reused and the priceLabel.text was getting cleared, the property was still maintaining it's old bool value. Once I reset it via cellForItem the problem went away.
10 hrs for that, smh
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.someBoolProperty = false
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}

How do I pass a value to a custom UICollectionViewCell?

I have a custom UICollectionViewCell that I am attempting to pass a value to from my view controller. I'm able to pass an image to the cell, but anything else comes up nil upon initialization.
Relevant code in the View Controller:
override func viewDidLoad() {
self.collectionView!.registerClass(MyCustomCell.self, forCellWithReuseIdentifier: "Cell")
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! MyCustomCell
cell.someValue = 5
cell.imageView.image = UIImage(named: "placeholder.png")
return cell
}
In the custom cell class:
var someValue: Int!
var imageView: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
imageView = UIImageView(frame: CGRectMake(0, 0, frame.width, frame.height))
contentView.addSubview(imageView)
let someValueLabel = UILabel()
someValueLabel.frame = CGRectMake(0, 75, 200, 30)
someValueLabel.text = "Value is: \(someValue)"
self.addSubview(someValueLabel)
}
The image is successfully passed from the UICollectionView and I am able to display it, but 'someValue' is always nil.
What am I doing wrong?
The init method is called much earlier than you think it is -- within the dequeue process -- when the cell object is constructed. Part of initialization process is to attach the UIViews designed in Storyboard. So that image works because the UIImageView is already in place as a container during the Storyboard (NIB) loading process, and you're just setting its internal image property later.
You have correctly set the value of someValue for all future use, during cell rendering and event handling. So, for example, if there's an #IBAction handler that runs after the cell is displayed and tapped on, it will indeed have access to someValue. That's where your test print should go. What are you ultimately using someValue for?
FOLLOWUP
So it's a simple error; you just need to set the text value in cellForRowAtIndexPath. You don't need a copy of model data in the cell (i.e. no need to have a someValue field in your cell), either. Just configure the UI dynamically from your (properly separated) model data:
instead of:
cell.someValue = 5
You just need, e.g.:
cell.someValueLabel.text = "\(indexPath.row)" // or where ever you're getting your underlying model data from
It's a misconception to use init for any of this. The only responsibility of init for table cells is to allocate memory. A cell is a completely dynamic, temporary object, and all of its properties that reflect Application data must be set in the cellForRowAtIndexPath method. The visual rendering of the cell waits for the cellForRowAtIndexPath method to finish, so there's no timing problem.
Init method is called when the UICollectionView is instantiated. You're logging "someValue" in the init method and it's too early. Image is rendered since you're working to the ImageView directly that has already been instantiated. Try to log imageView.image in the init method, it should be nil too (or maybe not nil because the cell is reused).
You should make your job in custom variables setters and getters, where you're sure that they're not nil.
var someValue: Int!{
didSet {
print("Passed value is: \(newValue)")
}
}
You are setting the value of someValue after the cell has been initialized.
You are calling print("Passed value is: \(someValue)") during the initialization process.
Set a break point on the init method of your cell class. You should see it pass through there before you assign the value 5 to that variable.

High memory usage in UICollectionView [duplicate]

This question already has answers here:
How to clear font cache filled with emoji characters?
(6 answers)
Closed 7 years ago.
My current assignment is a iOS keyboard extension, which among other things offers all iOS-supported Emoji's (yes, I know iOS has a builtin Emoji keyboard, but the goal is to have one included in the keyboard extension).
For this Emoji Layout, which is basically supposed to be a scroll view with all emojis in it in a grid order, I decided to use an UICollectionView, as it only creates a limited number of cells and reuses them. (There are quite a lot of emojis, over 1'000.) These cells simply contain a UILabel, which holds the emoji as its text, with a GestureRecognizer to insert the tapped Emoji.
However, as I scroll through the list, I can see the memory usage exploding for somewhere around 16-18MB to over 33MB. While this doesn't trigger a memory warning on my iPhone 5s yet, it may as well on other devices, as app extensions are only dedicated a very sparse amount of resources.
EDIT: Sometimes I do receive a memory warning, mostly when switching back to the 'normal' keyboard layout. Most times, the memory usage drops below 20MB when switching back, but not always.
How can I reduce the amount of memory used by this Emoji Layout?
class EmojiView: UICollectionViewCell {
//...
override init(frame: CGRect) {
super.init(frame: frame)
self.userInteractionEnabled = true
let l = UILabel(frame: self.contentView.frame)
l.textAlignment = .Center
self.contentView.addSubview(l)
let tapper = UITapGestureRecognizer(target: self, action: "tap:")
self.addGestureRecognizer(tapper)
}
override func prepareForReuse() {
super.prepareForReuse()
//We know that there only is one subview of type UILabel
(self.contentView.subviews[0] as! UILabel).text = nil
}
}
//...
class EmojiViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
//...
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//The reuse id "emojiCell" is registered in the view's init.
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("emojiCell", forIndexPath: indexPath)
//Get recently used emojis
if indexPath.section == 0 {
(cell.contentView.subviews[0] as! UILabel).text = recent.keys[recent.startIndex.advancedBy(indexPath.item)]
//Get emoji from full, hardcoded list
} else if indexPath.section == 1 {
(cell.contentView.subviews[0] as! UILabel).text = emojiList[indexPath.item]
}
return cell
}
//Two sections: recently used and complete list
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 2
}
}
let emojiList: [String] = [
"\u{1F600}",
"\u{1F601}",
"\u{1F602}",
//...
// I can't loop over a range, there are
// unused values and gaps in between.
]
Please let me know if you need more code and/or information.
Edit: My guess is that iOS keeps the rendered emojis somewhere in the memory, despite setting the text to nil before reuse. But I may be completely wrong...
EDIT: As suggested by JasonNam, I ran the keyboard using Xcode's Leaks tool. There I noticed two things:
VM: CoreAnimation goes up to about 6-7MB when scrolling, but I guess this may be normal when scrolling through a collection view.
Malloc 16.00KB, starting at a value in the kilobytes, shoots up to 17MB when scrolling through the whole list, so there is a lot of memory being allocated, but I can't see anything else actually using it.
But no leaks were reported.
EDIT2: I just checked with CFGetRetainCount (which still works when using ARC) that the String objects do not have any references left once the nil value in prepareForReuse is set.
I'm testing on an iPhone 5s with iOS 9.2, but the problem also appears in the simulator using a iPhone 6s Plus.
EDIT3: Someone had the exact same problem here, but due to the strange title, I didn't find it up to now. It seems the only solution is to use UIImageViews with UIImages in the list, as UIImages in UICollectionView's are properly released on cell reuse.
it's pretty interesting, in my testing project, i commented out the prepareForReuse part in the EmojiView, and the memory usage became steady, project started at 19MB and never goes above 21MB, the (self.contentView.subviews[0] as! UILabel).text = nil is causing the issues in my test.
I think you don't use storyboard to design the collection view. I searched around and found out that you need to register the class with identifier before you populate the collection view cell. Try to call the following method on viewDidLoad or something.
collectionView.registerClass(UICollectionViewCell.self , forCellWithReuseIdentifier: "emojiCell")
Since you have memory issues you should try lazy loading your labels.
// Define an emojiLabel property in EmojiView.h
var emojiLabel: UILabel!
// Lazy load your views in your EmojiView.m
lazy var emojiLabel: UILabel = {
var tempLabel: UIImageView = UILabel(frame: self.contentView.frame)
tempLabel.textAlignment = .Center
tempLabel.userInteractionEnabled = true
contentView.addSubview(tempLabel)
return tempLabel;
}()
override func prepareForReuse() {
super.prepareForReuse()
emojiLabel.removeFromSuperview()
emojiLabel = nil
}
//...
class EmojiViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
//...
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//The reuse id "emojiCell" is registered in the view's init.
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("emojiCell", forIndexPath: indexPath) as! EmojiView
//Get recently used emojis
if indexPath.section == 0 {
cell.emojiLabel.text = recent.keys[recent.startIndex.advancedBy(indexPath.item)]
//Get emoji from full, hardcoded list
} else if indexPath.section == 1 {
cell.emojiLabel.text = emojiList[indexPath.item]
}
return cell
}
That way you're certain that the label is released when you scroll.
Now I have one question. Why do you add a gesture recognizer to your EmojiViews ? UICollectionView already implements this functionality with its didSelectItemAtIndexPath: delegate. Allocating extra gestureRecognizers for each loaded cell is pretty heavy.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath){
let cell : UICollectionViewCell = collectionView.cellForItemAtIndexPath(indexPath) as! EmojiView
// Do stuff here
}
To sum up, I would recommand to get rid of your whole init function in EmojiViews.m, use lazy loading for the labels and didSelectItemAtIndexPath: delegate for the selection events.
NB : I'm not used to swift so my code might contain a few mistakes.

Swift: load images Async in UITableViewCell

I have a tableview that I created with code (without storyboard):
class MSContentVerticalList: MSContent,UITableViewDelegate,UITableViewDataSource {
var tblView:UITableView!
var dataSource:[MSC_VCItem]=[]
init(Frame: CGRect,DataSource:[MSC_VCItem]) {
super.init(frame: Frame)
self.dataSource = DataSource
tblView = UITableView(frame: Frame, style: .Plain)
tblView.delegate = self
tblView.dataSource = self
self.addSubview(tblView)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: nil)
let record = dataSource[indexPath.row]
cell.textLabel!.text = record.Title
cell.imageView!.downloadFrom(link: record.Icon, contentMode: UIViewContentMode.ScaleAspectFit)
cell.imageView!.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
print(cell.imageView!.frame)
cell.detailTextLabel!.text = record.SubTitle
return cell
}
}
and in other class I have an extension method for download images Async:
extension UIImageView
{
func downloadFrom(link link:String?, contentMode mode: UIViewContentMode)
{
contentMode = mode
if link == nil
{
self.image = UIImage(named: "default")
return
}
if let url = NSURL(string: link!)
{
print("\nstart download: \(url.lastPathComponent!)")
NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, _, error) -> Void in
guard let data = data where error == nil else {
print("\nerror on download \(error)")
return
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
print("\ndownload completed \(url.lastPathComponent!)")
self.image = UIImage(data: data)
}
}).resume()
}
else
{
self.image = UIImage(named: "default")
}
}
}
I used this function in other places and worked correctly, Based on my logs I understand that images downloaded without problem (when the cell is rendered) and after download of image, The cell UI not updated.
Also I tried to use caching library like Haneke but problem is exist and not change.
Please help me to understand mistakes
Thanks
After setting the image you should call self.layoutSubviews()
edit: corrected from setNeedsLayout to layoutSubviews
The issue is that the .subtitle rendition of UITableViewCell will layout the cell as soon as cellForRowAtIndexPath returns (overriding your attempt to set the frame of the image view). Thus, if you are asynchronously retrieving the image, the cell will be re-laid out as if there was no image to show (because you're not initializing the image view's image property to anything), and when you update the imageView asynchronously later, the cell will have already been laid out in a manner such that you won't be able to see the image you downloaded.
There are a couple of solutions here:
You can have the download update the image to default not only when there is no URL, but also when there is a URL (so you'll first set it to the default image, and later update the image to the one that you downloaded from the network):
extension UIImageView {
func download(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFill, placeholder: UIImage? = nil) {
contentMode = mode
image = placeholder
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
print("error on download \(error ?? URLError(.badServerResponse))")
return
}
guard 200 ..< 300 ~= response.statusCode else {
print("statusCode != 2xx; \(response.statusCode)")
return
}
guard let image = UIImage(data: data) else {
print("not valid image")
return
}
DispatchQueue.main.async {
print("download completed \(url.lastPathComponent)")
self.image = image
}
}.resume()
}
}
This ensures that the cell will be laid out for the presence of an image, regardless, and thus the asynchronous updating of the image view will work (sort of: see below).
Rather than using the dynamically laid out .subtitle rendition of UITableViewCell, you can also create your own cell prototype which is laid out appropriately with a fixed size for the image view. That way, if there is no image immediately available, it won't reformat the cell as if there was no image available. This gives you complete control over the formatting of the cell using autolayout.
You can also define your downloadFrom method to take an additional third parameter, a closure that you'll call when the download is done. Then you can do a reloadRowsAtIndexPaths inside that closure. This assumes, though, that you fix this code to cache downloaded images (in a NSCache for example), so that you can check to see if you have a cached image before downloading again.
Having said that, as I alluded to above, there are some problems with this basic pattern:
If you scroll down and then scroll back up, you are going to re-retrieve the image from the network. You really want to cache the previously downloaded images before retrieving them again.
Ideally, your server's response headers are configured properly so that the built in NSURLCache will take care of this for you, but you'd have to test that. Alternatively, you might cache the images yourself in your own NSCache.
If you scroll down quickly to, say, the 100th row, you really don't want the visible cells backlogged behind image requests for the first 99 rows that are no longer visible. You really want to cancel requests for cells that scroll off screen. (Or use dequeueCellForRowAtIndexPath, where you re-use cells, and then you can write code to cancel the previous request.)
As mentioned above, you really want to do dequeueCellForRowAtIndexPath so that you don't have to unnecessarily instantiate UITableViewCell objects. You should be reusing them.
Personally, I might suggest that you (a) use dequeueCellForRowAtIndexPath, and then (b) marry this with one of the well established UIImageViewCell categories such as AlamofireImage, SDWebImage, DFImageManager or Kingfisher. To do the necessary caching and cancelation of prior requests is a non-trivial exercise, and using one of those UIImageView extensions will simplify your life. And if you're determined to do this yourself, you might want to still look at some of the code for those extensions, so you can pick-up ideas on how to do this properly.
--
For example, using AlamofireImage, you can:
Define a custom table view cell subclass:
class CustomCell : UITableViewCell {
#IBOutlet weak var customImageView: UIImageView!
#IBOutlet weak var customTitleLabel: UILabel!
#IBOutlet weak var customSubtitleLabel: UILabel!
}
Add a cell prototype to your table view storyboard, specifying (a) a base class of CustomCell; (b) a storyboard id of CustomCell; (c) add image view and two labels to your cell prototype, hooking up the #IBOutlets to your CustomCell subclass; and (d) add whatever constraints necessary to define the placement/size of the image view and two labels.
You can use autolayout constraints to define dimensions of the image view
Your cellForRowAtIndexPath, can then do something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let record = dataSource[indexPath.row]
cell.customTitleLabel.text = record.title
cell.customSubtitleLabel.text = record.subtitle
if let url = record.url {
cell.customImageView.af.setImage(withURL: url)
}
return cell
}
With that, you enjoy not only basic asynchronous image updating, but also image caching, prioritization of visible images because we're reusing dequeued cell, it's more efficient, etc. And by using a cell prototype with constraints and your custom table view cell subclass, everything is laid out correctly, saving you from manually adjusting the frame in code.
The process is largely the same regardless of which of these UIImageView extensions you use, but the goal is to get you out of the weeds of writing the extension yourself.
oh my god, the layoutSubviews is not recommended to use directly
the right way to solve the problem is call:
[self setNeedsLayout];
[self layoutIfNeeded];
here, the two way have to call together.
try this, have a good luck.
Create your own cell by subclassing UITableViewCell. The style .Subtitle, which you are using, has no image view, even if the property is available. Only the style UITableViewCellStyleDefault has an image view.
Prefer SDWebImages library here is the link
it will download image async and cache the image also
and very easy to integrate into the project as well

Resources