Skip to content

Commit

Permalink
Return a download token and allow request cancellation
Browse files Browse the repository at this point in the history
  • Loading branch information
JanGorman committed Jul 19, 2019
1 parent fdd0a61 commit 1ce1365
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 26 deletions.
2 changes: 1 addition & 1 deletion MapleBacon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@
isa = PBXGroup;
children = (
77B73D651F553F4800874885 /* Assets.xcassets */,
77B73D541F553A8700874885 /* MapleBaconCacheTests.swift */,
777691581F5C238F00BCDFF9 /* DownloaderTests.swift */,
77CA13141FA2391B0017C949 /* ImageTransformerTests.swift */,
77B73D561F553A8700874885 /* Info.plist */,
77B73D541F553A8700874885 /* MapleBaconCacheTests.swift */,
775FE0B91F98E9E600D548E1 /* MapleBaconTests.swift */,
774654BB20D93FD6007DA2AC /* MockURLProtocol.swift */,
775FE0BB1F98EA3900D548E1 /* TestHelper.swift */,
Expand Down
25 changes: 23 additions & 2 deletions MapleBacon/Core/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private protocol DownloadStateDelegate: AnyObject {
private final class Download {

let task: URLSessionDataTask
let token: UUID
let progress: DownloadProgress?
var completions: [DownloadCompletion]
var data: Data
Expand All @@ -34,6 +35,7 @@ private final class Download {
self.progress = progress
self.completions = [completion]
self.data = data
self.token = UUID()
}

func start() {
Expand Down Expand Up @@ -77,24 +79,41 @@ public final class Downloader {
/// - url: The URL to download from
/// - progress: An optional download progress closure
/// - completion: The completion closure called once the download is done
public func download(_ url: URL, progress: DownloadProgress? = nil, completion: @escaping DownloadCompletion) {
/// - Returns: A download token `UUID`
public func download(_ url: URL, progress: DownloadProgress? = nil, completion: @escaping DownloadCompletion) -> UUID {
sessionDelegate.delegate = self

var token: UUID!
mutex.sync(flags: .barrier) {
let task: URLSessionDataTask
if let download = downloads[url] {
task = download.task
download.completions.append(completion)
token = download.token
} else {
let newTask = session.dataTask(with: url)
let download = Download(task: newTask, progress: progress, completion: completion, data: Data())
download.start()
downloads[url] = download
task = newTask
token = download.token
}

task.resume()
}
return token
}

/// Cancel a running download
///
/// - Parameter token: The token identifier of the the download
public func cancel(withToken token: UUID) {
guard let (url, download) = downloads.first(where: { $1.token == token }) else {
return
}
download.task.cancel()
download.finish()
clearDownload(for: url)
}

}
Expand All @@ -110,7 +129,9 @@ extension Downloader: DownloadStateDelegate {
}

fileprivate func clearDownload(for url: URL?) {
guard let url = url else { return }
guard let url = url else {
return
}
mutex.sync(flags: .barrier) {
downloads[url] = nil
}
Expand Down
44 changes: 29 additions & 15 deletions MapleBacon/Core/MapleBacon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ public final class MapleBacon {
/// - transformer: An optional transformer or transformer chain to apply to the image
/// - progress: An optional closure to track the download progress
/// - completion: The closure to call once the download is done. The completion is called on a background thread
/// - Returns: An optional download token `UUID` – if the image can be fetched from cache there won't be a token
@discardableResult
public func image(with url: URL,
transformer: ImageTransformer? = nil,
progress: DownloadProgress? = nil,
completion: @escaping ImageDownloadCompletion) {
fetchImage(with: url, transformer: transformer, progress: progress, completion: completion)
completion: @escaping ImageDownloadCompletion) -> UUID? {
return fetchImage(with: url, transformer: transformer, progress: progress, completion: completion)
}

/// Download or retrieve an data from cache
Expand All @@ -48,19 +50,35 @@ public final class MapleBacon {
/// - url: The URL to load (image) data from
/// - progress: An optional closure to track the download progress
/// - completion: The closure to call once the download is done. The completion is called on a background thread
/// - Returns: An optional download token `UUID` – if the data can be fetched from cache there won't be a token
@discardableResult
public func data(with url: URL,
progress: DownloadProgress? = nil,
completion: @escaping DataDownloadCompletion) {
fetchData(with: url, transformer: nil, progress: progress) { data, _ in
completion: @escaping DataDownloadCompletion) -> UUID? {
return fetchData(with: url, transformer: nil, progress: progress) { data, _ in
completion(data)
}
}

/// Pre-warms the image cache. Downloads the image if needed or loads it into memory.
///
/// - Parameter url: The URL to load an image from
public func preWarmCache(for url: URL) {
_ = fetchImage(with: url, transformer: nil, progress: nil, completion: nil)
}

/// Cancel a running download
///
/// - Parameter token: The token identifier of the the download
public func cancelDownload(withToken token: UUID) {
downloader.cancel(withToken: token)
}

private func fetchImage(with url: URL,
transformer: ImageTransformer?,
progress: DownloadProgress?,
completion: ImageDownloadCompletion?) {
fetchData(with: url, transformer: transformer, progress: progress) { [weak self] data, cacheType in
completion: ImageDownloadCompletion?) -> UUID? {
return fetchData(with: url, transformer: transformer, progress: progress) { [weak self] data, cacheType in
guard let self = self, let data = data, let image = UIImage(data: data) else {
completion?(nil)
return
Expand All @@ -82,11 +100,12 @@ public final class MapleBacon {
private func fetchData(with url: URL,
transformer: ImageTransformer?,
progress: DownloadProgress?,
completion: ((Data?, CacheType) -> Void)?) {
completion: ((Data?, CacheType) -> Void)?) -> UUID? {
let key = url.absoluteString
var token: UUID?
cache.retrieveData(forKey: key, transformerId: transformer?.identifier) { [weak self] data, cacheType in
guard let data = data else {
self?.downloader.download(url, progress: progress, completion: { data in
let downloadToken = self?.downloader.download(url, progress: progress, completion: { data in
guard let self = self, let data = data else {
completion?(nil, cacheType)
return
Expand All @@ -97,17 +116,12 @@ public final class MapleBacon {
}
completion?(data, cacheType)
})
token = downloadToken
return
}
completion?(data, cacheType)
}
return token
}

/// Pre-warms the image cache. Downloads the image if needed or loads it into memory.
///
/// - Parameter url: The URL to load an image from
public func preWarmCache(for url: URL) {
fetchImage(with: url, transformer: nil, progress: nil, completion: nil)
}

}
49 changes: 41 additions & 8 deletions MapleBaconTests/DownloaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,56 @@ final class DownloaderTests: XCTestCase {
private let url: URL = "https://www.apple.com/mapleBacon.png"
private let helper = TestHelper()

func testDownload() {
override func setUp() {
MockURLProtocol.requestHandler = { request in
return (HTTPURLResponse(), self.helper.imageResponseData())
}
super.setUp()
}

func testDownload() {
let configuration = MockURLProtocol.mockedURLSessionConfiguration()
let downloader = Downloader(sessionConfiguration: configuration)

waitUntil { done in
downloader.download(self.url) { data in
let token = downloader.download(self.url) { data in
expect(data).toNot(beNil())
done()
}
expect(token).toNot(beNil())
}
}

func testMultipleDownloads() {
MockURLProtocol.requestHandler = { request in
return (HTTPURLResponse(), self.helper.imageResponseData())
}
let configuration = MockURLProtocol.mockedURLSessionConfiguration()
let downloader = Downloader(sessionConfiguration: configuration)

let url1 = url
waitUntil { done in
downloader.download(url1) { data in
_ = downloader.download(url1) { data in
expect(data).toNot(beNil())
done()
}
}

let url2 = url
waitUntil { done in
downloader.download(url2) { data in
_ = downloader.download(url2) { data in
expect(data).toNot(beNil())
done()
}
}
}

func testSynchronousMultipleDownloadsOfSameURL() {
let configuration = MockURLProtocol.mockedURLSessionConfiguration()
let downloader = Downloader(sessionConfiguration: configuration)

waitUntil { done in
_ = downloader.download(self.url) { data in
expect(data).toNot(beNil())
}
_ = downloader.download(self.url) { data in
expect(data).toNot(beNil())
done()
}
Expand All @@ -59,11 +76,27 @@ final class DownloaderTests: XCTestCase {
let downloader = Downloader(sessionConfiguration: configuration)

waitUntil { done in
downloader.download(self.url) { data in
_ = downloader.download(self.url) { data in
expect(data).to(beNil())
done()
}
}
}

func testCancel() {
let configuration = MockURLProtocol.mockedURLSessionConfiguration()
let downloader = Downloader(sessionConfiguration: configuration)

var imageData: Data?
let token = downloader.download(url) { data in
imageData = data
}
downloader.cancel(withToken: token)

waitUntil { done in
expect(imageData).to(beNil())
done()
}
}

}
17 changes: 17 additions & 0 deletions MapleBaconTests/MapleBaconTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,21 @@ final class MapleBaconTests: XCTestCase {
}
}

func testCancel() {
let configuration = MockURLProtocol.mockedURLSessionConfiguration()
let downloader = Downloader(sessionConfiguration: configuration)
let mapleBacon = MapleBacon(cache: .default, downloader: downloader)

var imageData: Data?
let token = mapleBacon.data(with: url) { data in
imageData = data
}
mapleBacon.cancelDownload(withToken: token!)

waitUntil { done in
expect(imageData).to(beNil())
done()
}
}

}

0 comments on commit 1ce1365

Please sign in to comment.