Currently I have a collection view of thumbnail images, Upon pressing that thumbnail image cell then it should call a function that would retrieve the Image data via API and shows the full image via the hidden ImageView.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
dataManager.downloadAttachment(id: attachments[indexPath.row].attachmentID)
if dataManager.dataHolder != nil {
attachmentImage.image = UIImage.init(data: dataManager.dataHolder!)
attachmentImage.isHidden = false
}
print(attachments[indexPath.row].attachmentID)
}
and
func downloadAttachment(id:Int) {
let finalUrl = "\(urlAttachment)\(id)/data"
if let url = URL(string: finalUrl){
var request = URLRequest(url: url)
request.setValue(apiKey, forHTTPHeaderField: header)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
return
}
print("Attachment Downloaded")
self.dataHolder = data
}
task.resume()
}
}
The obvious issue with this is that the image wouldn't show on the first attempt since it would still be retrieving the image and the dataHolder would still be nil, But if I tap on the cell twice then the image will be shown correctly.
Is there a simple way to maybe just tap once and make it shows a place holder until finished downloading and update the place holder with an actual image accordingly? Or any other proper way to handle this?
You can use closures to achieve what you asked for. The updated code looks like this.
func downloadAttachment(id:Int,completionHandler completion: #escaping ((Data)->Void)) {
let finalUrl = "\(urlAttachment)\(id)/data"
if let url = URL(string: finalUrl){
var request = URLRequest(url: url)
request.setValue(apiKey, forHTTPHeaderField: header)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
return
}
print("Attachment Downloaded")
self.dataHolder = data
completion()
}
task.resume()
}
}
Now in collectionViewDidSelectItemAt make this changes
func collectionView (_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
dataManager.downloadAttachment(id: attachments[indexPath.row].attachmentID,completionHandler: { data in
if let cell = collectionView.cellForItem(at: indexPath),
let data = dataManager.dataHolder,
let image = UIImage.init(data: data){
attachmentImage.image = image
attachmentImage.isHidden = false
}
})
}
I have UITableView that lists social media posts with images in them.
Once all the post details have loaded and the images cached it looks great but while it loads it often shows the wrong image with the wrong post.
I have been struggling and coming back to this issue for months. I don't think it is a loading issue it almost looks like iOS dumps the image an any old cell until it finds the right one but honestly I'm out of ideas.
Here is my image extension that also takes care of the caching:
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = UIImage(named: "loading")
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//No cache, so create new one and set image
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
}
And this is a shortened version of my UITableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let postImageIndex = postArray [indexPath.row]
let postImageURL = postImageIndex.postImageURL
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
cell.delegate = self
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
cell.postTitle.text = postArray [indexPath.row].postTitle
cell.postDescription.text = postArray [indexPath.row].postBody
return cell
}
FeedItem Class includes prepareForReuse() and looks like this:
override func prepareForReuse() {
super.prepareForReuse()
self.delegate = nil
self.postHeroImage.image = UIImage(named: "loading")
}
EDIT: Here is my method for retrieving data from Firebase:
func retrievePosts () {
let postDB = Database.database().reference().child("MyPosts")
postDB.observe(.childAdded) { (snapshot) in
let snapshotValue = snapshot.value as! Dictionary <String,AnyObject>
let postID = snapshotValue ["ID"]!
let postTitle = snapshotValue ["Title"]!
let postBody = snapshotValue ["Description"]!
let postImageURL = snapshotValue ["TitleImage"]!
let post = Post()
post.postTitle = postTitle as! String
post.postBody = postBody as! String
post.postImageURL = postImageURL as! String
self.configureTableView()
}
}
UITableView only uses a handful of cells (~ the max number of visible cells on screen) when displaying a collection of items, so you'll have more items than cells. This works because of the table view reusing mechanism, which means that the same UITableViewCell instance will be used for displaying different items. The reason why you are having problems with the images is because you aren't handling the cell reusing properly.
In the cellForRowAt function you call:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
While you scroll the table view, in different invocations of cellForRowAt this function will be called for the same cell, but (most probably) displaying the content of different items (because of the cell reusing).
Let's X be the cell you are reusing, then these are roughly the functions that will be called:
1. X.prepareForReuse()
// inside cellForRowAt
2. X.postHeroImage.loadImageUsingCacheWithUrlString(imageA)
// at this point the cell is configured for displaying the content for imageA
// and later you reuse it for displaying the content of imageB
3. X.prepareForReuse()
// inside cellForRowAt
4. X.postHeroImage.loadImageUsingCacheWithUrlString(imageB)
When the images are cached, then you will always have 1, 2, 3 and 4 in that order, that's why you don't see any issues in that case. However, the code that downloads an image and set it to the image view runs in a separate thread, so that order isn't guaranteed anymore. Instead of only the four steps above, you will have something like:
1. X.prepareForReuse()
// inside cellForRowAt
2. X.postHeroImage.loadImageUsingCacheWithUrlString(imageA)
// after download finishes
2.1 X.imageView.image = downloadedImage
// at this point the cell is configured for displaying the content for imageA
// and later you reuse it for displaying the content of imageB
3. X.prepareForReuse()
// inside cellForRowAt
4. X.postHeroImage.loadImageUsingCacheWithUrlString(imageB)
4.1 X.imageView.image = downloadedImage
In this case, because of concurrency, you could end up with the following cases:
1, 2, 2.1, 3, 4, 4.1: Everything is displayed properly (this will happen if you scroll slowly)
1, 2, 3, 2.1, 4, 4.1: In this case the first image finishes downloading after the call to reuse the cell finishes, so the old image will be displayed (wrongly) for a short period of time while the new one is downloaded, and then replaced.
1, 2, 3, 4, 2.1, 4.1: Similar to the case above.
1, 2, 3, 4, 4.1, 2.1: In this case the old image finishes downloading after the new one (there is no guaranty the downloads finish in the same order they started) so you will end up with the wrong image. This is the worst case.
For fixing this problem, let's turn our attention to the problematic piece of code inside the loadImageUsingCacheWithUrlString function:
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
// this is the line corresponding to 2.1 and 4.1 above
self.image = downloadedImage
}
})
}).resume()
As you can see, you are setting self.image = downloadedImage even when you aren't displayed the content associated to that image anymore, so what you need is some way to check if that's still the case. Since you define loadImageUsingCacheWithUrlString in an extension for UIImageView, then you don't have much context there to know whether you should display the image or not. Instead of that, I propose to move that function to an extension of UIImage that will return that image in a completion handler, and then call that function from inside your cell. It would look like:
extension UIImage {
static func loadImageUsingCacheWithUrlString(_ urlString: String, completion: #escaping (UIImage) -> Void) {
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
completion(cachedImage)
}
//No cache, so create new one and set image
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
completion(downloadedImage)
}
})
}).resume()
}
}
class FeedItem: UITableViewCell {
// some other definitions here...
var postImageURL: String? {
didSet {
if let url = postImageURL {
self.image = UIImage(named: "loading")
UIImage.loadImageUsingCacheWithUrlString(url) { image in
// set the image only when we are still displaying the content for the image we finished downloading
if url == postImageURL {
self.imageView.image = image
}
}
}
else {
self.imageView.image = nil
}
}
}
}
// inside cellForRowAt
cell.postImageURL = postImageURL
Another way to deal with this problem will be by using tableView(_:willDisplay:forRowAt:) for loading downloaded images from the cache and tableView(_:didEndDisplaying:forRowAt:) for removing the image from the cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
cell.delegate = self
cell.postTitle.text = postArray [indexPath.row].postTitle
cell.postDescription.text = postArray [indexPath.row].postBody
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let feedCell = cell as! FeedItem
if downloadImages.count > 0 {
cell.postHeroImage.image = downloadImages[indexPath.row]
}
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let feedCell = cell as! FeedItem
cell.postHeroImage.image = nil
}
If you are using CocoaPods, I would strongly recommend using Kingfisher for dealing with image downloads for your project
You need to download and cached the image based on the image URL itself and use it when load the table with that url.
Let's say you have an array of image URLs load the number of rows with this array and download image should be mapped to indexPath and cached. Later you can use it based on the indexPath with array.
The issue which you are facing is not sync row with mapped data in downloaded image. As TableViewCell deque and reuse the cell.
That is because tableView reuses it cells. So one cell could be responsible for multiple images with different urls.
So there is a simple solution for this:
Instead of passing the reference to the reusable cell, you should pass the IndexPath. It's value type and would not reuse.
Then when you have got your image from the async task, you can ask the TableView for the actual cell with .cellForRow(at: indexPath) function.
So, get rid of this line:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
and replace it with a function that takes the actual indexPath and maybe a reference to the tableView.
Watch this WWDC 2018 session for more information. It's about UICollectionView but same as UITableView.
Also you can get the indexPath and the tableView from the cell itself like this answer but make sure you done it BEFORE calling the async function.
you are using in your cellForRowAt function with a reusable cells, although the cell is ever load and unload information, we both know that when a picture is downloading, the downloading is not quick, you need download your images in any function except cellForRowAt. for example
if you have an array of urls
let arrayImages = ["url1", "url2", "url3"]
let downloadImages = [UIImage]()
var dispatchGroup = DispatchGroup()
extension for UIImage
import Foundation
import UIKit
extension UIImage {
func downloaded(from url: URL, completion: ((UIImage,String) -> Void)?) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.global().async() {
completion?(image,url.absoluteString)
}
}.resume()
}
func downloaded(from link: String, completion: ((UIImage,String) -> Void)?) {
guard let url = URL(string: link) else { return }
downloaded(from: url, completion: completion)
}
}
code for your view
override func viewWillAppear()
{
super.viewWillAppear(true)
for url in arrayImages
{
dispatchGroup.enter()
let imageDownloaded = UIImage()
imageDownloaded.downloaded(from: url) { (image, urlImage2) in
DispatchQueue.main.async {
self.downloadImages.append(image)
self.tableView.reloadData()
self.dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
cell.delegate = self
if downloadImages.count > 0 {
cell.postHeroImage.image = downloadImages[indexPath.row]
}
cell.postTitle.text = postArray [indexPath.row].postTitle
cell.postDescription.text = postArray [indexPath.row].postBody
return cell
}
if you have any doubts, please tell me. I will hope that this can help you
This question already has answers here:
Loading/Downloading image from URL on Swift
(39 answers)
Closed 3 years ago.
I am facing trouble getting image from my json url.
this is my json:
"bank_details": [
{
"id": 1,
"logo": "http://mortgagemarket.ae/webApi/public/mortgage_bank_icons/noorebank.png",
"name": abc company
}
]
my swift code to parse the image is this:
import UIKit
class BanksViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
final let BANKS_URL = "http://www.mortgagemarket.ae/webApi/api/manage_interest_rates"
#IBOutlet weak var tableView: UITableView!
var bankicon = [String]()
var bankname = [String]()
var bankid = [Int]()
let stringid: String = ""
override func viewDidLoad() {
super.viewDidLoad()
self.displayFromDb()
tableView.dataSource = self
tableView.delegate = self
}
func displayFromDb()
{
let tokensp = UserDefaults.standard.string(forKey: "tokenKey")
let url = NSURL(string: BANKS_URL+"?token="+tokensp!)
print(url)
URLSession.shared.dataTask(with: (url as?URL)!, completionHandler: {(data,response,error) ->
Void in
if let jsonObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary
{
print(jsonObj.value(forKey: "bank_details")!)
if let messageArray = jsonObj.value(forKey: "bank_details") as? NSArray
{
print(jsonObj.value(forKey: "bank_details")!)
for message in messageArray
{
if let messageDict = message as? NSDictionary
{
if let data = data {
if let bankname = messageDict.value(forKey: "bank_name")
{
self.bankname.append(bankname as! String)
print(bankname)
}
if let banklogo = messageDict.value(forKey: "logo")
{
self.bankicon.append(banklogo as! String)
print(banklogo)
}
if let bankid = messageDict.value(forKey: "id")
{
self.bankid.append(bankid as! Int)
print(bankid)
}
OperationQueue.main.addOperation({
self.tableView.reloadData()
})
}
}
}
}
}
}).resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (bankname.count)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! BanksTableViewCell
cell.bankicon.image = bankicon[indexPath.row] as? UIImage
cell.bankname.text = bankname[indexPath.row]
return (cell)
}
}
now When I run this code it is showing blank table cells. I dont know how to get image from url and display the images in table view cell. Please someone help me.
this is my whole code to get the all the json data into table view cell. Please someone help me
imageicon[indexPath.row] gives a urlStringand not the instance ofUIImage. You need to fetch the image from server using this urlString.
Use URLSession to fetch the image from server like,
if let url = URL(string: imageicon[indexPath.row]) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
DispatchQueue.main.async {
cell.imageicon.image = UIImage(data: data)
}
}
}.resume()
}
Your models should be like this:
/// Your response models
struct BankDetails: Codable {
let bank_details: [ImageUrl]
}
struct ImageUrl: Codable {
let logo: String
}
And then in your cell:
class MyCell: UITableViewCell {
/// create dataTask for cancel in prepareForReuse function
private var dataTask: URLSessionDataTask?
/// Like this
override public func prepareForReuse() {
super.prepareForReuse()
dataTask?.cancel()
}
func populate(with model: YourModel) {
/// You should set url in indexPath of your logo array([ImageUrl])
let url = model.url /// It's sample url for explain this is an url of your current index model
if let imageUrl = url {
downloaded(from: imageUrl)
}
}
func downloaded(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
contentMode = mode
dataTask = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.main.async() {
self.yourImageView.image = image
}
}
dataTask?.resume()
}
}
In your Controller's tableView cellForRowAt function:
let model = models[indexPath.row]
cell.populate(with: model)
return cell
You can use the above models, and create displayFromDb like this:
func displayFromDb() {
let tokensp = UserDefaults.standard.string(forKey: "tokenKey")
let url = NSURL(string: BANKS_URL+"?token="+tokensp!)
if let myUrl = url {
URLSession.shared.dataTask(with: myUrl) { (data, response , error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let data = try decoder.decode(BankDetails.self, from: data)
print("my logo array is: \(data.bank_details)")
// TODO: - So you get urls
} catch let err {
print("Err", err)
}
}.resume()
}
}
I am making a swift app where i am downloading data from API. which gives a JSON.And from there i am putting image url in an imageArray and movieTiteUrl in movieTitleArray. but when i am showing them to collection view they are showing data but that data is not related. To download images i am using AlamofireImage Below code will help you to understand my problem better.
inside ViewDidLoad
var imageUrlArray = [String]()
var imageArray = [UIImage]()
var movieTitleArray = [String]()
UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "movieCell", for: indexPath) as? MovieCell else { return UICollectionViewCell() }
cell.movieName.image = imageArray[indexPath.row]
cell.movieNameLbl.text = movieTitleArray[indexPath.row]
return cell
}
An extension which download data and download images
func downloadImages(handler: #escaping (_ status: Bool)-> ()){
imageArray = []
movieTitleArray = []
for url in imageUrlArray{
Alamofire.request(url).responseImage(completionHandler: { (response) in
guard let image = response.result.value else { return }
self.imageArray.append(image)
if self.imageArray.count == self.imageUrlArray.count {
handler(true)
}
})
}
}
func retriveData(handler : #escaping (_ status: Bool) -> ()){
print(getPopularMovies(pageNumber: 1))
Alamofire.request(getPopularMovies(pageNumber: 1)).responseJSON { (response) in
guard let json = response.result.value as? Dictionary<String, AnyObject> else { return }
let dataDictArray = json["results"] as! [Dictionary<String, AnyObject>]
for data in dataDictArray {
guard let imageUrl = data["poster_path"] as? String else { return }
guard let name = data["original_title"] as? String else { return }
let updatedImageUrl = getFullImageUrl(imageUrl: imageUrl)
self.imageUrlArray.append(updatedImageUrl)
self.movieTitleArray.append(name)
}
handler(true)
}
}
func updateDataToCollectionView(){
retriveData{(finished) in
if finished{
self.downloadImages(handler: { (finishedDownloadingImage) in
if finishedDownloadingImage{
self.movieCollectionView.reloadData()
}
})
}
}
}
I have found two observation ( problems ).
As you are using Async request to download the images, it is not guaranteed that you will get the response in the requested order. for example, if you request movie1Image,movie2Image....it is not guarntee that first you will receive movie1Image and the movie2Image in the same order. So definitely it is not reliable. use Dictionary to solve this. [String:Data] string -> ImageUrl, Data -> ImageData
Why did you wait till all images get downloaded? any specific scenario or any business requirement. Ideally, for a greater User Experience and for interaction, it is not recommended to wait till all the images get downloaded.
I am able to find the response data in array but not able to download and populate in Collection View. I have tried to upload the image from the image container from application but not able to download and upload by API
Code
func get_data_from_url(){
//API calls
let url = NSURL(string: "http://android.eposapi.co.uk/?app_id=A1A2A3A4&app_key=K1K2K3K4&request=gallery")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
let postString = "gallery"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request){
data,response,error in
if error != nil{
print("error=\(error)")
return
}
//Print out response object
print("response= \(response)")
//print response body
// let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
// print("response data = \(responseString!)")
var json: NSArray!
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as? NSArray
print(json)
print(json[0])
} catch {
print(error)
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView!.reloadData()
}
}
task.resume()
override func viewDidLoad() {
super.viewDidLoad()
get_data_from_url()
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as UICollectionViewCell
// Configure the cell
let image = cell.viewWithTag(1) as! UIImageView
image.image = images[indexPath.row]
return cell
}
Tell me any one solution how to download images of anytype (either png or jpg) and populate in the UICollectionView using mutable urlRequest/
I have done exactly this recently, it is a Memory game that fetches images from instagram and displays the images in a UICollectionView. Please checkout the SwiftIntro project on Github
I fetch images using Alamofire:
func prefetchImages(urls: [URLRequestConvertible], done: Closure) {
imageDownloader.downloadImages(URLRequests: urls) {
response in
done()
}
}
This is a "prefetch" solution, then I can retrieve the images using this function:
func imageFromCache(url: NSURL) -> UIImage? {
guard let cache = imageCache else { return nil }
let imageFromCache = cache.imageForRequest(NSURLRequest(URL: url), withAdditionalIdentifier: nil)
return imageFromCache
}
Checkout the ImagePrefetcher class.
The MemoryDataSourceClass which implements the UICollectionViewDataSource and UICollectionViewDelegate protocols returns UICollectionViewCells of type CardCVCell which contains an UIImageView created in its .Xib. I set the image on the UIImageView in this method:
func updateWithModel(model: Model) {
guard let card = model as? CardModel else { return }
guard let cachedImage = ImagePrefetcher.sharedInstance.imageFromCache(card.imageUrl) else { return }
cardFrontImageView.image = cachedImage
flipped = card.flipped
}
Sorry about the ImagePrefetcher.sharedInstance Singleton ;), Singletons are bad (as discussed here and here)! I have not yet set up Dependency Injection using amazing Swinject, but will do so soon! :)
Try another way like NSURLSession to download the data
var url: NSURL = NSURL(string:"http://android.eposapi.co.uk/?app_id=A1A2A3A4&app_key=K1K2K3K4&request=gallery")!
var downloadPhotoTask: NSURLSessionDownloadTask =
NSURLSession.sharedSession().downloadTaskWithURL(url, completionHandler: {(location: NSURL, response: NSURLResponse, error: NSError) -> Void in
var downloadedImage: UIImage = UIImage.imageWithData(NSData.dataWithContentsOfURL(location))
})
downloadPhotoTask.resume()