I'm developing a chat app, I'm having problem showing the Avatar to my JSQMessagesViewController
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
var avatar = UIImage()
let people = FIRDatabase.database().reference().child("people").child(senderId)
people.observeEventType(.Value, withBlock: {
snapshot -> Void in
let dict = snapshot.value as! Dictionary<String, AnyObject>
let imageUrl = dict["profileImage"] as! String
if imageUrl.hasPrefix("gs://") {
FIRStorage.storage().referenceForURL(imageUrl).dataWithMaxSize(INT64_MAX, completion: { (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
avatar = UIImage.init(data: data!)!
})
}
})
let AvatarJobs = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(avatar, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
return AvatarJobs
}
The problem here is, when I'm trying to pull the image of the sender from firebase, I'm getting a blank image, but when i try to use this let AvatarJobs = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(UIImage(named: "icon.png"), diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault)) it's working fine, What do you think is the problem here? Thanks!
If I may suggest an alternative? Why don't you have a dictionary:
var avatars = [String: JSQMessagesAvatarImage]()
let storage = FIRStorage.storage()
And use the following function:
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource!
{
let message = messages[indexPath.row]
return avatars[message.senderId]
}
And create the avatars in viewDidLoad (or where ever )
createAvatar(senderId, senderDisplayName: senderDisplayName, user: currentUser, color: UIColor.lightGrayColor())
with a function
func createAvatar(senderId: String, senderDisplayName: String, user: FBUser, color: UIColor)
{
if self.avatars[senderId] == nil
{
//as you can see, I use cache
let img = MyImageCache.sharedCache.objectForKey(senderId) as? UIImage
if img != nil
{
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(img, diameter: 30)
// print("from cache")
}
else if let photoUrl = user.pictureURL where user.pictureURL != ""
{
// the images are very small, so the following methods work just fine, no need for Alamofire here
if photoUrl.containsString("https://firebasestorage.googleapis.com")
{
self.storage.referenceForURL(photoUrl).dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil)
{
//deal with error
}
else
{
let newImage = UIImage(data: data!)
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage!, forKey: senderId, cost: data!.length)
}
}
}
else if let data = NSData(contentsOfURL: NSURL(string:photoUrl)!)
{
let newImage = UIImage(data: data)!
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage, forKey: senderId, cost: data.length)
}
else
{
//etc. blank image or image with initials
}
}
}
else
{
//etc. blank image or image with initials
}
}
for Cache I have a custom class
import Foundation
class MyImageCache
{
static let sharedCache: NSCache =
{
let cache = NSCache()
cache.name = "MyImageCache"
cache.countLimit = 200 // Max 200 images in memory.
cache.totalCostLimit = 20*1024*1024 // Max 20MB used.
return cache
}()
}
Let me know if that helps
I would suggest trying to isolate your problems. I don't know if the issue is with JSQMessagesAvatarImageFactory I think the issue may be that you do not have the image downloaded by the time the cell needs to be displayed. I would make sure that you are getting something back from fireBase before you try and set it to your avatar. A closure is normally how I do this something like
func getImageForUser(id: String, completiion() -> Void) {
//Add your logic for retrieving from firebase
let imageFromFirebase = firebaserReference.chiledWithID(id)
completion(image)
}
Then in your
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
var avatarImage = JSQAavatarImage()
getImageForUser {
self.avatarImage = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(imageFromFirebase, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
self.collectionView.reloadItemAtIndexPath(indexPath)
}
That way it waits till the response is back and then reloads the cell once it is there.
Let me know if you have any other questions.
Related
For study purposes, I'm creating a app to show a list of some star wars ships. It fetches my json (locally) for the ship objects (it has 4 ships for this example).
It's using a custom cell for the table view.
The table populates without problems, if I already have the images downloaded (in user documents) or not.
My starshipData array is populated by my DataManager class by delegate.
I removed some code to make the class smaller, I can show everything if needed.
Ok, so the problem happens (very rarely) when I press the sorting button.
The way I'm doing it is after recovering or downloading the image, I update the image field in starshipData array.
Here is my sorting method, pretty basic.
#objc private func sortByCost(sender: UIBarButtonItem) {
starshipData.sort { $0.costInCredits < $1.costInCredits }
starshipTableView.reloadData()
}
Here are the implementations of the tableView.
First I use the cellForRowAt method to populate the fast/light data.
// MARK: -> cellForRowAt
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StarshipCell", for: indexPath) as! StarshipCell
let starship = starshipData[indexPath.row]
// update cell properties
cell.starshipNameLabel.text = starship.name
cell.starshipManufacturerLabel.text = starship.manufacturer
cell.starshipCostLabel.text = currencyFormatter(value: starship.costInCredits)
// only populate the image if the array has one (the first time the table is populated,
// the array doesn't have an image, it'll need to download or fetch it in user documents)
if starship.image != nil {
cell.starshipImgView.image = starship.image
}
// adds right arrow indicator on the cell
cell.accessoryType = .disclosureIndicator
return cell
}
Here I use the willDisplay method to download or fetch the images, basically the heavier data.
// MARK: -> willDisplay
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// update cell image
let cell = cell as! StarshipCell
let imageUrl = starshipData[indexPath.row].imageUrl
let starshipName = starshipData[indexPath.row].name
let index = indexPath.row
// if there isn't any image on the cell, proceed to manage the image
if cell.starshipImgView.image == nil {
// only instantiate spinner on imageView position if no images are set
let spinner = UIActivityIndicatorView(style: .medium)
startSpinner(spinner: spinner, cell: cell)
// manage the image
imageManager(starshipName: starshipName, imageUrl: imageUrl, spinner: spinner, cell: cell, index: index) { (image) in
self.addImageToCell(cell: cell, spinner: spinner, image: image)
}
}
}
Here is where I think the problem is as my knowledge in swift and background threads are still in development.
I found out with print logs that the times the cell doesn't show the correct image is because the array does not have the image for that index, so the cell shows the image from the last time the table was populated/loaded.
I wonder if it's because the background threads didn't have enough time to update the starshipArray with the fetched/downloaded image before the user pushing the sort button.
The thing is, if the table was populated correctly the first time, when the sort button is pushed, the starshipData array should already have all images, as you can see in the imageManager method, after the image is unwrappedFromDocuments, I call updateArrayImage to update the image.
Maybe it's the amount of dispatchesQueues being used? Are the completion handler and dispatchQueues used correctly?
private func imageManager(starshipName: String, imageUrl: URL?, spinner: UIActivityIndicatorView, cell: StarshipCell, index: Int, completion: #escaping (UIImage) -> Void) {
// if json has a string on image_url value
if let unwrappedImageUrl = imageUrl {
// open a background thread to prevent ui freeze
DispatchQueue.global().async {
// tries to retrieve the image from documents folder
let imageFromDocuments = self.retrieveImage(imageName: starshipName)
// if image was retrieved from folder, upload it
if let unwrappedImageFromDocuments = imageFromDocuments {
// TO FORCE THE PROBLEM DESCRIBED, PREVENT ONE SHIP TO HAVE IT'S IMAGE UPDATED
// if (starshipName != "Star Destroyer") {
self.updateArrayImage(index: index, image: unwrappedImageFromDocuments)
// }
completion(unwrappedImageFromDocuments)
}
// if image wasn't retrieved or doesn't exists, try to download from the internet
else {
var image: UIImage?
self.downloadManager(imageUrl: unwrappedImageUrl) { data in
// if download was successful
if let unwrappedData = data {
// convert image data to image
image = UIImage(data: unwrappedData)
if let unwrappedImage = image {
self.updateArrayImage(index: index, image: unwrappedImage)
// save images locally on user documents folder so it can be used whenever it's needed
self.storeImage(image: unwrappedImage, imageName: starshipName)
completion(unwrappedImage)
}
}
// if download was not successful
else {
self.addImageNotFound(spinner: spinner, cell: cell)
}
}
}
}
}
// if json has null on image_url value
else {
addImageNotFound(spinner: spinner, cell: cell)
}
}
Here are some of the helper methods I use on imageManager, if necessary.
// MARK: - Helper Methods
private func updateArrayImage(index: Int, image: UIImage) {
// save image in the array so it can be used when cells are sorted
self.starshipData[index].image = image
}
private func downloadManager(imageUrl: URL, completion: #escaping (Data?) -> Void) {
let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 5
return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
}()
var dataTask: URLSessionDataTask?
dataTask?.cancel()
dataTask = session.dataTask(with: imageUrl) { [weak self] data, response, error in
defer {
dataTask = nil
}
if let error = error {
// use error if necessary
DispatchQueue.main.async {
completion(nil)
}
}
else if let response = response as? HTTPURLResponse,
response.statusCode != 200 {
DispatchQueue.main.async {
completion(nil)
}
}
else if let data = data,
let response = response as? HTTPURLResponse,
response.statusCode == 200 { // Ok response
DispatchQueue.main.async {
completion(data)
}
}
}
dataTask?.resume()
}
private func addImageNotFound(spinner: UIActivityIndicatorView, cell: StarshipCell) {
spinner.stopAnimating()
cell.starshipImgView.image = #imageLiteral(resourceName: "ImageNotFound")
}
private func addImageToCell(cell: StarshipCell, spinner: UIActivityIndicatorView, image: UIImage) {
DispatchQueue.main.async {
spinner.stopAnimating()
cell.starshipImgView.image = image
}
}
private func imagePath(imageName: String) -> URL? {
let fileManager = FileManager.default
// path to save the images on documents directory
guard let documentPath = fileManager.urls(for: .documentDirectory,
in: FileManager.SearchPathDomainMask.userDomainMask).first else { return nil }
let appendedDocumentPath = documentPath.appendingPathComponent(imageName)
return appendedDocumentPath
}
private func retrieveImage(imageName: String) -> UIImage? {
if let imagePath = self.imagePath(imageName: imageName),
let imageData = FileManager.default.contents(atPath: imagePath.path),
let image = UIImage(data: imageData) {
return image
}
return nil
}
private func storeImage(image: UIImage, imageName: String) {
if let jpgRepresentation = image.jpegData(compressionQuality: 1) {
if let imagePath = self.imagePath(imageName: imageName) {
do {
try jpgRepresentation.write(to: imagePath,
options: .atomic)
} catch let err {
}
}
}
}
private func startSpinner(spinner: UIActivityIndicatorView, cell: StarshipCell) {
spinner.center = cell.starshipImgView.center
cell.starshipContentView.addSubview(spinner)
spinner.startAnimating()
}
}
To sum all up, here is the unordered table, when you open the app: unordered
The expected result (happens majority of time), after pushing the sort button: ordered
The wrong result (rarely happens), after pushing the sort button: error
I'll gladly add more info if needed, ty!
First, consider move the cell configuration for the UITableViewCell class. something like this:
class StarshipCell {
private var starshipNameLabel = UILabel()
private var starshipImgView = UIImageView()
func configure(with model: Starship) {
starshipNameLabel.text = model.name
starshipImgView.downloadedFrom(link: model.imageUrl)
}
}
Call the configure(with: Starship) method in tableView(_:cellForRowAt:).
The method downloadedFrom(link: ) called inside the configure(with: Starship) is provide by following extension
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIView.ContentMode = .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() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String?, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
if let link = link {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
}
I created the setAvatarImage function to download an UIImage which is then getting returned in a default collection view function. I can see in the console that the UIImage is getting downloaded, thus the return value should be the UIImage. However no image is displayed in the collection view.
func setAvatarImages(_ senderUid: String!, completionhandler: #escaping (UIImage!) -> Void) {
let ref = DatabaseReference.users(uid: senderUid).reference()
ref.observe(.value, with: { (snapshot) in
let user = User(dictionary: snapshot.value as! [String : Any])
user.downloadProfilePicture(completion: { (image, error) in
if image != nil {
let profileImage = image
print(profileImage!)
completionhandler(profileImage!)
} else if error != nil {
print(error?.localizedDescription ?? "")
}
})
})
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.item]
var avatarImage: JSQMessagesAvatarImage!
setAvatarImages(message.senderUID) { (profileImage) in
avatarImage = JSQMessagesAvatarImageFactory.avatarImage(with: profileImage, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
print(avatarImage)
}
return avatarImage
}
I think there is a problem with the asynchronous return function. But I thought, that giving the setAvatarImage function a completionhandler should solve that problem.
Can anyone give me some advise, so that the downloaded Image is returned!!
EDIT !!!
Taking Alex suggestions into account I changed the setAvatarImage function.
First I Created an dictionary:
var profileImages = [String : UIImage]()
The intention was to get all images into profileImages with the user.uid as the key within the viewdidLoad.
func setAvatarImages() {
for user in chat.users {
print(user.fullName)
user.downloadProfilePicture(completion: { (image, error) in
print(image!)
if let profileImage = image {
self.profileImages[user.uid] = profileImage
} else if error != nil {
print(error?.localizedDescription ?? "")
}
})
}
}
SetAvatarImages gets called in the viewDidLoad.
To return the AvatarImage in the default function from the JSQMessagesViewController I did that:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.item]
let avatarImage = JSQMessagesAvatarImageFactory.avatarImage(with: profileImages[message.senderUID], diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
return avatarImage
}
The cool thing was that it did work, for a few tries. But suddenly it crashes when downloading the image in the user.downloadProfileImage function. The only thing I changed before it started to crash everytime was that I deleted the breaking points in the console to see how fast it works ... Does anyone has an idea why it crashes now.
And yeah I know that it is better to add some Caching for the images, but first this issue has to be solved!
Finally got it!
I just had to add a completionhandler to the setAvatarImages function!
func setAvatarImages(completion: #escaping ([String : UIImage]) -> Void) {
for user in chat.users {
print(user.fullName)
user.downloadProfilePicture(completion: { (image, error) in
print(image!)
if let profileImage = image {
self.profileImages[user.uid] = profileImage
completion(self.profileImages)
} else if error != nil {
print(error?.localizedDescription ?? "")
}
})
}
}
and then in viewDidLoad
setAvatarImages(completion: { (profileUserImages) in
print(profileUserImages)
})
Next stop caching!
My print statement shows that the function is called 4771 times in about 15 seconds, obviously resulting in a crash.
This is the function:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
count += 1
print("\n\nAvatar func called \(count)\n")
let databaseRef = FIRDatabase.database().reference()
let message = messages[indexPath.item]
let placeHolderImage = UIImage(named: "Logo")
let avatarImage = JSQMessagesAvatarImage(avatarImage: nil, highlightedImage: nil, placeholderImage: placeHolderImage)
if let messageID = message.senderId {
// Check cache for avatar
if imageCache.object(forKey: messageID as NSString) != nil {
DispatchQueue.main.async {
avatarImage!.avatarImage = imageCache.object(forKey: messageID as NSString)
avatarImage!.avatarHighlightedImage = imageCache.object(forKey: messageID as NSString)
self.collectionView.reloadData()
}
} else {
// If avatar isn't cached, fire off a new download
databaseRef.child("users").child(messageID).observe(.value, with: { (snapshot) in
if let profilePic = (snapshot.value as AnyObject!)!["profilePicture"] as! String! {
let profilePicURL: URL = URL(string: profilePic)!
Alamofire.request(profilePicURL)
.responseImage { response in
if let downloadedImage = response.result.value {
imageCache.setObject(downloadedImage, forKey: message.senderId as NSString)
DispatchQueue.main.async {
avatarImage!.avatarImage = imageCache.object(forKey: message.senderId as NSString)
avatarImage!.avatarHighlightedImage = imageCache.object(forKey: message.senderId as NSString)
self.collectionView.reloadData()
}
}
}
}
})
}
}
return avatarImage
}
What's causing the loop? There's only one user (me) to get an avatar for anyway. I'm somewhat new to programming and am trying to figure out how to work with a cache... my intention with this function is to check if the user's avatar is cached, and if so, use it. If not, fire off a new download from Firebase. But I am messing up badly apparently - How can I write this so it efficiently checks the cache and/or downloads the image, and doesn't get stuck in a loop?
You are calling reloadData in your function, which will cause this function to be called again, which calls reloadData and so on; you have created an infinite loop.
You only need to reload anything in the case where you initially return a placeholder and then subsequently retrieve the avatar from the network. In this case it is very wasteful to reload the whole collection view; you simply need to reload the affected item:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
count += 1
print("\n\nAvatar func called \(count)\n")
let databaseRef = FIRDatabase.database().reference()
let message = messages[indexPath.item]
let placeHolderImage = UIImage(named: "Logo")
let avatarImage = JSQMessagesAvatarImage(avatarImage: nil, highlightedImage: nil, placeholderImage: placeHolderImage)
if let messageID = message.senderId {
// Check cache for avatar
if let cacheObject = imageCache.object(forKey: messageID as NSString) {
avatarImage!.avatarImage = cacheObject
avatarImage!.avatarHighlightedImage = cacheObject
} else {
// If avatar isn't cached, fire off a new download
databaseRef.child("users").child(messageID).observe(.value, with: { (snapshot) in
if let profilePic = (snapshot.value as AnyObject!)!["profilePicture"] as! String! {
let profilePicURL: URL = URL(string: profilePic)!
Alamofire.request(profilePicURL)
.responseImage { response in
if let downloadedImage = response.result.value {
imageCache.setObject(downloadedImage, forKey: message.senderId as NSString)
DispatchQueue.main.async {
self.collectionView.reloadItems(at:[indexPath])
}
}
}
}
})
}
}
return avatarImage
}
I have a TableView and I am filling it with data retrieved from database. Everything works fine except the images. Because of the cell reuse behaviour, and I am fetching image in cellForRowAtIndexPath. I chose to fetch images in cellForRowAtIndexPath because in the details retrieving function (which is triggered in viewDidLoad), I need to do another request, which is causing other problems (reloading tableview before storing image url)
The problem is that when I scroll fast, the resuable cells bugs while displaying user images
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
}
var theUser =
func fetchData() {
//.. after data is retrieved
var innerDict = [String:String]()
if let user = details.value![key] {
if let name = user["name"] {
// works
innerDict["name"] = name
}
if let image = user["imageName"] {
// gets the image name but at this point I need to;
// a) retrieve the url here (with another call), which will eventually fail
// to catch up with `innerDict` so `innerDict` won't contain `image` variable.
// ie;
myRef.downloadURLWithCompletion { (URL, error) -> Void in }
// b) Store the `image` name in innerDict and download image from url
// in `cellForRowAtIndexPath`. I chose this method:
innerDict["image"] = image
}
user[id] = innerDict
tableView.reloadData()
}
Now the tableView as usual.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = ...
// more stuff
if let imageName = user["image"] {
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://bucket.com").child(user[id]).child(imageName)
storageRef.downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil) {
// handle
} else {
// I thought using Haneke would help to cache the image
cell.image.hnk_setImage(URL!)
}
}
}
This is the closest one I could reach. However images bug on displaying when I scroll fast.
Edit:
I also tried using this approach but it's downloading the same image multiple times with this method, so it takes time for the same images to displayed.
islandRef.dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
// Uh-oh, an error occurred!
} else {
let image: UIImage! = UIImage(data: data!)
cell.userImage.hnk_setImage(image, key: "\(userID)")
}
}
However, with top approach the speed was very fast. The only problem of the above code was the glitch when I scroll fast.
Edit 2
var images = [UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ItemCell", forIndexPath: indexPath) as! ItemDetailTableViewCell
let item = items[indexPath.section][indexPath.row]
if let uid = item["owner"] as? String {
if let user = users[uid] {
if let imageName = user["image"] {
if let img: UIImage = images[indexPath.row] { // crash here "fatal error: Index out of range"
cell.userImage.image = img
}
} else {
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://bucket").child(uid).child(imageName)
storageRef.downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil) {
} else {
dispatch_async(dispatch_get_main_queue(), {
cell.userImage.hnk_setImageFromURL(URL!)
self.images[indexPath.row] = cell.image.image!
})
}
}
}
}
}
I think you should save images from url and show images when the cell is going to reuse, hopefully this will fix your glitch
var myImages = [String: UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ItemCell", forIndexPath: indexPath) as! ItemDetailTableViewCell
let item = items[indexPath.section][indexPath.row]
if let img: UIImage = myImages["\(indexPath.section)\(indexPath.row)"] {
cell.image.image = img
} else {
if let uid = item["owner"] as? String {
if let imageName = user["image"] {
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://bucket.com").child(user[id]).child(imageName)
storageRef.downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil) {
cell.image = UIImage(named: "placeholder") // put default Image when failed to download Image
} else {
dispatch_async(dispatch_get_main_queue(), {
cell.image.hnk_setImage(URL!)
// Store the image in to our cache
self.myImages["\(indexPath.section)\(indexPath.row)"]= cell.image.image
})
}
}
}
}
}
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”