I have two UICollectionView:
1 UICollectionView - Post
2 UICollectionView - Categories
Logic:
When user tap on any categories, that we send new api request and then update post collection view
In my UICollectionViewCell have two conditions.
1 Image Item
2 Video Item
Below my code:
class PopularPostsCell: UICollectionViewCell {
#IBOutlet weak var myImage : UIImageView!
func setupItems(childPostsItem: ChildPostsModel, index: Int) {
if childPostsItem.media_type != "video" {
myImage.image = nil
myImage.sd_setImage(with: URL(string: childPostsItem.media), placeholderImage: UIImage(named: ""))
} else {
DispatchQueue.global(qos: .background).async {
let locImage = Helpers.videoSnapshot(filePathLocal: childPostsItem.media)
DispatchQueue.main.async {
self.myImage.image = nil
self.myImage.image = locImage
}
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
myImage.image = nil
}
}
if video, I need get image from Video URL below my code:
static func videoSnapshot(filePathLocal: String) -> UIImage? {
do {
let asset = AVURLAsset(url: URL(string: filePathLocal)!, options: nil)
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(0, 1), actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
return thumbnail
} catch let error {
print("*** Error generating thumbnail: \(error.localizedDescription)")
return nil
}
}
Problem: If I choose several item in category very fast, my cell use random image. If I choose category slowly, that my cell use image ok.
My screen first: if I choose category fast
second: if I choose category slowly, in the screen everything is good.
If I don't use background async thread, all ok, but everything starts to slow down
Please, any help.
You are using a background thread and the main thread in a sequential manner while by definition they are parallel and don't wait for one another. Therefore in :
DispatchQueue.global(qos: .background).async {
let locImage = Helpers.videoSnapshot(filePathLocal: childPostsItem.media)
DispatchQueue.main.async {
self.myImage.image = nil
self.myImage.image = locImage
}
}
The two threads are completely asynchronous. You can't expect the background thread to be finished before the main thread starts.
There are ways to communicate thread events with signals and semaphores but in your case I think using asynchronous functions with completion handlers would be way more appropriate.
Check out how it works : https://stackoverflow.com/a/50531255/5922920
Related
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.
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
I'm trying to fix a problem with downloading an image asynchronously in a TableView in Swift. This is my Problem: I download the image from a url asynchronously, but if I scroll quickly the TableView my pictures begin to rotate.(The images alternate until the correct one appears).
This is my Download Async Code and imageCache
let imageCache = NSCache()
//DOWNLOAD Image ASINC
extension UIImageView {
public func imageFromServerURL(url: String){
if(imageCache.objectForKey(url) != nil){
self.image = imageCache.objectForKey(url) as? UIImage
}else{
let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
let task = session.dataTaskWithURL(NSURL(string: url)!, completionHandler: { (data, response, error) -> Void in
if error == nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: url)
self.image = downloadedImage
}
})
}
else {
print(error)
}
})
task.resume()
}
}
}
and Which I recall in the TableView so:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("record_charts", forIndexPath: indexPath) as! myTableViewCell
let url_img = "https://image/download.jpg"
cell.immagine.imageFromServerURL(url_img)
return cell
}
This is the gif to show you the problem better
This is due to the reuse mechanism of iOS's table view.
You can make some modification to your code to fix this:
class AsyncImageView: UIImageView {
private var currentUrl: String? //Get a hold of the latest request url
public func imageFromServerURL(url: String){
currentUrl = url
if(imageCache.objectForKey(url) != nil){
self.image = imageCache.objectForKey(url) as? UIImage
}else{
let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
let task = session.dataTaskWithURL(NSURL(string: url)!, completionHandler: { (data, response, error) -> Void in
if error == nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let downloadedImage = UIImage(data: data!) {
if (url == currentUrl) {//Only cache and set the image view when the downloaded image is the one from last request
imageCache.setObject(downloadedImage, forKey: url)
self.image = downloadedImage
}
}
})
}
else {
print(error)
}
})
task.resume()
}
}
}
Note #1: I was whiteboard coding the modification, so not sure if the code has correct syntax.
Note #2: Instead of declaring a new subclass of UIImageView, you can use associated objects.
Note #3: I strongly suggest you use AlamoFireImage, it has a category for UIImageView which is exactly what you need in this case (and future cases too).
This is because of cell reuse. I will try to explain. Suppose you have 10 cells each having a different image (Images 1 to 10) but only 5 cells fit on the screen. The table starts to load and the first cell requests image 1 to be put in an image view and that starts happening in the background but the table is scrolled before the background loading of the image finishes and the first cell is scrolled of the screen. Now that cell will be reused let's say by the sixth cell which requests image 6. You background request for image 1 then finishes and as it is still holding a reference to the cell image 1 is put in the image view. Then your background process for image 6 finishes and that replaces the image with the new version. It will be even worse if image 6 finishes loading before image 1 as you then get image 6 put in the cell and it's then replaced by image 1.
What you need to do is implement some method so that when the image is available you can check that it is still the correct one to use. I don't think you are going to be able to do that making the function an extension of ImageView so you probably need some kind of central image provider or something similar.
You need to add cancellation method in UIImageView extension, and call it or in tableView(_:willDisplay:forRowAt:) or in prepareForReuse() of UITableViewCell
or you can cancel request as in SDWebImage's web cache
I have read a similar post in Reusable cell old image showing and I am still getting the same issues. Essentially I have a TableView that downloads images from amazon s3 as you scroll down everything works good until you get to about the 12th or 13th image. What happens is that the image in the row before shows up in the new row for about 2 seconds while the new image is being downloaded . This is my code ( I'm still new at swift and learning IOS) . The stream_image_string as the full URL to download the images and PicHeight is an integer saved with the image height since every image usually has a different height .
var Stream_Cache = NSCache<AnyObject, AnyObject>()
var stream_image_string = [String]()
var PicHeight = [Int]()
This below is inside the UITableViewCell, first I check if there is a url which it will contain more than 0 characters . I then check if the image/url is saved in the Cache if not then I download it .
if stream_image_string[indexPath.row].characters.count > 0 {
if let image = Stream_Cache.object(forKey: stream_image_string[indexPath.row] as AnyObject) as? UIImage {
DispatchQueue.main.async(execute: { () -> Void in
cell.stream_image.image = image
})
} else {
if cell.stream_image != nil {
let strCellImageURL = self.stream_image_string[indexPath.row]
let imgURL: NSURL = NSURL(string: strCellImageURL)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
cell.Stream_Image_Height.constant = CGFloat(Float(cell.pic_height!))
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
cell.stream_image.image = UIImage(data: data!)
} else {
cell.Stream_Image_Height.constant = 0
cell.stream_image.image = nil
}
})
});
task.resume()
}
}
} else {
cell.Stream_Image_Height.constant = 0
}
In my UITableViewCell file I set the image to a default image in case it wasn't done loading the new image but it hasn't worked
class HomePageTVC: UITableViewCell {
#IBOutlet weak var stream_image: UIImageView!
var pic_height: Int?
override func awakeFromNib() {
super.awakeFromNib()
stream_image.image = #imageLiteral(resourceName: "defaultImage")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
any suggestions would be great
You're facing a pretty common cell reuse issue. When you dequeue a cell that was used before, it may already have an image installed in its image view. Set the image to nil before beginning an async download:
if let imageString = stream_image_string[indexPath.row].characters,
!imageString.isEmpty {
if let image = Stream_Cache.object(forKey: imageString) as? UIImage {
cell.stream_image.image = image
} else {
//Clear out any old image left in the recycled image view.
cell.stream_image.image = nil
//Your code to download the image goes here
}
}
Note that there's no need to wrap the cell.stream_image.image = image code in a call to DispatchQueue.main.async(). That code will be run on the main thread.
You do, however, need the second DispatchQueue.main.async() wrapper around the code inside the dataTask's completionHandler, since URLSession's completion handers are called on a background queue by default.
I'm new to gcd and multithreading / concurrency / parallelism. I wrote this example project to test some things out. I have 3 methods, a list of image urls, and an imageView in the interface.
Each method does essentially the same thing, with minor variations.
loadAllImagesSlow() loops through the image urls, creating the NSData from each url, creating a UIImage and setting the imageView's image to that image.
loadAllImagesFast() dispatches that entire task to a global concurrent queue.
newMethod() loops through the image urls and dispatches each task to a global concurrent queue. This was the fastest! :D Exciting stuff. For me anyway.
When I run loadAllImagesFast or newMethod, I see the interface quickly cycle through each image, ending at the last one. But for loadAllImagesSlow, I don't see any images; only the last image appears at the very end.
I believe loadAllImagesSlow is running on the main thread (serial queue), because i don't get responses when I tap the screen (the other functions don't block the UI). But since it's a serial queue, I don't understand why the imageView doesn't display the images one at a time. Since the main queue is serial, everything's supposed to be executed before following code. But that isn't happening with the imageView's image being set.
Any ideas why that might be? I don't need this for a particular project; I'm simply trying to understand the behaviour.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var urlArray = [
"https://img0.ndsstatic.com/wallpapers/76df262f429c799124461286c5ee64b1_large.jpeg",
"https://www.walldevil.com/wallpapers/a74/olivia-wilde-women-actresses-green-eyes-ponytails.jpg",
"https://richestcelebrities.org/wp-content/uploads/2014/11/Olivia-Wilde-Net-Worth.jpg",
"https://cdn.pcwallart.com/images/olivia-wilde-wallpaper-3.jpg",
"https://kingofwallpapers.com/olivia-wilde/olivia-wilde-006.jpg",
"https://img0.ndsstatic.com/wallpapers/b39ca6a58368a76a92d56739d6e2da31_large.jpeg"
]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.view.addGestureRecognizer(tapGesture)
print("starting")
self.loadAllImagesSlow()
}
func handleTap(sender: UITapGestureRecognizer) {
print("tap")
}
func loadAllImagesSlow() { // takes around 4 seconds. UI is unresponsive throughout. Only last image gets shown
let startTime = Date()
for each in self.urlArray {
let url = URL(string: each)
do {
let data = try Data(contentsOf: url!)
self.imageView.image = UIImage(data: data)
} catch {
print("error")
}
}
let endTime = Date()
let executionTime = endTime.timeIntervalSince(startTime)
print("Execution time: \(executionTime)")
}
func loadAllImagesFast() { // takes around 4 seconds, but UI is responsive! Cycles through the images!
DispatchQueue.global(qos: .userInitiated).async {
let startTime = Date()
for each in self.urlArray {
let url = URL(string: each)
do {
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
} catch {
print("error")
}
}
let endTime = Date()
let executionTime = endTime.timeIntervalSince(startTime)
print("Execution time: \(executionTime)")
}
}
func newMethod() { // much much faster than 'loadAllImagesFast'
// This method harnesses the power of the concurrent queue by dispatching lots of smaller tasks that run concurrently, rather than dispatching one large task that runs concorruntly alongside nothing else in that queue; essentially, sequientially, on a seaprate queue.
for each in self.urlArray {
DispatchQueue.global(qos: .userInitiated).async {
let url = URL(string: each)
do {
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
} catch {
print("error")
}
}
}
}
}