URLCache (CS193P Assignment 6) - swift

I'm now on Stanford iOS Swift Assignment 6 where one of the required tasks is to use URLCache to cache the image on the local disk. After days of googling, I still couldn't figure out how to use it. It'd be helpful if anyone could point me to a good guide!
My code is like this now after trying to understand the official documentation. It doesn't help that the official doc doesn't have sample codes I could refer to :(
let urlCache = URLCache.shared
The required task is to set a cache and specify the size limit. I tried initialising URLCache and pass the size in the parameters. It works but storing and getting the cache doesn't seem to work. If URLCache is initialised every time the app (or view controller) is launched, wouldn't it ignore the previous cache that was created and stored?
I think there's no issue with this code? reading data from cache
if let cachedURLResponse = urlCache.cachedResponse(for: URLRequest(url: url)) {
if let fetchedImage = UIImage(data: cachedURLResponse.data) {
image = fetchedImage
}
}
I'm lost on writing data to cache
urlCache.storeCachedResponse(CachedURLResponse(response: URLResponse(), data: imageData), for: URLRequest(url: url))
How to initialise URLResponse properly? I looked at the init method and it also requires url to be passed in as parameter. Found this strange since url is in URLRequest() too. Am I doing it wrong?
Helpful advice much appreciated!

You can use URLCache by making a request for the image data with URLSession then using the data and response available in its completion handler, for example:
import UIKit
class GalleryCollectionViewController: UICollectionViewController, UICollectionViewDragDelegate, UICollectionViewDropDelegate, UICollectionViewDelegateFlowLayout {
// MARK: - Model
var gallery: Gallery?
// MARK: - Properties
private var cache = URLCache.shared
private var session = URLSession(configuration: .default)
override func viewDidLoad() {
super.viewDidLoad()
cache = URLCache(memoryCapacity: 100, diskCapacity: 100, diskPath: nil) // replace capacities with your own values
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GalleryCell", for: indexPath)
if let galleryCell = cell as? GalleryCollectionViewCell {
galleryCell.image = nil
galleryCell.imageView.isHidden = true
if let url = gallery?.images[indexPath.item].url {
let request = URLRequest(url: url.imageURL) // imageURL from Utilities.swift of Stanford iOS course
if let cachedResponse = cache.cachedResponse(for: request), let image = UIImage(data: cachedResponse.data) {
galleryCell.image = image
galleryCell.imageView.isHidden = false
} else {
DispatchQueue.global(qos: .userInitiated).async { [weak self, weak galleryCell] in
let task = self?.session.dataTask(with: request) { (urlData, urlResponse, urlError) in
DispatchQueue.main.async {
if urlError != nil { print("Data request failed with error \(urlError!)") }
if let data = urlData, let image = UIImage(data: data) {
if let response = urlResponse {
self?.cache.storeCachedResponse(CachedURLResponse(response: response, data: data), for: request)
}
galleryCell?.image = image
} else {
galleryCell?.image = UIImage(named: "placeholder")
}
galleryCell?.imageView.isHidden = false
}
}
task?.resume()
}
}
}
}
return cell
}
}

Related

Showing UIImageView after finished loading

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

Why URL not working correctly when tried to turn image?

I have question about firebase. I have image's URL in my database and I show them in collectionView cells but in this point I have some problem which is some image not loaded correctly their URLs are different but images are same. I tried lots of things but I can't solve it. My URLs starting with 'https' and App Transport Security Setting, Allow Arbitrary Loads = YES. So these are not solved my problem. Here my code which are firebase and adding this URL to imageViews. Please help me! Thanks!
func firebaseCon() {
let ref = Database.database().reference().child("cells")
ref.observe(.childAdded) { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
let dataCon = ItemCellImage()
dataCon.itemImageName = dict["itemimagename"] as? String
dataCon.itemTitleLabel = dict["itemimagelabel"] as? String
//print(dataCon.itemImageName, dataCon.itemTitleLabel)
self.itemler.append(dataCon)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
}
}
And here from data to image:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! itemsCell
let dataGelen = itemler[indexPath.row]
cell.titleLabel.text = dataGelen.itemTitleLabel
if let cellDataImage = dataGelen.itemImageName {
let url = URL(string: cellDataImage)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
cell.itemsimageView.image = UIImage(data: data!)
self.imageDeneme = UIImage(data: data!)!
}
}.resume()
}
return cell
}
When I tried to change image url from firebase some of them working correctly and some of them show just previous image. Is there any way to show exactly correct image in every change?
For this kind of problem due to Reusing concept you can use prepareForReuse() function provide by cells.
override func prepareForReuse() {
yourImageView.image = nil //Or any placeholder image
}
This function is called when OS is about to reuse your cell, thus you can use it as a kind of reset to your cell appearance.

Why does it take so long for the images to load into my tableView if the image files being downloaded have a maximum size of 30kb? [duplicate]

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 4 program showing data after downloading images but data is not related

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.

Downloading Image from Api and populate Image in CollectionViewController using Swift

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