I need to cache the image in Swift on iOS 8. I have a custom Table Cell View.
Here's my code:
import UIKit
class WorksTableViewCell: UITableViewCell {
#IBOutlet var workImage: UIImageView!
#IBOutlet var workTitle: UILabel!
#IBOutlet var workDescription: UILabel!
func configureCellWith(work: Work){
workTitle.text = work.title
workDescription.text = work.description
if let url = NSURL(string: work.image) {
if let data = NSData(contentsOfURL: url) {
var thumb = UIImage(data: data)
workImage.image = thumb
}
}
}
}
Create a dictionary mapping a String to a UIImage in the table's view controller and modify the data in the cellForRowAtIndexPath function. Jameson Quave has a nice tutorial on this very issue found here. It's updated to use new Swift 1.2 syntax as well.
Thanks I fixed like this:
import UIKit
class WorksTableViewCell: UITableViewCell {
#IBOutlet var workImage: UIImageView!
#IBOutlet var workTitle: UILabel!
#IBOutlet var workDescription: UILabel!
var imageCache = [String:UIImage]()
func configureCellWith(work: Work){
workTitle.text = work.title
workDescription.text = work.description
var imgURL = NSURL(string: work.image)
// If this image is already cached, don't re-download
if let img = imageCache[work.image] {
workImage.image = img
}else {
// The image isn't cached, download the img data
// We should perform this in a background thread
let request: NSURLRequest = NSURLRequest(URL: imgURL!)
let mainQueue = NSOperationQueue.mainQueue()
NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data)
// Store the image in to our cache
self.imageCache[work.image] = image
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
self.workImage.image = image
})
}else {
println("Error: \(error.localizedDescription)")
}
})
}
}
}
You can also create a UIImage extension and just call the async function in the configureCellWith function if you want a cleaner solution for Swift 3.
import Foundation
import UIKit
let imageCache = NSCache <AnyObject,AnyObject>()
extension UIImageView {
func loadUsingCache(_ theUrl: String) {
self.image = nil
//check cache for image
if let cachedImage = imageCache.object(forKey: theUrl as AnyObject) as? UIImage{
self.image = cachedImage
return
}
//otherwise download it
let url = URL(string: theUrl)
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
//print error
if (error != nil){
print(error!)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!){
imageCache.setObject(downloadedImage, forKey: theUrl as AnyObject)
self.image = downloadedImage
}
})
}).resume()
}
}
Related
I have the following code for my tableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
// Reset the image in the cell
cell.coverImageView.image = nil
// Get the recipe that the tableView is asking about
let recipeInTable = recipe[indexPath.row]
cell.displayRecipe(recipe: recipeInTable, indexPathRow: indexPath.row)
return cell
}
This is my custom cell class, where I am caching the image data to pull from:
class CustomCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var coverImageView: UIImageView!
var recipeToDisplay:Recipe?
var recipeToPullFrom:Recipe!
func displayRecipe(recipe:Recipe, indexPathRow:Int) {
recipeToPullFrom = recipe
DispatchQueue.main.async {
self.titleLabel.text = self.recipeToPullFrom.title
if self.recipeToPullFrom.image == nil {
return
}
else {
let urlString = self.recipeToPullFrom.image
if let imageData = CacheManager.retrieveData(urlString!) {
self.coverImageView.image = UIImage(data: imageData)
return
}
let url = URL(string: urlString!)
guard url != nil else {
print("Could not create url object")
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error == nil && data != nil {
CacheManager.saveData(urlString!, data!)
if self.recipeToPullFrom.image == urlString {
DispatchQueue.main.async {
// Display the image data in the imageView
self.coverImageView.image = UIImage(data: data!)
}
}
DispatchQueue.main.async {
self.coverImageView.image = UIImage(data: data!)
}
} // End if
} // End dataTask
// Kick off the dataTask
dataTask.resume()
}
}
}
}
And finally my Cache Manager:
class CacheManager {
static var imageDictionary = [String:Data]()
static func saveData(_ url:String, _ imageData:Data) {
// Save the image data along with the url
imageDictionary[url] = imageData
}
static func retrieveData(_ url:String) -> Data? {
// Return the saved imageData or nil
return imageDictionary[url]
}
}
From what I've researched, adding the following in my tableView function should have reset the image in my cell before inputting a new one: cell.coverImageView.image = nil.
Is there something I'm missing? I've also noticed that only my images are showing in the wrong cells. Could I be doing something incorrect with retrieving the image data from cache?
Any direction or support is much appreciated!
Since your images are downloaded asynchronously then it can be downloaded after the cell been reused for another object. What you can do:
When your image is ready to be displayed (downloaded) you need to check if it should be displayed in that cell.
Or
You can also hold a reference to the URLSessionDataTask and cancel the it before reusing the cell by overriding the prepareForReuse method.
I don't think that you need here an asynchronously call especially, when you using .main in other .main. But If you what so you what so make some thing like
DispatchQueue.global(qos: .userInteractive).async {
// Never do here ui code - label, view, images.. anything; or would crash
DispatchQueue.main.async {
// To do here you ui updates
}
}
here is some refactored code with reuse method. I agree with
Hach3m. U need to cancel requests if you don't it anymore, when next cell is about to shown
class CustomCell: UITableViewCell {
#IBOutlet private weak var titleLabel: UILabel!
#IBOutlet private weak var coverImageView: UIImageView!
private var dataTask: URLSessionDataTask?
func displayRecipe(recipe: Recipe, indexPathRow: Int) {
titleLabel.text = recipe.title enter code here
guard let urlString = recipe.image else { return }
if let imageData = CacheManager.retrieveData(urlString) {
coverImageView.image = UIImage(data: imageData)
return
}
guard let url = URL(string: urlString) else { return print("Could not create url object") }
dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return self.clear() }
CacheManager.saveData(urlString, data)
self.coverImageView.image = UIImage(data: data)
}
dataTask?.resume()
}
override func prepareForReuse() {
super.prepareForReuse()
clear()
}
private func clear() {
dataTask?.cancel()
dataTask = nil
}
}
}
When I scroll my UITableView quickly, imageView shows the wrong images from API. (i.e. Samsung image shown in Brexit article and so on).
Here`s an extension allowing me to download images(maybe I can change something here):
extension UIImageView {
func donwloadImage(from url: String) {
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
if error != nil {
print(error)
return
}
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}
task.resume()
}
}
Try any of the following lib:
https://github.com/SDWebImage/SDWebImage
https://github.com/onevcat/Kingfisher
Below is the example that shows correct handling. Please have a try.
class CustomViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
imageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
task?.resume()
}
}
definitely use a library for this.
advice
https://github.com/kean/Nuke
nuke is very simple.
Nuke.loadImage(with: url, into: imageView)
[][!I want load image from download url from firebase using image cache how can I can create an inheritance where my image gets loaded I guess I am missing OOP concept somewhere. my image view is swipe label, how too create an instance where I can display the image on my view.
import UIKit
import Firebase
class swipeLabelViewController: UIViewController {
#IBOutlet weak var UserAgeText: UILabel!
var user:User? {
didSet {
let userNameSwipe = user?.userName
userNameLabel.text = userNameSwipe
let userAgeSwipe = user?.userAge
UserAgeText.text = userAgeSwipe
guard let profileImageUrl = user?.profileImageUrl else {
return }
profileImageView.loadImage(with: profileImageUrl)
print(profileImageUrl)
// let userFetchedImage = user?.profileImageUrl
// swipeLabel.image = userFetchedImage
// self.swipeLabel.image = UIImage(contentsOfFile: profileImageUrl)
}
}
var profileImageView: CustomImageView = {
let iv = CustomImageView()
return iv
}()
var imageI : UIImage!
// var swipepic = CustomImageView()
#IBOutlet weak var swipeLabel: UIImageView!
#IBOutlet weak var userNameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
swipeLabel.image = imageI
let gesture = UIPanGestureRecognizer(target: self, action:
selector(wasDragged(gestureRecognizer:)))
swipeLabel.addGestureRecognizer(gesture)
fetchCurrentUserData()
}
// user model class``
class User {
// attributes getting users info so i can set up from firebase
var userName:String!
var userAge:String!
var uid:String!
var profileImageUrl: String!
init (uid:String,dictionary:Dictionary) {
self.uid = uid
if let userName = dictionary [ "userName" ] as? String{
self.userName = userName
}
if let userAge = dictionary [ "userAge" ] as? String{
self.userAge = userAge
}
if let profileImageUrl = dictionary["profileImageURL"] as? String {
self.profileImageUrl = profileImageUrl
}
}
}
// and lastly image cache class customImageView
import Foundation
import UIKit
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()
}
}
no errors I just can't see image on my swipelabel UIImage
You need to make a separate request to download the image from the url that firebase returns. Here is a playground example:
//: A SpriteKit based Playground
import PlaygroundSupport
import UIKit
enum ImageDownloadError: Error {
case failedToConvertDataToImage
}
func dowloadImage(from url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let image = data.flatMap(UIImage.init(data:)) else {
return completion (.failure(error ?? ImageDownloadError.failedToConvertDataToImage))
}
completion(.success(image))
}.resume()
}
let imageView = UIImageView()
dowloadImage(from: URL(string: "https://res.cloudinary.com/demo/image/upload/sample.jpg")!) { [weak imageView] result in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let image):
imageView?.image = image
}
}
imageView.frame = .init(origin: .zero, size: .init(width: 300, height: 300))
PlaygroundPage.current.liveView = imageView
PlaygroundPage.current.needsIndefiniteExecution = true
I'm woking on a project in swift 3.0 where I cache the response from the server by using NSCache as to populate them in a UITableView. However for some reason I'm only seeing few images loading when the app loads for the first time, but if If i scroll and come back I see everything (end of retrieving the response from the server I reload my tableview too, but seems that not the case). I'm not sure what I''m exactly missing here, the code as bellow as to show how I cache the images.
let imageCache = NSCache<AnyObject, AnyObject>()
var imageURLString : String?
extension UIImageView {
public func imageFromServerURL(urlString: String) {
imageURLString = urlString
if let url = URL(string: urlString) {
image = nil
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if error != nil{
print(error as Any)
return
}
DispatchQueue.main.async(execute: {
if let imgaeToCache = UIImage(data: data!){
if imageURLString == urlString {
self.image = imgaeToCache
}
imageCache.setObject(imgaeToCache, forKey: urlString as AnyObject)// calls when scrolling
}
})
}) .resume()
}
}
}
I think this would be a better approach using subclassing rather than extension, (taking help from Jageen's comment, as we cannot contain stored properties inside extension so we use the idea of encapsulation)
let imageCache = NSCache<AnyObject, AnyObject>()
class CustomImageView: UIImageView {
var imageUrlString: String?
func loadImageUsingUrlString(_ urlString: String) {
let url = URL(string: urlString)
imageUrlString = urlString
image = nil
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
if self.imageUrlString == urlString {
self.image = imageToCache
}
imageCache.setObject(imageToCache!, forKey: urlString as AnyObject)
}
}.resume()
}
}
-Now use this subclass as the type of imageViews that you are showing on the screen
Here the images are downloading and stored in cache just fine. The problem lies in the updation of tableview cells.
When the table view is loading the cells on to the table the images are not downloaded yet. But once the image is downloaded we have to selectively update the cell so that the image is displayed instantly.
Since you are scrolling , the tableview calls 'cellForRowatIndexpath' again which updates the cell showing the downloaded images while scrolling.
If you still wish to use the extension , I suggest you add the tableView and indexpath as the parameters so that we can call reload specific row and have the view updated instantly.
I have updated the table reload code and structure of the function defined in extension. Let me know how it goes.
let imageCache = NSCache<AnyObject, AnyObject>()
var imageURLString : String?
extension UIImageView {
public func imageFromServerURL(urlString: String, tableView : UITableView, indexpath : IndexPath)) {
imageURLString = urlString
if let url = URL(string: urlString) {
image = nil
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if error != nil{
print(error as Any)
return
}
DispatchQueue.main.async(execute: {
if let imgaeToCache = UIImage(data: data!){
if imageURLString == urlString {
self.image = imgaeToCache
}
imageCache.setObject(imgaeToCache, forKey: urlString as AnyObject)// calls when scrolling
tableView.reloadRows(at: [indexpath], with: .automatic)
}
})
}) .resume()
}
}
Saving Images in UIImageView Swift 5 with Xcode 14.1 and above through URLCache :-
class CacheImageView: UIImageView {
let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
var diskCacheURL:URL {
self.cachesURL.appendingPathComponent("DownloadCache")
}
var cache:URLCache {
URLCache(memoryCapacity: 10_000_000, diskCapacity: 1_000_000_000, directory: diskCacheURL)
}
var session:URLSession {
let config = URLSessionConfiguration.default
config.urlCache = cache
return URLSession(configuration: config)
}
func downloadImageFrom(urlString: String, imageMode: UIView.ContentMode) {
guard let url = URL(string: urlString) else { return }
downloadImageFrom(url: url, imageMode: imageMode)
}
func downloadImageFrom(url: URL, imageMode: UIView.ContentMode) {
contentMode = imageMode
let req = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
self.session.dataTask(with: req) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async {
let imageToCache = UIImage(data: data)
self.image = imageToCache
}
}.resume()
}
}
Uses:
var imageViewAstronomy: CacheImageView = CacheImageView()
imageViewAstronomy.downloadImageFrom(urlString: yourStringUrlOfImage, imageMode: .scaleAspectFit)
I know there are many answers about this error but I cannot seem to figure it out. I am receiving the error Thread 1: EXC_BAD_ACCESS (code=257, address=0x200000003) on the line where the function is called. My table cell view controller is as follows.
import UIKit
class NewsTableViewCell: UITableViewCell {
#IBOutlet weak var postImageView: CustomImageView!
#IBOutlet weak var postTitleLabel:UILabel!
#IBOutlet weak var authorLabel:UILabel!
#IBOutlet weak var dateLabel: UILabel!
var article: Article? {
didSet {
postTitleLabel.text = article?.title
authorLabel.text = article?.author
dateLabel.text = article?.date
setupArticleImage()
}
}
func setupArticleImage() {
postImageView.loadImageUsingUrlString("http://theblakebeat.com/uploads/873463.jpg")
}
This code calls the function loadImageUsingUrlString, which is located in my extensions.swift file. It is called for each table view cell that is loaded in order to load its image. The code for extensions.swift is as follows.
import UIKit
let imageCache = NSCache()
class CustomImageView: UIImageView {
var imageUrlString: String?
func loadImageUsingUrlString(urlString: String) {
imageUrlString = urlString
let url = NSURL(string: urlString)
image = nil
if let imageFromCache = imageCache.objectForKey(urlString) as? UIImage {
self.image = imageFromCache
return
}
NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: { (data, respones, error) in
if error != nil {
print(error)
return
}
dispatch_async(dispatch_get_main_queue(), {
let imageToCache = UIImage(data: data!)
if self.imageUrlString == urlString {
self.image = imageToCache
}
imageCache.setObject(imageToCache!, forKey: urlString)
})
}).resume()
}
}
Thank you in advance for any help.
You did not set the class CustomImageView to the postImageView in the IBInspector:
Your extensions.swift is in the right Target?
In XCode select the file in Project Navigator and check in File Inspector the section Target Membership