swift 3 iOS tableview datasource memory - ios

I have a tableview which acts as a newsfeed. The cells are filled from an array of newsfeed items. I get the JSON from the Server, create newsfeed items from that input and attach them to my newsfeed array. a newsfeed item contains a title, a description and an imageurl string.
At:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
var item = self.feed!.items[indexPath.row]
if (item.messageType == 1){
cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
cell.title.text = item.title
cell.description.text = item.contentText
if (item.imageURL as URL == URL(string: "noPicture")!)
{
cell.picture.image = UIImage(named:"empty")
}
else{
if (item.cachedImage == UIImage(named:"default-placeholder")){
let request = URLRequest(url: item.imageURL as URL)
cell.picture.image = item.cachedImage
cell.dataTask = self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
OperationQueue.main.addOperation({ () -> Void in
if error == nil && data != nil {
let image = UIImage(data: data!)
if (image != nil){
self.feed!.items[indexPath.row].cachedImage = image!
}
cell.picture.image = image
}
})
})
cell.dataTask?.resume()
}else
{
cell.picture.image = item.cachedImage
}
}
}
the cells from the rows get filled with my newsfeeditem data.
But since i keep all my newsfeeditems inside an array, the memory usage is high and gehts higher for each additional newsfeeditem. I want it to work with endless scrolling like twitter, so i wonder how experienced developers tackle this memory issue.

Your problem is in this lines or wherever you try to hold UIImage inside your array, this is really not advised and will cause crash due to memory since image is very large data and not advised to persist it in your RAM with UIImage inside array:
self.feed!.items[indexPath.row].cachedImage = image!
What you need to do is basically after fetch your image from URL, you save it to your app's documents folder and save the name or it's path that can distinct your image in cachedImage (just change the type to string or sth) and refetch it from your app's document folder when you need to show it in cellForRow
Flow: Fetch image -> save to disk and persist path in array -> refetch from disk with the path in cellForRow

Related

An Alamofire request for each cell of a table view

I want to populate a table view by parsing JSON data received thanks to several Alamofire requests. However, the thing is that each cell has a different request from the others. Each cell makes a request based on IndexPath. Since the calls are asynchronous, not all requests are achieved...
My code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var name = ""
let url = "https://........"+items[indexPath.section][indexPath.row]+"......"
Alamofire.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
let object = swiftyJsonVar["results"][0]
print(object)
name = object["name"].stringValue
cell.textLabel?.text = name
}
}
return cell
}
How would it be possible to achieve every request? Thanks in advance!
You should keep track of hooking indexpath with every request so you know what the value returned is for
struct Item
{
var indexPath:NSIndexPath!
func getData(index:NSIndexPath)
{
let url = "https://........"+items[indexPath.section][indexPath.row]+"......"
Alamofire.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
let object = swiftyJsonVar["results"][0]
globalArr// store indexpath and value
/// remove indexoath from allDownloads
/// send refresh to table
}
}
}
}
in cell for row
if(indexPath in globalArr)
{
// show it
}
else
{
// check whether it's download is in progress to avoid multiple same requests when scroll because of dequeuing
if(allDownloads doesn't contain inexpath)
{
// create object of item and start download
// allDownloads.add(object)
}
}
globalArr: keep track of all downloaded objects
allDownloads : keep track of in progress downloads

Created an image cache for a UITableViewCell but only one image is displayed

I've had a great help in creating a functional image cache for a UITableViewCell in cellForRowAtIndex. Unfortunately, with the code below, only one image is displayed over and over. I was under the impression that cellForRowAtIndexPath was like a for loop, running again for each row. Therefore, I'm wondering why only one image is displayed.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
var oneRestaurant: Restaurant = tablerestaurantarray[indexPath.row]
if let cachedVersion = cache.object(forKey: "image") {
oneRestaurant = cachedVersion
} else {
cache.setObject(oneRestaurant, forKey: "image")
}
cell?.picture?.image = oneRestaurant.value(forKey: "image") as! UIImage?
let restaurant = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.value(forKey: "Name") as? String
return cell!
}
Update 2:
Results from the added breakpoint
You use the same NSCache key ("image") for different objects. That's why only the first Restaurant object is saved into the cache. For all other cells you look for the object cached for key "image" and get the same previously saved Restaurant object back.
You have to use different keys for caching different Restaurant objects. Try to append the index path to the cache key:
let key = "restaurant \(indexPath)"
if let cachedVersion = cache.object(forKey: key) {
oneRestaurant = cachedVersion
} else {
cache.setObject(oneRestaurant, forKey: key)
}
I don't quite understand why you want to cache restaurant objects though. You already have them in the tablerestaurantarray, so there won't be any benefit caching them. Maybe your intention was caching the images?

