Is it safe to mark the FileManager Sendable? - swift

I have an extension on the FileManager that uses the new concurrency:
try await Task.detached(priority: .background) closure and then uses self to create a directory enumerator with FileManager.enumerator(at:includingPropertiesForKeys:options:errorHandler:).
On Xcode 14 I get the warning:
I am trying to figure out if it's safe to mark FileManager as Sendable:
extension FileManager: #unchecked Sendable {}.
It seems that FileManager is thread safe with some caveats: The methods of the shared FileManager object can be called from multiple threads safely.
Any reason why Apple did not mark the FileManager as Sendable? I would think they have a reason not to do that in which case I should definitely avoid marking it sendable, or maybe they just did not get to it?
Full code (adapted from src):
func allocatedSizeOfDirectory(at directoryURL: URL) async throws -> UInt64 {
try await Task.detached(priority: .background) {
// The error handler simply stores the error and stops traversal
var enumeratorError: Error?
func errorHandler(_: URL, error: Error) -> Bool {
guard (error as NSError).code != NSFileReadNoPermissionError else { return true }
enumeratorError = error
return false
}
// We have to enumerate all directory contents, including subdirectories.
let enumerator = self.enumerator(at: directoryURL,
includingPropertiesForKeys: Array(allocatedSizeResourceKeys),
options: [],
errorHandler: errorHandler)!
// We'll sum up content size here:
var accumulatedSize: UInt64 = 0
// Perform the traversal.
for item in enumerator {
// Bail out on errors from the errorHandler.
if enumeratorError != nil { break }
// Add up individual file sizes.
let contentItemURL = item as! URL
accumulatedSize += try contentItemURL.regularFileAllocatedSize()
}
// Rethrow errors from errorHandler.
if let error = enumeratorError { throw error }
return accumulatedSize
}.value
}

Related

How to check if one of URLSession tasks returned an error and if so to stop code execution?

I need to make 2 API calls simultaneously. I have 2 URLs for the calls, and if one of the calls will return any error I want to stop all the code execution.
How I tried to do it:
I have a function called performRequest() with a completion block. I call the function in my ViewController to update the UI - show an error/or a new data if all was successful. Inside it I create a URLSession tasks and then parse JSON:
I created an array with 2 urls:
func performRequest(_ completion: #escaping (Int?) -> Void) {
var urlArray = [URL]()
guard let urlOne = URL(string: "https://api.exchangerate.host/latest?base=EUR&places=9&v=1") else { return }
guard let urlTwo = URL(string: "https://api.exchangerate.host/2022-05-21?base=EUR&places=9") else { return }
urlArray.append(urlOne)
urlArray.append(urlTwo)
}
Then for each of the url inside the array I create a session and a task:
urlArray.forEach { url in
let session = URLSession(configuration: .ephemeral)
let task = session.dataTask(with: url) { data, _, error in
if error != nil {
guard let error = error as NSError? else { return }
completion(error.code)
return
}
if let data = data {
let printData = String(data: data, encoding: String.Encoding.utf8)
print(printData!)
DispatchQueue.main.async {
self.parseJSON(with: data)
}
}
}
task.resume()
}
print("all completed")
completion(nil)
}
For now I receive print("all completed") printed once in any situation: if both tasks were ok, if one of them was ok or none of them.
What I want is to show the print statement only if all tasks were completed successfully and to stop executing the code if one of them returned with error (for example if we will just delete one of the symbols in url string which will take it impossible to receive a data).
How can I do it correctly?

How to wait until data from network call comes and only then return value of a function #Swift

I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!
Service class:
class PunkApiService{
var beers = [Beer]()
func loadList(at page: Int){
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
guard error == nil else {
print(error!)
return
}
//MARK: - Handling data came
guard let data = data else{
print("Failed to load data")
return
}
do{
let beers = try JSONDecoder().decode([Beer].self, from: data)
self.beers.append(contentsOf: beers)
}
catch{
print("Failed to decode data")
}
}
task.resume()
}
And Interactor class(without completion handler or dispatch group):
class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
service.loadList(at: page)
return service.beers
}
Added: my attempt with completion handler
var beers: [Beer]
func loadList(at page: Int, completion: ()->()){
service.loadList(at: page)
completion()
}
func completion(){
beers.append(contentsOf: service.beers)
}
loadList(at: 1) {
completion()
}
This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:
class PunkApiService {
func loadList(at page: Int) async {
// change function to await for task result
let (data, error) = try await URLSession.shared.data(from: url)
let beers = try JSONDecoder().decode([Beer].self, from: data)
...
return beers
}
}
class BeersListInteractor: BeersListInteractorProtocol {
func loadList(at page: Int) async -> [Beer]{
let beers = await service.loadList(at: page)
return service.beers
}
}
See a good explanation here
I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.
func loadList(at page: Int, completion: #escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}
This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).
Here's how you would now call the function:
service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}
if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}
Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.

How to get the download progress with the new try await URLSession.shared.download(...)

Apple just introduced async/await and a bunch of Foundation functions that use them. I'm downloading a file using the new async/await pattern, but i cannot seem to get the download progress.
(downloadedURL, response) = try await URLSession.shared.download(for: dataRequest, delegate: self) as (URL, URLResponse)
As you can see, there is a delegate, and i have tried conforming my class to the URLSessionDownloadDelegate and implementing the urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) function, but it never gets called.
I also tried creating a new URLSession and setting it's delegate to the same class, in hopes that URLSession would call this function instead, but it never gets called and the file still happily downloads. But i need the progress, how do i get it please?
A few observations:
The delegate in download(for:delegate:) is a URLSessionTaskDelegate, not a URLSessionDownloadDelegate, so there are not any assurances that download-specific delegate methods would be called.
FWIW, in Use async/await with URLSession, they illustrate the delegate as being used for authentication challenges, not for download progress.
With traditional URLSessionTask methods, the download progress is not called if you call downloadTask(with:completionHandler:), but rather only if you call the rendition without the completion handler, downloadTask(with:). As Downloading Files from Websites says:
If you want to receive progress updates as the download proceeds, you must use a delegate.
If the new download(for:delegate:) uses downloadTask(with:completionHandler:) behind the scenes, one can easily imagine why one might not see the download progress being reported.
But all of that is academic. Bottom line, you do not see progress reported with download(for:delegate:) or download(from:delegate:). So, if you want to see progress as the download proceeds, you have a few options:
Implement bytes(from:) as suggested by Won and update your progress as bytes come in.
As an aside, I might advise streaming it to a file (e.g., an OutputStream) rather than appending it to a Data, to mirror the memory characteristics of a download task. But, his answer illustrates the basic idea.
Fall back to the delegate-based downloadTask(with:) solution.
If you want to write your own version that reports progress, you could do something like:
extension URLSession {
func download(from url: URL, delegate: URLSessionTaskDelegate? = nil, progress parent: Progress) async throws -> (URL, URLResponse) {
try await download(for: URLRequest(url: url), progress: parent)
}
func download(for request: URLRequest, delegate: URLSessionTaskDelegate? = nil, progress parent: Progress) async throws -> (URL, URLResponse) {
let progress = Progress()
parent.addChild(progress, withPendingUnitCount: 1)
let bufferSize = 65_536
let estimatedSize: Int64 = 1_000_000
let (asyncBytes, response) = try await bytes(for: request, delegate: delegate)
let expectedLength = response.expectedContentLength // note, if server cannot provide expectedContentLength, this will be -1
progress.totalUnitCount = expectedLength > 0 ? expectedLength : estimatedSize
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
guard let output = OutputStream(url: fileURL, append: false) else {
throw URLError(.cannotOpenFile)
}
output.open()
var buffer = Data()
if expectedLength > 0 {
buffer.reserveCapacity(min(bufferSize, Int(expectedLength)))
} else {
buffer.reserveCapacity(bufferSize)
}
var count: Int64 = 0
for try await byte in asyncBytes {
try Task.checkCancellation()
count += 1
buffer.append(byte)
if buffer.count >= bufferSize {
try output.write(buffer)
buffer.removeAll(keepingCapacity: true)
if expectedLength < 0 || count > expectedLength {
progress.totalUnitCount = count + estimatedSize
}
progress.completedUnitCount = count
}
}
if !buffer.isEmpty {
try output.write(buffer)
}
output.close()
progress.totalUnitCount = count
progress.completedUnitCount = count
return (fileURL, response)
}
}
With:
extension OutputStream {
/// Write `Data` to `OutputStream`
///
/// - parameter data: The `Data` to write.
func write(_ data: Data) throws {
try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
throw OutputStreamError.bufferFailure
}
var bytesRemaining = buffer.count
while bytesRemaining > 0 {
let bytesWritten = write(pointer, maxLength: bytesRemaining)
if bytesWritten < 0 {
throw OutputStreamError.writeFailure
}
bytesRemaining -= bytesWritten
pointer += bytesWritten
}
}
}
}
Note:
This uses a small buffer to avoid trying to load the entire asset into memory at one time. It writes the results to a file as it goes along.
This is important if the assets might be large (which, generally, is why we use download rather than data).
Note that expectedContentLength can sometimes be -1, in which case we do not know the size of the file being downloaded. The above handles that scenario.
The logic for guestimating the progress when the size of the asset is unknown is a matter of personal preference. Above I use an estimated asset size and adjust the progress. It won't be terribly accurate but at least it reflects some progress as the download proceeds.
I include a try Task.checkCancellation() so that the download task is cancellable.
I use Progress to report the progress back to the parent. You can hook in and display this however you want, but is especially simple if you are using a UIProgressView.
Anyway, you can then do things like:
func startDownloads(_ urls: [URL]) async throws {
let cachesFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let progress = Progress()
progressView.observedProgress = progress // assuming you are just updating a `UIProgressView` with the overall progress of all the downloads
try await withThrowingTaskGroup(of: Void.self) { group in
progress.totalUnitCount = Int64(urls.count)
for url in urls {
group.addTask {
let destination = cachesFolder.appendingPathComponent(url.lastPathComponent) // obviously, put the resulting file wherever you want
let (url, _) = try await URLSession.shared.download(from: url, progress: progress)
try? FileManager.default.removeItem(at: destination)
try FileManager.default.moveItem(at: url, to: destination)
}
}
try await group.waitForAll()
}
}
You can use URLSession.shared.bytes(from: imageURL) and for await in to loop over.
URLSession.shared.bytes returns (URLSession.AsyncBytes, URLResponse). AsyncBytes is async a sequence that can be looped over using for await in.
func fetchImageInProgress(imageURL: URL) async -> UIImage? {
do {
let (asyncBytes, urlResponse) = try await URLSession.shared.bytes(from: imageURL)
let length = (urlResponse.expectedContentLength)
var data = Data()
data.reserveCapacity(Int(length))
for try await byte in asyncBytes {
data.append(byte)
let progress = Double(data.count) / Double(length)
print(progress)
}
return UIImage(data: data)
} catch {
return nil
}
}
It shows like below progressive fetching image.

