DispatchQueue.main.async causes the search to hang. Swift - ios

I have a tableView, each cell is loaded with an image from the internet via DispatchQueue.main.async.
I implemented a search on an array, the data from which is output to a table. Because of DDispatchQueue.main.async, the emulator starts to hang a lot, but if you remove it, everything works fine, how do I implement loading images without causing a load?
Image upload code:
DispatchQueue.main.async {
if let url = URL(string: "https://storage.googleapis.com/iex/api/logos/\(stock.displaySymbol).png") {
if let data = try? Data(contentsOf: url) {
self.stockLogoImageView.image = UIImage(data: data)
self.imageLoadingIndicator.stopAnimating()
}
}
}
Search extension code:
extension StocksViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
searchStocks(searchController.searchBar.text!)
}
func searchStocks(_ searchText: String) {
searchStocksList = stocks.filter({(stock: Stock) -> Bool in
return stock.displaySymbol.lowercased().contains(searchText.lowercased()) || stock.description.lowercased().contains(searchText.lowercased())
})
stocksTableView.reloadData()
}
}

Don't do networking on the main queue, It's the UIKit work that must be done on the main queue.
// Network on background queue
if let url = URL(string: ....),
let data = try? Data(contentsOf: url) {
let img = UIImage(data: data)
// Dispatch back to main to update UI
DispatchQueue.main.async {
self.stockLogoImageView.image = img
self.imageLoadingIndicator.stopAnimating()
}
}

Related

How to set a variable that runs on background thread and needs to access ui

I am fetching a data source in background. There are 2 urls and I choose it with a tabBar. To know which url I need to access, I use navigationController?.tabBarItem.tag. But It throws an error of "navigationController must be used from main thread only". I've tried to wrap it with DispatchQueue.main.async but it didn't work. Any fix or new approach appreciated.
override func viewDidLoad() {
super.viewDidLoad()
performSelector(inBackground: #selector(fetchJSON), with: nil)
}
#objc func fetchJSON() {
let urlString: String
if navigationController?.tabBarItem.tag == 0 {a
urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
} else {
urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
}
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
parse(json: data)
return
}
}
performSelector(onMainThread: #selector(showError), with: nil, waitUntilDone: false)
}
Move the logic that needs to access the UI to the main thread, then pass the result as an argument to your function on the background thread.
Here, there's several issues:
The performSelector(…) methods are quite low-level and not a good solution with Swift. Avoid these, they have issues and make it cumbersome to pass arguments around. Use GCD or async/await instead.
Using the synchronous Data(contentsOf: …) is also not a good idea. If you would asynchronous solutions you wouldn't run into the threading issue in the first place.
I really suggest you look into the second problem (e.g. using a DataTask), as it completely eliminates your threading issues, but here's a simple way to refactor your existing code using GCD that should already work:
override func viewDidLoad() {
super.viewDidLoad()
let urlString: String
if navigationController?.tabBarItem.tag == 0 {a
urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
} else {
urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
}
DispatchQueue.global(qos: .utility).async {
self.fetchJSON(urlString)
}
}
func fetchJSON(_ urlString: String) {
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
parse(json: data)
return
}
}
DispatchQueue.main.async {
self.showError()
}
}

kingfisher causing main thread issue

