Skip to content

Commit 994b9fb

Browse files
authored
Merge pull request #2548 from onevcat/fix/duplicate-download-finish-callback
Fix duplicate didFinishDownloadingImageForURL delegate callback
2 parents ed1bc14 + 20d2372 commit 994b9fb

3 files changed

Lines changed: 130 additions & 22 deletions

File tree

Sources/Networking/ImageDownloader.swift

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,6 @@ open class ImageDownloader: @unchecked Sendable {
332332
sessionDelegate.onResponseReceived.delegate(on: self) { (self, response) in
333333
await (self.delegate ?? self).imageDownloader(self, didReceive: response)
334334
}
335-
sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
336-
let (url, result) = value
337-
do {
338-
let value = try result.get()
339-
self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
340-
} catch {
341-
self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
342-
}
343-
}
344335
sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
345336
(self.delegate ?? self).imageDownloader(self, didDownload: task.sharedData, with: task)
346337
}

Sources/Networking/SessionDelegate.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ open class SessionDelegate: NSObject, @unchecked Sendable {
4848

4949
let onValidStatusCode = Delegate<Int, Bool>()
5050
let onResponseReceived = Delegate<URLResponse, URLSession.ResponseDisposition>()
51-
let onDownloadingFinished = Delegate<(URL, Result<URLResponse, KingfisherError>), Void>()
5251
let onDidDownloadData = Delegate<SessionDataTask, Data?>()
5352

5453
let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, (URLSession.AuthChallengeDisposition, URLCredential?)>()
@@ -199,18 +198,6 @@ extension SessionDelegate: URLSessionDataDelegate {
199198
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
200199
guard let sessionTask = self.task(for: task) else { return }
201200

202-
if let url = sessionTask.originalURL {
203-
let result: Result<URLResponse, KingfisherError>
204-
if let error = error {
205-
result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
206-
} else if let response = task.response {
207-
result = .success(response)
208-
} else {
209-
result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask)))
210-
}
211-
onDownloadingFinished.call((url, result))
212-
}
213-
214201
let result: Result<(Data, URLResponse?), KingfisherError>
215202
if let error = error {
216203
result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))

Tests/KingfisherTests/ImageDownloaderTests.swift

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,85 @@ class ImageDownloaderTests: XCTestCase {
125125
waitForExpectations(timeout: 5, handler: nil)
126126
}
127127