Images showing with activityIndicator as images is being refreshed

I am working on VirtualTourist project on Udacity and we are required to display images as the images are being refreshed through a Flickr Client. Currently my implementation is such that the refresh will only show once the images are fully downloaded. The project requires such that the images are shown as an when they are downloaded, and while the downloading is happening, an activity indicator is displayed instead.
My code as follows:
#IBAction func newCollectionButtonDidTap(_ sender: Any) {
print("FETCHING NEW COLLECTION...")
DispatchQueue.main.async {
self.photos.removeAll()
self.collectionView.reloadData()
print("Reload Data newColVC")
}
for items in fetchedResultsController.fetchedObjects! {
context.delete(items as! NSManagedObject)
}
loadPhotos()
}
func loadPhotos() {
newColActivityIndicator.startAnimating()
newColActivityIndicator.isHidden = false
newCollectionButton.isUserInteractionEnabled = false
newCollectionButton.alpha = 0.4
FlickrClient.sharedInstance.getPhotos(pin.coordinate.latitude as AnyObject, lon: pin.coordinate.longitude as AnyObject, { (results, error) in
if let error = error {
print(error)
} else {
if results != nil {
for result in results! {
let picture = Photo(pin: self.pin, imageData: result as NSData, context: self.context)
self.photos.append(picture)
}
do {
try self.delegate.stack?.saveContext()
} catch let error as NSError {
print("Error saving context in loadPhotos(): \(error.localizedDescription)")
}
DispatchQueue.main.async {
self.collectionView.reloadData()
self.newColActivityIndicator.stopAnimating()
self.newColActivityIndicator.isHidden = true
self.newCollectionButton.isUserInteractionEnabled = true
self.newCollectionButton.alpha = 1.0
print("Reload Data loadPhotos")
}
}
}
})
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageViewCell
cell.activityIndicator.startAnimating()
cell.activityIndicator.isHidden = false
let photo = photos[indexPath.row]
if let photoImage = photo.imageData {
print("PhotoImage present")
DispatchQueue.main.async {
cell.activityIndicator.stopAnimating()
cell.activityIndicator.isHidden = true
cell.imageView?.image = UIImage(data: photoImage as Data)
}
} else {
print("PhotosImage don have")
}
return cell
}
Some advice here is much appreciated, thanks!
So firstly when you tap newCollectionButton you are removing all the data stored in core data as well as removing all those which are being displayed in collection view. So at this moment, collection view doesnot have anything to display. as a result it should be empty showing nothing.
Now, i think your loadPhotos methods returns the collection of photos from flicker which you add it to your local array and coredata. datasource of collection view must be using data from photos array.
You remove the activity indicator and then reload the collection view. At this point, you already have the necessary data for the images to be shown available. So cellForItemAt retrives data from local array, and create image and displays it. Maybe this process is happenning so fast, that your cell.activityIndicator.startAnimating() & cell.activityIndicator.stopAnimating() is executing much faster.
Since your question is unclear, & your loadPhotos fetches collection of photos data, if you want that activityIndicator to be shown in each cell, until all the image gets downloaded, you can
Remove just imagedata present in Photo object stored in local/coredata
make cell to show activity indicator if there is no image data present in each cell
reloadData() so that activity indicator starts
call loadPhotos
After downloading all photos, clear photo array, store downloaded Photos in array and call reloadData() so that all the images gets displayed.