So the app works in test mode but as soon as I went to build for release I got this main thread issue.
UIImageView.image must be used from main thread only
According the the error I am not calling something on the main thread, yet the line it has thrown the thread error at is blank (see screenshot)
So I can only guess what they talking about is the code directly under that line?
code
#objc func nowplaying(){
let jsonURLString = "https://api.drn1.com.au/station/playing"
guard let feedurl = URL(string: jsonURLString) else { return }
URLSession.shared.dataTask(with: feedurl) { (data,response,err)
in
guard let data = data else { return }
do{
let nowplaying = try JSONDecoder().decode(Nowplayng.self, from: data)
nowplaying.data.forEach {
DispatchQueue.main.async {
self.artist.text = nowplaying.data.first?.track.artist
self.song.text = nowplaying.data.first?.track.title
}
print($0.track.title)
if var strUrl = nowplaying.data.first?.track.imageurl {
strUrl = strUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
//MusicPlayer.shared.nowplaying(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
MusicPlayer.shared.getArtBoard(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
}
}
I can only guess it is because kingfisher wants a loading picture or something. But unclear?
It is your responsibility to call Kingfisher's UI-extension methods on UI thread.
Before:
self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
After:
DispatchQueue.main.async {
self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
}
I believe you could always run your code in main thread in your own processor, by using:
king fisher process image on download thread so that can cause this problem
public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image?
return DispatchQueue.main.sync {
let image = ... // Your code needs to be performed in UI thread
return image
}
}

Swift Grand Central Dispatch Queues and UIImages

I know this type of question has been asked 1e7 times but I have come across a specific issue that I don't think has been covered/is blatantly obvious but I am too novice to fix it on my own.
I have the following code snippet within my cellForRowAt method in a TableViewController:
let currentDictionary = parser.parsedData[indexPath.row] as Dictionary<String,String>
let urlString = currentDictionary["media:content"]
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(data: data!)
}
}
}
Which executes fine, downloads the images and assigns them to the UIImageView of each tableViewCell.
There is a finite delay when scrolling the table as the images are downloaded 'on the fly' so to speak.
What I want to do is pre-download all these images and save them in a data structure so they are fetched from URL's less frequently.
I have tried the following implementation:
var thumbnail = UIImage()
for item in parser.parsedData {
let currentDictionary = item as Dictionary<String,String>
let title = currentDictionary["title"]
let link = currentDictionary["link"]
let urlString = currentDictionary["media:content"]
let url = NSURL(string: urlString!)
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL)
DispatchQueue.main.sync {
thumbnail = UIImage(data: data!)!
}
}
}
var newsArticle: News!
newsArticle = News(title: title!, link: link!, thumbnail: thumbnail)
news.append(newsArticle)
Where news is my data structure. This code also executes fine, however each thumbnail is a 0x0 sized image, size {0, 0} orientation 0 scale 1.000000, according to the console output.
Does anyone have any ideas how to download these images but not immediately assign them to a UIImageView, rather store them for later use?
The problem is that you create your newsArticle before the global dispatch queue even started to process your url. Therefore, thumbnail is still the empty UIImage() created in the very first line.
You'll have to create the thumbnail inside the inner dispatch closure, like:
for item in parser.parsedData {
guard let currentDictionary = item as? Dictionary<String,String> else { continue /* or some error handling */ }
guard let title = currentDictionary["title"] else { continue /* or some error handling */ }
guard let link = currentDictionary["link"] else { continue /* or some error handling */ }
guard let urlString = currentDictionary["media:content"] else { continue /* or some error handling */ }
guard let url = URL(string: urlString) else { continue /* or some error handling */ }
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.sync {
if let thumbnail = UIImage(data: data) {
let newsArticle = News(title: title, link: link, thumbnail: thumbnail)
news.append(newsArticle)
}
}
}
}
}
By the way, your very first code (cellForRow...) is also broken: You must not reference the cell inside the dispatch closure:
DispatchQueue.main.async {
// Never do this
cell.thumbnailImageView.image = UIImage(data: data!)
}
Instead, reference the IndexPath, retrieve the cell inside the clousure, and go on with that cell. But as you already mentioned, there are many many entries on stackoverflow regarding this issue.

Swift 3 : URL Image makes UITableView scroll slow issue

