diff --git a/Source/UIImageView+AlamofireImage.swift b/Source/UIImageView+AlamofireImage.swift index 60435ef1..84d54cf6 100644 --- a/Source/UIImageView+AlamofireImage.swift +++ b/Source/UIImageView+AlamofireImage.swift @@ -38,30 +38,56 @@ extension UIImageView { case FlipFromLeft(NSTimeInterval) case FlipFromRight(NSTimeInterval) case FlipFromTop(NSTimeInterval) + case Custom( + duration: NSTimeInterval, + animationOptions: UIViewAnimationOptions, + animations: (UIImageView, Image) -> Void, + completion: (Bool -> Void)? + ) private var duration: NSTimeInterval { switch self { - case None: return 0.0 - case CrossDissolve(let duration): return duration - case CurlDown(let duration): return duration - case CurlUp(let duration): return duration - case FlipFromBottom(let duration): return duration - case FlipFromLeft(let duration): return duration - case FlipFromRight(let duration): return duration - case FlipFromTop(let duration): return duration + case None: return 0.0 + case CrossDissolve(let duration): return duration + case CurlDown(let duration): return duration + case CurlUp(let duration): return duration + case FlipFromBottom(let duration): return duration + case FlipFromLeft(let duration): return duration + case FlipFromRight(let duration): return duration + case FlipFromTop(let duration): return duration + case Custom(let duration, _, _, _): return duration } } private var animationOptions: UIViewAnimationOptions { switch self { - case None: return .TransitionNone - case CrossDissolve: return .TransitionCrossDissolve - case CurlDown: return .TransitionCurlDown - case CurlUp: return .TransitionCurlUp - case FlipFromBottom: return .TransitionFlipFromBottom - case FlipFromLeft: return .TransitionFlipFromLeft - case FlipFromRight: return .TransitionFlipFromRight - case FlipFromTop: return .TransitionFlipFromTop + case None: return .TransitionNone + case CrossDissolve: return .TransitionCrossDissolve + case CurlDown: return .TransitionCurlDown + case CurlUp: return .TransitionCurlUp + case FlipFromBottom: return .TransitionFlipFromBottom + case FlipFromLeft: return .TransitionFlipFromLeft + case FlipFromRight: return .TransitionFlipFromRight + case FlipFromTop: return .TransitionFlipFromTop + case Custom(_, let animationOptions, _, _): return animationOptions + } + } + + private var animations: ((UIImageView, Image) -> Void) { + switch self { + case Custom(_, _, let animations, _): + return animations + default: + return { $0.image = $1 } + } + } + + private var completion: (Bool -> Void)? { + switch self { + case Custom(_, _, _, let completion): + return completion + default: + return nil } } } @@ -204,15 +230,56 @@ extension UIImageView { If the image is cached locally, the image is set immediately. Otherwise the specified placehoder image will be set immediately, and then the remote image will be set once the image request is finished. - If a `completion` closure is specified, it is the responsibility of the closure to set the image of the image - view before returning. If no `completion` closure is specified, the default behavior of setting the image is - applied. + The `completion` closure is called after the image download and filtering are complete, but before the start of + the image transition. Please note it is no longer the responsibility of the `completion` closure to set the + image. It will be set automatically. If you require a second notification after the image transition completes, + use a `.Custom` image transition with a `completion` closure. The `.Custom` `completion` closure is called when + the image transition is finished. - parameter URL: The URL used for the image request. - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the image view will not change its image until the image request finishes. - parameter filter: The image filter applied to the image after the image request is finished. - parameter imageTransition: The image transition animation applied to the image when set. + - parameter completion: A closure to be executed when the image request finishes. The closure has no + return value and takes three arguments: the original request, the response from + the server and the result containing either the image or the error that occurred. + If the image was returned from the image cache, the response will be `nil`. + */ + public func af_setImageWithURL( + URL: NSURL, + placeholderImage: UIImage?, + filter: ImageFilter?, + imageTransition: ImageTransition, + completion: ((NSURLRequest?, NSHTTPURLResponse?, Result) -> Void)?) + { + af_setImageWithURLRequest( + URLRequestWithURL(URL), + placeholderImage: placeholderImage, + filter: filter, + imageTransition: imageTransition, + completion: completion + ) + } + + /** + Asynchronously downloads an image from the specified URL, applies the specified image filter to the downloaded + image and sets it once finished while executing the image transition. + + If the image is cached locally, the image is set immediately. Otherwise the specified placehoder image will be + set immediately, and then the remote image will be set once the image request is finished. + + The `completion` closure is called after the image download and filtering are complete, but before the start of + the image transition. Please note it is no longer the responsibility of the `completion` closure to set the + image. It will be set automatically. If you require a second notification after the image transition completes, + use a `.Custom` image transition with a `completion` closure. The `.Custom` `completion` closure is called when + the image transition is finished. + + - parameter URLRequest: The URL request. + - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the + image view will not change its image until the image request finishes. + - parameter filter: The image filter applied to the image after the image request is finished. + - parameter imageTransition: The image transition animation applied to the image when set. - parameter completion: A closure to be executed when the image request finishes. The closure has no return value and takes three arguments: the original request, the response from the server and the result containing either the image or the error that occurred. @@ -263,26 +330,20 @@ extension UIImageView { strongSelf.af_activeRequest = nil - guard completion == nil else { + if let image = result.value { completion?(request, response, result) - return - } - if let image = result.value { - switch imageTransition { - case .None: - strongSelf.image = image - default: - UIView.transitionWithView( - strongSelf, - duration: imageTransition.duration, - options: imageTransition.animationOptions, - animations: { - strongSelf.image = image - }, - completion: nil - ) - } + UIView.transitionWithView( + strongSelf, + duration: imageTransition.duration, + options: imageTransition.animationOptions, + animations: { + imageTransition.animations(strongSelf, image) + }, + completion: imageTransition.completion + ) + } else { + completion?(request, response, result) } } ) diff --git a/Tests/UIImageViewTests.swift b/Tests/UIImageViewTests.swift index 5d6d1db4..a59dcb5c 100644 --- a/Tests/UIImageViewTests.swift +++ b/Tests/UIImageViewTests.swift @@ -326,14 +326,29 @@ class UIImageViewTestCase: BaseTestCase { let expectation8 = expectationWithDescription("image download should succeed") imageView.imageObserver = { - imageTransitionsComplete = true expectation8.fulfill() } - ImageDownloader.defaultInstance.imageCache?.removeAllImages() imageView.af_setImageWithURL(URL, imageTransition: .FlipFromTop(0.1)) waitForExpectationsWithTimeout(timeout, handler: nil) + let expectation9 = expectationWithDescription("image download should succeed") + imageView.imageObserver = { + imageTransitionsComplete = true + expectation9.fulfill() + } + ImageDownloader.defaultInstance.imageCache?.removeAllImages() + imageView.af_setImageWithURL( + URL, + imageTransition: .Custom( + duration: 0.5, + animationOptions: UIViewAnimationOptions(), + animations: { $0.image = $1 }, + completion: nil + ) + ) + waitForExpectationsWithTimeout(timeout, handler: nil) + // Then XCTAssertTrue(imageTransitionsComplete, "image transitions complete should be true") XCTAssertNotNil(imageView.image, "image view image should not be nil") @@ -373,7 +388,7 @@ class UIImageViewTestCase: BaseTestCase { // Then XCTAssertTrue(completionHandlerCalled, "completion handler called should be true") - XCTAssertNil(imageView.image, "image view image should be nil when completion handler is not nil") + XCTAssertNotNil(imageView.image, "image view image should be not be nil when completion handler is not nil") XCTAssertTrue(result?.isSuccess ?? false, "result should be a success case") } @@ -408,6 +423,49 @@ class UIImageViewTestCase: BaseTestCase { XCTAssertTrue(result?.isFailure ?? false, "result should be a failure case") } + func testThatCompletionHandlerAndCustomTransitionHandlerAreBothCalled() { + // Given + let imageView = UIImageView() + + let completionExpectation = expectationWithDescription("image download should succeed") + let transitionExpectation = expectationWithDescription("image transition should complete") + + var completionHandlerCalled = false + var transitionCompletionHandlerCalled = false + + var result: Result? + + // When + imageView.af_setImageWithURL( + URL, + placeholderImage: nil, + filter: nil, + imageTransition: .Custom( + duration: 0.1, + animationOptions: UIViewAnimationOptions(), + animations: { $0.image = $1 }, + completion: { _ in + transitionCompletionHandlerCalled = true + transitionExpectation.fulfill() + } + ), + completion: { _, _, responseResult in + completionHandlerCalled = true + result = responseResult + + completionExpectation.fulfill() + } + ) + + waitForExpectationsWithTimeout(timeout, handler: nil) + + // Then + XCTAssertTrue(completionHandlerCalled, "completion handler called should be true") + XCTAssertTrue(transitionCompletionHandlerCalled, "transition completion handler called should be true") + XCTAssertNotNil(imageView.image, "image view image should be not be nil when completion handler is not nil") + XCTAssertTrue(result?.isSuccess ?? false, "result should be a success case") + } + // MARK: - Cancellation func testThatImageDownloadCanBeCancelled() { @@ -481,7 +539,7 @@ class UIImageViewTestCase: BaseTestCase { // Then XCTAssertFalse(completion1Called, "completion 1 called should be false") XCTAssertTrue(completion2Called, "completion 2 called should be true") - XCTAssertNil(imageView.image, "image view image should be nil when completion handler is not nil") + XCTAssertNotNil(imageView.image, "image view image should not be nil when completion handler is not nil") XCTAssertTrue(result?.isSuccess ?? false, "result should be a success case") } }