dmytro-anokhin / url-image

AsyncImage before iOS 15. Lightweight, pure SwiftUI Image view, that displays an image downloaded from URL, with auxiliary views and local cache.
MIT License
1.1k stars 96 forks source link

Image cache size total #160

Closed xoniq closed 2 years ago

xoniq commented 3 years ago

Hi,

Thank you for this wonderful library. One feature I would like to add for the benefit of the user is the total cache size. I would like the user to see how much space is used for photo caching, and then making the user able to clear it when desired.

I found this issue #33 which almost got to the point where we could do this, but got stale. Following up on that feature request, is there a quick way outside of the plugin to gather the used space from the cache dir? I've tried many snippets online but none of them is giving me something.

// Provided in the issue #33
let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let directoryURL = cachesURL.appendingPathComponent("URLImage", isDirectory: true)

// Random script found online, goes directly to the 'catch'
do{
   let resources = try directoryURL.resourceValues(forKeys:[.fileSizeKey])
   let fileSize = resources.fileSize!
   print ("\(fileSize)")
}catch{
   print("Error on file size")
}

Side question (but related) to not clutter the issue list:

Is it possible to add a print during debug when an image is being showed from the cache instead of being downloaded, to ensure the cache works? (Given the fact that the app size on the iPhone doesn't really increase, I want to make sure caching does what it should do)

dmytro-anokhin commented 3 years ago

Hi,

the directory is still in the caches folder and named "URLImage". If you don't see it - offline cache is not enabled.

To enable logs set log_detail = log_all in Log.swift file.

xoniq commented 3 years ago

Am I doing something wrong in the next example? I commented after the line it prints.

This is my scene delegate:

var body: some Scene {
    let urlImageService = URLImageService(fileStore: URLImageFileStore(), inMemoryStore: URLImageInMemoryStore())

    WindowGroup {
        HomeView()
            .environment(\.urlImageService, urlImageService)
            .onOpenURL(perform: { url in
                print("onOpenURL: perform")
                ApplicationDelegate.shared.application(UIApplication.shared, open: url, sourceApplication: nil, annotation: UIApplication.OpenURLOptionsKey.annotation)
            })
    }
}

And this is what I call in HomeView:

let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let directoryURL = cachesURL.appendingPathComponent("URLImage", isDirectory: true)

do{
    let resources = try directoryURL.resourceValues(forKeys:[.fileSizeKey])
    print(resources)
    if resources.fileSize != nil{
        let fileSize = resources.fileSize!
        print ("\(fileSize)")
    }else{
        print("Filesize is nil") // This one triggers, even if a previous run has loaded images
    }
}catch{
    print("Error on file size")
}

Hi,

the directory is still in the caches folder and named "URLImage". If you don't see it - offline cache is not enabled.

To enable logs set log_detail = log_all in Log.swift file.

I did set it to

private let log_detail = log_all

But I don't see anything in the log output from Xcode.

dmytro-anokhin commented 2 years ago

From what I understand: code works as expected. It's just fileSize is only applicable to regular files. You need to traverse the directory (and subdirectories) to sum up file sizes.

Regarding logs: I'm not sure why it's not working for you, I'm using it all the time.

xoniq commented 2 years ago

Alright I got the cache size collection working.

For future reference; I found a snippet online for extending the URL component using this:

extension URL {
    /// check if the URL is a directory and if it is reachable
    func isDirectoryAndReachable() throws -> Bool {
        guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else {
            return false
        }
        return try checkResourceIsReachable()
    }

    /// returns total allocated size of a the directory including its subFolders or not
    func directoryTotalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
        guard try isDirectoryAndReachable() else { return nil }
        if includingSubfolders {
            guard
                let urls = FileManager.default.enumerator(at: self, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil }
            return try urls.lazy.reduce(0) {
                    (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
            }
        }
        return try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil).lazy.reduce(0) {
                 (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
                    .totalFileAllocatedSize ?? 0) + $0
        }
    }

    /// returns the directory total size on disk
    func sizeOnDisk() throws -> String? {
        guard let size = try directoryTotalAllocatedSize(includingSubfolders: true) else { return nil }
        URL.byteCountFormatter.countStyle = .file
        guard let byteCount = URL.byteCountFormatter.string(for: size) else { return nil}
        return byteCount
    }
    private static let byteCountFormatter = ByteCountFormatter()
}

Then on the area where the cache size need to be displayed:

var cacheSizeLocal = ""
do {
    let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let directoryURL = cachesURL.appendingPathComponent("URLImage", isDirectory: true)
    if let sizeOnDisk = try directoryURL.sizeOnDisk() {
        cacheSizeLocal = sizeOnDisk // Example: 3.15 GB
    }
} catch {
    print(error)
}
print(cacheSizeLocal)

In my case it returns 17.4 MB so its working now.

Just tested it with cache removal, which works fine. First was 17.4 MB, then I tapped the clear button in my app which runs the following:

let urlImageService = URLImageFileStore()
urlImageService.removeAllImages()

After that it reloads the cache count function, and the response now is 401 KB

dmytro-anokhin commented 2 years ago

Thank you for sharing code snippet. Note: directory won't be completely empty because removeAllImages deletes image files and records, not the database used to index files.