large uitableview (1000 rows) freezes on reloadData() - ios

I have a UITableView with about 1000 rows. I also have a timer running every 6 seconds that fetches data from a web service. Each time I call reloadData() there is a blip - my app freezes very noticeably for a brief moment. This is very evident when scrolling.
I tried fetching about 400 rows only and the blip disappears. Any tips how to get rid of this while still fetching the 1000 rows?
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
UITableViewDataSource code:
extension ItemViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}

The problem is being caused because you are storing the items from the response and then updating the table view from the same OperationQueue, meaning that the UI thread is being blocked while your array is being updated. Using an operation queue in itself is not an optimal way to schedule tasks if you do not need fine grain control over the task (such as cancelling and advanced scheduling, like you don't need here). You should instead be using a DispatchQueue, see here for more.
In order to fix your issue, you should update your array from the background completion handler, then update your table.
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
strongSelf.items = itemsFromResponse
// update the table on the main (UI) thread
DispatchQueue.main.async {
strongSelf.itemsTableView.reloadData()
}
}
}
You should also maybe look into a more efficient way to fetch new data, because reloading the entire dataset every 6 seconds is not very efficient in terms of data or CPU on the user's phone.

The problem is you are reloading data every 6 seconds, so if the data is so big you're reloading 1000 rows every 6 seconds. I recommend you request the data and compare if there's new data so in that case you need to reload data or you simply ask to refresh once. For example:
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
if(strongSelf.items != itemsFromResponse){
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}

Related

Table View error on reloading sections - seems to be some race condition

