Closure Capture Memory Leak issue with UITableView - ios

In willDisplay method, I get UIImage and IndexPath from a callback closure. I am using tableView inside that closure. Should I need to make that tableView weak to avoid possible memory leaks, or is it not an issue to use strong tableView?
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? ArtistTableViewCell else { return }
guard let imageUrl = cell.viewModel.artistImage() else { return }
// Download image callback closure returns UIImage, IndexPath, and error
ImageDownloadService.shared.downloadImage(imageUrl,indexPath:indexPath) { [weak tableView] (image, index, error) in
DispatchQueue.main.async {
guard let getIndexPath = index else { return }
guard let getImage = image else { return }
guard let getCell = tableView?.cellForRow(at: getIndexPath) as? ArtistTableViewCell else { return }
getCell.setArtistImage(getImage)
}
}
}

It’s not necessary to capture tableView explicitly because it’s provided as local variable in the first parameter of the willDisplay method.
Therefore it will not cause a memory leak.
There is a simple rule: Don’t capture anything which is locally accessible inside the method.
Feel free to prove it with Instruments.

Locale variables are not captured by closure as they are within the same scope, so you don't need to make tableview as weak reference.

weak is preferred. If you retain the tableView and dismiss the view controller while it downloads an image the table view object (and its cells) won't be deallocated until the call download finishes. (however no retain cycle will occur)

Related

UITableView got displayed before data from API using Codable

When I run the application I can see a blank table like the below screenshot loaded for certain milliseconds and then loading the table with actual data.As the items array is having 0 elements at the beginning, numberOfRowsInSection returns 0 and the blank table view is loading. Is it like that?Please help me on this
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
I changed above code to the one below, but same issue exists and in debug mode I found out that the print("Item array is empty") is executing twice, then the blank table view is displaying for a fraction of seconds, after that the actual API call is happening and data is correctly displayed in the tableview
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if items.isEmpty{
print("Item array is empty")
return 0
} else {
return self.items.count
}
}
import UIKit
class MainVC: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var bookslideShow: UIView!
#IBOutlet weak var bookTableView: UITableView!
var items : [Items] = []
override func viewDidLoad() {
super.viewDidLoad()
bookTableView.dataSource = self
bookTableView.delegate = self
self.view.backgroundColor = UIColor.lightGray
bookTableView.rowHeight = 150
// self.view.backgroundColor = UIColor(patternImage: UIImage(named: "background.jpeg")!)
self.fetchBooks { data in
self.items.self = data
DispatchQueue.main.async {
self.bookTableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if items.isEmpty{
print("Item array is empty")
return 0
} else {
return self.items.count
//bookTableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell",for:indexPath) as! BookCell
//cell.backgroundColor = UIColor(red: 180, green: 254, blue: 232, alpha: 1.00)
let info = items[indexPath.row].volumeInfo
cell.bookTitle.text = info.title
cell.bookCategory.text = info.categories?.joined(separator: ",")
cell.bookAuthor.text = info.authors?.joined(separator: ", ")
let imageString = (info.imageLinks?.thumbnail)!
if let data = try? Data(contentsOf: imageString) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
cell.bookImage.image = image
}
}
}
return cell
}
func fetchBooks(comp : #escaping ([Items])->()){
let urlString = "https://www.googleapis.com/books/v1/volumes?q=quilting"
let url = URL(string: urlString)
guard url != nil else {
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { [self] (data, response, error) in
//check for errors
if error == nil && data != nil{
//parse json
do {
let result = try JSONDecoder().decode(Book.self, from: data!)
comp(result.items)
}
catch {
print("Error in json parcing\(error)")
}
}
}
//make api call
dataTask.resume()
}
}
The delegates methods may be called multiple times. If you want to remove those empty cells initially. You can add this in viewDidLoad:
bookTableView.tableFooterView = UIView()
You are crossing the network for the data. That can take a long time especially if the connection is slow. An empty tableview isn't necessarily bad if you are waiting on the network as long as the user understands what's going on. Couple of solutions,
Fetch the data early in application launch and store locally. The problem with this approach is that the user may not ever need the downloaded resources. For instance if instantgram did that it would be a huge download that wasn't needed for the user. If you know the resource is going to be used entirely get it early or at least a small part of it that you know will be used.
2)Start fetching it early even before the segue. In your code you need it for the table view but you're waiting all the way until view did load. This is pretty late in the lifecycle.
3)If you have to have the user wait on a resource let them know you're loading. Table View has a refresh control that you can call while you are waiting on the network or use a progress indicator or spinner. You can even hide your whole view and present a view so the user knows what's going on.
Also tableview is calling the datasource when it loads automatically and you're calling it when you say reloadData() in your code, that's why you get two calls.
So to answer your question this can be accomplished any number of ways, you could create a protocol or a local copy of the objects instance ie: MainVC in your presentingViewController then move your fetch code to there and set items on the local copy when the fetch comes back. And just add a didset to items variable to reload the tableview when the variable gets set. Or you could in theory at least perform the fetch block in the segue passing the MainVC items in the block.
For instance
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let vc = segue.destination as MainVC
self.fetchBooks { data in
vc.items.self = data // not sure what the extra self is?
DispatchQueue.main.async {
vc.bookTableView.reloadData()
}
}
}
Since the closure captures a strong pointer you can do it this way.
Normally I will do data task as below code show, please see the comments in code.
// show a spinner to users when data is loading
self.showSpinner()
DispatchQueue.global().async { [weak self] in
// Put your heavy lifting task here,
// get data from some completion handler or whatever
loadData()
// After data is fetched OK, push back to main queue for UI update
DispatchQueue.main.async {
self?.tableView.reloadData()
// remove spinner when data loading is complete
self?.removeSpinner()
}
}
After making sure that you have the reloadData() called, make sure your constraints for labels/images are correct. This makes sure that you're labels/images can be seen within the cell.

