TableViewController search results don't load properly because of a lag - ios

So I have an app that pulls up movie results when I type in a search. (Like IMDb.) I use a free API from themoviedb.org to load the results. I load them in a TableViewController. I load the posters for the results using a mod on the .dataTaskWithRequest method. to make it synchronous. Other than that, it's just basic API sending and receiving for the titles, genres, and years of the movies or TV Shows.
Now my app lags when I type too fast, this isn't completely because of the synchronous loading, because it still happens when I don't load images at all, but image loading makes the app lag, too. Now this is an issue in and of itself, but the problem is that when the app loads the words on to the screen, and is done with the lag, the results are the results of part of the word I have on screen. For example, if I type "The Simpsons" too fast, I get results for "The Sim", but if I backspace once, and retype "The Simpsons", the results reload correctly. Something that complicates things even more, is that sometimes I get the top result only being one of the old, partial results, and the rest are normal and loaded underneath.
Here is a video explaining the situation. The first time i type down "the simpsons", you can see the lag. I typed it all really fast, but it lags past the word "the". When it is done loading, it loads up a beowulf result that shouldn't even be there. I have no idea what's going on and it's driving me nuts. Even when I don't load images, and the typing doesn't lag, the results still don't update.
Here are the relevant code snippets, if you want any more, feel free to ask. I just don't want to bombard you with too much code at once:
This updates search results when text is typed in search bar:
extension SearchTable : UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
//To Handle nils
var searchBarText = searchController.searchBar.text
if (searchBarText == nil) {
searchBarText = ""
}
searchBarText! = searchBarText!.condenseWhitespace()
//To Handle Disallowed Characters
searchBarText = searchBarText!.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
//Find Results from themoviedb
let urlString = "https://api.themoviedb.org/3/search/multi?query=" + searchBarText! + "&api_key= (I can't post the api key publicly online, sorry)"
let results = NSURL(string: urlString)
if (results == nil) {
//Server Error
}
//Wire Up Results with matchingItems Array
let task = NSURLSession.sharedSession().dataTaskWithURL(results!) { (data, response, error) -> Void in
if let jsonData = data {
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)
if var results = jsonData["results"] as? [NSDictionary] {
if results.count > 0 {
//Clean out non-english results:
//I wrote the function, it shouldn't be the source of the lag, but I can still provide it.
self.cleanArray(&results)
self.matchingItems = results
} else {
self.matchingItems = []
}
}
} catch {
//JSON Serialization Error
}
}
}
task.resume()
self.tableView.reloadData()
}
}
Then, after I get the results, I reload the table using the two required methods from a TableViewDataSource:
//Table Data Source
extension SearchTable {
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matchingItems.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as! CustomCell
//Safe-Guard. This shouldn't be needed if I understood what I was doing
if (indexPath.row < matchingItems.count) {
cell.entry = matchingItems[indexPath.row] //404
//Name & Type & Year
//This is only for TV Shows, I removed the rest for simplicity
cell.title.text = matchingItems[indexPath.row]["name"] as? String
cell.icon.image = UIImage(named: "tv.png")
let date = (matchingItems[indexPath.row]["first_air_date"] as? String)
cell.year.text = date == nil ? "" : "(" + date!.substringToIndex(date!.startIndex.advancedBy(4)) + ")"
//Genre
//Code here removed for simplicity
//Poster
cell.poster.image = UIImage(named: "Placeholder.jpg")
if let imagePath = matchingItems[indexPath.row]["poster_path"] as? String {
let url = NSURL(string: "http://image.tmdb.org/t/p/w185" + imagePath)
let urlRequest = NSURLRequest(URL: url!)
let session = NSURLSession.sharedSession()
//Synchronous Request
let semaphore = dispatch_semaphore_create(0)
let task = session.dataTaskWithRequest(urlRequest) { data, response, error in
if let poster = UIImage(data: data!) {
cell.poster.image = poster
}
dispatch_semaphore_signal(semaphore)
}
task.resume()
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
return cell
}
}
Thanks!

First of all, I strongly recommend you don't use synchronous request, mainly because it blocks your UI until it finish and this is a very bad responsiveness for an app.
In your case you can place a placeholder for the UIImage and when the request finish substitute it for the correct image.
Regarding your issue of typing faster, it's called throttle or debounce, Apple recommends:
Performance issues. If search operations can be carried out very rapidly, it is possible to update the search results as the user is typing by implementing the searchBar:textDidChange: method on the delegate object. However, if a search operation takes more time, you should wait until the user taps the Search button before beginning the search in the searchBarSearchButtonClicked: method. Always perform search operations a background thread to avoid blocking the main thread. This keeps your app responsive to the user while the search is running and provides a better user experience.
But if you until want it to handle yourself you can see this two good answers explaining how to handle it correctly:
How to throttle search (based on typing speed) in iOS UISearchBar?
How can I debounce a method call?
I recommend you handle it as Apple recommends or you can change your philosophy and adopt some libraries that handle it for your automatically like:
Bond
RxSwift
The first one in more easy to learn, the second one needs to learn Reactive Programming and concepts of Functional Programming, It's up to you.
I hope this help you.

Just for people who may be struggling in the future with this same issue. First of all, read my comment to Victor Sigler's answer.
Here were the issues:
1 - I searched for the results online using .dataTaskWithURL() This is an asynchronous method which ran in the background while the code continued. So on occasion, the table would reload before the new results were in. See this thread for more information. I highly recommend checking this tutorial on concurrency if you are serious about learning Swift:
www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1
2 - The images lagged because of the search being synchronous, as Victor said. His answer pretty much handles the rest, so read it!

Related

IOS app crashes on a line of code if there's no internet connection, how can I prevent this

The code this and it crashes on "try!", but I don't know how to catch the error and it has it be explicit otherwise it won't work.
func downloadPicture2(finished: () -> Void) {
let imageUrlString = self.payments[indexPath.row].picture
let imageUrl = URL(string: imageUrlString!)!
let imageData = try! Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
The short answer is don't use try! - Use do/try/catch and recover from the problem in the catch clause.
For example -
func downloadPicture2(finished: () -> Void) {
cell.profilePicture.image = nil
if let imageUrlString = self.payments[indexPath.row].picture,
let imageUrl = URL(string: imageUrlString) {
do {
let imageData = try Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
}
catch {
print("Error fetching image - \(error)")
}
}
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
Now you have code that won't crash if the url is invalid or there is no network, but there are still some serious issues with this code.
Data(contentsOf:) blocks the current thread while it fetches the data. Since you are executing on the main thread this will freeze the user interface and give a poor user experience.
Apple specifically warns not to do this
Important
Don't use this synchronous initializer to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
Rather, you should use an asynchronous network operations, such as a dataTask.
This code operates on cell - an external property. Once you move to asynchronous code you will probably be fetching images for multiple cells simultaneously. You should pass the relevant cell to this function to avoid clashes.
The use of the network isn't particularly efficient either; assuming this is part of a table or collection view, cells are reused as the view scrolls. You will repeatedly fetch the same image as this happens. Some sort of local caching would be more efficient.
If it is possible to use external frameworks in your project (i.e. your employer doesn't specifically disallow it) then I strongly suggest you look at a framework like SDWebImage or KingFisher. They will make this task much easier and much more efficient.

Using dispatch_async to load images in background

I am trying to update a progress bar as my images load. I've read several answers here and tried formatting my code many different ways. I'm trying to read in the images and update the progress bar. Then, will all the images are loaded call the code to process them. The best result I've got was some code that works most of the time. However, if I'm dealing with a situation where it is pulling in a lot of images, I get weird errors. I think it is going ahead and running the continue code before all the images are fully loaded. When I remove the dispatch_async, the code works fine but the progress bar does not update.
func imageLocXML(il:String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
pieceImages.append(image!)
self.numImagesLoaded += 1
self.updateProgressBar()
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
}
There a number of issues:
This code isn't thread safe, because you have race condition on numImagesLoaded. This could, theoretically, result in continueLoad to be called more than once. You can achieve thread safety by synchronizing numImagesLoaded by dispatching updates to this (and other model objects) back to the main queue.
Like DashAndRest said, you have to dispatch the UI update to the main queue, as well.
When you made this asynchronous, you introduced a network timeout risk when you initiate a lot of requests. You can solve this by refactoring the code to use operation queues instead of dispatch queues and specify maxConcurrentOperationCount.
The images are being added to an array:
Because these tasks run asynchronously, they're not guaranteed to complete in any particular order, and thus the array won't be in order. You should save the images in a dictionary, in which case the order no longer matters.
Just like numImagesLoaded, the pieceImages isn't thread safe.
You are using a lot of forced unwrapping, so if any requests failed, this would crash.
But to address this, we have to step back and look at the routine calling this method. Let's imagine that you have something like:
var pieceImages = [UIImage()]
func loadAllImages() {
for imageUrl in imageURLs {
imageLocXML(imageUrl)
}
}
func imageLocXML(il:String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.pieceImages.append(image!)
self.numImagesLoaded += 1
self.updateProgressBar()
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
}
I'm suggesting that you replace that with something like:
var pieceImages = [String: UIImage]()
func loadAllImages() {
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 4
let completionOperation = NSBlockOperation {
self.continueLoad()
}
for imageURL in imageURLs {
let operation = NSBlockOperation() {
if let url = NSURL(string: imageURL), let data = NSData(contentsOfURL: url), let image = UIImage(data: data) {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.numImagesLoaded += 1
self.pieceImages[imageURL] = image
self.updateProgressBar()
}
}
}
queue.addOperation(operation)
completionOperation.addDependency(operation)
}
NSOperationQueue.mainQueue().addOperation(completionOperation)
}
Having said that, I think there are deeper issues here:
Should you be loading images in advance like this at all? We would generally advise lazy loading of images, only loading them when needed.
If you're going to load images into a structure like this, you should gracefully handle memory pressure, purging it upon low memory warning. You then need to gracefully handle what to do when you go to retrieve an image and it's been purged due to memory pressure (leading you right back to a just-in-time lazy loading pattern).
We'd generally advise against synchronous network requests (the NSData(contentsOfURL:_)). We'd generally use NSURLSession which is cancellable, offers richer error handling, etc. Admittedly, that complicates the above code even further (probably leading me down the road of asynchronous NSOperation subclass), but you should at least be aware of the limitations of contentsOfURL.
Try DISPATCH_QUEUE_PRIORITY_DEFAULT this for background queue as:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.pieceImages.append(image!)
self.numImagesLoaded += 1
dispatch_async(dispatch_get_main_queue(), {
//UI must be updated on main thread queue
self.updateProgressBar()
})
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
UI must be updated on MAIN thread queue!
You also have to use self for accessing pieceImages.

populate UITableView with Json image URL calls swift

I'm new into programming in Swift and so far I'm downloading Json data as Strings and populate a UITableView. The images inside are url links. The problem is that the data are many, 53, so 53 url calls to get the images as well. At first, I was doing everything in the main thread so it was lagging a lot. Now I've put the code that does the http calls in an async method. The app does not lag but
1) The images don't download in order (I don't mind about that much although it would be nicer)
2) The download is slow, the memory hits around 250-270mb and the cpu around 40-50% while the network is around 500kb/s.
I don't own an iPhone to do a real check but with those numbers I see that the app uses a lot of resources. I wonder why the network is so slow though. Using 3g-4g must be faster and less stressing in my opinion and I don't know what the emulator is using.
So my question, is there any way for my app to go any faster or use less resources?
Below the code that puts the data on the TableView. It takes below 2 seconds to fill the table with the strings and a lot of time to download all the images.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "GameTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! GameTableViewCell
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
cell.nameLabel?.text = games[indexPath.row].name
cell.releaseDateLabel?.text = games[indexPath.row].releaseDate
dispatch_async(backgroundQueue, {
for _ in self.games{
if let url = NSURL(string: self.games[indexPath.row].gameImage!) {
if let data = NSData(contentsOfURL: url){
cell.photoImageView?.contentMode = UIViewContentMode.ScaleAspectFit
cell.photoImageView?.image = UIImage(data: data)
}
}
}
})
return cell
}
You're downloading the images every time you want to show a cell. You need to use NSCache or any other third-party solution or something as simple as NSMutableDictionary for caching the images that downloads successfully so you don't download them every time.
You can also use the existing third-party solutions like AlamofireImage and SDWebImage which provide async downloading of images, showing placeholder image, and caching
I was skeptical because I have never used an outside library but erasing 5-10 lines of code to just type the code below.. well, it saves a lot of time and trouble.
if let url = NSURL(string: self.games[indexPath.row].gameImage!) {
cell.photoImageView?.hnk_setImageFromURL(url)
}

