kingfisher causing main thread issue - ios

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
}
}

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()
}
}

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.

Crash and Received memory warning

I have below for loop to download files from server to my iPad 3 device.
Running hundred of files, I got error and app crash. The console shown me "Received memory warning. Same logic running on my iPad Air was passed. Anyone can adivse how to resolve the problem.
iPad 3 -> iOS 9.3.5
iPad Air -> iOS 10.3.3
func download() {
for (index, subJson): (String, JSON) in serverJson! {
for (_, subJson): (String, JSON) in subJson {
let filepath = subJson["path"].stringValue
let nUpdated = subJson["updated"].stringValue
if let oUpdated = localJson?[index].array?.filter({ $0["path"].string == filepath}).first?["updated"].stringValue {
if (oUpdated == nUpdated)
{
DispatchQueue.main.async { () -> Void in
self.progressView.progress = Float(self.count) / Float(self.totalCount)
}
count += 1
continue
}
}
var absPath = filepath
let strIdx = absPath.index(absPath.startIndex, offsetBy: 2)
if (absPath.hasPrefix("./"))
{
absPath = absPath.substring(from: strIdx)
}
let sourceUrl = URL(string: self.sourceUrl.appending(absPath))
do {
let fileData = try NSData(contentsOf: sourceUrl!, options: NSData.ReadingOptions())
try writeFileToLocal(absPath, fileData)
} catch {
print(error)
}
DispatchQueue.main.async { () -> Void in
self.progressView.progress = Float(self.count) / Float(self.totalCount)
}
//print("Path: \(absPath)")
//print("%: \(Float(count) / Float(totalCount))")
count += 1
}
}
do {
// Remove temp json file first if exists.
if fileManager.fileExists(atPath: oldManifestPath) {
try? fileManager.removeItem(atPath: oldManifestPath)
}
// Write temp json file to local.
try data?.write(to: oldManifestUrl)
self.defaults.set(hashes, forKey: "LastHash")
} catch {
print(error)
}
DispatchQueue.main.async {
self.progressView.isHidden = true
self.changeViewProperties(2)
}
}
}
private func writeFileToLocal(_ url:String, _ data:NSData) throws {
let url = URL(string: url)
let path = url?.deletingLastPathComponent().relativePath
let file = url?.lastPathComponent
let documentsPath = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let filePath = documentsPath.appendingPathComponent(path!)
var isDir: ObjCBool = false
if !FileManager.default.fileExists(atPath: (filePath?.path)!, isDirectory:&isDir) {
try FileManager.default.createDirectory(atPath: filePath!.path, withIntermediateDirectories: true, attributes: nil)
}
FileManager.default.changeCurrentDirectoryPath((filePath?.path)!)
try data.write(toFile: file!, options: .atomicWrite)
print("Update: \(filePath!), \(file!)")
FileManager.default.changeCurrentDirectoryPath(documentsPath.path!)
}
Then I call the function "download" in DispatchQueue.
DispatchQueue.global().async {
self.downloadFiles()
}
Memory waring is occuring as you are processing hundreds of Things on main thread just a Trick that you can try out if it helps you take that function in background queue instead of main Queue , you may get some ui warnings which you need to manage as updating ui in background thread
I do not know if it will help you or not but you an just give it a try for once
DispatchQueue.global(qos: .background).async
{
//BackGround Queue Update
self.downloadFiles()
DispatchQueue.main.async
{
//Main Queue Update With All UI
}
}
as background thread is always good to manage huge processing for example kingfisher library take data on main thread and return on main thread but process it in background thread

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

Swift 3 warning for dispatch async

I have this code:
DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.default).async {
let url = URL(string: itemImageURL )
let data = try? Data(contentsOf: url!)
if data != nil {
DispatchQueue.main.async{
cell.advImage!.image = UIImage(data: data!)
}
}
}
I get this warning in Swift 3:
'default' was deprecated in iOS 8.0: Use qos attributes instead
on the first line.
Haven't found yet a solution. Has anybody?
try qos: DispatchQoS.QoSClass.default instead of priority: DispatchQueue.GlobalQueuePriority.default
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
let url = URL(string: itemImageURL )
let data = try? Data(contentsOf: url!)
if data != nil {
DispatchQueue.main.async{
cell.advImage!.image = UIImage(data: data!)
}
}
}
Instead of using priority parameter:
DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.default).async {
// ...
}
use qos parameter that uses a different enum DispatchQoS.QoSClass.default but you can also use its enum value as just .default:
DispatchQueue.global(qos: .default).async {
// ...
}
Swift 3 has brought many changes on GCD(Grand Central Dispatch).
If you're creating a property using the Dispatch Framework and updating the UI with some animation within a function it might look something like this.
let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
// dispatch_after says that it will send this animation every nsec
queue.asyncAfter(deadline: when) {
DispatchQueue.main.async(execute: {
self.animate(withDuration: 0.5, animations: {
self.image.setWidth(35)
self.image.setHeight(35)
})
})
}
Below code is tested for Swift 3.0 on Xcode 8.2.1
DispatchQueue.global(qos: .background).async {
let img2 = Downloader.downloadImageWithURL(imageURLs[1])
// Background Thread
DispatchQueue.main.async {
// Run UI Updates
self.imageView2.image = img2
}
}
where property of QoS are :
background, utility, `default`, userInitiated, userInteractive and unspecified
Refer this apple document for more details.

Resources