128+
// `imageDownloader(_:didFinishDownloadingImageForURL:with:error:)` must be called exactly once
129+
// per download. Historically two paths invoked it for the same completion (`onDownloadingFinished`
130+
// in `setupSessionHandler()` and `reportDidDownloadImageData` in `startDownloadTask`'s `onTaskDone`
131+
// handler), so success and network-error downloads notified the delegate twice, while a
132+
// data-modifying failure first reported a success (nil error) and then a failure.
133+
func testDidFinishDownloadingDelegateCalledOncePerSuccessfulDownload() {
134+
let exp = expectation(description: #function)
135+
136+
let url = testURLs[0]
137+
stub(url, data: testImageData)
138+
139+
let delegate = DownloadFinishCountingDelegate()
140+
downloader.delegate = delegate
141+
142+
downloader.downloadImage(with: url) { result in
143+
XCTAssertNotNil(result.value)
144+
XCTAssertEqual(delegate.finishedEvents.count, 1)
145+
exp.fulfill()
146+
}
147+
waitForExpectations(timeout: 3, handler: nil)
148+
_ = delegate // keep the weak delegate alive for the duration of the test
149+
}
150+
151+
func testDidFinishDownloadingDelegateCalledOncePerFailedDownload() {
152+
let exp = expectation(description: #function)
153+
154+
let url = testURLs[0]
155+
stub(url, data: testImageData, statusCode: 404)
156+
157+
let delegate = DownloadFinishCountingDelegate()
158+
downloader.delegate = delegate
159+
160+
downloader.downloadImage(with: url) { result in
161+
XCTAssertNotNil(result.error)
162+
XCTAssertEqual(delegate.finishedEvents.count, 1)
163+
exp.fulfill()
164+
}
165+
waitForExpectations(timeout: 3, handler: nil)
166+
_ = delegate
167+
}
168+
169+
func testDidFinishDownloadingDelegateCalledOncePerErroredDownload() {
170+
let exp = expectation(description: #function)
171+
172+
let url = testURLs[0]
173+
stub(url, errorCode: NSURLErrorNotConnectedToInternet)
174+
175+
let delegate = DownloadFinishCountingDelegate()
176+
downloader.delegate = delegate
177+
178+
downloader.downloadImage(with: url) { result in
179+
XCTAssertNotNil(result.error)
180+
XCTAssertEqual(delegate.finishedEvents.count, 1)
181+
exp.fulfill()
182+
}
183+
waitForExpectations(timeout: 3, handler: nil)
184+
_ = delegate
185+
}
186+
187+
func testDidFinishDownloadingDelegateConsistentWhenDataModifyingFails() {
188+
let exp = expectation(description: #function)
189+
190+
let url = testURLs[0]
191+
stub(url, data: testImageData)
192+
193+
let delegate = NilModifierCountingDelegate()
194+
downloader.delegate = delegate
195+
196+
downloader.downloadImage(with: url) { result in
197+
XCTAssertNotNil(result.error)
198+
XCTAssertEqual(delegate.finishedEvents.count, 1)
199+
// The single event should report the failure, not a success.
200+
XCTAssertNotNil(delegate.finishedEvents.first?.error)
201+
exp.fulfill()
202+
}
203+
waitForExpectations(timeout: 3, handler: nil)
204+
_ = delegate
205+
}
206+
128207
func testDownloadWithModifyingRequest() {
129208
let exp = expectation(description: #function)
130209

@@ -794,6 +873,57 @@ class TaskResponseCompletion: ImageDownloaderDelegate {
794873
}
795874
}
796875

876+
class DownloadFinishCountingDelegate: ImageDownloaderDelegate, @unchecked Sendable {
877+
private let lock = NSLock()
878+
private var _finishedEvents: [(url: URL, response: URLResponse?, error: (any Error)?)] = []
879+
880+
var finishedEvents: [(url: URL, response: URLResponse?, error: (any Error)?)] {
881+
lock.lock()
882+
defer { lock.unlock() }
883+
return _finishedEvents
884+
}
885+
886+
func imageDownloader(
887+
_ downloader: ImageDownloader,
888+
didFinishDownloadingImageForURL url: URL,
889+
with response: URLResponse?,
890+
error: (any Error)?)
891+
{
892+
lock.lock()
893+
defer { lock.unlock() }
894+
_finishedEvents.append((url, response, error))
895+
}
896+
}
897+
898+
// Implements the data-modifying method directly instead of subclassing
899+
// `DownloadFinishCountingDelegate`: a protocol requirement satisfied by a protocol extension in the
900+
// parent is not re-dispatched to a subclass override through the existential.
901+
class NilModifierCountingDelegate: ImageDownloaderDelegate, @unchecked Sendable {
902+
private let lock = NSLock()
903+
private var _finishedEvents: [(url: URL, response: URLResponse?, error: (any Error)?)] = []
904+
905+
var finishedEvents: [(url: URL, response: URLResponse?, error: (any Error)?)] {
906+
lock.lock()
907+
defer { lock.unlock() }
908+
return _finishedEvents
909+
}
910+
911+
func imageDownloader(
912+
_ downloader: ImageDownloader,
913+
didFinishDownloadingImageForURL url: URL,
914+
with response: URLResponse?,
915+
error: (any Error)?)
916+
{
917+
lock.lock()
918+
defer { lock.unlock() }
919+
_finishedEvents.append((url, response, error))
920+
}
921+
922+
func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? {
923+
return nil
924+
}
925+
}
926+
797927
final class URLModifier: ImageDownloadRequestModifier {
798928
let url: URL?
799929
init(url: URL?) {

0 commit comments

Comments
 (0)