I am trying to make a UIImageView class / extension that can load an image from a URL.
the image view should not get confused with concurrency when scrolling through a UICollectionView and the data task should be cancellable. Also downloaded images should be cached (I'm not sure if NSCache or URLCache should be used here)
I would like to utilise the latest Swift APIs if possible.
Is this the correct usage of Task?
I have written this code and I am wondering if anybody can spot anything wrong or anything they believe could improve the code:
import Foundation
import UIKit
class PHImageView: UIImageView {
static var imageCache = NSCache<AnyObject, AnyObject>()
var imageURL: URL?
var currentTask: Task<(), Error>?
func setImage(url: URL , contentMode: ContentMode = .scaleAspectFit) {
imageURL = url
image = nil
let urlRequest = URLRequest(url: url)
if let image = PHImageView.imageCache.object(forKey: url.absoluteString as AnyObject) as? UIImage {
currentTask?.cancel()
currentTask = nil
DispatchQueue.main.async {
self.image = image
self.contentMode = contentMode
}
} else {
currentTask = Task {
do {
let (data, _) = try await URLSession(configuration: .default).data(for: urlRequest)
guard url == self.imageURL else {
if let imageURL = imageURL {
setImage(url: imageURL, contentMode: contentMode)
}
return
}
let image = UIImage(data: data)
PHImageView.imageCache.setObject(image as AnyObject, forKey: url.absoluteString as AnyObject)
DispatchQueue.main.async {
self.image = image
self.contentMode = contentMode
}
}
catch {
print(error)
}
}
}
}
deinit {
currentTask?.cancel()
}
}
I have a problem with loading images from firebase. I have two functions. One of them collect info about user, second one load users avatar image. Unfortunately images load after function creates new user. I know it will be problem with asynchronous of Firebase but I don't know how to set up DispatchQueue to work properly. Can you help me with that?
// function that load user image in user manager class
func loadUserImage(contactUserID: String, completion: #escaping (UIImage) -> Void) {
let userID = Auth.auth().currentUser!.uid
var userImageRef = self.storage.child("\(userID)/userImage.jpg")
var image = UIImage()
if contactUserID != "" {
userImageRef = self.storage.child("\(contactUserID)/userImage.jpg")
}
userImageRef.getData(maxSize: 5 * 1024 * 1024) { (data, error) in
if let error = error {
print("Error with retrieving data: \(error.localizedDescription)")
} else {
if data?.count != 0 {
image = UIImage(data: data!)!
} else {
image = UIImage(systemName: "person.circle.fill")!
}
completion(image)
}
}
}
// function that load user in contact manager class
func loadContactList(completion: #escaping ([User]) -> Void) {
let currentUserID = Auth.auth().currentUser!.uid
db.collection("contacts")
.document(currentUserID)
.collection("userContacts")
.addSnapshotListener { (querySnapshot, error) in
var contactList = [User]()
if let error = error {
print("Error with retrieving data from DB: \(error.localizedDescription)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for document in snapshotDocuments {
let data = document.data()
let uid = data["uid"] as! String
let name = data["name"] as! String
let email = data["email"] as! String
var contact = User(email: email, name: name, userID: uid)
DispatchQueue.global().sync {
self.userService.loadUserImage(contactUserID: uid) { (image) in
contact.photoURL = image
}
}
contactList.append(contact)
contactList.sort {
$0.name < $1.name
}
completion(contactList)
}
}
}
}
}
// Function implementation in viewController
func loadContactList() {
self.contactService.loadContactList { (contactArray) in
self.contactList = contactArray
self.tableView.reloadData()
}
}
What you can do is to store the image url in the firebase database and after that create this extension:
import UIKit
let imageCache: NSCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(urlString: String) {
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
let url = URL(string: urlString)
if let data = try? Data(contentsOf: url!) {
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data) {
imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
self.image = downloadedImage
}
})
}
}
}
And call:
if let url = data["imgUrl"] as? String {
self.myImageView.loadImageUsingCacheWithUrlString(urlString: url)
}
For that what you need to do is to create and initialize an UIImage object. If you are working with cell classes you need to create this object in the cell.
Hello everyone I am trying to take my image from the firebase database and present it into an image view as the profile image of the user. The first thing I do is create a method to retrieve the photo data in an array
class PhotoService {
static func retrievePhotos(completion: #escaping ([Photo]) -> Void) {
//get a databas refrence
let db = Firestore.firestore()
//get data from "photos" collection
db.collection("photos").getDocuments { (snapshot, Error) in
//check for errors
if Error != nil {
//error in retrieving photos
return
}
//get all the documents
let documents = snapshot?.documents
//check that documents arent nil
if let documents = documents {
//create an array to hold all of our photo structs
var photoArray = [Photo]()
//loop through documents, get a photos struct for each
for doc in documents {
//create photo struct
let p = Photo(snapshot: doc)
if p != nil {
//store it in an array
photoArray.insert(p!, at: 0)
}
}
//pass back the photo array
completion(photoArray)
}
}
}
Then I call that class and attempt to display the Image in the image view
#IBOutlet var profilePictureImageView: UIImageView!
var photos = [Photo]()
override func viewDidLoad() {
super.viewDidLoad()
//call the photo service to retrieve the photos
PhotoService.retrievePhotos { (retrievedPhotos) in
//set the photo array to the retrieved photos
self.photos = retrievedPhotos
//make the image view a circle
self.profilePictureImageView.layer.cornerRadius = self.profilePictureImageView.bounds.height / 2
self.profilePictureImageView.clipsToBounds = true
//make the image view display the photo
var photo:Photo?
func displayPhoto(photo:Photo) {
//check for errors
if photo.photourl == nil {
return
}
//download the image
let photourl = URL(string: photo.photourl!)
//check for erorrs
if photourl == nil {
return
}
//use url session to download the image asynchronously
let session = URLSession.shared
let dataTask = session.dataTask(with: photourl!) { (data, response, Error) in
//check for errors
if Error == nil && data != nil {
//let profilePictureImageView = UIImage()
let image = UIImage(data: data!)
//set the image view
DispatchQueue.main.async {
self.profilePictureImageView.image = image
}
}
}
dataTask.resume()
}
}
}
}
I dont understand what I am doing wrong and why the image is not showing up if anyone can explain this to me and give me an example with code that would be amazing, explain it as though you are explaining it to a kid in grade 5
A quick way to get Image from Firebase and assigning it to an ImageView can be done easily in these Steps.
Function to Get PhotoUrl
//Function to get photo of loggedin User
func getUrl(Completion:#escaping((String)->())) {
let userID = Auth.auth().currentuser?.uid
let db = Firestore.firestore().collection("photos").document(userID)
db.getDocument { (docSnapshot, error) in
if error != nil {
return
} else {
guard let snapshot = docSnapshot, snapshot.exists else {return}
guard let data = snapshot.data() else {return}
let imageUrl = data["photoUrl"] as! String
completion(imageUrl)
}
}
}
To download image and assign it to image view
//Call this function in VC where your `ImageView` is
func getImage(Url:String){
DispatchQueue.global().async {
let url = URL(string: Url)
if let data = try? Data(contentsOf: url!) {
DispatchQueue.main.async {
self.profilePictureImageView.image = UIImage(data: data)
}
}
}
}
}
Call these inside viewDidLoad like this:
getUrl{(url) in
getImage(Url:url)
}
I have UIImageView and I want to download images in cache if exist, I've used extension func.
I have this code but not working:
extension UIImageView {
func loadImageUsingCache (_ urlString : String) {
let imageCache = NSCache<AnyObject, AnyObject>()
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) {
self.image = cachedImage as? UIImage
return
}
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if data != nil {
if let image = UIImage(data: data!) {
imageCache.setObject(image, forKey: urlString as AnyObject)
DispatchQueue.main.async(execute: {
self.image = image
})
}
}
}.resume()
}
}
You're creating the new NSCache object for each image and not retain it.
You should create object variable instead of local one. It won't work in extension in this case. Also you can try to use URLCache.shared instead.
// It's Perfect Solution //
var imageCache = String: UIImage
class CustomImageView: UIImageView {
var lastImgUrlUsedToLoadImage: String?
func loadImage(with urlString: String) {
// set image to nil
self.image = nil
// set lastImgUrlUsedToLoadImage
lastImgUrlUsedToLoadImage = urlString
// check if image exists in cache
if let cachedImage = imageCache[urlString] {
self.image = cachedImage
return
}
// url for image location
guard let url = URL(string: urlString) else { return }
// fetch contents of URL
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to load image with error", error.localizedDescription)
}
if self.lastImgUrlUsedToLoadImage != url.absoluteString {
return
}
// image data
guard let imageData = data else { return }
// create image using image data
let photoImage = UIImage(data: imageData)
// set key and value for image cache
imageCache[url.absoluteString] = photoImage
// set image
DispatchQueue.main.async {
self.image = photoImage
}
}.resume()
}
}
I am working on show image from url async. I have tried to create a new thread for download image and then refresh on main thread.
func asyncLoadImg(product:Product,imageView:UIImageView){
let downloadQueue = dispatch_queue_create("com.myApp.processdownload", nil)
dispatch_async(downloadQueue){
let data = NSData(contentsOfURL: NSURL(string: product.productImage)!)
var image:UIImage?
if data != nil{
image = UIImage(data: data!)
}
dispatch_async(dispatch_get_main_queue()){
imageView.image = image
}
}
}
When I was trying to debug that, when it comes to dispatch_async(downloadQueue), it jumps out the func. Any suggestion? Thx
**Swift 5.0+ updated Code :
extension UIImageView {
func imageFromServerURL(_ URLString: String, placeHolder: UIImage?) {
self.image = nil
//If imageurl's imagename has space then this line going to work for this
let imageServerUrl = URLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
if let url = URL(string: imageServerUrl) {
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
//print("RESPONSE FROM API: \(response)")
if error != nil {
print("ERROR LOADING IMAGES FROM URL: \(error)")
DispatchQueue.main.async {
self.image = placeHolder
}
return
}
DispatchQueue.main.async {
if let data = data {
if let downloadedImage = UIImage(data: data) {
self.image = downloadedImage
}
}
}
}).resume()
}
}
}
Now wherever you required just do this to load image from server url :
Using swift 5.0 + updated code using placeholder image :
UIImageView.imageFromServerURL(URLString:"here server url",placeHolder: placeholder image in uiimage format)
Simple !
Use extension in Swift3. To resolve Network problem i recommend you use NSCache:
import UIKit
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCache(withUrl urlString : String) {
let url = URL(string: urlString)
self.image = nil
// check cached image
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
// if not, download image from url
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
if let image = UIImage(data: data!) {
imageCache.setObject(image, forKey: urlString as NSString)
self.image = image
}
}
}).resume()
}
}
Hope it help!
Carrying on from Shobhakar Tiwari's answer, I think its often helpful in these cases to have a default image in case of error, and for loading purposes, so I've updated it to include an optional default image:
Swift 3
extension UIImageView {
public func imageFromServerURL(urlString: String, defaultImage : String?) {
if let di = defaultImage {
self.image = UIImage(named: di)
}
URLSession.shared.dataTask(with: NSURL(string: urlString)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error ?? "error")
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
self.image = image
})
}).resume()
}
}
This solution make scrolling really fast without unnecessary image updates.
You have to add the url property to our cell class:
class OfferItemCell: UITableViewCell {
#IBOutlet weak var itemImageView: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
var imageUrl: String?
}
And add extension:
import Foundation
import UIKit
let imageCache = NSCache<AnyObject, AnyObject>()
let imageDownloadUtil: ImageDownloadUtil = ImageDownloadUtil()
extension OfferItemCell {
func loadImageUsingCacheWithUrl(urlString: String ) {
self.itemImageView.image = nil
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.itemImageView.image = cachedImage
return
}
DispatchQueue.global(qos: .background).async {
imageDownloadUtil.getImage(url: urlString, completion: {
image in
DispatchQueue.main.async {
if self.imageUrl == urlString{
imageCache.setObject(image, forKey: urlString as AnyObject)
self.itemImageView.image = image
}
}
})
}
}
}
You can also improve it and extract some code to a more general cell class i.e. CustomCellWithImage to make it more reusable.
Here this code might help you.
let cacheKey = indexPath.row
if(self.imageCache?.objectForKey(cacheKey) != nil){
cell.img.image = self.imageCache?.objectForKey(cacheKey) as? UIImage
}else{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
if let url = NSURL(string: imgUrl) {
if let data = NSData(contentsOfURL: url) {
let image: UIImage = UIImage(data: data)!
self.imageCache?.setObject(image, forKey: cacheKey)
dispatch_async(dispatch_get_main_queue(), {
cell.img.image = image
})
}
}
})
}
With this image will download and cache without lagging the table view scroll
The most common way in SWIFT 4 to load async images without blink or changing images effect is use to custom UIImageView class like this one:
//MARK: - 'asyncImagesCashArray' is a global varible cashed UIImage
var asyncImagesCashArray = NSCache<NSString, UIImage>()
class AyncImageView: UIImageView {
//MARK: - Variables
private var currentURL: NSString?
//MARK: - Public Methods
func loadAsyncFrom(url: String, placeholder: UIImage?) {
let imageURL = url as NSString
if let cashedImage = asyncImagesCashArray.object(forKey: imageURL) {
image = cashedImage
return
}
image = placeholder
currentURL = imageURL
guard let requestURL = URL(string: url) else { image = placeholder; return }
URLSession.shared.dataTask(with: requestURL) { (data, response, error) in
DispatchQueue.main.async { [weak self] in
if error == nil {
if let imageData = data {
if self?.currentURL == imageURL {
if let imageToPresent = UIImage(data: imageData) {
asyncImagesCashArray.setObject(imageToPresent, forKey: imageURL)
self?.image = imageToPresent
} else {
self?.image = placeholder
}
}
} else {
self?.image = placeholder
}
} else {
self?.image = placeholder
}
}
}.resume()
}
}
example of use this class in UITableViewCell bellow:
class CatCell: UITableViewCell {
//MARK: - Outlets
#IBOutlet weak var catImageView: AyncImageView!
//MARK: - Variables
var urlString: String? {
didSet {
if let url = urlString {
catImageView.loadAsyncFrom(url: url, placeholder: nil)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
}
}
One of the best way is to used SDWebImage.
Swift Example:
import SDWebImage
imageView.sd_setImage(with: URL(string: "ImageUrl"), placeholderImage: UIImage(named: "placeholder.png"))
Objective C Example:
#import <SDWebImage/UIImageView+WebCache.h>
[imageView sd_setImageWithURL:[NSURL URLWithString:#"ImageUrl"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];