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.
Related
I have a UITableView and during the initial loading of my app it sends multiple API requests. As each API request returns, I add a new row to the UITableView. So the initial loading adds rows in random orders at random times (Mostly it all happens within a second).
During cell setup, I call an Async method to generate an MKMapKit MKMapSnapshotter image.
I've used async image loading before without issue, but very rarely I end up with the image in the wrong cell and I can't figure out why.
I've tried switching to DiffableDataSource but the problem remains.
In my DiffableDataSource I pass a closure to the cell that is called when the image async returns, to fetch the current cell in case it's changed:
let dataSource = DiffableDataSource(tableView: tableView) {
(tableView, indexPath, journey) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "busCell", for: indexPath) as! JourneyTableViewCell
cell.setupCell(for: journey) { [weak self] () -> (cell: JourneyTableViewCell?, journey: Journey?) in
if let self = self
{
let cell = tableView.cellForRow(at: indexPath) as? JourneyTableViewCell
let journey = self.sortedJourneys()[safe: indexPath.section]
return (cell, journey)
}
return(nil, nil)
}
return cell
}
Here's my cell setup code:
override func prepareForReuse() {
super.prepareForReuse()
setMapImage(nil)
journey = nil
asyncCellBlock = nil
}
func setupCell(for journey:Journey, asyncUpdateOriginalCell:#escaping JourneyOriginalCellBlock) {
self.journey = journey
// Store the async block for later
asyncCellBlock = asyncUpdateOriginalCell
// Map
if let location = journey.location,
(CLLocationCoordinate2DIsValid(location.coordinate2D))
{
// Use the temp cached image for now while we get a new image
if let cachedImage = journey.cachedMap.image
{
setMapImage(cachedImage)
}
// Request an updated map image
journey.createMapImage {
[weak self] (image) in
DispatchQueue.main.async {
if let asyncCellBlock = self?.asyncCellBlock
{
let asyncResult = asyncCellBlock()
if let cell = asyncResult.cell,
let journey = asyncResult.journey
{
if (cell == self && journey.id == self?.journey?.id)
{
self?.setMapImage(image)
// Force the cell to redraw itself.
self?.setNeedsLayout()
}
}
}
}
}
}
else
{
setMapImage(nil)
}
}
I'm not sure if this is just a race condition with the UITableView updating several times in a small period of time.
I think this is because when the image is available then that index is not there. Since the table view cells are reusable, it loads the previous image since the current image is not loaded yet.
if let cachedImage = journey.cachedMap.image
{
setMapImage(cachedImage)
}
else {
// make imageView.image = nil
}
I can see you already cache the image but I think you should prepare the cell for reuse like this:
override func prepareForReuse() {
super.prepareForReuse()
let image = UIImage()
self.yourImageView.image = image
self.yourImageView.backgroundColor = .black
}
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
I'm implementing an activity indicator to show while the image is fetched/loads. However, the activity indicator sometimes shows up twice in the same frame.
I checked the code numerous times and even tried other methods such as a counter matching the row number. Any idea why this is showing up twice? (see image below)
Activity Indicator Code (inside cellForRowAtIndexPath):
// start indicator when loading images
var indicatorPhoto: MaterialActivityIndicatorView! = MaterialActivityIndicatorView(style: .Small)
indicatorPhoto.center = cell.mainRestaurantImageView.center
cell.mainRestaurantImageView.addSubview(indicatorPhoto)
indicatorPhoto!.startAnimating()
cell.mainRestaurantImageView.loadInBackground {
(success: UIImage!, error: NSError!) -> Void in
if ((success) != nil) {
// stop indicator when loading images
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
} else {
println("Unsuccessful Fetch Image")
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
}
}
Update:
This is the rest of the cellForRowAtIndexPath code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
cell.nameLabel.text = restaurantNames[indexPath.row]
cell.photoNameLabel.text = photoNames[indexPath.row]
cell.cityLabel.text = " " + addressCity[indexPath.row]
cell.distanceLabel?.text = arrayRoundedDistances[indexPath.row] + "mi"
// check if there are images
if foodPhotoObjects.isEmpty { } else {
var restaurantArrayData = self.foodPhotoObjects[indexPath.row] as PFObject
cell.mainRestaurantImageView.image = UIImage(named: "") // set placeholder
cell.mainRestaurantImageView.file = restaurantArrayData["SmPhotoUploaded"] as PFFile
// start indicator when loading images
var indicatorPhoto: MaterialActivityIndicatorView! = MaterialActivityIndicatorView(style: .Small)
indicatorPhoto.center = cell.mainRestaurantImageView.center
cell.mainRestaurantImageView.addSubview(indicatorPhoto)
indicatorPhoto!.startAnimating()
cell.mainRestaurantImageView.loadInBackground {
(success: UIImage!, error: NSError!) -> Void in
if ((success) != nil) {
// stop indicator when loading images
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
} else {
println("Unsuccessful Fetch Image")
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
}
}
cell.mainRestaurantImageView.contentMode = .ScaleAspectFill
cell.mainRestaurantImageView.clipsToBounds = true
}
return cell
}
Update 2
// create indicator when loading images
var indicatorPhoto : MaterialActivityIndicatorView? = cell.mainRestaurantImageView.viewWithTag(123) as? MaterialActivityIndicatorView;
if indicatorPhoto == nil{
indicatorPhoto = MaterialActivityIndicatorView(style: .Small)
indicatorPhoto!.center = cell.mainRestaurantImageView.center
indicatorPhoto!.tag = 123
cell.mainRestaurantImageView.addSubview(indicatorPhoto!)
indicatorPhoto!.startAnimating()
}
This is showing multiple times because you're adding it multiple times. In fact every time when the else case of foodPhotoObjects.isEmpty is called.
This is because, the first line of your method:
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
dequeues the cell from table view. The dequeue works as follow:
It maintains a queue based on the identifier.
If there's no cell in the queue, it creates a new cell.
If there's already a cell, it returns that cell to you. Which will be re-used.
So what you're doing is, you're adding MaterialActivityIndicatorView every time to the cell, whether it was added previously or not.
Solution:
Add a custom view to your cell from xib, and set its class to
MaterialActivityIndicatorView. And get the reference here to
hide/show and animation.
Check the sub-views of cell.mainRestaurantImageView and see if
there's already a MaterialActivityIndicatorView, get its reference
and do animation and stuff. If there's no subview as MaterialActivityIndicatorView, create one and add it to the image view as subview. You'll use the tag property for this.
The second approach can be done something like this:
//first find the activity indication with tag 123, if its found, cast it to its proper class
var indicatorPhoto : MaterialActivityIndicatorView? = cell.mainRestaurantImageView.viewWithTag(123) as? MaterialActivityIndicatorView;
if indicatorPhoto == nil{
//seems it wasn't found as subview initialize here and add to mainRestaurantImageView with tag 123
}
//do rest of the stuff.
I am using parse to store and retrieve some data, which I then load into a UITableview, each cell contains some text and image, however when I open my tableview, any cells in the view do not show images until I scroll them out of view and back into view (I guess this is calling cellForRowAtIndexPath). Is there a way to check when all images are downloaded and then reload the tableview?
func loadData(){
self.data.removeAllObjects()
var query = PFQuery(className:"Tanks")
query.orderByAscending("createdAt")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// The find succeeded.
for object in objects {
self.data.addObject(object)
}
} else {
// Log details of the failure
NSLog("Error: %# %#", error, error.userInfo!)
}
self.tableView.reloadData()
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
self.cell = tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath!) as TankTableViewCell // let cell:TankTableViewCell
let item:PFObject = self.data.objectAtIndex(indexPath!.row) as PFObject
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.rating = item.objectForKey("rating") as NSNumber
cell.productName.text = item.objectForKey("prodName") as? String
cell.companyName.text = item.objectForKey("compName") as? String
self.cell.reviewTv.text = item.objectForKey("review") as? String
let userImageFile = item.objectForKey("image") as PFFile
userImageFile.getDataInBackgroundWithBlock({
(imageData: NSData!, error: NSError!) -> Void in
if (error == nil) {
let image = UIImage(data:imageData)
self.cell.productImage.image = image
}
}, progressBlock: {
(percentDone: CInt) -> Void in
if percentDone == 100{
}
})
self.setStars(self.rating)
// Configure the cell...
UIView.animateWithDuration(0.5, animations: {
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.cell.reviewTv.scrollRangeToVisible(0)
})
return cell
}
The problem is that you use self.cell and that you change that reference each time a cell is returned. So, when the images are loaded they are all set into the last cell to be returned, which probably isn't on screen (or at least not fully).
Really you should be capturing the cell in the completion block of the image download (and checking that the cell is still linked to the same index path).
Also, you should cache the downloaded images so you don't always download the data / recreate the image.
You could set up a delegate method in your UITableViewController that gets called by another controller class that fetches the images. I doubt that's what you want to do though.
What you should do is initialize the cells with a default image, and have the cell controller itself go and fetch the image in the background, and update its UIImageView when the fetch completes. You definitely don't want to wait around for all images to load before reloading the table because a.) that takes a long time, and b.) what if one fails or times out?
Once the cell has loaded its image, if it is swapped out by the recycler and swapped back in, you can simply get the cached image by calling getData instead of getDataInBackground as long as isDataAvailable is true.
After your line:
self.cell.productImage.image = image
Try Adding:
cell.layoutSubviews() or self.cell.layoutSubviews()
It should render the subview, or your image in this case, on the first table view.
I use table with dequeueReusableCellWithIdentifier and afnetworking+uiimageview. Some of my cells have images, some haven't. If I scroll my table before an image has loaded, success block will put image in reused wrong cell. For example image was in cell #2, but after scroll it appears in cell number #8 because #8 was on second position in that moment. Is it possible to use setImageWithURLRequest with dequeueReusableCellWithIdentifier together?
My code:
let cell = tableView.dequeueReusableCellWithIdentifier("simpleCell", forIndexPath: indexPath) as UITableViewCell!
cell.textLabel.text = fields[indexPath.row]["name"] as String
cell.imageView.image = nil
if let image = fields[indexPath.row]["image"] as? String {
if (image != "") {
let image_url = NSURL(string: image)
let url_request = NSURLRequest(URL: image_url)
let placeholder = UIImage(named: "no_photo")
cell.imageView.setImageWithURLRequest(url_request, placeholderImage: placeholder, success: { [weak cell] (request:NSURLRequest!,response:NSHTTPURLResponse!, image:UIImage!) -> Void in
if let cell_for_image = cell {
cell_for_image.imageView.image = image
cell_for_image.setNeedsLayout()
}
}, failure: { [weak cell]
(request:NSURLRequest!,response:NSHTTPURLResponse!, error:NSError!) -> Void in
if let cell_for_image = cell {
cell_for_image.imageView.image = nil
cell_for_image.setNeedsLayout()
}
})
}
}
return cell
sorry if my question duplicates another. I found a lot of similar questions, but I haven't found solution. I tried to add reload
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
into my success block, but it doesn't help.
UPDATE:
I also noticed, that I have not this problem in cases when all my cells has an images. If I understood correctly the reason is: AFNetworking is aborting previous request for the same cell, when try to request new image. But If I haven't image in cell it will not abort. How can I do it manually?
When the cell is reused by the table view, the image download is still processing in the background. When it completes, cell points to a reused cell with different content.
You have two options:
Call cancelImageRequestOperation on the image view after cell.imageView.image = nil
In the completion handler, don't refer to cell; instead use your data model to request the correct cell from the table view.
Check if the cell is visible
let visibleCells = tableView.visibleCells as NSArray
cell.imageView.setImageWithURLRequest(url_request, placeholderImage: placeholder, success: { [weak cell] (request:NSURLRequest!,response:NSHTTPURLResponse!, image:UIImage!) -> Void in
if let cell_for_image = cell {
if(visibleCells.containsObject(cell)) {
cell_for_image.imageView.image = image
cell_for_image.setNeedsLayout()
}
}
}, failure: { [weak cell]
(request:NSURLRequest!,response:NSHTTPURLResponse!, error:NSError!) -> Void in
if let cell_for_image = cell {
cell_for_image.imageView.image = nil
cell_for_image.setNeedsLayout()
}
})