I have some hard to solve crashes. They happen in application from time to time not in regular way. I think the problem maybe with some race conditions and synchronizations.
I am using such pattern.
1) reloadData()
2) reloadSection1(), reloadSection2(), reloadSection2(), etc.
3) I have tap events that can do reload like reloadData()
4) There are also Socket.IO messages that can cause reloadData(), reloadSectionN()
5) I try to use debouncers to execute only last reload of given type.
6) debouncers use Serial Queue to execute task serially one by one in case that request-response-reload last longer then new socket.io event arrives
7) section/table reloadin must happen on UI thread so at the end I moves to it using DispatchQueue.main.async { }
8) I have even tried to wrap data excesses/ reloads into semaphores to block modification by other threads.
But errors can happens and app can crash in spite of this. And I have no idea what may cause it.
Below I place the most important parts of code.
I have such instance properties:
private let debouncer = Debouncer()
private let debouncer1 = Debouncer()
private let debouncer2 = Debouncer()
private let serialQueue = DispatchQueue(label: "serialqueue")
private let semaphore = DispatchSemaphore(value: 1)
Here Debouncer.debounce instance method
func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: #escaping (() -> Void) ) -> () -> Void {
return { [weak self] in
guard let self = self else { return }
self.currentWorkItem?.cancel()
self.currentWorkItem = DispatchWorkItem {
action()
}
if let workItem = self.currentWorkItem {
queue.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
}
Here are debounced reloads of table view and its sections
func debounceReload() {
let debounceReload = debouncer.debounce(delay: .milliseconds(500), queue: serialQueue) {
self.reloadData()
}
debounceReload()
}
func debounceReloadOrders() {
let debounceReload = debouncer1.debounce(delay: .milliseconds(500), queue: serialQueue) {
self.reloadOrdersSection(animating: false)
}
debounceReload()
}
This debounce methods can be invoked on tap, pull to refresh screen navigation, or Socket.IO events (here possible multiple events at the same time if there are multiple users).
Each reload called by debounce reload start and ends with such methods (in between there are synchronous requests to remote api that may take some time (and are executed on this serialqueue). All debouncers reuse the same serial queue (so they should not conflict/race with each other and caused data inconsistency while reloading tableview or its sections).
private func startLoading() {
print("startLoading")
activityIndicator.startAnimating()
activityIndicator.isHidden = false
tableView.isHidden = true
// cancel section reload
debouncer1.currentWorkItem?.cancel()
debouncer2.currentWorkItem?.cancel()
}
private func stopLoading() {
guard debouncer.currentWorkItem?.isCancelled != true else { return }
self.semaphore.wait()
print("stopLoading")
tableView.reloadData()
activityIndicator.isHidden = true
refreshControl.endRefreshing()
tableView.isHidden = false
self.semaphore.signal()
}
Above are added this additional semaphores as additional check to ensure data consistency.
func startLoading(section: Int, animating: Bool = true) {
self.semaphore.wait()
print("startLoading section \(section)")
tableView.beginUpdates()
if animating {
self.data[section] = .loadingSpinner
}
tableView.reloadSections([section], with: .none)
tableView.endUpdates()
self.semaphore.signal()
}
func stopLoading(section: Int, model: Model) {
self.semaphore.wait()
print("stopLoading section \(section)")
if section == 0 {
guard debouncer1.currentWorkItem?.isCancelled != true else { return }
} else if section == 1 {
guard debouncer2.currentWorkItem?.isCancelled != true else { return }
}
tableView.beginUpdates()
self.data[section] = model
tableView.reloadSections([section], with: .none)
tableView.endUpdates()
self.semaphore.signal()
}
private func clearData() {
self.semaphore.wait()
print("clearData")
data.removeAll()
self.semaphore.signal()
}
I think this extra semaphores should not be required as this is using serial queue so all request-response-reload are executed on serial queue. Maybe some problem is that I need to switch from serial queue to main queue in order to clear/add spinner-reload and then fill-data/reload table or section. But I think it should last shorter then next data replacement happens. I consider moving self.data.append(model1) to semaphore critical section in stopLoading() for reloadData() using self.data = data assignment in this critical section.
Example error that I have encountered:
Fatal Exception: NSInternalInconsistencyException Invalid update:
invalid number of rows in section 0. The number of rows contained in
an existing section after the update (3) must be equal to the number
of rows contained in that section before the update (5), plus or minus
the number of rows inserted or deleted from that section (0 inserted,
0 deleted) and plus or minus the number of rows moved into or out of
0x195bef098 +[_CFXNotificationTokenRegistration keyCallbacks]
3 Foundation 0x1966b2b68 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
4 UIKitCore 0x1c23ecc78 -[UITableView _endCellAnimationsWithContext:]
5 UIKitCore 0x1c24030c8 -[UITableView endUpdates]
6 MyApplication 0x104b39230 MyViewController.reload(section:) + 168
> that section (0 moved in, 0 moved out).
I have also seen errors on cellForRow function, this errors happen several times a week, app is rather used multiple times a day so it is hard to repeat this error. I have tried to send simple refreshing sockets from POSTman but they do not change underling data (row count) and I think is way everything works then ok.
UPDATE
I have updated stopLoading() to stopLoading(data:) to have datasource update and tableView.reload on main queue.
So all startLoading, stopLoading, reload methods are performed on DispatchQueue.main.async { }.
private func stopLoading(data: [Model]) {
guard debouncer.currentWorkItem?.isCancelled != true else { return }
self.semaphore.wait()
print("stopLoading")
self.data = data
tableView.reloadData()
activityIndicator.isHidden = true
refreshControl.endRefreshing()
tableView.isHidden = false
self.semaphore.signal()
}

Handle thousands of socket message in iOS

I am working on a code where I need to display socket messages in the tableview and tableview get scroll to bottom with slow animation. I handle that but if the message load from socket is continues (consider 20/30 message per second) then the UI get freeze. I need to show the message like Facebook do on live screen one or two messages per iteration.
Here is my code
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 0.025, target: self, selector: #selector(self.scrollTableView(_:)), userInfo: nil, repeats: true)
self.designLayout()
}
#objc func scrollTableView(_ timer: Timer) {
guard messagesData.count > 0 else {
return
}
if tableView.contentSize.height > tableView.bounds.height {
tableView.contentInset.top = 0
}
tableView.scrollToRow(at: IndexPath(row: messagesData.count - 1, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
}
self.socket.on(“key”) {data, ack in
print("data type is \(type(of: data))")
let arrayValue = data as Array<Any>
DispatchQueue.global(qos: .background).async {
self.handleCountsAndMessaging(data: arrayValue)
}
}
func handleCountsAndMessaging(data: Array<Any>) {
if let arrData = data[0] as? NSMutableDictionary {
if(arrData.object(forKey: "text") != nil) {
self.tableDataLoading(str: String(describing: arrData.object(forKey: "text")!))
}
}
}
func tableDataLoading() {
print ("sttttttt \(str)")
DispatchQueue.main.async {
self.messagesData.add(str)
self.tableView.reloadData()
}
}
Some times I get messages like 200 at time from socket and again 200 while processing previous messages, CPU consumption is showing like 120 and UI get freeze.
Thanks in advance.
Well, there is 'live' and 'live':)
This a phone we are talking about, so it's useless to update the screen every 0.025 seconds... you can easily slow down your fetch time but let's work one problem at a time :
1/ Don't use tableView.scrollToRow, but rather 'insertRows(at indexPaths: [IndexPath], with animation: UITableViewRowAnimation)' to insert a new row, this way if your user is reading, something, he won't be disturbed if you add 200 new item
https://developer.apple.com/documentation/uikit/uitableview/1614879-insertrows
2/ Slow down your UI updates, you are overusing your main thread... You can fetch every 25ms if you like, but I recommend using a throttling mechanism to update the UI only if you get new content, and only if last UI update wasn't less than 5 seconds ago.

Swift - TableView datasource from URLSession task

I have to populate a TableView with some data fetched with an URLSession task. The source is an XML file, so i parse it into the task. The result of parsing, is an array that i pass to another function that populate another array used by TableView Delegates.
My problem is that TableView Delegates are called before task ends, so tha table is empty when i start the app, unless a data reloading (so i know that parsing and task work fine).
Here is viewDidLoad function. listOfApps is my TableView
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
checkInstalledApps(apps: <ARRAY POPULATED>)
listOfApps.delegate = self
listOfApps.dataSource = self
}
}
fetchData is the function where i fetch the XML file and parse it
func fetchData() {
let myUrl = URL(string: "<POST REQUEST>");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"
let postString = "firstName=James&lastName=Bond";
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.parser = XMLParser(data: data!)
self.parser.delegate = self
}
task.resume()
}
while checkInstalledApps is the function where i compose the array used by TableView Delegates.
func checkInstalledApps(apps: NSMutableArray){
....
installedApps.add(...)
installedApps.add(...)
....
}
So, for example, to set the number of rows i count installedApps elements
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (installedApps.count == 0) {
noApp = true
return 1
}
return installedApps.count
}
that are 0. Obviously, if i reload data, it's all ok.
My problem is the async call: first of that i used an XML accessible via GET request, so i can use XMLParser(contentsOf: myUrl) and the time is not a problem. Maybe if the XML will grow up, also in this way i will have some trouble, but now i've to use a POST request
I've tried with DispatchGroup, with a
group.enter() before super.viewDidLoad
group.leave() after task.resume()
group.wait() after checkInstalledApps()
where group is let group = DispatchGroup(), but nothing.
So, how can i tell to the tableview delegate to wait the task response and the next function?
thanks in advance
I would forget about DispatchGroup, and change a way of thinking here (you don't want to freeze the UI until the response is here).
I believe you can leave the fetchData implementation as it is.
In XMLParserDelegate.parserDidEndDocument(_:) you will be notified that the XML has been parsed. In that method call checkInstalledApps to populate the model data. After that simply call listOfApps.reloadData() to tell the tableView to reload with the new data.
You want to call both checkInstalledApps and listOfApps.reloadData() on the main thread (using DispatchQueue.main.async {}).
Also keep listOfApps.delegate = self and listOfApps.dataSource = self in viewDidLoad as it is now.
The cleaner way is to use an empty state view / activityIndicator / loader / progress hud (whatever you want), informing the user that the app is fetching/loading datas,
After the fetch is done, just reload your tableview and remove the empty state view / loader
Your problem is caused by the fact that you currently have no way in knowing when the URLSession task ended. The reloadData() call occurs almost instantly after submitting the request, thus you see the empty table, and a later table reload is needed, though the new reload should be no sooner that the task ending.
Here's a simplified diagram of what happens:
Completion blocks provide here an easy-to-implement solution. Below you can find a very simplistic (and likely incomplete as I don't have all the details regarding the actual xml parsing) solution:
func fetchData(completion: #escaping () -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.parser = XMLParser(data: data!)
self.parser.delegate = self
completion()
}
override func viewDidLoad() {
super.viewDidLoad()
fetchData() { [weak self] in self?.tableView.reloadData() }
}
Basically the completion block will complete the xml data return chain.

slow app entering foreground

My app gets stuck in the icon for about 4-5 seconds when entering foreground, if the networking is bad. In almost all the main loading pages I am fetching images from a server. I have nothing on applicationDidEnterBackground() nor applicationWillEnterForeground().
Here the code in the Main View controller where I have a collection view with images:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(homeReuseIdentifier, forIndexPath: indexPath)as! HomeViewCell
// Reload the collection view after insert item at indexPath
cvHome.reloadItemsAtIndexPaths([indexPath])
let entry = dataCell.infoCell[indexPath.row]
cell.backImageView.image = nil
let stringURL = "https://.....\(entry.filename)"
let image = UIImage(named: entry.filename)
cell.backImageView.image = image
if cell.backImageView.image == nil {
if let image = dataCell.cachedImage(stringURL) {
cell.backImageView.image = image
} else {
request = dataCell.getNetworkImage(stringURL) { image in
cell.backImageView.image = image
}
}
}
cell.titleLabel.text = entry.title
cell.authorLabel.text = entry.author
// Fix the collection view cell in size
cell.contentView.frame = cell.bounds
cell.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
// To edit the cell for deleting
cell.editing = editing
return cell
}
And this is the getNetworkImage function with Alamofire:
func getNetworkImage(urlString: String, completion: (UIImage -> Void)) -> (ImageRequest) {
let queue = decoder.queue.underlyingQueue
let request = Alamofire.request(.GET, urlString)
let imageRequest = ImageRequest(request: request)
imageRequest.request.response(
queue: queue,
responseSerializer: Request.imageResponseSerializer(),
completionHandler: { response in
guard let image = response.result.value else {
return
}
let decodeOperation = self.decodeImage(image) { image in
completion(image)
self.cacheImage(image, urlString: urlString)
}
imageRequest.decodeOperation = decodeOperation
}
)
return imageRequest
}
I'm not sure if you're networking code is bad since you haven't provided any.
My guess is, fetching the data is taking way too long, and it's blocking the UI. So currently, your app is synchronous. So tasks are executed one at a time. the reason your UI is blocked is because it has to wait for the networking to finish.
What you need to do is perform the fetching in the background, and always keep your UI on the main queue.
If you are calling Synchronous you have to wait till the task is finishing.We can start next task once we finish the current task.When you are fetching image from server make sure that whether you are using Synchronous or not.When you don't wait for a second go with Asynchronous.You don't need to wait while fetch the images from server.You can do other tasks when fetching the images from server.
For this GCD is good one to use.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Do the back ground process here
......Fetch the image here
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI here
.....Do your UI design or updation design part here
});
});
Global Queue
Asynchronous task will be performed here.
It does not wait.
Asynchronous function does not block the current thread of execution from proceeding on to the next function.
Main Queue
We must always access UI Kit classes on the main thread.
If you don't want to wait a seconds,go with GCD

