I am loading images into a UICollectionView with Swift. I am getting the images asynchronously. While the UI is scrolling and not locking up, there are still some problems. I am getting some repeats, and they are loading into the view fairly slow. I created a video to show exactly the problem:
https://www.youtube.com/watch?v=nQmAXpbrv1w&feature=youtu.be
My code takes mostly from the accepted answer to this question: Loading/Downloading image from URL on Swift.
Here is my exact code.
Below my UICollectionViewController class declaration, I declare a variable:
let API = APIGet()
In the viewDidLoad of the UICollectionViewController, I call:
override func viewDidLoad() {
API.getRequest()
API.delegate = self
}
In my APIGet class, I have the function, getRequest:
func getRequest() {
let string = "https://api.imgur.com/3/gallery/t/cats"
let url = NSURL(string: string)
let request = NSMutableURLRequest(URL: url!)
request.setValue("Client-ID \(cliendID)", forHTTPHeaderField: "Authorization")
let session = NSURLSession.sharedSession()
let tache = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if let antwort = response as? NSHTTPURLResponse {
let code = antwort.statusCode
print(code)
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let data = json["data"] as? NSDictionary {
if let items = data["items"] as? NSArray {
var x = 0
while x < items.count {
if let url2 = items[x]["link"] {
self.urls.append(url2! as! String)
}
x++
}
self.delegate?.retrievedUrls(self.urls)
}
}
})
}
catch {
}
}
}
tache.resume()
}
The above function does exactly what it should: It gets the JSON from the endpoint, and gets the URLs of the images from the JSON. Once all the URLs are retrieved, it passes them back to the UICollectionViewController.
Now, for my code in the UICOllectionViewController. This is the function that receives the URLs from the APIGet class.:
func retrievedUrls(x: [String]) {
//
self.URLs = x
var y = 0
while y < self.URLs.count {
if let checkedURL = NSURL(string: self.URLs[y]) {
z = y
downloadImage(checkedURL)
}
y++
}
}
Then, these 2 functions take care of the downloading of the image:
func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
completion(data: data, response: response, error: error)
}.resume()
}
func downloadImage(url: NSURL){
getDataFromUrl(url) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
if let image = UIImage(data: data) {
self.images.append(image)
self.collectionView?.reloadData()
}
}
}
}
When I call self.collectionView.reloadData() after the image is loaded, then every time a new image is loaded, the collectionView flashes and the cells shift around. Yet, if I don't call self.collectionView.reloadData() at all, then the collectionView never reloads after the images are loaded, and the cells remain empty. How can I get around this? I want the images to just load right into their respective cells, and not have the collectionView blink/flash or have cells shift around, or get temporary duplicates.
My cellForRowAtIndexPath is simply:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell1", forIndexPath: indexPath) as! cell1
if indexPath.row > (images.count - 1) {
cell.image.backgroundColor = UIColor.grayColor()
}
else {
cell.image.image = images[indexPath.row]
}
return cell
}
This is my first foray into networking in iOS, so I would prefer not to default to a library (alamofire, AFNetowrking, SDWebImage, etc), so I can learn how to do it from the ground up. Thanks for reading all this!
EDIT: Here is my final code, taken mostly from the answer below:
func retrievedUrls(x: [String]) {
//
self.URLs = x
self.collectionView?.reloadData()
var y = 0
while y < self.URLs.count {
if let checkedURL = NSURL(string: self.URLs[y]) {
downloadImage(checkedURL, completion: { (image) -> Void in
})
}
y++
}
}
func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
completion(data: data, response: response, error: error)
}.resume()
}
func downloadImage(url: NSURL, completion: (UIImage) -> Void) {
getDataFromUrl(url) { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
if let image = UIImage(data: data) {
self.images.append(image)
let index = self.images.indexOf(image)
let indexPath = NSIndexPath(forRow: index!, inSection: 0)
self.collectionView?.reloadItemsAtIndexPaths([indexPath])
}
}
}
}
My cellForItemAtIndexPath didn't change.
I'm not sure how familiar you are with closures and the like, but you are using them as part of NSURLSession so I'll assume a passing familiarity.
In your function downloadImage(_:) you could rewrite it instead to have the signature
func downloadImage(url: NSURL, completion: (UIImage) -> Void)
Which allows you to at-time-of-calling decide what else to do with that image. downloadImage would be modified as:
func downloadImage(url: NSURL, completion: (UIImage) -> Void) {
getDataFromUrl(url) { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
if let image = UIImage(data: data) {
self.images.append(image)
completion(image)
}
}
}
}
All that we changed, is we pass our image into the newly defined completion block.
Now, why is this useful? retreivedUrls(_:) can be modified in one of two ways: either to assign the image to the cell directly, or to reload the cell at the corresponding index (whichever is easier; assigning directly is guaranteed not to cause flicker, though in my experience, reloading a single cell doesn't either).
func retrievedUrls(x: [String]) {
//
self.URLs = x
var y = 0
while y < self.URLs.count {
if let checkedURL = NSURL(string: self.URLs[y]) {
z = y
downloadImage(checkedURL, completion: { (image) -> Void in
// either assign directly:
let indexPath = NSIndexPath(forRow: y, inSection: 0)
let cell = collectionView.cellForItemAtIndexPath(indexPath) as? YourCellType
cell?.image.image = image
// or reload the index
let indexPath = NSIndexPath(forRow: y, inSection: 0)
collectionView?.reloadItemsAtIndexPaths([indexPath])
})
}
y++
}
}
Note that we use the completion block, which will only be called after the image has been downloaded and appended to the images array (same time that collectionView?.reloadData() used to be called).
However! Of note in your implementation, you are appending images to the array, but may not be maintaining the same ordering as your URLs! If some images are not downloaded in the exactly same order as that in which you pass in URLs (which is quite likely, since it's all async), they will get mixed up. I don't know what your application does, but presumably you will want images to be assigned to cells in the same order as the URLs you were given. My suggestion is to change your images array to be of type Array<UIImage?>, and filling it with the same amount of nils as you have URLs, and then modifying cellForItemAtIndexPath to check if the image is nil: if nil, assign the UIColor.grayColor(), but if it's non-nil, assign it to the cell's image. This way you maintain ordering. It's not a perfect solution, but it requires the least modification on your end.
Related
I have a table view that loads images from s3 bucket and set some data with the images in my cell.
I call my cell at
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellDish:DishTableViewCell = tableView.dequeueReusableCell(withIdentifier: "DishCell", for: indexPath) as! DishTableViewCell
cellDish.setDish(dish: brain.listOfDishes[indexPath.row])
return cellDish }
in my tableviewcell I have a func called setDish :
func setDish (dish: Dish)
{
var StringPrice = dish.DishPrice
StringPrice.append("$")
self.la_name.text = dish.DishName
self.la_price.text = StringPrice
self.la_des.text = dish.DishDes
self.downloadData(dish: dish, completion: { success in
guard success else { return }
DispatchQueue.main.async {
self.dish_image.image = UIImage(data: dish.DishData!)!
}
})
}
func downloadData(dish:Dish,completion: #escaping (Bool) -> Void) {
let transferUtility = AWSS3TransferUtility.default()
let expression = AWSS3TransferUtilityDownloadExpression()
let s3Bucket = "<my bucket name>"
transferUtility.downloadData(fromBucket: s3Bucket, key: dish.DishImage, expression: expression) {(task, url, data, error) in
if error != nil {print(error)
completion(false)
}
else {
dish.DishData = data!
}
completion(true)
}
}
I want it to show me the dish data without the image until the image is downloaded and then show it to me as well (I want it to be not on the main thread of course ).
I'm not sure why but right now all the cells download their images and only then everything loads up together.
Swift 3 Updated Code :
Load url asynchronous will update automatically
extension UIImageView
{
public func imageFromServerURL(urlString: String)
{
URLSession.shared.dataTask(with: NSURL(string: urlString)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error)
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
self.image = image
})
}).resume()
}}
Swift 2.2 Code :
extension UIImageView {
public func imageFromServerURL(urlString: String) {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: urlString)!, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error)
return
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let image = UIImage(data: data!)
self.image = image
})
}).resume()
}}
I am very new to swift and need some help with fetching images from URLs and storing them into a dictionary to reference into a UITableView. I've checked out the various threads, but can't find a scenario which meets by specific need.
I currently have the names of products in a dictionary as a key with the image URLs linked to each name:
let productLibrary = ["Product name 1":"http://www.website.com/image1.jpg",
"Product name 2":"http://www.website.com/image2.jpg"]
I would need to get the actual images into a dictionary with the same product name as a key to add to the UITableView.
I currently have the images loading directly in the tableView cellForRowAt function, using the following code, but this makes the table view unresponsive due to it loading the images each time the TableView refreshes:
cell.imageView?.image = UIImage(data: try! Data(contentsOf: URL(string:
productLibrary[mainPlaces[indexPath.row]]!)!))
mainPlaces is an array of a selection of the products listed in the productLibrary dictionary. Loading the images initially up-front in a dictionary would surely decrease load time and make the UITableView as responsive as I need it to be.
Any assistance would be greatly appreciated!
#Samarth, I have implemented your code as suggested below (just copied the extension straight into the root of the ViewController.swift file above class ViewController.
The rest, I have pasted below the class ViewController class as below, but it's still not actually displaying the images in the tableview.
I've tried to do exactly as you've advised, but perhaps I'm missing something obvious. Sorry for the many responses but I just can't seem to get it working. Please see my exact code below:
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.textLabel?.text = mainPlaces[indexPath.row]
downloadImage(url: URL(string: productLibrary[mainPlaces[indexPath.row]]!)!)
cell.imageView?.downloadedFrom(link: productLibrary[mainPlaces[indexPath.row]]!)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "ProductSelect", sender: nil)
globalURL = url[mainPlaces[indexPath.row]]!
}
func getDataFromUrl(url: URL, completion: #escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void) {
URLSession.shared.dataTask(with: url) {
(data, response, error) in
completion(data, response, error)
}.resume()
}
func downloadImage(url: URL) {
print("Download Started")
getDataFromUrl(url: url) { (data, response, error) in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
DispatchQueue.main.async() { () -> Void in
// self.imageView.image = UIImage(data: data)
/* If you want to load the image in a table view cell then you have to define the table view cell over here and then set the image on that cell */
// Define you table view cell over here and then write
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.imageView?.image = UIImage(data: data)
}
}
}
You can load the images synchronously or asynchronously in your project.
Synchronous: means that your data is being loaded on the main thread, so till the time your data is being loaded, your main thread (UI Thread) will be blocked. This is what is happening in your project
Asynchronous: means your data is being loaded on a different thread other than UI thread, so that UI is not being blocked and your data loading is done in the background.
Try this example to load the image asynchronously :
Asynchronously:
Create a method with a completion handler to get the image data from your url
func getDataFromUrl(url: URL, completion: #escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void) {
URLSession.shared.dataTask(with: url) {
(data, response, error) in
completion(data, response, error)
}.resume()
}
Create a method to download the image (start the task)
func downloadImage(url: URL) {
print("Download Started")
getDataFromUrl(url: url) { (data, response, error) in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
DispatchQueue.main.async() { () -> Void in
// self.imageView.image = UIImage(data: data)
/* If you want to load the image in a table view cell then you have to define the table view cell over here and then set the image on that cell */
// Define you table view cell over here and then write
// cell.imageView?.image = UIImage(data: data)
}
}
}
Usage:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print("Begin of code")
if let checkedUrl = URL(string: "your image url") {
imageView.contentMode = .scaleAspectFit
downloadImage(url: checkedUrl)
}
print("End of code. The image will continue downloading in the background and it will be loaded when it ends.")
}
Extension:
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
contentMode = mode
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() { () -> Void in
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
Usage:
imageView.downloadedFrom(link: "image url")
For your question
Do this while you are loading the images in your table view cell:
cell.imageView?.downloadedFrom(link: productLibrary[mainPlaces[indexPath.row]]! )
I managed to get this right using a dataTask method with a for loop in the viewDidLoad method, which then updated a global dictionary so it didn't have to repopulate when I switched viewControllers, and because it isn't in the tableView method, the table remains responsive while the images load. The url's remains stored as a dictionary linked to the products, and the dictionary then gets populated with the actual UIImage as a dictionary with the product name as the key. Code as follows:
if images.count == 0 {
for product in productLibrary {
let picurl = URL(string: product.value)
let request = NSMutableURLRequest(url: picurl!)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print(error)
} else {
if let data = data {
if let tempImage = UIImage(data: data) {
images.updateValue(tempImage, forKey: product.key)
}
}
}
}
task.resume()
}
}
I hope this helps, as this is exactly what I was hoping to achieve when I asked this question and is simple to implement.
Thanks to everyone who contributed.
So I am making a network request. I parse the JSON to custom Objects. In these objects there are urls which are suppose to bring back images. One of the URL returns an error message (404) so there ins't anything there! How can I set a default image in its place and stop my app from crashing? Here is my code! Thanks
import UIKit
class HomepageCollectionViewController: UICollectionViewController {
var imageCache = NSCache()
var hingeImagesArray = [HingeImage]()
var arrayToHoldConvertedUrlToUIImages = [UIImage]()
var task: NSURLSessionDataTask?
override func viewDidLoad() {
super.viewDidLoad()
// Makes the network call for HingeImages
refreshItems()
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return hingeImagesArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("imageReuseCell", forIndexPath: indexPath) as! ImageCollectionViewCell
let image = hingeImagesArray[indexPath.row]
if let imageURL = image.imageUrl {
if let url = NSURL(string: imageURL) {
//settingImageTpChache
if let myImage = imageCache.objectForKey(image.imageUrl!) as? UIImage {
cell.collectionViewImage.image = myImage
}else {
// Request images asynchronously so the collection view does not slow down/lag
self.task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
// Check if there is data returned
guard let data = data else {
return
}
// Create an image object from our data and assign it to cell
if let hingeImage = UIImage(data: data){
//cachedImage
self.imageCache.setObject(hingeImage, forKey: image.imageUrl!)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.collectionViewImage.image = hingeImage
//append converted Images to array so we can send them over to next view - only proble in that the only images converted at the ones you scrool to which is retarted
self.arrayToHoldConvertedUrlToUIImages.append(hingeImage)
print(self.arrayToHoldConvertedUrlToUIImages)
})
}
})
task?.resume()
}
}
}
return cell
}
you can check if error is not nil then set deafult image .
self.task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
if error != nil {
cell.collectionViewImage.image = UIImage(named:"default_image")
return
}
...
Try this:
let imageCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrl(urlString: String) {
self.image = nil
// check for cache
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
// download image from url
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) -> Void in
var image:UIImage
if error == nil {
if(UIImage(data: data!) != nil){
image = UIImage(data: data!)!
} else {
image = UIImage(named: "DefaultImage")!
}
} else {
print(error ?? "load image error")
return
}
DispatchQueue.main.async(execute: { () -> Void in
imageCache.setObject(image, forKey: urlString as AnyObject)
self.image = image
})
}).resume()
}
}
The key point is with 404 return message, data task error is still = nil and this time you must check UIImage(data: data!) != nil to prevent a “fatal error: unexpectedly found nil while unwrapping an Optional value”
In my app I'm using UICollectionView and I've decided to use it as in the code below:
class UserList: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var tview: UICollectionView!
let reuseIdentifier = "cell"
var items = NSMutableArray()
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = tview.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
let user:SingleUser = self.items[indexPath.item] as! SingleUser
cell.username.text = user.name
if let checkedUrl = NSURL(string: user.photo) {
cell.userImg.contentMode = .ScaleAspectFit
getDataFromUrl(checkedUrl) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
print(response?.suggestedFilename ?? "")
cell.userImg.image = UIImage(data: data)
}
}
}
return cell
}
So I have a cell with a UILabel and UIImage and for each object fetched from json I parse it and I assign user's data to that cell.
When I load the window I see usernames with photos of them, but when I start scrolling through the collection view the photos of users change and users get different photos than they should have.
I've read it might be something related to cell reusing (so far I know nothing about it), I just assumed it should be loaded once (when user opens this panel) and then left as it is. However it seems like this data is "fetched" each time user scrolls that list.
So how can I exactly fix my problem and be sure each time user scrolls the list - the data will be correct?
One more thing that might be useful here - method getDataFromUrl that fetches photos from urls:
func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
completion(data: data, response: response, error: error)
}.resume()
}
What Eugene said is correct, but if you don't want to change the code you have already too much. You can add an optional UIImage property on SingleUser, call it "image," then you can say something like:
if user.image == nil{
getDataFromUrl(checkedUrl) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
print(response?.suggestedFilename ?? "")
let image = UIImage(data: data)
user.image = image
//Check if this cell still has the same indexPath, else it has been dequeued, and image shouldn't be updated
if collectionView.indexPathForCell(cell) == indexPath
{
cell.userImg.image = image
}
}
}
}else{
cell.userImg.image = user.image!
}
}
I have a bucket in my AWS S3 backend full of images that I want to load onto my UICollectionViewCell's. What method should I implement to do so? I'm looking for the most efficient method as well.
I may note that currently in my project the frameworks I have are Alamofire, swiftyJSON, and Haneke (for caching purposes) although I do not know how to correctly use them to achieve my goal. I may also add that I'm using Parse.com as my BaaS, so if there is a method that can integrate parse in it, that would be welcomed as well.
So any suggestions In Swift?
The solution I suggested is to use Parse.com as the source of metadata for your books and s3 as the storage. I don't think you need any other components (and I'm particularly suspicious of net-code convenience wrappers for basic iOS stuff).
So I would (and did, to prove it works) setup a parse model like this...
And here's a vanilla ViewController in swift that has a collection view and an array of custom swift "Book" objects...
// ViewController.swift
import UIKit
import Parse
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak private var collectionView : UICollectionView!
var books : Array<Book>!
override func viewDidLoad() {
super.viewDidLoad()
self.books = []
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.loadBooks()
}
func loadBooks() {
var query = PFQuery(className: "Book")
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
let bookObjects = objects as! [PFObject]
for (index, object) in enumerate(bookObjects) {
self.books.append(Book(pfBook: object))
}
}
self.collectionView.reloadData()
}
}
Basic stuff. When the view appears, query parse for book objects, when they are returned, create swift "Book" wrappers around them and reload the collection view. Here's the swift Book class ...
// Book.swift
import Parse
class Book: NSObject {
var pfBook : PFObject
var coverImage : UIImage!
init(pfBook: PFObject) {
self.pfBook = pfBook
}
func fetchCoverImage(completion: (image: UIImage?, error: NSError?) -> Void) {
let urlString = self.pfBook["coverUrl"] as! String
let url = NSURL(string: urlString)
let request = NSURLRequest(URL: url!)
let queue = dispatch_get_main_queue()
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response: NSURLResponse?, data: NSData?, error: NSError?) in
if error == nil {
self.coverImage = UIImage(data: data!)
completion(image: self.coverImage, error: nil)
} else {
completion(image: nil, error: error)
}
}
}
}
The interesting method fetches the cover image from the url saved in the PFObject. (Check out the url column in the parse.com data browser. I took a shortcut here and used an nice dummy image generator on the web. You would need to implement a method with the same signature, but have it get the image from s3).
The only thing left is the collection view datasource. Back again in ViewController.swift...
// ViewController.swift
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.books.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell",
forIndexPath:indexPath) as! UICollectionViewCell
if cell.backgroundView == nil { // brand new cell
let v = UIImageView(frame:cell.bounds)
v.tag = 32
v.contentMode = .ScaleToFill
cell.backgroundView = v
}
let book = self.books[indexPath.row]
let coverImage = book.coverImage
if coverImage == nil {
book.fetchCoverImage({ (image, error) -> Void in
collectionView.reloadItemsAtIndexPaths([indexPath])
})
} else {
let imageView = cell.viewWithTag(32) as! UIImageView
imageView.image = book.coverImage
}
return cell
}
Notice what we do in the cellForRow... method: if the book object has a cached coverImage, we place that in the image view. Otherwise we tell the book to fetch it's cover art and, when finished, reload the cell at the current index path (we don't depend on the cell being current anymore, since time passes while the image is being fetched).
Here's what it looks like running...
I was facing this problem and have a nice and well working solution, how to load images from AWS S3 to UIImageView in cell or other views. To do it I additional use a SDWebImage SDK SDWebImage
So let's create an extension for UIImageView and import SDWebImage, then add image cache to caching image links only
import UIKit
import SDWebImage
let imageCache = NSCache<NSString, NSString>()
extension UIImageView {
func loadAsyncWithCacheFromAWSS3(_ link: String?, placeholder: UIImage? = UIImage(named: "placeholder")) {
guard let unwrappedlink = link else { return self.image = placeholder }
// Use your own image appearing style
self.sd_imageTransition = .fade
if !loadFromCache(unwrappedlink as NSString, placeholder: placeholder) {
let file = File(bucket: 'YOUR BUCKET', key: unwrappedlink, region: 'YOUR REGION')
AWSManager.shared.getFileURL(file: file) { [weak self] (string, url, error) in
guard let validURL = url else { self?.image = placeholder; return }
imageCache.setObject(NSString(string: validURL.absoluteString), forKey: NSString(string: unwrappedlink))
guard unwrappedlink == link else {
guard let key = link else { return }
self?.loadFromCashe(key as NSString, placeholder: placeholder)
return
}
self?.sd_setImage(with: validURL, placeholderImage: placeholder, options: [.scaleDownLargeImages])
}
}
}
// quick load from existing cashed link
#discardableResult
private func loadFromCache(_ key: NSString, placeholder: UIImage?) -> Bool {
guard let string = imageCache.object(forKey: key) else { return false }
let link = URL(string: String(string))
self.sd_setImage(with: link, placeholderImage: placeholder, options: [.scaleDownLargeImages])
return true
}
}
Here is a singleton class with getFileURL method
func getFileURL(file: File, completionHandler: #escaping (String?, URL?, Error?) -> Void) {
let getPreSignedURLRequest = AWSS3GetPreSignedURLRequest()
getPreSignedURLRequest.bucket = file.bucket
getPreSignedURLRequest.key = file.key
getPreSignedURLRequest.httpMethod = .GET
getPreSignedURLRequest.expires = Date().adjust(hour: 24, minute: 0, second: 0)
getPreSignedURLRequest.minimumCredentialsExpirationInterval = 10
AWSS3PreSignedURLBuilder.s3PreSignedURLBuilder(forKey: 'YOUR BUILDER URL').getPreSignedURL(getPreSignedURLRequest).continueWith { (task: AWSTask<NSURL>) -> Any? in
if let error = task.error as NSError? {
completionHandler(nil, nil, error)
return nil
}
let presignedURL = task.result
completionHandler(file.key, presignedURL as URL?, nil)
return nil
}
}
in Appdelegate in func applicationDidReceiveMemoryWarning(_ application: UIApplication) simple call imageCache.removeAllObjects()