Single UITableView with different custom UITableViewCells issue

I have added UITableView into UIScrollView, I have created an IBOutlet for height constraint of UITableView which helps me in setting the content size of UITableview.
I have 3 tabs and I switch tabs to reload data with different data source . Also the i have different custom cells when the tab changes.
So when the tab changes I call reloadData
here is my cellForRow function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
var cell:UITableViewCell!
let event:Event!
if(tableView == self.dataTableView)
{
let eventCell:EventTableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier, forIndexPath: indexPath) as! EventTableViewCell
eventCell.delegate = self
event = sectionsArray[indexPath.section].EventItems[indexPath.row]
eventCell.eventTitleLabel?.text = "\(event.title)"
eventCell.eventImageView?.image = UIImage(named: "def.png")
if let img = imageCache[event.imgUrl] {
eventCell.eventImageView?.image = img
}
else {
print("calling image of \(indexPath.row) \(event.imgUrl)")
// let escapedString = event.imgUrl.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
let session = NSURLSession.sharedSession()
do {
let encodedImageUrl = CommonEHUtils.urlEncodeString(event.imgUrl)
let urlObj = NSURL(string:encodedImageUrl)
if urlObj != nil {
let task = session.dataTaskWithURL(urlObj!, completionHandler: { ( data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
guard let realResponse = response as? NSHTTPURLResponse where
realResponse.statusCode == 200 else {
print("Not a 200 response, url = " + event.imgUrl)
return
}
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Store the image in to our cache
self.imageCache[event.imgUrl] = image
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate:EventTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? EventTableViewCell {
cellToUpdate.eventImageView?.image = image
}
})
}
})
task.resume()
}
} catch {
print("Cant fetch image \(event.imgUrl)")
}
}
cell = eventCell
}
else if(secodTabClicked)
{
let Cell2:cell2TableViewCell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier1, forIndexPath: indexPath) as! cell2TableViewCell
//Image loading again takes place here
cell = Cell2
}
else if(thirdTabClicked)
{
let Cell3:cell3TableViewCell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier2, forIndexPath: indexPath) as! cell3TableViewCell
//Image loading again takes place here
cell = Cell3
}
return cell
}
As you can see each tab has different custom cells with images.
Below are the problems I am facing
1) it takes time to reload data when I switch tabs and their is considerable lag time. On iphone 4s it is worse
2) When I open this page, first tab is selected by default, so when i scroll, everything works smoothly. But when i switch tabs, and when i scroll again after reloading of data, the scroll becomes jerky and immediately i get memory warning issue.
What I did so far?
1) I commented the image fetching code and checked whether that is causing jerky scroll, but its not.
2) I used time profiler, to check what is taking more time, and it points the "dequeueReusableCellWithIdentifier". So I dont know what is going wrong here.
Your code does not look "symmetric" with respect to cell set up when secodTabClicked and thirdTabClicked. I do not see firstTabClicked, and it looks to me that the condition that you are using to determine which tab is clicked overlaps with secodTabClicked and thirdTabClicked. In other words, you are probably getting into the top branch, and return EventTableViewCell when cell2TableViewCell or cell3TableViewCell are expected.
Refactoring your code to make type selection "symmetric" with respect to all three cell types should fix this problem.
Another solution could be making separate data sources for different tabs, and switching the data source instead of setting xyzTabClicked flags. You would end up with thee small functions in place of one big function, which should make your code easier to manage.

Swift UItableview with AlamofireImage crash when scrolling fast

