Image Caching – How to control when images are disposed? - swift

I need to cache images on disk but to limit them say to 20 images. I am trying Nuke library. When I run
Nuke.loadImage(with: url, options: options, into: imageView)
image is cached as long as I am inside my View Controller. When I leave the views, next time images are being fetched again. So how do I make Nuke (or other lib) save those images for specific image count or time.
Before trying Nuke I was just saving images to Documents folder of the app every time I fetch image. I am sure there is a better way.
Update: I was able to do it with Kingfisher.
func getFromCache(id: String) {
ImageCache.default.retrieveImage(forKey: id, options: nil) {
image, cacheType in
if let image = image {
self.galleryImageView.image = image
} else {
print("Not exist in cache.")
self.loadImage()
}
}
}
private func loadImage() {
ImageDownloader.default.downloadImage(with: url, options: [], progressBlock: nil) {
(image, error, url, data) in
if let image = image {
self.galleryImageView.image = image
ImageCache.default.store(image, forKey: id, toDisk: true)
}
}
}
If I understand correctly, first retrieveImage fetches images from disk cache. Later from memory cache. And it frees them only when memory warning received. I hope it does. God help us.

There are several advanced options for image caching. You can get a direct access to Memory Cache (default Nuke's ImagePipeline has two cache layers):
// Configure cache
ImageCache.shared.costLimit = 1024 * 1024 * 100 // Size in MB
ImageCache.shared.countLimit = 20 // You may use this option for your needs
ImageCache.shared.ttl = 90 // Invalidate image after 90 sec
// Read and write images
let request = ImageRequest(url: url)
ImageCache.shared[request] = image
let image = ImageCache.shared[request]
// Clear cache
ImageCache.shared.removeAll()
Also, use ImagePreheater class. Preheating (a.k.a. prefetching) means loading images ahead of time in anticipation of their use. Nuke lib provides an ImagePreheater class that does just that:
let preheater = ImagePreheater(pipeline: ImagePipeline.shared)
let requests = urls.map {
var request = ImageRequest(url: $0)
request.priority = .low
return request
}
// User enters the screen:
preheater.startPreheating(for: requests)
// User leaves the screen:
preheater.stopPreheating(for: requests)
You can use Nuke lib in combination with Preheat library which automates preheating of content in UICollectionView and UITableView. On iOS 10.0 and above you might want to use new prefetching APIs provided by iOS instead.

Related

Moving images to separate folder Xcode

I've been following Apple's own tutorials and currently trying out stuff with these files: https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation
I'm checking how to get images, but for my test it will be about 100images therefore I want them in a separate folder so it does not make the resource folder messy. So instead of all the images being placed in Resource, I made a folder called "img" inside resource.
This made Xcode throw an error saying it couldn't find the images. Even after I commented out all references to all images it still throws and error saying: "/Downloads/BuildingListsAndNavigation/Complete/Landmarks/Landmarks/Resources/umbagog.jpg: No such file or directory"
Here is Apple's own code, Models/Data.swift:
final class ImageStore {
typealias _ImageDictionary = [String: CGImage]
fileprivate var images: _ImageDictionary = [:]
fileprivate static var scale = 2
static var shared = ImageStore()
func image(name: String) -> Image {
let index = _guaranteeImage(name: name)
return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))
}
static func loadImage(name: String) -> CGImage {
guard
let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
fatalError("Couldn't load image \(name).jpg from main bundle.")
}
return image
}
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
if let index = images.index(forKey: name) { return index }
images[name] = ImageStore.loadImage(name: name)
return images.index(forKey: name)!
}
}
So my question is, how do I make it store and get images in the resource/img folder instead?
tl;dr
Clean your project after making structural changes. Use Shift + Command + K, then build your project Command + R
Looking at the tutorial, there isn't really much you have to do to get it to read the images from a different folder. What is important is that the images are uniquely named and contained within the app's bundle.
Inside the Data.swift file, there is a function called loadImage(name:) I have copied it here so you can easily read it.
static func loadImage(name: String) -> CGImage {
guard
let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
fatalError("Couldn't load image \(name).jpg from main bundle.")
}
return image
}
They key part of this is let url = Bundle.main.url(forResource: name, withExtension: "jpg"). This looks through your whole app bundle for a resource named name with the specified file extension. Once it finds it it returns the url of the resource in the bundle. That url is then used to open the image.
Downloading the tutorial project from the above link (and using the completed project); moving the images to a folder called img inside the Resources folder does not break the application. And that is all you have to do. You won't need to rename any files inside your project. Which is nice.
One thing that you will need to do, and it is probably why you are getting that error, is clean your project before rebuilding it, especially if you make structural changes. You can do that by going to to the menu Product -> Clean Build Folder, or you can also do it by pressing Shift + Command + K
Here is what is inside my Xcode project after moving the files.
Here is what is inside my directory
Why should you use the assets catalogue?
The reason for using the assets catalogue is that it allows you to have different sizes of your image, this means that big screen devices can show nice big images without loss of resolution. It also allows for app thinning, this is a process where assets that are not required by specific devices are stripped out, meaning that your app size is significantly smaller.
Asset catalogues can also have different folders within and you can have different catalogues, making it much easier to manage the assets that you have in your app.
In the resource folder there is a JSON file called landmarkData. The app is able to get the images from this file because the images are local to this file. When you added a new folder, all of the paths were likely messed up. In each data set of the JSON file set there is an “imageName”: image. Reformat the images like this if you really need the new folder:
“imageName”:”img/theImageName”
Hope that helps! You can always not use a new folder, but I can understand why you want it.