Unable to set a variable in UITableViewCell from parent ViewController

I have a UITableView and a custom TableViewCell class, I am setting a non-null value in viewDidLoad method, but while setting the value to UITableViewCell in cellForRow method , the value magically becomes nil. I am unable to access the set variable from my custom UITableViewCell
Here is the snippet of code. Print statement in ViewDidLoad prints the value while the one in cellForRowAtIndexPath returns nil
override func viewDidLoad() {
super.viewDidLoad()
Docco360Util.shared().getDoctorsWithResultBlock { (doctorObjects, error) in
if let err = error{
print(err)
}else{
self.doctors=doctorObjects
print(doctorObjects![1].professionalHeader)//output is printed here
self.doctorTableView.reloadData()
}
}
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifierForUsers = "DoctorTableViewCell"
var cell = tableView.dequeueReusableCell(withIdentifier: identifierForUsers, for: indexPath) as! DoctorTableViewCell
if cell == nil{
cell=DoctorTableViewCell(style: .default, reuseIdentifier: identifierForUsers)
}
print(self.doctors[indexPath.row].professionalHeader)//output in nil here
cell.doctor=self.doctors[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DoctorTableViewCell", for: indexPath) as! DoctorTableViewCell
print(self.doctors[indexPath.row].professionalHeader)
cell.doctor = self.doctors[indexPath.row]
return cell
}
First, you should ensure that the closure of Docco360Util.shared().getDoctorsWithResultBlock is executed in the main (UI) thread. If not, you should put it into a DispatchQueue.main.async block.
Then, you should verify that the result block is executed (e.g. the print statement prints something). Btw, why do you call print(doctorObjects![1].professionalHeader) with index 1 and not 0? In tableView(tableView:cellForRowAt:) you element with index 0 - maybe this doctor is nil?
You could start by setting a breakpoint to your property declaration, a watchpoint, or implement a property observer (willSet) and add a breakpoint her, to check if somebody is messing up with your variables.
If all this does not help, you might want to post more code, expecially all lines of code where you write your self.doctors array.
I could finally fix it. The problem was in declaration of variables. I was using Objective-C header file for declarations. While declaring I declared them as "weak" instead of "retain" and that was causing the problem.

Lazy image downloading on UITableViewCell using defer

Is it safe and does it make sense to defer an asynchronous image download for a cell? The idea behind this is that I want the callback function from URLSession.shared.image(...) to be executed after creating the cell and only once calling cellForRow(at: indexPath) is valid, since I think that without deferring this method at this point should return nil.
URLSession.shared.image is a private extension that runs a data task and gives a escaping callback only if the url provided in the argument is valid and contains an image.
setImage(image:animated) is a private extension that allows you to set an image in a UIImageView using a simple animation.
If defer is not the way to go, please indicate an alternative.
Any feedback is appreciate, thanks!
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = baseCell as! MyCell
let datum = data[indexPath.row]
cell.imageView.setImage(placeholderImage, for: .normal)
defer {
URLSession.shared.image(with: datum.previewURL) { image, isCached in
if let cell = tableView.cellForRow(at: indexPath) as? MyCell {
cell.imageView.setImage(image, animated: !isCached)
}
}
}
return cell
}
NSHipster have a good article on how / when to use defer, here.
I wouldn't use defer in such a way. The intention for defer is to clean up / deallocate memory in one block of code, rather than scattering it across many exit points.
Think about having multiple guard statements throughout a function and having to deallocate memory in every one of them.
You shouldn't use this to simply add additional code after the fact.
As mentioned by #jagveer there are many third party libraries that do this already, such as SDWebImage cache. AFNetworking and AlamoFire also have the same built in. No need to re-invent the wheel when its already been done.
In this case, the defer isn't being effective. defer sets up a block to be called when the scope exits, which you are doing immediately.
I think what you want to schedule the block to run in a different thread using Dispatch. You need to get back onto the main thread to update the UI.
As this can happen later, you need to make sure the cell is still being used for the same entry and has not been reused as the user has scrolled further. Fetching the cell again isn't a good idea if it has been reused as you'd end up triggering the initial call again. I usually add some identifier to a custom UITableViewCell class to check against.
Also, you're not creating the cell, just fetching it from some other variable. This is likely to be a problem, if there is more than one cell.
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "base") as! MyCell
cell.row = indexPath.row
let datum = data[indexPath.row]
cell.imageView.setImage(placeholderImage, for: .normal)
DispatchQueue.global().async {
// Runs in a background thread
URLSession.shared.image(with: datum.previewURL) { image, isCached in
DispatchQueue.main.async {
// Runs in the main thread; safe for updating the UI
// but check this cell is still being used for the same index path first!
if cell.row == indexPath.row {
cell.imageView.setImage(image, animated: !isCached)
}
}
}
}
return cell
}

