9

I have setup images on my app to work with firebase UI so that I can cache them easily, by following the docs https://firebase.google.com/docs/storage/ios/download-files

My code looks something like this

    guard let profileImageUrl = user?.url else { return }
    profileImageView.sd_setImage(with: profileImageUrl)

I am overwriting the image location to update the image like this

let storageRef = Storage.storage().reference(withPath: "/users/\(uid)/profilePhoto.jpg")
storageRef.putData(uploadData, metadata: nil, completion: { (metadata, err) in

If I go to the photo on a new device with no cache yet, it shows the new image. However, if i go to the image on a device that previously viewed it I am left seeing the old image.

EDIT: So I understand the cache works through SDWebImage. I am generating a storage reference to use with SDWebImage based on the users uid. The storage reference is therefore not changing, which is causing the issue.

user6520705
  • 705
  • 3
  • 11
  • 38
  • The Firebase Storage SDK doesn't cache images, so it seems like this is done by [SDWebImage](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage). For information on its caching behavior, see https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-cache-50 and https://stackoverflow.com/questions/41360747/how-can-i-get-the-data-of-cached-images-sdwebimage – Frank van Puffelen Jun 25 '19 at 19:05
  • The question is pretty unclear; the first code segment shows setting an image via a URL which appears unrelated to the question. The second section shows how you're uploading an image to storage, which again is unrelated to the question about cacheing an image. If you're cacheing an image can you show that code please? And then indicate how you are refreshing the cache so it will stay current - a cache is just that a cache, it's data is static and will become stale unless it's refreshed. Also, Firebase Storage doesn't cache images. – Jay Jun 25 '19 at 19:07
  • @jay I was a little confused earlier but Frank van Puffelens comment helped me understand a bit better how this works. I am using a firebase storage reference rather than a url, to set and cache the image using SDWebImage. That way I can generate the reference from the users uid on the fly without having to always query the database for a download url – user6520705 Jun 25 '19 at 20:04
  • A Firebase Storage Reference *is* a URL. It's a reference to where that file is stored in Firebase Storage. It in itself doesn't contain any data from the stored file which is why it's lightweight. You can think of it as a pointer to the actual data, not the data itself. It will not have anything to do with caching a file. You many want to read through the docs so you don't go down the wrong path or make an assumption that's incorrect. [Creating a storage reference on iOS](https://firebase.google.com/docs/storage/ios/create-reference). – Jay Jun 25 '19 at 21:48
  • That being said, take a look at Storage [download in memory](https://firebase.google.com/docs/storage/ios/download-files#download_in_memory) and also the download a local file section. If the file is small, like a thumbnail, just download in memory and use it like a variable. If it's larger, download a file and *then* keep a reference to where it's stored on disk - this is more of a caching technique that will alleviate having to download it from Storage every time you want to use it. BUT, if the images may change, you need to be notified that it changed so you can update the local file. – Jay Jun 25 '19 at 21:51

4 Answers4

3

When you load an image using SDWebImage, it caches the image forever and won't request it from network next time. when you upload a new image from another device the url of the image stays the same i.e. SDWebImage on previous device doesn't know the image has been updated. So you need to come up with an idea on how to notify previous device about the change.

below is how I think it should be done:

When you upload an image use a dynamic picture name. so that the url gets changed thus SDWebImage will load the new profile picture.


In the comments, you asked how would I reference to this dynamic url. You've two possible options(That I can think of):

1) using firebase database: In this we will create a node/reference on database like user/uid. In this we will store the dynamic imageurl with key image. on Any device when yo want to load the image on device you would read the url from the database. It will always point to new image url A nice example on how to do it is on this SO POST

Breif: User-> Upload Image-> Save the Url to Database

Read Picture Url from Firebase -> Load it via SDWebImage UIImageView

2) Using a text file: In this we will not use database. we will create a log file under same folder users/uid/log.txt file. whenever you upload an image to storage we will save the url in this text file and upload it in storage too. When you want to load the image then you first read the text file to get the imageUrl and then load the image using SDWebImage. This way is much similar to uploading and downloading the image file with one additional step that you have to read the text file to get the storage.

Brief: User-> Upload Image -> Put the url in text file and upload it too

Download the text file -> read the image url from file -> load the image via SDWebImage to UIImageVIew


I was reading the docs and found another way. This way you don't need to use dynamic url. you can use your static url. In this you need to query the metadata of the image:

// Create reference to the file whose metadata we want to retrieve
let forestRef =  Storage.storage().reference(withPath: "/users/\(uid)/profilePhoto.jpg")

// Get metadata properties
forestRef.getMetadata { metadata, error in
  if let error = error {
    // Uh-oh, an error occurred!
  } else {
    // Metadata now contains the metadata for 'profilePhoto.jpg'  
  }
}

File metadata contains common properties such as name, size, and contentType (often referred to as MIME type) in addition to some less common ones like contentDisposition and timeCreated.

Taken From

Whenever you query metadata save the timeCreated in UserDefaults and next time compare the timeCreated with saved timeCreated if there is difference remove image from SDWebImage Cache and then ask SDWebImage to load the image again.

Sahil Manchanda
  • 9,812
  • 4
  • 39
  • 89
  • If I am generating a firebase storage reference to set and cache the image with sdwebimage rather than a url how would I do something like this – user6520705 Jun 25 '19 at 20:05
  • @user6520705 I didn't understand your comment. can you please explain a bit more – Sahil Manchanda Jul 04 '19 at 11:06
  • I am using firebase storage reference which is unique to each user to get the image so the reference is always the same for the user. – user6520705 Jul 04 '19 at 15:29
  • Currently I use /users/\(uid)/profilePhoto.jpg as the reference, if I replace the image name with a dynamic one how can I reference this location in the future – user6520705 Jul 04 '19 at 15:36
  • @user6520705 I've updated my answer with possible options to save the imageUrl for easy access on every device. let me know if you've other question. would love to answer – Sahil Manchanda Jul 04 '19 at 17:33
1

The reason why your cache isn't refreshing is because you're always writing to the same path, you're just uploading a different image. I see you've been asking about creating a dynamic URL which will, in theory, create an image path that's not cached and therefore reload correctly.

But you don't need to do that. I had the same case and issue recently while working on an app that used Firebase. I had a player model which contained within itself a reference to the profile picture. The picture was stored using Firebase Storage and the folder path was /players/\(uid)/profilePhoto.jpg. The profile picture could be changed both by purchasing an avatar and using the camera to snap a photo. And since I was uploading to the same storage path every time, my image wasn't reloading either.

Now, SDWebImage cache is automatic - unless you specify differently, whenever you call someImageView.sd_setImage(with: imageUrl) the URL is cached via SDImageCache.

So all I do is delete the image from the cache whenever I get a new reference - in the didSet property observer of the profile picture.

    /// property to store the profile picture url
    internal var profilePictureUrl: String = "" {
      didSet {
        SDImageCache.shared().removeImage(forKey: Storage.storage().reference().child(profilePicture).fullPath, withCompletion: nil)
      }
    }

Now, every time the property is set, the url is deleted from cache and SDWebImage has to fetch it again. It does so when you call sd_setImage(with: imageUrl) and caches it again.

    /// public property to set the profile image on the button
    internal var profileImagePath: String? {
      didSet {
        if let path = profileImagePath {
          profileButton.imageView?.sd_setImage(with: Storage.storage().reference().child(path), placeholderImage: nil, completion: { [weak self] (image, error, _, _) in
            self?.profileButton.setImage(image ?? #imageLiteral(resourceName: "default-profile-pic"), for: .normal)
          })
        }
      }
    }

I have a button that displays my profile picture in its image view. When I get the new image and call a method to refresh the button's view the image is fetched and since the cache doesn't have a reference to the image path anymore, it'll fetch the newly uploaded image and recreate the cache path.

That's how I made this work for me. I hope that it solves your issue, as well. Feel free to ask for any additional clarifications and I will do my best to answer.

Edit: Sahil Manchanda pointed out to me that I'd misunderstood the full scope of the question.

My answer will work best on a single device.

If the issue is multiple device synchronisation you can still use the offered solution but with some more frequent UI updates - since the profilePictureUrl property gets updated every time the user's info is fetched from Firebase, the update may be delayed until the next time the user's info is read or the app is restarted.

You can also add a connectivity check before deleting the cache to avoid any losses caused by laggy bandwidth.

Another solution could be the use of silent notifications (meant for quick refresh operations) which will just serve as a "reminder" to the device that it's time to clear the cache. You can use Firebase's Cloud Messaging if you don't have any experience with servers or OneSignal.

  • You are going in the right direction and beautifully addressed the 'edit' part of the question. But the op is facing a bit different problem. – Sahil Manchanda Jul 05 '19 at 08:17
  • You've two devices A & B. You are logged in on both devices. You open the app on device A, it fetches and displays the profile picture. Now you open the app on device B. It fetches and displays the profile picture too. On Device B, you uploaded a new picture and with the help of your answer you successfully update the image on device B. but what about device A. it still shows the old picture. **How would you inform the device A that profile picture has been changed and update it.** – Sahil Manchanda Jul 05 '19 at 08:17
  • Well, in my case it wouldn't be a problem because I do the cache deletion every time I read the player 's info from Firebase. So when the user opens the app on device A the property's `didSet` will be called, the cache deleted and the image reloaded. If op is worried about caching when internet is down in a similar situation, a simple connectivity check before deleting the cache can solve that problem. – Teodora Georgieva Jul 05 '19 at 09:17
  • Other than that, maybe a silent notification can do the trick. Or dynamic picture urls, but then you'll need to store the image name somewhere on the device and will have to find a way to periodically empty the Firebase Storage folder so it doesn't get cluttered with old photos. – Teodora Georgieva Jul 05 '19 at 09:21
  • Silent Notification seems good. Please update your answer to include the possible solutions to solve this problem – Sahil Manchanda Jul 05 '19 at 09:27
  • @SahilManchanda and Teodora Georgieva would a solution where setting the profile image like this work? storageReference.downloadURL { (url, err) in self.profileImageView.sd_setImage(with: url) } or would this defeat the purpose of caching ? – user6520705 Jul 05 '19 at 14:47
  • @user6520705 I don’t see how this is supposed to help with notifying other devices of the change. That’s the issue here, right? How do you notify a device that already has cached the image that that same URL has a different image attached to it? How do you expect this new method to help? If it generates a new URL for the same reference, then yes, that means that caching isn’t doing anything. You're downloading the image every time. You’re better off just disabling it instead of jumping through these hoops. I haven't used this method though, so my answer isn't from experience. – Teodora Georgieva Jul 08 '19 at 07:43
1

After a time I found another better way, cause it uses only one network request than my previous solution. Now I'm using direct url to image in a bucket. And then I load it with Nuke(image loading and caching lib) that's respect native HTTP caching mechanism.


extension StorageReference {
    var url: URL {
        let path = fullPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
        return URL(string: "https://firebasestorage.googleapis.com/v0/b/\(bucket)/o/\(path)?alt=media")!
    }
}

Nuke.loadImage(with: imgRef.url, into: imageView)

MY OLD ANSWER:

It retrieves metadata from Storage and compares the creation date on the server with the creation date of the cached image. If cache outdated it will recached image from the server.

extension UIImageView {

    func setImage(with reference: StorageReference, placeholder: UIImage? = nil) {
        sd_setImage(with: reference, placeholderImage: placeholder) { [weak self] image, _, _, _ in
            reference.getMetadata { metadata, _ in
                if let url = NSURL.sd_URL(with: reference)?.absoluteString,
                    let cachePath = SDImageCache.shared.cachePath(forKey: url),
                    let attributes = try? FileManager.default.attributesOfItem(atPath: cachePath),
                    let cacheDate = attributes[.creationDate] as? Date,
                    let serverDate = metadata?.timeCreated,
                    serverDate > cacheDate {

                    SDImageCache.shared.removeImage(forKey: url) {
                        self?.sd_setImage(with: reference, placeholderImage: image, completion: nil)
                    }
                }
            }
        }
    }

}
Artem Sydorenko
  • 353
  • 4
  • 8
0

I was facing the same issue and solved it with the latest version so thought to add my answer here to help others.

SDWEBImage updated with a new feature for those images which will update on the same URL or path.

Feature Details Summary:

Even if the image is cached, respect the HTTP response cache control, and refresh the image from a remote location if needed. The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation. This option helps deal with images changing behind the same request URL, e.g. Facebook graph API profile pics. If a cached image is refreshed, the completion block is called once with the cached image and again with the final image. Declaration

SDWebImageRefreshCached = 1 << 3

Use this flag only if you can't make your URLs static with embedded cache busting parameters.

Declared In : SDWebImageDefine.h

So In short if we want to update the cached image which will update in the future on the same URL we can use the below method.

Image URL With Completion Block:

imageView.sd_setImage(with: <URL>,
                      placeholderImage: placeholderImage,
                      options: .refreshCached)
                { (image, error, type, url) in
                    <CODE>
                }

Image URL Without Completion Block:

imageView.sd_setImage(with: <URL>,
                      placeholderImage: placeholderImage,
                      options: .refreshCached)

Firebse Reference with completion block:

imageView.sd_setImage(with: reference,
                      maxImageSize: <MaxSize>,
                      placeholderImage: placeholderImage,
                      options: .refreshCached)
                { (image, error, type, ref) in
                    <#code#>
                }

Firebse Reference Without completion block:

imageView.sd_setImage(with: reference,
                      maxImageSize: <MaxSize>,
                      placeholderImage: placeholderImage,
                      options: .refreshCached)

So kudos to the SDWebImage team for adding this amazing feature into lib to make our life easy.

Hope this answer can help others.

CodeChanger
  • 7,953
  • 5
  • 49
  • 80