Swift: Get the correct file size for JPEG image

I have a UIImage object, say from the camera roll via PHAsset. The image is saved as a .jpg file:
asset.requestContentEditingInput(with: nil) { (input, nil) in
print(input?.fullSizeImageURL) // somefile.jpg
}
To get the file size should not data.count from this return the correct file size in bytes?
PHImageManager.default().requestImageData(for: asset, options: nil) { data, _, _, _ in
if let _data = data {
print(_data.count) // 6759240
}
}
The output for a particular image is 6759240 while fileSize() returns 2978548.0 (which is the right file size) bytes.
func fileSize(forURL url: Any) -> Double {
var fileURL: URL?
var fileSize: Double = 0.0
if (url is URL) || (url is String)
{
if (url is URL) {
fileURL = url as? URL
}
else {
fileURL = URL(fileURLWithPath: url as! String)
}
var fileSizeValue = 0.0
try? fileSizeValue = (fileURL?.resourceValues(forKeys: [URLResourceKey.fileSizeKey]).allValues.first?.value as! Double?)!
if fileSizeValue > 0.0 {
fileSize = (Double(fileSizeValue))
}
}
return fileSize
}
Does it mean someUIImage?.jpegData(compressionQuality: 1)?.count does not return the correct size of JPEG image file (if saved)?
One more thing, Is there any way to determine the image file size before writing it on the disk?
All of these is to compare the file size between the original and compressed image.
This sounds like a misunderstanding of what the various terms and calls refer to.
You have no direct access to a file stored in the user's Photo library. There may in fact be no such file; you should make no assumptions about the storage format. When you ask PHImageManager for an image's data, you are given the bitmap data, ready for use. Thus you should expect this data to be big, in exact proportion to the dimensions of the image. 6759240 is more than 6MB, which sounds about right on an older iPhone; a newer iPhone, takes 4032x3024 photos which is more than 8MB.
Then in a different part of your code you call fileSize(forURL:). Now you're looking at an actual file, in the file system, in a place where you can access it. If this is an image file, it is compressed; just how much it is compressed depends on the format. 2978548 is about 3MB which is pretty good for a JPEG compressed without too much lossiness.
Finally, you ask about UIImage jpegData(compressionQuality: 1)?.count. You do not show any code that actually calls that. But this is data ready for saving as a file directly with write(to:) and a URL, and I would expect it to be the same as fileSize(forURL:) if you were to check the very same file later.

Swift, Kingfisher Cache not working properly?

I am trying to cache images using the Cocoapod KingFisher, the code i am using does display the image from the database storage but it does no caching. I am curious as to know why?
The print always says "cache Result none". And i also notice that the images are not cached.
Code for calling the imageDownloader:
DownloadImage(imageId : nextUser.id, cardImage: secondProfilePic)
Code for downloading and caching, also for checking if cached.
func DownloadImage(imageId : String, cardImage : UIImageView){
let imagesStorageRef = Storage.storage().reference().child("profilepic/").child(imageId)
//Get URL For Cache
imagesStorageRef.downloadURL { url, error in
if let error = error {
// Handle any errors
cardImage.image = UIImage(named: "RentOutProfilePic")
print("Error")
} else {
// Get the download URL for '.jpg'
let pathURL = url
print("Sets Image")
cardImage.kf.indicatorType = .activity
cardImage.kf.setImage(with: pathURL,
options: [
.transition(.fade(0.3)),
.cacheOriginalImage
])
}
if let url = url{
let tempUrl:String = url.path
let cache = ImageCache.default
let cached = cache.imageCachedType(forKey: tempUrl)
print("cache Result \(cached)")
}
}
}
Kingfisher is using url.absoluteString as the cache key for an image by default. So in your code, url.path will always return you the result of "not cached".
You are trying to print the cache result as the same time when you set the image. At the first time, your image would be in download progress so you always get .none even you set the key correctly according to 1. In the following invocation of this method with the same id, you should get a cache result either as disk or memory.
I am not sure how did you get the conclusion of "the images are not cached". Kingfisher is doing cache based on url by default. If you have different urls every time (which is returned from your imagesStorageRef) you call the image view setting method, there would be no matching cache and downloading would happen. If this is your case, you can customize to use the imageId as the cache key instead. To do that, you need to specify another cache key. See this wiki section for more.