Updating a many-to-many relationship in Core Data with Swift

I have the following Core Data model:
And I'm trying to update the many-to-many relationship between Speaker and TalkSlot from a JSON I receive from a REST API call.
I have tried dozens of ways, replacing my many-to-many by 2 one-to-many's, removing from one side or the other, but one way or the other I keep getting EXC_BAD_ACCESS or SIGABRT and I just don't understand the proper way to do it. Here is the last thing I tried:
for speaker in NSArray(array: slot!.speakers!.allObjects){
if let speaker = speaker as? Speaker {
speaker.mutableSetValueForKey("talks").removeObject(slot!)
}
}
slot!.mutableSetValueForKey("speakers").removeAllObjects()
if let speakersArray = talkSlotDict["speakers"] as? NSArray {
for speakerDict in speakersArray {
if let speakerDict = speakerDict as? NSDictionary {
if let linkDict = speakerDict["link"] as? NSDictionary {
if let href = linkDict["href"] as? String {
if let url = NSURL(string: href) {
if let uuid = url.lastPathComponent {
if let speaker = self.getSpeakerWithUuid(uuid) {
speaker.mutableSetValueForKey("talks").addObject(slot!)
slot!.mutableSetValueForKey("speakers").addObject(speaker)
}
}
}
}
}
}
}
}
If it helps, the API I'm using is documented here as I'm trying to cache the schedule of a conference into Core Data in an Apple Watch extension. Note that I managed to store all the rest of the schedule without any issue. But for this relationship, each time I try to update it after storing it the first time, I get an EXC_BAD_ACCESS (or sometimes a SIGABRT), at a random place in my code of course. Any idea what I'm doing wrong?
OK, after reading a few other questions associating Core Data EXC_BAD_ACCESS errors and multi-threading, I noticed that I was doing my caching on a NSURLSession callback. Once I called my caching function on the main thread using the code below, the EXC_BAD_ACCESS errors completely disappeared and now the data seems to be saved and updated properly:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.cacheSlotsForSchedule(schedule, data: data)
})

