I have six custom cells. 3 cells contain imageView and some labels and others just contains labels. In viewDidLoad, I loaded all data from coredata and refreshing the tableview the problem is when table has cells (without imageview cell) it scrolls smoothly but when we add cell(which has imageview) it scrolls smoothly in down scrolling but it structs when I am up scrolling the tableview.I tried to reduce image quality but didn't work. how to solve this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let insuranceCell = tableView.dequeueReusableCell(withIdentifier: "insuranceCell") as! InsuranceCell
let pollutionCell = tableView.dequeueReusableCell(withIdentifier: "pollutionCell") as! PollutionCell
let servicingCell = tableView.dequeueReusableCell(withIdentifier: "servicingCell") as! ServicingCell
let challanPaidCell = tableView.dequeueReusableCell(withIdentifier: "challanPaidCell") as! ChallanPaidCell
let insuranceClaimCell = tableView.dequeueReusableCell(withIdentifier: "insuranceClaimCell") as! InsuranceClaimCell
let fuelRefillCell = tableView.dequeueReusableCell(withIdentifier: "fuelRefillCell") as! FuelRefillCell
let cellArr = sections[indexPath.section].cell
//Loading cell based on array details
switch cellArr[0] {
case is InsuranceDetails:
insuranceCell.setup(object: cellArr[indexPath.row])
return insuranceCell
case is Pollution:
pollutionCell.setup(object: cellArr[indexPath.row])
return pollutionCell
case is Servicing:
servicingCell.setup(object: cellArr[indexPath.row])
return servicingCell
case is ChallanPaid:
challanPaidCell.setup(object: cellArr[indexPath.row])
return challanPaidCell
case is InsuranceClaims:
insuranceClaimCell.setup(object: cellArr[indexPath.row])
return insuranceClaimCell
case is FuelRefills:
fuelRefillCell.setup(object: cellArr[indexPath.row])
return fuelRefillCell
default:
return insuranceCell
}
}
class InsuranceCell: UITableViewCell {
func setup(object : NSManagedObject){
guard let arr = object as? InsuranceDetails else {return}
lbAmountPaid.text = arr.amountPaid
lbAgency.text = arr.agency
lbVehiclevalidfrom.text = arr.vehicleValidFrom
lbVehiclevalidupto.text = arr.vehicleValidUpto
let imageData = arr.insurancePhoto ?? Data()
let image = UIImage(data: imageData)
let compimapge = image?.resized(withPercentage: 0.1)
insurancePhoto.image = compimapge
}
As Fabio says, don't unnecessarily dequeue all the cells all of the time.
However, the image creation and scaling is most likely where the stuttering is coming from. The naming of the parameter in your call to UIImage.resized(withPercentage: 0.1) suggests that your source images are truly massive (you are displaying them at 1/1000th of their original size!?). If the parameter name is misleading and 0.1 really means 1/10th, I suggest renaming the parameter (UIImage.resized(withFraction: 0.1) perhaps).
Having said all that, look to move the image scaling off the main thread. Something like this (un-tested):
class InsuranceCell: UITableViewCell {
func setup(object: NSManagedObject, in table: UITableview, at indexPath: IndexPath) {
guard let arr = object as? InsuranceDetails else {return}
lbAmountPaid.text = arr.amountPaid
lbAgency.text = arr.agency
lbVehiclevalidfrom.text = arr.vehicleValidFrom
lbVehiclevalidupto.text = arr.vehicleValidUpto
// Do time consuming stuff in the background
DispatchQueue.global(qos: .userInitiated).async {
let imageData = arr.insurancePhoto ?? Data()
let image = UIImage(data: imageData)
// This will be where all the time is going. 0.1% suggests your source
// image is massively oversized for this usage.
let compimage = image?.resized(withPercentage: 0.1)
// Always back to the main thread/queue for UI updates
DispatchQueue.main.async {
guard let cell = table.cellForRow(at: indexPath) as? InsuranceCell else {
// The cell we wanted to configure is no longer in view
return
}
// Don't be tempted to write straight to `self.insurancePhoto.image`,
// it may already have been reused for a different row
// if the user is scrolling quickly
cell.insurancePhoto.image = compimage
}
}
}
}
It requires the additional parameters in order to check that updating the cell is still appropriate once the scaling has completed.
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
}
I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.
I have a tableview that loads data from Firebase, and on load it seems to take ages, around 10-15 seconds before any data is shown in the tableview. it also appears thet the app is frozen while this data is loading.
my function for getting the data is: this is called in viewDidLoad
func getTrackData() {
let result = FIRDatabase.database().reference(withPath: "tracks")
result.observe(.value, with: { snapshot in
var newItems: [newTracks] = []
for item in snapshot.children {
let trackDetails = newTracks(snapshot: item as! FIRDataSnapshot)
newItems.append(trackDetails)
}
self.items = newItems
self.items.sort(by: {$0.distance < $1.distance})
self.tableView.reloadData()
})
}
and my tableview is as follows:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "TrackCell", for: indexPath) as? TrackCell {
let tr: newTracks!
if inSearchMode {
tr = filteredTrack[indexPath.row]
cell.configureCell(track: tr)
} else {
tr = items[indexPath.row]
cell.configureCell(track: tr)
}
cell.configureCell(track: tr)
cell.completion = {
let coordinate = CLLocationCoordinate2DMake(tr.lat,tr.lon)
let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: coordinate,addressDictionary:nil))
mapItem.name = tr.name
mapItem.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey :MKLaunchOptionsDirectionsModeDriving])
return()
}
cell.completion1 = {
let url = URL(string: tr.link)!
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.open(url)
}
return()
}
return cell
} else {
return UITableViewCell()
}
}
My question is, is this code written ok, or is there a better way to do it. I have a concern that my function is getting each item and then reloading the tableview, and this may be slowing things down significantly. My data isnt massive, here is an example.
that shows 1 full record, there are 131 of those records in total.
Figured this out now, I had a calculation for distance in miles from current location being worked out with each set of data being loaded. This significantly slowed down the tableview population.
Just need to figure how to do this calculation now, but maybe after the data has loaded
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.
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