Load UIImage from PHAsset Image absoluteString Swift 4

I am saving a list of file names/paths so I can load the image at a later time to upload it.
When the user selects the images from the camera roll, I get back this
file:///Users/admin/Library/Developer/CoreSimulator/Devices/B31CE61D-FB46-41F0-B254-B66B9335E1E4/data/Media/DCIM/100APPLE/IMG_0005.JPG
But when I try to load up the image,
if let image = UIImage(named: filepath) {
imageView.image = image
}
It doesn't load.
How do I load an image from a filepath?
The code I use to get the file path
func getURL(ofPhotoWith mPhasset: PHAsset, completionHandler : #escaping ((_ responseURL : URL?) -> Void)) {
let options = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
mPhasset.requestContentEditingInput(with: options, completionHandler: { (contentEditingInput, info) in
completionHandler(contentEditingInput!.fullSizeImageURL)
})
}
func add(images: [PHAsset]) {
for image in images {
getURL(ofPhotoWith: image) { (imgURL) in
if let imgURL = imgURL {
print ("ImageURL: \(imgURL.absoluteString)")
}
}
}
}
I am saving a list of file names/paths so I can load the image at a later time to upload it.
PHContentEditingInput is the wrong tool for that job. As the names of that class and the functions you're using to get one suggest, it's for content editing — tasks like applying a filter to an asset in the library.
When PHContentEditingInput gives you a file URL, it's granting you temporary access to that file. PhotoKit makes no guarantee that the asset in question will always be backed by a file at that URL, and even if it is, PhotoKit revokes temporary access to that URL when the owning PHContentEditingInput is deallocated.
A user's Photos library isn't a directory full of image files — it's a database, where each asset can have data resources stored in one or more files, which might or might not even be in local storage at all times. If you want to upload assets to an external service and preserve all the original data, you need an API that's meant for getting data resources. PhotoKit gives you two choices for that:
If you want just some image representation of the current state of the asset, use PHImageManager. This downloads and/or generates image data ready for you to save as a file, incorporating whatever edits the user has already applied to the asset:
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageData(for: myAsset, options: options) { data, uti, orientation, info in
// save `data` to file / upload to web service
// use `uti` to find out what file type it is
}
If you want the original image data resources — that is, enough data that you could back up and restore the asset, including features like in-progress edits, Live Photo modes, and RAW format image data — use PHAssetResource and PHAssetResourceManager:
let resources = PHAssetResource.resources(for: myAsset)
let options = PHAssetResourceRequestOptions()
options.isNetworkAccessAllowed = true
for resource in resources {
let outputURL = myOutputDirectory.appendingPathComponent(resource.originalFilename)
PHAssetResourceManager.default().writeData(for: resource, to: outputURL, options: options) { error in
// handle error if not nil
}
}
I am saving a list of file names/paths so I can load the image at a later time to upload it when the user selects the images from the camera roll
Don't. The thing to save so that you can retrieve something from the camera roll at a later time is the PHAsset's localIdentifier. You can use that to get the same asset again later, and now you can ask for the associated image.

Firebase Storage retrieval images

I'm trying to get my head around Firebase Storage.
I've seen online and tried two methods of getting my images.
What is the difference between these two? (both work).
So after I get the photoUrl from my Firebase Database:
1.
if let data = NSData(contentsOfURL: NSURL(string:photoUrl)!)
{
let myImage = UIImage(data: data)!
MyImageCache.sharedCache.setObject(myImage, forKey: self.key)
//etc
}
2.
self.storage.referenceForURL(photoUrl).dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil)
{
print(error)
}
else
{
let myImage = UIImage(data: data!)
MyImageCache.sharedCache.setObject(myImage!, forKey: self.key)
//etc
}
}
Regarding the first method, you shouldn't be using that for network calls. From the docs:
Do not use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated. Instead, for non-file URLs, consider using the dataTaskWithURL:completionHandler: method of the NSURLSession class. See URL Session Programming Guide for details.
The second method is baked into the firebase framework and provides you with convenience methods for downloading an image, i.e. it gives you the option to specify images size. This is probably optimised for getting images and is in most scenarios the preferred method.