I have a problem. I have this code in Swift but when i scrolling in the tableview its very laggy and i dont know what the problem is..??
Its downloading image data that is like 20mb each (i think)..
Thanks!
func downloadJsonWithTask() {
let url = NSURL(string: urlString)
var downloadTask = URLRequest(url: (url as? URL)!, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 200)
downloadTask.httpMethod = "GET"
URLSession.shared.dataTask(with: downloadTask, completionHandler: {(data, response, error) -> Void in
let jsonData = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments)
print(jsonData)
}).resume()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
cell.nameLabel.text = nameArray[indexPath.row]
cell.dobLabel.text = dobArray[indexPath.row]
let imgURL = NSURL(string: imgURLArray[indexPath.row])
if imgURL != nil {
let data = NSData(contentsOf: (imgURL as? URL)!)
cell.imgView.image = UIImage(data: data as! Data)
}
return cell
}
///for showing next detailed screen with the downloaded info
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = self.storyboard?.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
vc.imageString = imgURLArray[indexPath.row]
vc.imageString2 = imgURL2Array[indexPath.row]
vc.imageString3 = imgURL3Array[indexPath.row]
vc.imageString4 = imgURL4Array[indexPath.row]
vc.imageString5 = imgURL5Array[indexPath.row]
vc.imageString6 = imgURL6Array[indexPath.row]
vc.nameString = nameArray[indexPath.row]
vc.dobString = dobArray[indexPath.row]
vc.txtString = txtArray[indexPath.row]
vc.contactString = contactArray[indexPath.row]
self.navigationController?.pushViewController(vc, animated: true)
}
The problem because of this line
let data = NSData(contentsOf: (imgURL as? URL)!)
it blocks the main thread , you have to run it in other queue , beside it re downloads the image every scroll ( no cache ) so it's better to use SDWebImage
cell.imgView.sd_setImage(with: URL(string: "http://www.example.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
This will always lag.
Your requesting image in main thread.
Try to use SDWebCache Library : https://github.com/rs/SDWebImage .
1 : It will cachce intially image and will avoid multiple HTTP requests.
2 : Works in background so will not affect the main thread.
Use method imageview.sd_imageFromUrl(URL(....)!)
It's very clear. You asking the Image data from main thread. Whenever you perform timeconsuming operation on the Main Thread, then the App will be laggy.
let data = NSData(contentsOf: (imgURL as? URL)!)
The best solution that worked for me so far is using AlamofireImage or Kingfisher. That needs you to learn about adding library into your project. Try to use Cocoapods for convenience.
I assume you were able to use Cocoapods here.
In case you want to caching support, Kingfisher comes first. Caching means you will need to make Request to server only for the first time or if something changed. It will save your resources etc. All you need to do is importing Kingfisher module in ViewController then it automatically extend UIImageView capability with Kingfisher extension kf
Here is the sample code
//ViewController.swift
import Kingfisher
// Your cellForRowAt
...
if let url = URL(string: imgURLArray[indexPath.row]) {
cell.imgView.kf.setImage(with: url)
}
...
Kingfisher will perform caching strategy under the hood so you can focus on the app.
In order to put default image which will be very usefull to inform user about the image, (user expects there is something in blank white box -- UIImageView while image is nil -- in their screen isn't?) Kingfisher helps you with placeholder
let defaultImage = UIImage(named: "default_img")!
cell.imgView.kf.setImage(with: url, placeholder: defaultImage)
Get the complete insight about Kingfisher here Kingfisher's Github Page
Related
I have a TableView with ImageViews inside each cell. I want the images to get loaded once and remain like that but it seems that the images get loaded (downloaded, I'm getting them from an external API) as they get into visible area for user. It seems like a lazy load or something like that and I would like to disable it because if I scroll down then come back up most of the images get misplaced.
TableViewController.swift
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)
BusinessLayer.swift
func getChampionThumbnailImage (championId: Int) -> UIImage {
return dataLayerRiot.getChampionThumbnailImage(championId: championId)
}
DataLayerRiot.swift
func getChampionThumbnailImage (championId: Int) -> UIImage {
var image: UIImage!
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
session.dataTask(with: url!) {(data, response, error) in
if error != nil {
print("ERROR")
semaphore.signal()
}
else {
image = UIImage(data: data!)!
semaphore.signal()
}
}.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
return image
}
Anyone know how to disable them loading as they get into visible area for the user and just have them "stored"?
EDIT
I am dequeuing the cell using the default way
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Match", for: indexPath) as? TableViewCell
...
}
Is there a better way of doing it?
EDIT 2
I also need to specify that I am unable to install libraries because this is a university project and I am able to work only on university's MACs (because I don't own one) therefore I am unable to install packages without administrator privileges.
You should save a task at memory like:
let task = = session.dataTask() {}
And after you can cancel it anywhere by:
task.cancel()
Alternatively, if the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
Try SDWebImage for lazy loading the images in the UITableViewCell or UICollectionViewCell. Install it through cocoapods into your project.
It is an asynchronous memory + disk image caching with automatic cache expiration handling.
https://github.com/SDWebImage/SDWebImage
Code:
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
cell?.mainChampImageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder.png"))
It sounds like you could benefit from doing some image caching. There are multiple ways to go about doing so, but from your example, it doesn't look like you need to go through the trouble of adding an entire library to do so. You can do it in a simple manner using NSCache.
I created a class called ImageCache, and in this case it is a singleton, so that the cache is accessible throughout the entire application.
import UIKit
class ImageCache: NSObject {
static let sharedImageCache = ImageCache()
// Initialize cache, specifying that your key type is AnyObject
// and your value type is AnyObject. This is because NSCache requires
// class types, not value types so we can't use <URL, UIImage>
let imageCache = NSCache<AnyObject, AnyObject>()
// Here we store the image, with the url as the key
func add(image: UIImage, for url: URL) {
// we cast url as AnyObject because URL is not a class type, it's a value type
imageCache.setObject(image, forKey: url as AnyObject)
}
// This allows us to access the image from cache with the URL as the key
// (e.g. cache[URL])
func fetchImage(for url: URL) -> UIImage? {
var image: UIImage?
// Casting url for the same reason as before, but we also want the result
// as an image, so we cast that as well
image = imageCache.object(forKey: url as AnyObject) as? UIImage
return image
}
}
So now we have some relatively simple caching in place. Now for how to use it:
func getChampionThumbnailImage (championId: Int) -> UIImage {
var image: UIImage!
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
// Before, downloading the image, we check the cache to see if it exists and is stored.
// If so, we can grab that image from the cache and avoid downloading it again.
if let cachedImage = ImageCache.sharedImageCache.fetchImage(for: url) {
image = cachedImage
return image
}
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
session.dataTask(with: url!) {(data, response, error) in
if error != nil {
print("ERROR")
semaphore.signal()
}
else {
image = UIImage(data: data!)!
// Once the image is successfully downloaded the first time, add it to
// the cache for later retrieval
ImageCache.sharedImageCache.add(image: image, for: url!)
semaphore.signal()
}
}.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
return image
}
The reason the images are re-downloading is because a table view doesn't have unlimited cells. What happens is, as you scroll down, the cells that go off the screen are then recycled and re-used, so when you scroll back up, the images have to be grabbed again because they've been emptied out.
You can avoid downloading the images again by implementing caching.
Another way you can avoid having incorrect images is setting your image view to nil before you re-download the image. For example:
cell?.mainChampImageView = nil
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)
All of the above, along with making sure that you are dequeuing cells properly should address your issue.
I have an image in tableview that is downloaded from a Json, everything works perfect but when scrolling before seeing the corresponding image it loads another for a few seconds (these images are those that are already visible in the table).
The structure of my data is:
struct Data: Decodable {
let name: String
let img: String
let phone: String
let linktaller: String
let web: String
}
The code of my cell where the image is loaded is:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? AseguradorasTableViewCell else { return UITableViewCell() }
cell.titleLbl.text = company[indexPath.row].name
.
.
.
// load image
if let imageURL = URL(string: company[indexPath.row].img) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: imageURL)
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.myImage.image = image
}
}
}
}
return cell
}
The function to load the data is:
func downloadJSON() {
let url = URL(string: "http://myserver.com/data.json")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error == nil {
do {
self.company = try JSONDecoder().decode([Data].self, from: data!)
print(self.company)
DispatchQueue.main.async {
self.tableView.reloadData()
self.refreshControl.endRefreshing()
}
} catch let jsonError{
print("error + \(jsonError)")
}
}
}.resume()
}
See image for more detail:
Any suggestions are welcome to fix this problem.
In UITableView dequeueReusableCell- Each UITableViewCell will be reused several times with different data(image).
In your case, every cellForRowAt is called, the image will be load from server so it will have delay.
Solution: You must to cache image with url in local app when the image load finish.
(1)- Use SDWebImage - with cache support
(2)- You can save image in a array -> in cellForRowAt load from this array if existed and load from server if does not exist
(image from internet)
Add the following class for cache image support:
class ImageLoader {
var cache = NSCache<AnyObject, AnyObject>()
class var sharedInstance : ImageLoader {
struct Static {
static let instance : ImageLoader = ImageLoader()
}
return Static.instance
}
func imageForUrl(urlString: String, completionHandler:#escaping (_ image: UIImage?, _ url: String) -> ()) {
let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData
if let imageData = data {
let image = UIImage(data: imageData as Data)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
return
}
let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
if error == nil {
if data != nil {
let image = UIImage.init(data: data!)
self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
}
} else {
completionHandler(nil, urlString)
}
}
downloadTask.resume()
}
}
In the cell, load the image as follows:
// Load image
let fimage = company[indexPath.row].img
ImageLoader.sharedInstance.imageForUrl(urlString: fimage, completionHandler: { (image, url) in
if image != nil {
cell.myImage.image = image
}
})
With that, the download of the images should work correctly
Because of when ever the cell is showing, you download the image from internet by
let data = try? Data(contentsOf: imageURL)
You should
Check if image in imageURL has cached or not
If cached, load image from local
If not cache, download it from internet, then cache it.
Or just simple using SDWebImage or anything else, it will auto check the step 1 to 3 for you :D
For example by using SDWebImage
import SDWebImage
imageView.sd_setImage(with: URL(string: "your_image_url"))
This is a classic cell reuse problem. You should install a placeholder image, or nil, into the image view of each cell in your tableView(cellForRowAt:) method before you begin the download. That will clear out the previous image that was installed into the cell, and then the async download can run in the background and install the image once it's done loading.
To resolve similar issues, I changed my code to coordinate the downloading of images with the creation of tableView cells, storing the images in a local array.
I create a dictionary array to hold the downloaded images, using the url string as the key:
imagesArray = [String:UIImage]()
Then, at the point in the code where each image completes downloading, I add the image to the array and insert one new row into the tableView:
imagesArray.updateValue(UIImage(data: data!)!, forKey: imageURL as! String)
tableView.beginUpdates()
tableView.insertRows(at:[IndexPath(row: imagesArray.count-1, section: 0)], with: .automatic)
tableView.endUpdates()
tableView.reloadData()
I also maintain a separate array of information elements for each image, including the image url string as one element. This allows me to present the correct items in the tableView cell:
cell.itemNameLabel.text = itemRecords[indexPath.row].itemName
cell.itemImage.image = imagesArray[itemRecords[indexPath.row].imageURL]
While the images are downloading, I present a progress indicator spinner.
Once the images are all downloaded and are loaded into the imagesArray, there is NO delay in presenting as the user scrolls up and down to view the listed cells, and reused cells are loaded with the correct images.
Note: Please no libraries. This is important for me to learn. Also, there are a variety of answers on this but none that I found solves the issue nicely. Please don't mark as duplicate. Thanks in advance!
The problem I have is that if you scroll really fast in the table, you will see old images and flickering.
The solution from the questions I read is to cancel the URLSession
data request. But I do not know how to do that at the correct place
and time. There might be other solutions but not sure.
This is what I have so far:
Image cache class
class Cache {
static let shared = Cache()
private let cache = NSCache<NSString, UIImage>()
var task = URLSessionDataTask()
var session = URLSession.shared
func imageFor(url: URL, completionHandler: #escaping (image: Image? error: Error?) -> Void) {
if let imageInCache = self.cache.object(forKey: url.absoluteString as NSString) {
completionHandler(image: imageInCache, error: nil)
return
}
self.task = self.session.dataTask(with: url) { data, response, error in
if let error = error {
completionHandler(image: nil, error: Error)
return
}
let image = UIImage(data: data!)
self.cache.setObject(image, forKey: url.absoluteString as NSString)
completionHandler(image: image, error: nil)
}
self.task.resume()
}
}
Usage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let myImage = images[indexPath.row]
if let imageURL = URL(string: myImage.urlString) {
photoImageView.setImage(from: imageURL)
}
return cell
}
Any thoughts?
Swift 3:
Flickering can be avoided by this way:
Use the following code in public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
cell.photoImageView.image = nil //or keep any placeholder here
cell.tag = indexPath.row
let task = URLSession.shared.dataTask(with: imageURL!) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() {
if cell.tag == indexPath.row{
cell.photoImageView.image = UIImage(data: data)
}
}
}
task.resume()
By checking cell.tag == indexPath.row, we are assuring that the imageview whose image we are changing, is the same row for which the image is meant to be. Hope it helps!
A couple of issues:
One possible source of flickering is that while you're updating the image asynchronously, you really want to clear the image view first, so you don't see images for prior row of reused/dequeued table view cell. Make sure to set the image view's image to nil before initiating the asynchronous image retrieval. Or, perhaps combine that with "placeholder" logic that you'll see in lots of UIImageView sync image retrieval categories.
For example:
extension UIImageView {
func setImage(from url: URL, placeholder: UIImage? = nil) {
image = placeholder // use placeholder (or if `nil`, remove any old image, before initiating asynchronous retrieval
ImageCache.shared.image(for: url) { [weak self] result in
switch result {
case .success(let image):
self?.image = image
case .failure:
break
}
}
}
}
The other issue is that if you scroll very quickly, the reused image view may have an old image retrieval request still in progress. You really should, when you call your UIImageView category's async retrieval method, you should cancel and prior request associated with that cell.
The trick here is that if you're doing this in a UIImageView extension, you can't just create new stored property to keep track of the old request. So you'd often use "associated values" to keep track of prior requests.
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 load some pictures from different URL in order to add them to my Table View Cells. Each cell contains an UIImageView and the pictures (UIImage) are not loading correctly.
Sometimes the URL connection fails, and sometimes not. I'm going crazy,
help me please!!
This is the part of my code that tries to download a picture from an specific URL:
let imageURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/Angels_Stadium.JPG/1920px-Angels_Stadium.JPG")!
let task = URLSession.shared.dataTask(with: imageURL as URL) { (data, response, error) in
guard error == nil, let data = data else { return }
let downloadedImage = UIImage(data: data)
self.foto = downloadedImage!
tableWiew.reloadData()
}
task.resume()
This code is inside the init function of my class (User). After instantiate the class, I try to add the picture to my tableview in other class like this:
let user:User = User(json: obj, tableWiew: self.tableView)
addCell(cell: cell, name: user.nombre, job: user.puesto, nIdeas: "0", mProp: "0", image: user.foto)
The "addCell" method just creates a custom cell and inserts it into my tableView. That's working fine except for the damn picture.
If I add a picture from my Assets it works fine, but I don't know what's wrong when I try to add a picture from an URL.
Please download an image inside cellForRowAt method of UITableViewDataSource and if possible use SDWebImage that will help you with many options eg placeholder. It handles the caching internally and it is async.
let imageURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/Angels_Stadium.JPG/1920px-Angels_Stadium.JPG")!
let request = URLRequest(url: imageURL as URL)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
// print("Task completed")
// print(response)
DispatchQueue.main.async(execute: {
self.imgVW.image = UIImage(data: data!)
})
})
task.resume()