From af6905b7c503112f19a5c6a5d71339cec706520a Mon Sep 17 00:00:00 2001 From: Jordan Guggenheim Date: Wed, 21 Oct 2020 16:13:32 -0400 Subject: [PATCH] Updated setImage to setImageUrl for Improved API and cancelling logic (#17) * Modified setImage to setImageUrl, added computed var for easier api, added tests. * Change with to url * Removed duplicate url in naming * Only support normal for UIButton extension since we can't monitor multiple download receipts with the current implementation --- Example/Example/ViewController.swift | 2 +- .../Extensions/UIButton+ImageDownloader.swift | 19 ++++- .../UIImageView+ImageDownloader.swift | 16 ++++- Tests/UIButtonImageDownloaderTests.swift | 70 ++++++++++++++----- Tests/UIImageViewImageDownloaderTests.swift | 68 +++++++++++++----- 5 files changed, 137 insertions(+), 38 deletions(-) diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift index 350a3af..bb25995 100644 --- a/Example/Example/ViewController.swift +++ b/Example/Example/ViewController.swift @@ -15,7 +15,7 @@ final class ViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - imageView.ok.setImage(with: URL(string: "https://www.gstatic.com/webp/gallery/4.webp")!) + imageView.ok.setImage(url: URL(string: "https://www.gstatic.com/webp/gallery/4.webp")!) } } diff --git a/Sources/Extensions/UIButton+ImageDownloader.swift b/Sources/Extensions/UIButton+ImageDownloader.swift index 96ace95..e8af2c8 100644 --- a/Sources/Extensions/UIButton+ImageDownloader.swift +++ b/Sources/Extensions/UIButton+ImageDownloader.swift @@ -9,11 +9,24 @@ import UIKit public extension ObjectWrapper where T: UIButton { + + var imageUrl: URL? { + get { + imageDownloaderReceipt?.url + } + set { + setImage(url: newValue) + } + } - func setImage(with url: URL, - for state: UIControl.State = .normal, + func setImage(url: URL?, imageDownloader: ImageDownloading = ImageDownloader.shared, completionHandler: ImageDownloader.CompletionHandler? = nil) { + guard let url = url else { + cancelImageDownload(imageDownloader: imageDownloader) + return + } + if imageDownloaderReceipt != nil { assertionFailure("Active Download In Progress, Cancel Before Starting a New Request") } @@ -25,7 +38,7 @@ public extension ObjectWrapper where T: UIButton { switch result { case .success(let image): DispatchQueue.executeAsyncOnMain { - self.object.setImage(image, for: state) + self.object.setImage(image, for: .normal) } case .failure: diff --git a/Sources/Extensions/UIImageView+ImageDownloader.swift b/Sources/Extensions/UIImageView+ImageDownloader.swift index ab10ea0..a249547 100644 --- a/Sources/Extensions/UIImageView+ImageDownloader.swift +++ b/Sources/Extensions/UIImageView+ImageDownloader.swift @@ -9,10 +9,24 @@ import UIKit public extension ObjectWrapper where T: UIImageView { + + var imageUrl: URL? { + get { + imageDownloaderReceipt?.url + } + set { + setImage(url: newValue) + } + } - func setImage(with url: URL, + func setImage(url: URL?, imageDownloader: ImageDownloading = ImageDownloader.shared, completionHandler: ImageDownloader.CompletionHandler? = nil) { + guard let url = url else { + cancelImageDownload(imageDownloader: imageDownloader) + return + } + if imageDownloaderReceipt != nil { assertionFailure("Active Download In Progress, Cancel Before Starting a New Request") } diff --git a/Tests/UIButtonImageDownloaderTests.swift b/Tests/UIButtonImageDownloaderTests.swift index 747512c..c434698 100644 --- a/Tests/UIButtonImageDownloaderTests.swift +++ b/Tests/UIButtonImageDownloaderTests.swift @@ -30,7 +30,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { button = UIButton(type: .custom) } - func test_setImage_itSetsTheImageDownloadReceipt() { + func test_setImageUrl_itSetsTheImageDownloadReceipt() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -38,12 +38,12 @@ final class UIButtonImageDownloaderTests: XCTestCase { XCTAssertNil(button.ok.imageDownloaderReceipt) - button.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + button.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) XCTAssertNotNil(button.ok.imageDownloaderReceipt) } - func test_setImage_whenSuccess_itNilsTheImageDownloadReceipt() { + func test_setImageUrl_whenSuccess_itNilsTheImageDownloadReceipt() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -53,7 +53,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { let expectation = self.expectation(description: "Nil Receipt on Completion") - button.ok.setImage(with: url, for: .normal, imageDownloader: imageDownloader) { (result, receipt) in + button.ok.setImage(url: url, imageDownloader: imageDownloader) { (result, receipt) in switch result { case .success: break @@ -71,7 +71,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { wait(for: [expectation], timeout: 5) } - func test_setImage_whenFailure_itNilsTheImageDownloadReceipt() { + func test_setImageUrl_whenFailure_itNilsTheImageDownloadReceipt() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), Data()) @@ -81,7 +81,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { let expectation = self.expectation(description: "Nil Receipt on Completion") - button.ok.setImage(with: url, for: .normal, imageDownloader: imageDownloader) { (result, receipt) in + button.ok.setImage(url: url, imageDownloader: imageDownloader) { (result, receipt) in switch result { case .success: XCTFail() @@ -107,7 +107,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { XCTAssertNil(button.ok.imageDownloaderReceipt) - button.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + button.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) XCTAssertNotNil(button.ok.imageDownloaderReceipt) @@ -116,7 +116,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { XCTAssertNil(button.ok.imageDownloaderReceipt) } - func test_setImage_whenSuccessAndCompletionHandler_itForwardsCompletionHandler() { + func test_setImageUrl_whenSuccessAndCompletionHandler_itForwardsCompletionHandler() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -136,12 +136,12 @@ final class UIButtonImageDownloaderTests: XCTestCase { expectation.fulfill() } - button.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: completionHandler) + button.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: completionHandler) wait(for: [expectation], timeout: 20) } - func test_setImage_whenFailureAndCompletionHandler_itForwardsCompletionHandler() { + func test_setImageUrl_whenFailureAndCompletionHandler_itForwardsCompletionHandler() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), Data()) @@ -161,12 +161,12 @@ final class UIButtonImageDownloaderTests: XCTestCase { expectation.fulfill() } - button.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: completionHandler) + button.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: completionHandler) wait(for: [expectation], timeout: 20) } - func test_setImage_whenSuccessAndNoCompletionHandler_itSetsTheImageForState() { + func test_setImageUrl_whenSuccessAndNoCompletionHandler_itSetsTheImageForState() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -174,20 +174,19 @@ final class UIButtonImageDownloaderTests: XCTestCase { XCTAssertNil(button.imageView?.image) - button.ok.setImage(with: url, for: .highlighted, imageDownloader: imageDownloader, completionHandler: nil) + button.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) let expectation = XCTestExpectation(description: "Image Downloader UIButton Success Response") DispatchQueue.main.asyncAfter(deadline: .now() + MockAsyncUrlProtocol.deadline + 0.1) { - XCTAssertNil(self.button.image(for: .normal)) - XCTAssertNotNil(self.button.image(for: .highlighted)) + XCTAssertNotNil(self.button.image(for: .normal)) expectation.fulfill() } wait(for: [expectation], timeout: 20) } - func test_setImage_whenFailureAndNoCompletionHandler_itDoesNotSetTheImage() { + func test_setImageUrl_whenFailureAndNoCompletionHandler_itDoesNotSetTheImage() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), Data()) @@ -195,7 +194,7 @@ final class UIButtonImageDownloaderTests: XCTestCase { let expectation = XCTestExpectation(description: "Image Downloader UIImageView Failure Response") - button.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + button.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) DispatchQueue.main.asyncAfter(deadline: .now() + MockAsyncUrlProtocol.deadline + 0.1) { XCTAssertNil(self.button.imageView?.image) @@ -204,4 +203,41 @@ final class UIButtonImageDownloaderTests: XCTestCase { wait(for: [expectation], timeout: 20) } + + func test_setImageUrl_whenNil_itCancelsTheImageDownloadButLeavesTheImage() { + MockUrlProtocol.requestHandler = { request in + XCTAssertEqual(request.url, self.url) + return (HTTPURLResponse(), self.expectedImageData) + } + + XCTAssertNil(button.ok.imageDownloaderReceipt) + + let mockImageDownloader: MockImageDownloader = .init() + + button.ok.setImage(url: url, imageDownloader: mockImageDownloader, completionHandler: nil) + + button.imageView?.image = expectedImage + + XCTAssertNotNil(button.ok.imageDownloaderReceipt) + XCTAssertEqual(mockImageDownloader.cancelCallCount, 0) + + button.ok.setImage(url: nil, imageDownloader: mockImageDownloader) + + XCTAssertNil(button.ok.imageDownloaderReceipt) + XCTAssertEqual(mockImageDownloader.cancelCallCount, 1) + XCTAssertNotNil(button.imageView?.image) + } + + func test_imageUrl_whenNilUrl_itCancelsTheDownload() { + XCTAssertNil(button.ok.imageDownloaderReceipt) + + button.ok.imageUrl = url + + XCTAssertNotNil(button.ok.imageDownloaderReceipt) + XCTAssertEqual(button.ok.imageUrl, url) + + button.ok.imageUrl = nil + + XCTAssertNil(button.ok.imageDownloaderReceipt) + } } diff --git a/Tests/UIImageViewImageDownloaderTests.swift b/Tests/UIImageViewImageDownloaderTests.swift index 813767f..6dccf1f 100644 --- a/Tests/UIImageViewImageDownloaderTests.swift +++ b/Tests/UIImageViewImageDownloaderTests.swift @@ -30,7 +30,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { imageView = UIImageView() } - func test_setImage_itSetsTheImageDownloadReceipt() { + func test_setImageUrl_itSetsTheImageDownloadReceipt() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -38,12 +38,12 @@ final class UIImageViewImageDownloaderTests: XCTestCase { XCTAssertNil(imageView.ok.imageDownloaderReceipt?.url) - imageView.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + imageView.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) XCTAssertNotNil(imageView.ok.imageDownloaderReceipt?.url) } - func test_setImage_whenSuccess_itNilsTheImageDownloadReceipt() { + func test_setImageUrl_whenSuccess_itNilsTheImageDownloadReceipt() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -53,7 +53,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { let expectation = self.expectation(description: "Nil Receipt on Completion") - imageView.ok.setImage(with: url, imageDownloader: imageDownloader) { (result, receipt) in + imageView.ok.setImage(url: url, imageDownloader: imageDownloader) { (result, receipt) in switch result { case .success: break @@ -71,7 +71,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { wait(for: [expectation], timeout: 5) } - func test_setImage_whenFailure_itNilsTheImageDownloadReceipt() { + func test_setImageUrl_whenFailure_itNilsTheImageDownloadReceipt() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), Data()) @@ -81,7 +81,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { let expectation = self.expectation(description: "Nil Receipt on Completion") - imageView.ok.setImage(with: url, imageDownloader: imageDownloader) { (result, receipt) in + imageView.ok.setImage(url: url, imageDownloader: imageDownloader) { (result, receipt) in switch result { case .success: XCTFail() @@ -107,7 +107,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { XCTAssertNil(imageView.ok.imageDownloaderReceipt) - imageView.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + imageView.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) XCTAssertNotNil(imageView.ok.imageDownloaderReceipt) @@ -116,7 +116,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { XCTAssertNil(imageView.ok.imageDownloaderReceipt) } - func test_setImage_whenSuccessAndCompletionHandler_itForwardsCompletionHandler() { + func test_setImageUrl_whenSuccessAndCompletionHandler_itForwardsCompletionHandler() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -136,12 +136,12 @@ final class UIImageViewImageDownloaderTests: XCTestCase { expectation.fulfill() } - imageView.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: completionHandler) + imageView.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: completionHandler) wait(for: [expectation], timeout: 20) } - func test_setImage_whenFailureAndCompletionHandler_itForwardsCompletionHandler() { + func test_setImageUrl_whenFailureAndCompletionHandler_itForwardsCompletionHandler() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), Data()) @@ -161,12 +161,12 @@ final class UIImageViewImageDownloaderTests: XCTestCase { expectation.fulfill() } - imageView.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: completionHandler) + imageView.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: completionHandler) wait(for: [expectation], timeout: 20) } - func test_setImage_whenSuccessAndNoCompletionHandler_itSetsTheImage() { + func test_setImageUrl_whenSuccessAndNoCompletionHandler_itSetsTheImage() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), self.expectedImageData) @@ -174,7 +174,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { XCTAssertNil(imageView.image) - imageView.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + imageView.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) let expectation = XCTestExpectation(description: "Image Downloader UIImageView Success Response") @@ -186,7 +186,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { wait(for: [expectation], timeout: 20) } - func test_setImage_whenFailureAndNoCompletionHandler_itDoesNotSetTheImage() { + func test_setImageUrl_whenFailureAndNoCompletionHandler_itDoesNotSetTheImage() { MockUrlProtocol.requestHandler = { request in XCTAssertEqual(request.url, self.url) return (HTTPURLResponse(), Data()) @@ -194,7 +194,7 @@ final class UIImageViewImageDownloaderTests: XCTestCase { let expectation = XCTestExpectation(description: "Image Downloader UIImageView Failure Response") - imageView.ok.setImage(with: url, imageDownloader: imageDownloader, completionHandler: nil) + imageView.ok.setImage(url: url, imageDownloader: imageDownloader, completionHandler: nil) DispatchQueue.main.asyncAfter(deadline: .now() + MockAsyncUrlProtocol.deadline + 0.1) { XCTAssertNil(self.imageView.image) @@ -203,5 +203,41 @@ final class UIImageViewImageDownloaderTests: XCTestCase { wait(for: [expectation], timeout: 20) } - + + func test_setImageUrl_whenNil_itCancelsTheImageDownloadButLeavesTheImage() { + MockUrlProtocol.requestHandler = { request in + XCTAssertEqual(request.url, self.url) + return (HTTPURLResponse(), self.expectedImageData) + } + + XCTAssertNil(imageView.ok.imageDownloaderReceipt) + + let mockImageDownloader: MockImageDownloader = .init() + + imageView.ok.setImage(url: url, imageDownloader: mockImageDownloader, completionHandler: nil) + + imageView.image = expectedImage + + XCTAssertNotNil(imageView.ok.imageDownloaderReceipt) + XCTAssertEqual(mockImageDownloader.cancelCallCount, 0) + + imageView.ok.setImage(url: nil, imageDownloader: mockImageDownloader) + + XCTAssertNil(imageView.ok.imageDownloaderReceipt) + XCTAssertEqual(mockImageDownloader.cancelCallCount, 1) + XCTAssertNotNil(imageView.image) + } + + func test_imageUrl_whenNilUrl_itCancelsTheDownload() { + XCTAssertNil(imageView.ok.imageDownloaderReceipt) + + imageView.ok.imageUrl = url + + XCTAssertNotNil(imageView.ok.imageDownloaderReceipt) + XCTAssertEqual(imageView.ok.imageUrl, url) + + imageView.ok.imageUrl = nil + + XCTAssertNil(imageView.ok.imageDownloaderReceipt) + } }