From 2368bb8d560388f1015b09312b39007ca98a6b6e Mon Sep 17 00:00:00 2001 From: Isvvc Date: Wed, 28 Apr 2021 15:10:35 -0600 Subject: [PATCH 1/3] Add option to return thumbnail first when fetching image Closes #31 --- Sources/WebDAV/WebDAV+Images.swift | 42 ++++++++++++++++++++++++++--- Sources/WebDAV/WebDAV.swift | 11 +++++++- Tests/WebDAVTests/WebDAVTests.swift | 27 ++++++++++++++++++- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/Sources/WebDAV/WebDAV+Images.swift b/Sources/WebDAV/WebDAV+Images.swift index 1d834a6..02b7356 100644 --- a/Sources/WebDAV/WebDAV+Images.swift +++ b/Sources/WebDAV/WebDAV+Images.swift @@ -67,6 +67,16 @@ public struct ThumbnailProperties: Hashable { public extension WebDAV { + enum ThumbnailPreviewMode { + /// Only show a preview thumbnail if there is already one loaded into memory. + case memoryOnly + /// Allow the disk cache to be loaded if there are no thumbnails in memory. + /// Note that this can be an expensive process and is run on the main thread. + case diskAllowed + /// Load the specific thumbnail if available, from disk if not in memory. + case specific(ThumbnailProperties) + } + //MARK: Images /// Download and cache an image from the specified file path. @@ -75,16 +85,40 @@ public extension WebDAV { /// - account: The WebDAV account. /// - password: The WebDAV account's password. /// - options: Options for caching the results. Empty set uses default caching behavior. - /// - completion: If account properties are invalid, this will run immediately on the same thread. - /// Otherwise, it runs when the network call finishes on a background thread. + /// - preview: Behavior for running the completion closure with cached thumbnails before the full-sized image is fetched. + /// Note that `.diskAllowed` will load all thumbnails for the given image + /// will be fetched on the main thread, which can be an expensive process. + /// `.memoryOnly` and `.specific()` are recommended. + /// - completion: If account properties are invalid, this will run immediately on the same thread with an error. + /// Otherwise, it will run on the main thread with a preview if available and a `preview` mode is provided, + /// then runs on a background thread when the network call finishes. /// - image: The image downloaded, if successful. /// The cached image if it has already been downloaded. /// - cachedImageURL: The URL of the cached image. /// - error: A WebDAVError if the call was unsuccessful. `nil` if it was. /// - Returns: The request identifier. @discardableResult - func downloadImage(path: String, account: A, password: String, caching options: WebDAVCachingOptions = [], completion: @escaping (_ image: UIImage?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? { - cachingDataTask(cache: imageCache, path: path, account: account, password: password, caching: options, valueFromData: { UIImage(data: $0) }, completion: completion) + func downloadImage(path: String, account: A, password: String, caching options: WebDAVCachingOptions = [], preview: ThumbnailPreviewMode? = .none, completion: @escaping (_ image: UIImage?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? { + cachingDataTask(cache: imageCache, path: path, account: account, password: password, caching: options, valueFromData: { UIImage(data: $0) }, placeholder: { + // Load placeholder thumbnail + var thumbnails = self.getAllMemoryCachedThumbnails(forItemAtPath: path, account: account)?.values + + switch preview { + case .none: + return nil + case .specific(let properties): + return self.getCachedThumbnail(forItemAtPath: path, account: account, with: properties) + case .memoryOnly: + break + case .diskAllowed: + // Only load from disk if there aren't any in memory + if thumbnails?.isEmpty ?? true { + thumbnails = self.getAllMemoryCachedThumbnails(forItemAtPath: path, account: account)?.values + } + } + let largestThumbnail = thumbnails?.max(by: { $0.size.width * $0.size.height < $1.size.width * $1.size.height }) + return largestThumbnail + }, completion: completion) } //MARK: Thumbnails diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index 06c3fa4..e92956a 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -384,7 +384,10 @@ extension WebDAV { //MARK: Standard Requests - func cachingDataTask(cache: Cache, path: String, account: A, password: String, caching options: WebDAVCachingOptions, valueFromData: @escaping (_ data: Data) -> Value?, completion: @escaping (_ value: Value?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? { + func cachingDataTask( + cache: Cache, path: String, account: A, password: String, + caching options: WebDAVCachingOptions, valueFromData: @escaping (_ data: Data) -> Value?, placeholder: (() -> Value?)? = nil, + completion: @escaping (_ value: Value?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? { // Check cache @@ -407,10 +410,16 @@ extension WebDAV { } } + // Cached data was not returned. Continue with network fetch. + if options.contains(.removeExistingCache) { try? deleteCachedData(forItemAtPath: path, account: account) } + if let placeholderValue = placeholder?() { + completion(placeholderValue, nil) + } + // Create network request guard let request = authorizedRequest(path: path, account: account, password: password, method: .get) else { diff --git a/Tests/WebDAVTests/WebDAVTests.swift b/Tests/WebDAVTests/WebDAVTests.swift index 8e742bb..6c3298a 100644 --- a/Tests/WebDAVTests/WebDAVTests.swift +++ b/Tests/WebDAVTests/WebDAVTests.swift @@ -511,7 +511,7 @@ final class WebDAVTests: XCTestCase { XCTAssertNoThrow(try webDAV.deleteCachedData(forItemAtPath: imagePath, account: account)) } - //MARK: Image Cache + //MARK: Images func testDownloadImage() { guard let (account, password) = getAccount() else { return XCTFail() } @@ -628,6 +628,30 @@ final class WebDAVTests: XCTestCase { XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path)) } + func testThumbnailPlaceholder() { + guard let (account, password) = getAccount() else { return XCTFail() } + guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else { + return XCTFail("You need to set the image_path in the environment.") + } + + let expectation = XCTestExpectation(description: "Fetch image") + // Check that the completion closure is run first + // on the preview then again for the image itself. + expectation.expectedFulfillmentCount = 2 + + downloadThumbnail(imagePath: imagePath, account: account, password: password) + + try? webDAV.deleteCachedData(forItemAtPath: imagePath, account: account) + webDAV.downloadImage(path: imagePath, account: account, password: password, preview: .memoryOnly) { image, error in + XCTAssertNil(error) + XCTAssertNotNil(image) + expectation.fulfill() + } + try? webDAV.deleteCachedData(forItemAtPath: imagePath, account: account) + + wait(for: [expectation], timeout: 10.0) + } + //MARK: OCS func testColorHex() { @@ -800,6 +824,7 @@ final class WebDAVTests: XCTestCase { ("testThumbnailCacheURL", testThumbnailCacheURL), ("testSpecificThumbnailCache", testSpecificThumbnailCache), ("testGeneralThumbnailCache", testGeneralThumbnailCache), + ("testThumbnailPlaceholder", testThumbnailPlaceholder), // OCS ("testTheme", testTheme), ("testColorHex", testColorHex) From a42084f924b5dfb13bc84bda516665a7b4fe3902 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Wed, 28 Apr 2021 16:53:23 -0600 Subject: [PATCH 2/3] Run thumbnail preview fetching on utility thread Closes #36 --- Sources/WebDAV/WebDAV+Images.swift | 7 +++---- Sources/WebDAV/WebDAV.swift | 8 ++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/WebDAV/WebDAV+Images.swift b/Sources/WebDAV/WebDAV+Images.swift index 02b7356..cd03f27 100644 --- a/Sources/WebDAV/WebDAV+Images.swift +++ b/Sources/WebDAV/WebDAV+Images.swift @@ -86,11 +86,10 @@ public extension WebDAV { /// - password: The WebDAV account's password. /// - options: Options for caching the results. Empty set uses default caching behavior. /// - preview: Behavior for running the completion closure with cached thumbnails before the full-sized image is fetched. - /// Note that `.diskAllowed` will load all thumbnails for the given image - /// will be fetched on the main thread, which can be an expensive process. - /// `.memoryOnly` and `.specific()` are recommended. + /// Note that `.diskAllowed` will load all thumbnails for the given image which can be an expensive process. + /// `.memoryOnly` and `.specific()` are recommended unless you do not know what thumbnails exist. /// - completion: If account properties are invalid, this will run immediately on the same thread with an error. - /// Otherwise, it will run on the main thread with a preview if available and a `preview` mode is provided, + /// Otherwise, it will run on a utility thread with a preview (if available and a `preview` mode is provided), /// then runs on a background thread when the network call finishes. /// - image: The image downloaded, if successful. /// The cached image if it has already been downloaded. diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index e92956a..487a0c5 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -416,9 +416,12 @@ extension WebDAV { try? deleteCachedData(forItemAtPath: path, account: account) } - if let placeholderValue = placeholder?() { - completion(placeholderValue, nil) + let placeholderTask = DispatchWorkItem { + if let placeholderValue = placeholder?() { + completion(placeholderValue, nil) + } } + DispatchQueue.global(qos: .utility).async(execute: placeholderTask) // Create network request @@ -436,6 +439,7 @@ extension WebDAV { return completion(nil, error) } else if let data = data, let value = valueFromData(data) { + placeholderTask.cancel() // Cache result if !options.contains(.removeExistingCache), !options.contains(.doNotCacheResult) { From f68cc901874392699edbd789f014e65f0731bc99 Mon Sep 17 00:00:00 2001 From: Isaac Lyons Date: Wed, 28 Apr 2021 17:03:31 -0600 Subject: [PATCH 3/3] Document thumbnail previews --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 1b533d4..8b8259f 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,20 @@ Image functions include: _Why is there no `deleteCachedImage` or `cachedImageURL` function when there is `getCachedThumbnail` and `cachedThumbnailURL`?_ Images are stored in the disk cache the same way as data. The image-specific functions exist as a convenience for converting the data to UIImages and caching them in memory that way. Since the cached data URL does not change whether the data is an image or not, `deleteCachedData` and `cachedDataURL` can be used for images. +#### Thumbnail preview + +If there are already cached thumbnails for the image you are trying to fetch, you can use the `preview` parameter to specify that you would like to get that thumbnail first while the full-size image is downloading. + +```swift +webDAV.downloadImage(path: imagePath, account: account, password: password, preview: .memoryOnly) { image, error in + // Display the image. + // This will run once on the largest cached thumbnail (if there are any) + // and again with the full-size image. +} +``` + +See [Thumbnails](#thumbnails) for more details on thumbnails. + ### Thumbnails Along with downloading full-sized images, you can download **thumbnails** from Nextcloud servers.