Asynchronously Loading Image constantly causes image to flicker?

I have a header view for every UITableViewCell. In this header view, I load a picture of an individual via an asynchronous function in the Facebook API. However, because the function is asynchronous, I believe the function is called multiple times over and over again, causing the image to flicker constantly. I would imagine a fix to this issue would be to load the images in viewDidLoad in an array first, then display the array contents in the header view of the UITableViewCell. However, I am having trouble implementing this because of the asynchronous nature of the function: I can't seem to grab every photo, and then continue on with my program. Here is my attempt:
//Function to get a user's profile picture
func getProfilePicture(completion: (result: Bool, image: UIImage?) -> Void){
// Get user profile pic
let url = NSURL(string: "https://graph.facebook.com/1234567890/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
override func viewDidLoad() {
self.getProfilePicture { (result, image) -> Void in
if(result == true){
println("Loading Photo")
self.creatorImages.append(image!)
}
else{
println("False")
}
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
headerView.headerImage.image = self.creatorImages[section]
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
As seen by the program above, I the global array that I created called self.creatorImages which holds the array of images I grab from the Facebook API is always empty and I need to "wait" for all the pictures to populate the array before actually using it. I'm not sure how to accomplish this because I did try a completion handler in my getProfilePicture function but that didn't seem to help and that is one way I have learned to deal with asynchronous functions. Any other ideas? Thanks!
I had the same problem but mine was in Objective-C
Well, the structure is not that different, what i did was adding condition with:
headerView.headerImage.image
Here's an improved solution that i think suits your implementation..
since you placed self.getProfilePicture inside viewDidLoad it will only be called once section==0 will only contain an image,
the code below will request for addition image if self.creatorImages's index is out of range/bounds
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
if (section < self.creatorImages.count) // validate self.creatorImages index to prevent 'Array index out of range' error
{
if (headerView.headerImage.image == nil) // prevents the blinks
{
headerView.headerImage.image = self.creatorImages[section];
}
}
else // requests for additional image at section
{
// this will be called more than expected because of tableView.reloadData()
println("Loading Photo")
self.getProfilePicture { (result, image) -> Void in
if(result == true) {
//simply appending will do the work but i suggest something like:
if (self.creatorImages.count <= section)
{
self.creatorImages.append(image!)
tableView.reloadData()
println("self.creatorImages.count \(self.creatorImages.count)")
}
//that will prevent appending excessively to data source
}
else{
println("Error loading image")
}
}
}
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
You sure have different implementation from what i have in mind, but codes in edit history is not in vain, right?.. hahahaha.. ;)
Hope i've helped you.. Cheers!

Resources