Skip to content

Commit

Permalink
Merge pull request #35 from Isvvc/thumbnail-preview
Browse files Browse the repository at this point in the history
Thumbnail Preview
  • Loading branch information
skjiisa authored Apr 28, 2021
2 parents 6e3c667 + f68cc90 commit 38b15c9
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 6 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 37 additions & 4 deletions Sources/WebDAV/WebDAV+Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -75,16 +85,39 @@ 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 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 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.
/// - 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<A: WebDAVAccount>(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<A: WebDAVAccount>(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
Expand Down
15 changes: 14 additions & 1 deletion Sources/WebDAV/WebDAV.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,10 @@ extension WebDAV {

//MARK: Standard Requests

func cachingDataTask<A: WebDAVAccount, Value: Equatable>(cache: Cache<AccountPath, Value>, path: String, account: A, password: String, caching options: WebDAVCachingOptions, valueFromData: @escaping (_ data: Data) -> Value?, completion: @escaping (_ value: Value?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
func cachingDataTask<A: WebDAVAccount, Value: Equatable>(
cache: Cache<AccountPath, Value>, 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

Expand All @@ -407,10 +410,19 @@ extension WebDAV {
}
}

// Cached data was not returned. Continue with network fetch.

if options.contains(.removeExistingCache) {
try? deleteCachedData(forItemAtPath: path, account: account)
}

let placeholderTask = DispatchWorkItem {
if let placeholderValue = placeholder?() {
completion(placeholderValue, nil)
}
}
DispatchQueue.global(qos: .utility).async(execute: placeholderTask)

// Create network request

guard let request = authorizedRequest(path: path, account: account, password: password, method: .get) else {
Expand All @@ -427,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) {
Expand Down
27 changes: 26 additions & 1 deletion Tests/WebDAVTests/WebDAVTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -800,6 +824,7 @@ final class WebDAVTests: XCTestCase {
("testThumbnailCacheURL", testThumbnailCacheURL),
("testSpecificThumbnailCache", testSpecificThumbnailCache),
("testGeneralThumbnailCache", testGeneralThumbnailCache),
("testThumbnailPlaceholder", testThumbnailPlaceholder),
// OCS
("testTheme", testTheme),
("testColorHex", testColorHex)
Expand Down

0 comments on commit 38b15c9

Please sign in to comment.