Why can I access Core Data fetched entities right after fetching them but not when I'm in a different scope?

so here's the problem I had today. In one method of mine on a project I've been working on I fetch entities using core data, and it seems to work. Here's my method:
func getVehicles() {
let moc = DataController().managedObjectContext
let vehicleFetch = NSFetchRequest(entityName: "VehicleEntity")
do {
allVehicles = try moc.executeFetchRequest(vehicleFetch) as! [VehicleEntity]
} catch {
fatalError("Failed to fetch vehicles: \(error)")
}
}
I call that in my viewDidLoad method right after calling super.viewDidLoad.
I'm loading a property of those entities into a tableview and I do so like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("vehicleCell")
cell!.textLabel!.text = allVehicles[indexPath.item].vehicleType!
return cell!
}
That works great, and it returns the .vehicleType property from all the entities and and puts it in the table. Here's where my problem occurs, in the method that gets called when a row is selected:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedVehicle = allVehicles[indexPath.item]
if let del = delegate {
del.updateCurrentVehicle(selectedVehicle)
}
self.navigationController?.popViewControllerAnimated(true)
}
Selected vehicle comes back with a bunch of weird data. Interacting with it in the console at a breakpoint has the .vehicleType property coming back as nil and it gives me an exception when I try to access other properties, all of which are of type NSNumber. If I go and fetch the data again like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedVehicle: VehicleEntity
let moc = DataController().managedObjectContext
let vehicleFetch = NSFetchRequest(entityName: "VehicleEntity")
do {
selectedVehicle = try moc.executeFetchRequest(vehicleFetch)[indexPath.item] as! VehicleEntity
// allVehicles = fetchedVehicles
} catch {
fatalError("Failed to fetch vehicles: \(error)")
}
if let del = delegate {
del.updateCurrentVehicle(selectedVehicle)
}
self.navigationController?.popViewControllerAnimated(true)
}
Why am I not able to access the properties of entities in that array in my first way of selecting the row, but I am by reselecting it? Is this some weird bug, or some nuance of Swift I was unaware of?
If anyone could help explain this, I'd really appreciate it. I'm glad I made it work, but I'd really like to understand why one way works and the other way doesnt.
Edit in response to the comments:
VehicleEntity is in fact a subclass of NSManagedObject. Here is the code for updateCurrentVehicle:
protocol SavedVehicleDelegate {
func updateCurrentVehicle(newVehicle: VehicleEntity)
}
and
func updateCurrentVehicle(newVehicle: VehicleEntity) {
setVehicleData(newVehicle)
}
Although the selectedVehicle has bad data before it gets passed into that method. I set a breakpoint in both the cellForRowAtIndexPath and didSelectRowAtIndexPath methods, and in the former the data is fine and correct, but in the second the properties of the vehicle object are as I've described above.
The updateCurrentVehicle method is what I use to pass the selected VehicleEntity object back to the parent view (this is in a navigation controller)
I use the indexPath.item only because the tableview isn't divided up into sections, so the item just returns the overall index in the tableview as a whole, or that's my understanding of it anyways.

