简体   繁体   中英

Firebase Storage Image Cache not updating SWIFT

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.

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 ie 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.

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.

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)
                    }
                }
            }
        }
    }

}

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, eg 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM