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.