Im using AlamofireImage to load images in an async way.
It works quite well except when I scroll very fast the app crashes.
I assume it is because when maybe more than 10 requests are being sent in a very short period of time the app crashes (when I scroll fast).
I also see a sudden spike in memory usage.
When I scroll slowly and maybe 4 requests are sent in a short period it does not crash.
Does anyone have a hint on how to prevent this? How can I cancel requests of invisible cells where the user has been scrolled by?
Here is the code:
// Dequeue your cell and other code goes here.
// with as! the cell is set to the custom cell class: DemoCell
// afterwards all data can be loaded from the JSON response into the cells
override func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier("FoldingCell",
forIndexPath: indexPath) as! DemoCell
cell.delegate = self
//tag the cell with the indexpath row number to make sure the loaded asynch image corresponds to the right cell
cell.tag = indexPath.row
//clear cell of eventually reused images
cell.schoolCoverImage.image = UIImage()
cell.schoolBiggerImage.image = UIImage()
//TODO: set all custom cell properties here (retrieve JSON and set in cell), use indexPath.row as arraypointer
let resultList = self.items["result"] as! [[String: AnyObject]]
let itemForThisRow = resultList[indexPath.row]
cell.schoolNameClosedCell.text = itemForThisRow["name"] as! String
cell.schoolNameOpenedCell.text = itemForThisRow["name"] as! String
self.schoolIdHelperField = itemForThisRow["name"] as! String
cell.schoolIntroText.text = itemForThisRow["name"] as! String
// set the button's tag like below.
cell.innerCellButton.tag = indexPath.row
//call method when button inside cell is tapped
cell.innerCellButton.addTarget(self, action: #selector(MainTableViewController.cellButtonTapped(_:)), forControlEvents: .TouchUpInside)
cell.schoolIntroText.text = "We from xx University..."
//handle the image from a separate API call
let schoolIdNumber = itemForThisRow["sco_id"] as! NSInteger
let schoolIdString = String(schoolIdNumber)
//TOCHeck: maybe Id is not correct and should be replaced by indexCount
let imageNameString = itemForThisRow["image"] as! String
//only load the image of the cell which is visible in the screen
// print("current cells visible?")
// print(tableView.visibleCells)
// print("currentCell")
// print(cell.tag)
// if(tableView.visibleCells.contains(cell)) {
let urlRequest = NSURLRequest(URL: NSURL(string: "https://ol-web- test.herokuapp.com/olweb/api/v1/schools/"+schoolIdString+"/image/"+imageNameString)!)
print(urlRequest)
//does cell number/tag match current indexpath row?
if(cell.tag == indexPath.row) {
//use cache in case image has been saved to cache already, otherwise get image from networking
if(self.photoCache.imageForRequest(urlRequest) != nil) {
cell.schoolCoverImage.image = photoCache.imageForRequest(urlRequest)
cell.schoolBiggerImage.image = photoCache.imageForRequest(urlRequest)
print("image from cache loaded")
}
else
{
self.imageDownloader.downloadImage(URLRequest: urlRequest) { response in
print(response.request)
print(response.response)
debugPrint(response.result)
if let image = response.result.value {
print("here comes the printed image:: ")
print(image)
print(schoolIdString)
//set image to the cell
cell.schoolCoverImage.image = image
cell.schoolBiggerImage.image = image
self.photoCache.addImage(image, forRequest: urlRequest)
print("image from network loaded and added to cache")
print(self.photoCache.memoryCapacity.description)
print(self.photoCache.memoryUsage.description)
}
}
}
}
return cell
}
EDIT: Log error is a NullPointer
30/image/Beet_Language_Bournemouth_1.jpeg }
fatal error: unexpectedly found nil while unwrapping an Optional va lue
Code line:
let urlRequest = NSURLRequest(URL: NSURL(string: "https://ol-web- test.herokuapp.com/olweb/api/v1/schools/"+schoolIdString+"/image/"+imageNameString)!)
I load here the params schoolIdString and imageNameString from a previous query.
Thx for the answers. It was corrupt data from the database which made the URL corrupt

Resources