I have an extension to print image URL on UIImageView. But I think the problem is my tableView is so slow because of this extension. I think I need to open thread for it. How can I create a thread in this extension or do you know another solution to solve this problem?
My code :
extension UIImageView{
func setImageFromURl(stringImageUrl url: String){
if let url = NSURL(string: url) {
if let data = NSData(contentsOf: url as URL) {
self.image = UIImage(data: data as Data)
}
}
}
}
You can use the frameworks as suggested here, but you could also consider "rolling your own" extension as described in this article
"All" you need to do is:
Use URLSession to download your image, this is done on a background thread so no stutter and slow scrolling.
Once done, update your image view on the main thread.
Take one
A first attempt could look something like this:
func loadImage(fromURL urlString: String, toImageView imageView: UIImageView) {
guard let url = URL(string: urlString) else {
return
}
//Fetch image
URLSession.shared.dataTask(with: url) { (data, response, error) in
//Did we get some data back?
if let data = data {
//Yes we did, update the imageview then
let image = UIImage(data: data)
DispatchQueue.main.async {
imageView.image = image
}
}
}.resume() //remember this one or nothing will happen :)
}
And you call the method like so:
loadImage(fromURL: "yourUrlToAnImageHere", toImageView: yourImageView)
Improvement
If you're up for it, you could add a UIActivityIndicatorView to show the user that "something is loading", something like this:
func loadImage(fromURL urlString: String, toImageView imageView: UIImageView) {
guard let url = URL(string: urlString) else {
return
}
//Add activity view
let activityView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
imageView.addSubview(activityView)
activityView.frame = imageView.bounds
activityView.translatesAutoresizingMaskIntoConstraints = false
activityView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true
activityView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true
activityView.startAnimating()
//Fetch image
URLSession.shared.dataTask(with: url) { (data, response, error) in
//Done, remove the activityView no matter what
DispatchQueue.main.async {
activityView.stopAnimating()
activityView.removeFromSuperview()
}
//Did we get some data back?
if let data = data {
//Yes we did, update the imageview then
let image = UIImage(data: data)
DispatchQueue.main.async {
imageView.image = image
}
}
}.resume() //remember this one or nothing will happen :)
}
Extension
Another improvement mentioned in the article could be to move this to an extension on UIImageView, like so:
extension UIImageView {
func loadImage(fromURL urlString: String) {
guard let url = URL(string: urlString) else {
return
}
let activityView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
self.addSubview(activityView)
activityView.frame = self.bounds
activityView.translatesAutoresizingMaskIntoConstraints = false
activityView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
activityView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
activityView.startAnimating()
URLSession.shared.dataTask(with: url) { (data, response, error) in
DispatchQueue.main.async {
activityView.stopAnimating()
activityView.removeFromSuperview()
}
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
self.image = image
}
}
}.resume()
}
}
Basically it is the same code as before, but references to imageView has been changed to self.
And you can use it like this:
yourImageView.loadImage(fromURL: "yourUrlStringHere")
Granted...including SDWebImage or Kingfisher as a dependency is faster and "just works" most of the time, plus it gives you other benefits such as caching of images and so on. But I hope this example shows that writing your own extension for images isn't that bad...plus you know who to blame when it isn't working ;)
Hope that helps you.
I think, that problem here, that you need to cache your images in table view to have smooth scrolling. Every time your program calls cellForRowAt indexPath it downloads images again. It takes time.
For caching images you can use libraries like SDWebImage, Kingfisher etc.
Example of Kingfisher usage:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! CustomCell
cell.yourImageView.kf.setImage(with: URL)
// next time, when you will use image with this URL, it will be taken from cache.
//... other code
}
Hope it helps
Your tableview slow because you load data in current thread which is main thread. You should load data other thread then set image in main thread (Because all UI jobs must be done in main thread). You do not need to use third party library for this just change your extension with this:
extension UIImageView{
func setImageFromURl(stringImageUrl url: String){
if let url = NSURL(string: url) {
DispatchQueue.global(qos: .default).async{
if let data = NSData(contentsOf: url as URL) {
DispatchQueue.main.async {
self.image = UIImage(data: data as Data)
}
}
}
}
}
}
For caching image in background & scroll faster use SDWebImage library
imageView.sd_setImage(with: URL(string: "http://image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
https://github.com/rs/SDWebImage

Downloaded image not being displayed iOS Swift

I'm just starting to learn how to make network requests in iOS Swift. Below is a very simple image request where everything seems to be working. The task downloads the image with no errors but the imageView never displays the downloaded image. Any help would be greatly appreciated.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let imageURL = NSURL(string: "https://en.wikipedia.org/wiki/Baseball#/media/File:Angels_Stadium.JPG")!
let task = NSURLSession.sharedSession().dataTaskWithURL(imageURL) { (data, response, error) in
if error == nil {
let downloadedImage = UIImage(data: data!)
performUIUpdatesOnMain {
self.imageView.image = downloadedImage
}
}
}
task.resume()
}
}
Your code is working fine except for the fact you're using a wrong URL and for that your downloadedImage is coming nil because it can't create an UIImage for this data, the correct URL is:
https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/Angels_Stadium.JPG/1920px-Angels_Stadium.JPG
Update your code code as the above code and everything should be work fine:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let imageURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/Angels_Stadium.JPG/1920px-Angels_Stadium.JPG")!
let task = NSURLSession.sharedSession().dataTaskWithURL(imageURL) { (data, response, error) in
guard error == nil, let data = data else { return }
let downloadedImage = UIImage(data: data)
dispatch_async(dispatch_get_main_queue()) {
self.imageView.image = downloadedImage
}
}
task.resume()
}
I hope this help you.
If you are getting an error from NSURLSession your current code would fail silently. Don't do that.
Add a print statement inside your data task's completion block that logs the value of error and of data. Also log downloadedImage once you convert data to an image.
Finally, show us the code for your performUIUpdatesOnMain function.

Resources