Reloading table causes flickering

I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.
Every time I make a network call, I also call self.tableView.reloadData() on the main thread. Since each cell has an image, you can see flickering - the cell images flash white.
I tried implementing this solution but I don't know where to put it in my code or how to. My code is Swift and that is Objective-C.
Any thoughts?
Update To Question 1
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(R.reuseIdentifier.searchCell.identifier, forIndexPath: indexPath) as! CustomTableViewCell
let book = booksArrayFromNetworkCall[indexPath.row]
// Set dynamic text
cell.titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.authorsLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
// Update title
cell.titleLabel.text = book.title
// Update authors
cell.authorsLabel.text = book.authors
/*
- Getting the CoverImage is done asynchronously to stop choppiness of tableview.
- I also added the Title and Author inside of this call, even though it is not
necessary because there was a problem if it was outside: the first time a user
presses Search, the call for the CoverImage was too slow and only the Title
and Author were displaying.
*/
Book.convertURLToImagesAsynchronouslyAndUpdateCells(book, cell: cell, task: task)
return cell
}
cellForRowAtIndexPath uses this method inside it:
class func convertURLToImagesAsynchronouslyAndUpdateCells(bookObject: Book, cell: CustomTableViewCell, var task: NSURLSessionDataTask?) {
guard let coverImageURLString = bookObject.coverImageURLString, url = NSURL(string: coverImageURLString) else {
return
}
// Asynchronous work being done here.
task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue(), {
// Update cover image with data
guard let data = data else {
return
}
// Create an image object from our data
let coverImage = UIImage(data: data)
cell.coverImageView.image = coverImage
})
})
task?.resume()
}
When I scroll to the bottom of the table, I detect if I reach the bottom with willDisplayCell. If it is the bottom, then I make the same network call again.
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == booksArrayFromNetworkCall.count {
// Make network calls when we scroll to the bottom of the table.
refreshItems(currentIndexCount)
}
}
This is the network call code. It is called for the first time when I press Enter on the search bar, then it is called everytime I reach the bottom of the cell as you can see in willDisplayCell.
func refreshItems(index: Int) {
// Make to network call to Google Books
GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in
guard let books = books else {
return
}
self.footerView.hidden = false
self.currentIndexCount += 10
self.booksArrayFromNetworkCall += books
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache.
I would recommend to use SDWebImage to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling convertURLToImagesAsynchronouslyAndUpdateCells, and you will see that it will not flash again.
I dont' program in Swift but I see it is as simple as cell.imageView.sd_setImageWithURL(myImageURL). And it's done!
Here's an example of infinite scroll using insertRowsAtIndexPaths(_:withRowAnimation:)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var dataSource = [String]()
var currentStartIndex = 0
// We use this to only fire one fetch request (not multiple) when we scroll to the bottom.
var isLoading = false
override func viewDidLoad() {
super.viewDidLoad()
// Load the first batch of items.
loadNextItems()
}
// Loads the next 20 items using the current start index to know from where to start the next fetch.
func loadNextItems() {
MyFakeDataSource().fetchItems(currentStartIndex, callback: { fetchedItems in
self.dataSource += fetchedItems // Append the fetched items to the existing items.
self.tableView.beginUpdates()
var indexPathsToInsert = [NSIndexPath]()
for i in self.currentStartIndex..<self.currentStartIndex + 20 {
indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .Bottom)
self.tableView.endUpdates()
self.isLoading = false
// The currentStartIndex must point to next index.
self.currentStartIndex = self.dataSource.count
})
}
// #MARK: - Table View Data Source Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = dataSource[indexPath.row]
return cell
}
// #MARK: - Table View Delegate Methods
func scrollViewDidScroll(scrollView: UIScrollView) {
if isLoading == false && scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
isLoading = true
loadNextItems()
}
}
}
MyFakeDataSource is irrelevant, it's could be your GoogleBooksClient.getBooksFromGoogleBooks, or whatever data source you're using.
Try to change table alpha value before and after calling [tableView reloadData] method..Like
dispatch_async(dispatch_get_main_queue()) {
self.aTable.alpha = 0.4f;
self.tableView.reloadData()
[self.aTable.alpha = 1.0f;
}
I have used same approach in UIWebView reloading..its worked for me.

Resources