Asynchronous thread in Swift - How to handle?

I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)

swift - order of functions - which code runs when?

I have an issue with my code and I think it could be related to the order in which code is called.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
private var tasks = [Task]()
override func willActivate() {
let taskUrl = "http://myjsonurl.com"
downloadJsonTask(url: taskUrl)
print(tasks.count) // EMPTY
super.willActivate()
}
func downloadJsonTask(url: String) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
do
{
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("somehting went wrong after downloading")
}
}.resume()
}
}
I'm defining the private var tasks and fill it with the downloadJsonTask function but after the function ran the print(tasks.count) gives 0.
When I call print(downloadedTasks.tasks.count) it gives 4.
I think that in sequence of time the tasks variable is empty when I print it and it is filled later on.
When you are trying to print number of tasks in willActivate(), function downloadJsonTask(url: String) hasn't been completed yet, so you have empty array because tasks haven't been set yet.
You should add completion handler to downloadJsonTask just like this:
(don't forget to pass completion as parameter of function)
func downloadJsonTask(url: String, completion: #escaping () -> Void) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
completion()
return
}
do {
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("something went wrong after downloading")
}
completion() // This is moment when code which you write inside closure get executed
}.resume()
}
In your willActivate() use this function like this:
downloadJsonTask(url: taskUrl) {
print(tasks.count)
}
So that means when you get your data, your code inside curly braces will get executed.
You’re correct in your assumption that tasks has not yet been assigned a value when it’s first printed.
The thing is network requests are performed asynchronously. It means that iOS does not wait until downloadJsonTask(url:) is finished but continues executing the code right away (i.e. it calls print(tasks.count) immediately after the network request started, without waiting for it to produce any results).
The piece of code inside brackets after URLSession.shared.dataTask(with:) is called a completion handler. This code gets executed once the network request is competed (hence the name). The tasks variable is assigned a value only when the request is finished. You can make sure it works by adding print(self.tasks.count) after self.tasks = downloadedTasks.tasks:
self.tasks = downloadedTasks.tasks
print(self.tasks)
print(downloadedTasks.tasks.count)