I am trying to build a chat app. I request the messages through API. Since the chat might be an image(a URL for that image in this case), and every image has different height / width ratio, I’m trying to fetch their ratios before reloading the table view. Rather than GCD, I’d like to make use of the new Swift Concurrency.
This is the method that requests the messages. After receiving the response, it fetches the ratios(if that message is an image), and updates the UI.
private func requestMessages() {
APIManager.requestMessages { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let result):
guard let messageSections = result.messageSections else {
alert(message: result.description)
return
}
self.messageSections = messageSections
// fetches the ratios, and then update the UI
Task { [weak self] in
try await self?.fetchImageRatios()
DispatchQueue.main.async { [weak self] in // breakpoint 1
guard let self = self else { return }
self.tableView.reloadData() // breakpoint 2
}
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
In the Task { } above, it calls the async method fetchImageRatios() which looks like this:
private func fetchImageRatios() async throws {
try await withThrowingTaskGroup(of: CGFloat?.self) { group in
for messageSection in messageSections {
for message in messageSection {
guard let imageURLString = message.imageURL,
let imageURL = URL(string: imageURLString) else {
continue
}
group.addTask { [weak self] in
try await self?.fetchRatio(from: imageURL)
}
for try await ratio in group {
message.ratio = ratio
}
}
}
print("messageSections 1 - inside withThrowingTaskGroup closure")
dump(messageSections)
}
print("messageSections 2 - outside withThrowingTaskGroup closure")
dump(messageSections)
}
In this method fetchImageRatios(), it iterates through messageSections(the type is [[Message]], please check the code block below) in for loops, and fetches the ratio of each image from its image URL. Since fetching the ratio does not need to be serial, I wanted to implement it as concurrent, so I put the each fetching process in a TaskGroup, as child tasks. (Also wanted to try structured concurrency.)
struct Message: Codable {
var username: String?
var message: String?
var time: String?
var read: Bool?
var imageURL: String?
var ratio: CGFloat?
}
Struct Message has ratio property, and I give that the value of the ratio I fetch. (Actually, here, it doesn’t have to wait for all the fetchings to be finished before updating the ratio property, but I didn’t know how else I can do it without waiting, especially because I thought it’s better to access each message in the for loops anyways. If there’s a better idea, please let me know.)
Now my main question is here:
When the method fetchRatio(from imageURL: URL) is like #1, with Data(contentsOf: URL), it does work. It shows each image perfectly with their own ratio. It also prints "messageSections 1 - inside withThrowingTaskGroup closure" and "messageSections 2 - outside withThrowingTaskGroup closure”, and dumps messageSections in fetchImageRatios() without a problem. But, it does give the purple warning saying “Synchronous URL loading of https://www.notion.so/front-static/meta/default.png should not occur on this application's main thread as it may lead to UI unresponsiveness. Please switch to an asynchronous networking API such as URLSession.”
#1
private func fetchRatio(from imageURL: URL) async throws -> CGFloat? {
// Data(contentsOf:)
guard let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) else { return nil }
let width = image.size.width
let height = image.size.height
let ratio = height / width
return ratio
}
But, when the method fetchRatio(from imageURL: URL) is like #2, which uses URLSession.shared.data(from: URL), I don’t see anything in my table view. It’s just empty. It doesn’t even print "messageSections 1 - inside withThrowingTaskGroup closure" or "messageSections 2 - outside withThrowingTaskGroup closure”, and no dumping messageSections. I guessed maybe it doesn’t reload the table view so I put breakpoints at // breakpoint 1 and // breakpoint 2 in requestMessages() but the app doesn’t even stop there which is so confusing. It does iterate through for try await ratio in group though.
#2
private func fetchRatio(from imageURL: URL) async throws -> CGFloat? {
// URLSession - data(from:)
let (data, _) = try await URLSession.shared.data(from: imageURL)
guard let image = UIImage(data: data) else { return nil }
let width = image.size.width
let height = image.size.height
let ratio = height / width
return ratio
}
I’m guessing it’s because URLSession.shared.data(from: imageURL) is being called with try await, but I feel like it should still work as expected.. What am I missing?
Why does it not even print, dump, or go into the DispatchQueue.main.async block when it’s the second case(#2)?
Any ideas to improve my codes are welcome. I really appreciate your help in advance.
As the error message is warning you, you should avoid the
Data(contentsOf:)approach as that runs synchronously, blocking the calling thread. TheURLSessionmethod,try await data(from:)is the way to go. It has lots of other advantages, too (it is cancelable, offers more meaningful error handling, supports more complicated requests and HTTP auth challenges, etc.). There is no reason not to use theURLSessionapproach, and lots of reasons to avoid theData(contentsOf:)approach.You say you want to adopt Swift concurrency. Then you should eliminate the use of closures in
requestMessagesand useasync-await. And do not useTask {…}. And do not useDispatchQueue.main.async {…}.For example, a Swift concurrency rendition of
requestMessageswould be:Now that assumes you have an
asyncrendition ofrequestMessagesinAPIManager. Either refactor that or, for now, just write a wrapper:If you haven't seen it already, you might want to check out WWDC 2021 video Swift concurrency: Update a sample app.
Needless to say, the whole idea of retrieving images (which is a very expensive and slow process) in order to determine the image ratios is a non-starter. You should update the cells’ aspect ratios on the fly. Generally, we would download the images just-in-time, and update the cell accordingly. But there is not enough here for us to diagnose that particular problem any further.