Repeat code for whole array

I am using some Facebook IDs in my app, and I have an array of serveral ID's, the array can be 10 numbers but can also be 500 numbers..
Right now the numbers are displayed in a tableview, and I want all the results there too, so they need to be in an array.
let profileUrl = NSURL(string:"http://www.facebook.com/" + newArray[0])!
let task = NSURLSession.sharedSession().dataTaskWithURL(profileUrl) {
(data, response, error) -> Void in
// Will happen when task completes
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
dispatch_async(dispatch_get_main_queue(),
{ () -> Void in
let websiteArray = webContent!.componentsSeparatedByString("pageTitle\">")
//print(websiteArray[1])
let secondArray = websiteArray[1].componentsSeparatedByString("</title>")
print(secondArray[0])
})
}
}
this code takes the first number of the array, goes to facebook.com/[the actual number], and then downloads the data and splits the data into pieces, so that the data that I want it in the secondArray[0]. I want to do this for every number of the array, take the result data and put it back into an array. I have no idea how to do this because you don't know how much numbers there are gonna be etc, does someone has a good solution for this?
Any help would be appreciated, really!
Thanks
You have several problems here, and you should take them one at at a time to build up to your solution.
First, forget the table for the moment. Don't worry at all about how you're going to display these results. Just focus on getting the results in a simple form, and then you'll go back and convert that simple form into something easy to display, and then you'll display it.
So first, we want this in a simple form. That's a little bit complicated because it's all asynchronous. But that's not too hard to fix.
func fetchTitle(identifier: String, completion: (title: String) -> Void) {
let profileUrl = NSURL(string:"http://www.facebook.com/" + identifier)!
let task = NSURLSession.sharedSession().dataTaskWithURL(profileUrl) {
(data, response, error) -> Void in
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
let websiteArray = webContent!.componentsSeparatedByString("pageTitle\">")
let secondArray = websiteArray[1].componentsSeparatedByString("</title>")
let title = secondArray[0]
completion(title: title)
}
}
task.resume()
}
Now this is still pretty bad code because it doesn't handle errors at all, but it's a starting point, and the most important parts are here. A function that takes a string, and when it's done fetching things, calls some completion handler.
(Regarding error handling, note how many places this code would crash if it were returned surprising data. Maybe the data you get isn't a proper string. Maybe it's not formatted like you think it is. Every time you use ! or subscript an array, you run the risk of crashing. Try to minimize those.)
So you might then wrap it up in something like:
var titles = [String]()
let identifiers = ["1","2","3"]
let queue = dispatch_queue_create("titles", DISPATCH_QUEUE_SERIAL)
dispatch_apply(identifiers.count, queue) { index in
let identifier = identifiers[index]
fetchTitle(identifier) { title in
dispatch_async(queue) {
titles.append(title)
}
}
}
This is just code to get you on the right track and start studying the right things. It certainly would need work to be production quality (particularly to handle errors).
Once you have something that returns your titles correctly, you should be able to write a program that does nothing but take a list of identifiers and prints out the list of titles. Then you can add code to integrate that list into your tableview. Keep the parts separate. The titles are the Model. The table is the View. Read up on the Model-View-Controller paradigm, and you'll be in good shape.
To repeat code for whole array put your code in a loop and run that loop from 0 to array.count-1
You don't need to know how many items there will be an array. You can just get the count at run time array.count here array is your array.
I hope this is what you wanted to know, your question doesn't make much sense though.

Resources