I'm trying to understand the way to use [AlamofireImage][1]
i have a few questions:
when i use:
Alamofire.request("https://httpbin.org/image/png").responseImage { response in
if let image = response.result.value {
//Do Somthing
}
}
is that all i need to do to use swift native URLCache?
Does it caching it automatically?
after the first request above, can i use AlamofiremImage fetch method?
// Fetch
let cachedImage = imageCache.image(withIdentifier: "https://httpbin.org/image/png")
or do i have to use URLCache cachedResponse(for: URLRequest) method?
(if the answer to 2 is "no") Do i need to actively save the image cache with alamfireImage Add method after the request response?
// Add
imageCache.add(image, withIdentifier: "https://httpbin.org/image/png")
(with relation to question 1) isn't it creating additional cache to the same image and url (once with URLCache and another with AlamofireImage add method)?
thanks!
Related
I'm creating an application in ios where I load images from an api using a UITableView and UITableViewCell.
Since the UITableView reuses cells, old images were appearing when I scroll fast. In order to prevent this, I set a default image using a system image(SF symbols).
I also use a cache to store urls to images.
Everything works as it should but now I think of it I'm sending a network request to retrieve that systemImage each time which seems incredibly inefficient since I was using a cache in order to reduce the total network calls in the first place.
Is there way around this or is this a tradeoff I must make?
Code is below.
//use default image from SF symbols
let defaulticon = UIImage(systemName: "photo")?.withTintColor(.gray, renderingMode: .alwaysOriginal)
DispatchQueue.main.async {
cell.mealImage.image = defaulticon
}
guard cell.meal?.strMealThumb != nil else {
print("Category Image doesn't exist")
return
}
//use cache
if let imageData = model.imagecache.object(forKey: cell.meal!.strMealThumb as NSString) {
print("using cache")
DispatchQueue.main.async {
cell.mealImage.image = imageData
}
}
else {
let url = URL(string: cell.meal!.strMealThumb)
let session = URLSession.shared.dataTask(with: url!) { data, response, error in
if error == nil && data != nil {
let image = UIImage(data: data!)
//self.model.imagecache[cell.meal!.strMealThumb] = image
self.model.imagecache.setObject(image!, forKey: cell.meal!.strMealThumb as NSString)
DispatchQueue.main.async {
cell.mealImage.image = image
}
}
}
session.resume()
}
}
Override prepareForReuse method in UITableViewCell and add code in this function to clean up unrequited data that could persist from previous usage of the cell. In your example assign the default image in this function to produce better result.
You asked:
I set a default image using a system image(SF symbols).
...
Everything works as it should but now I think of it I'm sending a network request to retrieve that systemImage each time which seems incredibly inefficient since I was using a cache in order to reduce the total network calls in the first place.
No, UIImage(systemName:) does not make a network request. And it caches the image, itself, as the documentation says:
This method checks the system caches for an image with the specified name and returns the variant of that image that is best suited for the main screen. If a matching image object is not already in the cache, this method creates the image from the specified system symbol image. The system may purge cached image data at any time to free up memory. Purging occurs only for images that are in the cache but are not currently being used.
FWIW, you can empirically verify that this does not perform a network request disconnecting from the network and trying to use it. You will see it works fine, even when disconnected.
FWIW, there is a very small performance gain (less than a millisecond?) by keeping a reference to that tinted system image and reusing it, rather than fetching the cached system image and re-tinting it. But the performance improvement is negligible.
The code this and it crashes on "try!", but I don't know how to catch the error and it has it be explicit otherwise it won't work.
func downloadPicture2(finished: () -> Void) {
let imageUrlString = self.payments[indexPath.row].picture
let imageUrl = URL(string: imageUrlString!)!
let imageData = try! Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
The short answer is don't use try! - Use do/try/catch and recover from the problem in the catch clause.
For example -
func downloadPicture2(finished: () -> Void) {
cell.profilePicture.image = nil
if let imageUrlString = self.payments[indexPath.row].picture,
let imageUrl = URL(string: imageUrlString) {
do {
let imageData = try Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
}
catch {
print("Error fetching image - \(error)")
}
}
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
Now you have code that won't crash if the url is invalid or there is no network, but there are still some serious issues with this code.
Data(contentsOf:) blocks the current thread while it fetches the data. Since you are executing on the main thread this will freeze the user interface and give a poor user experience.
Apple specifically warns not to do this
Important
Don't use this synchronous initializer 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.
Rather, you should use an asynchronous network operations, such as a dataTask.
This code operates on cell - an external property. Once you move to asynchronous code you will probably be fetching images for multiple cells simultaneously. You should pass the relevant cell to this function to avoid clashes.
The use of the network isn't particularly efficient either; assuming this is part of a table or collection view, cells are reused as the view scrolls. You will repeatedly fetch the same image as this happens. Some sort of local caching would be more efficient.
If it is possible to use external frameworks in your project (i.e. your employer doesn't specifically disallow it) then I strongly suggest you look at a framework like SDWebImage or KingFisher. They will make this task much easier and much more efficient.
I want to retrieve the image that is stored in the storage of an user and place it next to his name in a custom UITableViewCell. The problem now is that the tableview will load when the images aren't done downloading (I think?), causing the application to crash because the image array is nil. So what is the correct way to load the tableview? I think, for the user experience, it is important that the tableviewcell image should be shown even if the images aren't done downloading, and present them a default image that is saved in the assists. I thought about making an array with UIImages that links to the default asset of loading an image and changing the image to the profile picture when it is done downloading. But I really have no clue how to do that. This is what I got so far about downloading the image:
let storage = FIRStorage.storage()
let storageRef = storage.reference(forURL: "link.appspot.com")
channelRef?.observeSingleEvent(of: .value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict{
let UIDs = each.value["userID"] as? String
if let allUIDS = UIDs{
let profilePicRef = storageRef.child((allUIDS)+"/profile_picture.png")
profilePicRef.data(withMaxSize: 1 * 500 * 500) { data, error in
if let error = error {
}
if (data != nil)
{
self.playerImages.append(UIImage (data: data!)!)
}
}
}
let userNames = each.value["username"] as? String
if let users = userNames{
self.players.append(users)
}
}
}
self.tableView.reloadData()
})
This is in the cellForRow
cell.playersImage.image = playerImages[indexPath.row] as UIImage
My rules, haven't changed it from the default rules:
service firebase.storage {
match /b/omega-towers-f5beb.appspot.com/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
Thank you.
Regarding user experience, you are correct. It is standard to have some sort of default image when loading an image from a URL. A great library to use for image caching and using default assets in its' place is AlamofireImage
Vandan Patel's answer is correct in saying you need to ensure your array is not nil when loading the tableview. You will be given a completion block to handle any extra work you would like to do with your image, using the AlamofireImage library.
This is all assuming you are getting a correct image URL back for your Firebase users.
You should call tableView.reloadData() when the images are done downloading. One important thing, initialize your playerImages as playerImages = [UIImage]() instead of playerImages: [UIImage]!. if it's empty, it wouldn't show your array is nil.
Update:
if let players = playerImages {
//code
}
I'm trying to present images from url in a collection view. So far I was using AFNetworking for this, but now I'm moving to Alamofire.
I can not find a proper way to present the image like I was doing with the method 'setImageWithURLRequest: placeholderImage: success:'
Do I need to use AFNetworking in order to do this right? Is there a Alamofire way to do this that I'm skipping?
Thanks in advance
Edit
I'm currently using AlamofireImage
Here is the code I'm using:
Alamofire.request(.GET, urlString)
.responseImage { response in
if let image = response.result.value {
self.imageView.image = image
}
}
My problem is that this code is inside 'cellForItemAtIndexPath' method, and every time I scroll the images are recharged again and again. This wasn't happening with AFNetworking
You want to use the UIImageView extension in Alamofire to set the image from a URL. There is some very detailed documentation in the README.
let imageView = UIImageView(frame: frame)
let URL = NSURL(string: "https://httpbin.org/image/png")!
imageView.af_setImageWithURL(URL)
Best of luck!
I have two different view controllers that both download the same JSON file from the Internet at runtime in viewDidLoad.
I want to prevent them from downloading the same file twice but instead, use the first JSON file downloaded by the initial view controller to pass it to the second view controller.
This is so that I can shorten my app loading time.
To achieve this goal, you need to make business logic such as downloading JSON independent from MVC's C(Controller).
Usually add a class file named XXXModel. Use this Model as a singleton. In this singleton you will need to implement functions of downloading and saving JSON data. You'd better save JSON data to local with a key named after its URL.
And in your Controllers, always call the singleton to download JSON.
You can create a singleton dictionary with url as key and bool as the value then save true for downloaded url. that way, you can keep track of which url's contents u have
var isDownloaded = [NSURL : Bool] // singleton in appdelegate
if let url = NSURL(string: urlString) {
if isDownloaded[url] != nil && !isDownloaded[url]! {
if let data = try? NSData(contentsOfURL: url, options: []) {
let json = JSON(data: data)
isDownloaded.updateValue(true, forKey: url) //mark it back to false if you delete the data for some reason.
}
} else {
//get it from memory
}
}