-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Download support #33
Download support #33
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hola, 👏
thank you, finally checked the PR, I would say the current approach is feasible and nice. I found minor changes we can improve
NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleDownloadRouter.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleDownloadRouter.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadRow.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadRowViewModel.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🪙
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift
Outdated
Show resolved
Hide resolved
.onAppear { | ||
viewModel.onAppear() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.onAppear { | |
viewModel.onAppear() | |
} | |
.task { | |
await viewModel.onAppear() | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 thinking also about better name for viewModel function, maybe startDownloadTasks maybe better? Or any other suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not starting the download, so I'd suggest something like startObservingDownloadProgress.
func onAppear() { | ||
Task { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
method not used and not necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👋
The sample app crashes on both the simulator and real device when multiple downloads are ongoing (at 5 downloads). Reproducible on every try.
See crash logs.
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift
Outdated
Show resolved
Hide resolved
public func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { | ||
downloadStateDict[downloadTask]?.downloadedBytes = totalBytesWritten | ||
downloadStateDict[downloadTask]?.totalBytes = totalBytesExpectedToWrite | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be the place where (accessing the downloadStateDict
) it's not thread-safe and perhaps causing the crash? Maybe using an actor would be safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 similar we have for running calls and retry mechanism
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, all props which are being mutated should be thread safe
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My pieces also added
Thanks guys for great comments
NetworkingSampleApp/NetworkingSampleApp/Extensions/DownloadAPIManager+instance.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift
Outdated
Show resolved
Hide resolved
.onAppear { | ||
viewModel.onAppear() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 thinking also about better name for viewModel function, maybe startDownloadTasks maybe better? Or any other suggestions?
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift
Outdated
Show resolved
Hide resolved
public func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { | ||
downloadStateDict[downloadTask]?.downloadedBytes = totalBytesWritten | ||
downloadStateDict[downloadTask]?.totalBytes = totalBytesExpectedToWrite | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 similar we have for running calls and retry mechanism
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The crash is now fixed with the actors 👍
From further testing, the memory graph showed that there is a memory leak — the DownloadAPIManager
is not being released.
This is what I've changed to test it:
- Initialize the
DownloadAPIManager
only once in theDownloadsViewModel
and pass this instance down to theDownloadProgressViewModel
instead of usingshared
.
Going from the examples list to Downloads and back (without tapping "Download") will cause the memory leak. This seems to be the source:
urlSession = URLSession(
configuration: urlSessionConfiguration,
delegate: self,
delegateQueue: OperationQueue()
)
By design, the DownloadAPIManager should be used as a singleton object. URLSession keeps strong reference and Apple also recommends using a shared urlSession, otherwise we'll need the user to free the urlSession manually by calling either
I added this as an option in case somebody would like to use a non-singleton instance in the future so they could invalidate the session manually by calling
Give it a try 👍 |
When testing the app on the simulator, when I tap on the |
Yes, that was just exemplary to test out the support for simultaneous downloads. Let's keep it as the original - one download showcase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another round of minor updates
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift
Outdated
Show resolved
Hide resolved
NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏼
do { | ||
/// If retry fails (retryCount is 0 or Task.sleep thrown), catch the error and process it with `ErrorProcessing` plugins. | ||
try await sleepIfRetry( | ||
for: error, | ||
endpointRequest: endpointRequest, | ||
retryConfiguration: retryConfiguration | ||
) | ||
|
||
return try await downloadRequest( | ||
endpointRequest, | ||
resumableData: resumableData, | ||
retryConfiguration: retryConfiguration | ||
) | ||
} catch { | ||
/// error processing | ||
throw await errorProcessors.process(error, for: endpointRequest) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this could go in another method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I happy with final result. No code is perfect but we enhanced it a lot in PR. @tonyskansf can you just confirm your concerns about strong reference in URLSession delegate are fixed or explained?
Thank you all!
👏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By design, the DownloadAPIManager should be used as a singleton object. URLSession keeps strong reference and Apple also recommends using a shared urlSession, otherwise we'll need the user to free the urlSession manually
What specific part of the implementation suggests using the DownloadAPIManager
as a singleton prior to 4f25236? I thought it was just for the sake of the example app 😄
Out of curiosity and scope of this PR, is it possible to let the concrete implementation of the DownloadAPIManaging
handle the URL session instead of leaving this task up to the dev who is error-prone? For example, the URL session could be invalidated upon the deinitialization of the manager instance. That way, the lifecycle of the URL session would be bound to it. 🤔
// DownloadAPIManager
deinit {
invalidateSession()
}
Co-authored-by: Tony Ngo <tonyngo.jr@gmail.com>
There was no documentation, only Matej's intent I guess, that's why I added that in as well. The thing is, that deinit method of the manager instance will never get called because of the strong reference, so the user has to do it externally. 😮💨 I guess the only way would be to create the session every time a download is requested and then destroy it after the download is finished. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the only way would be to create the session every time a download is requested and then destroy it after the download is finished.
If that would make sense that could be an improvement. I think for now, whoever will use this lib and want this behavior can provide a different DownloadAPIManaging
implementation or subclass the one provided.
I think I'll try that but in a separate enhancement PR to test if it's a better solution 👍 |
A rough implementation of a working
DownloadAPIManager
with an example scene.