I have been using AsyncImage for my project which gets the images using urls. I thought AsyncImage is the best way to go since SwiftUI provides a ready to use component for it.
However, if a image load fails, then I am getting a common error and not a detailed error message of why the url request failed (eg: no permissions to access URL or some other reason).
The reason I am constantly seeing is The operation couldn’t be completed. (SwiftUI.(unknown context at $1d2600574).LoadingError error 1.)
Executable code:
import SwiftUI
class Foo {
var title: String
var url: String
var image: Image?
init(title: String, url: String, image: Image? = nil) {
self.title = title
self.url = url
self.image = image
}
}
struct ContentViewA: View {
@State var showSheetA: Bool = false
var body: some View {
VStack {
Text("This is main view")
}
.onAppear{
showSheetA = true
}
.sheet(isPresented: $showSheetA) {
SheetViewA()
}
}
}
struct SheetViewA: View {
@State var data = [
Foo(title: "Image E", url: "https://t3.ftcdn.net/jpg/10/08/34/50/360_F_1008345045_VOe4ziz83vb6d3E3X6KI00qHyLd32D4l.jpg123", image: nil)
]
@State var panelDetent: PresentationDetent = .medium
var body: some View {
NavigationStack {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 10) {
ForEach(data, id: \.title) { item in
if let urlObject = URL(string: item.url) {
AsyncImage(url: urlObject,
scale: 1.0,
transaction: Transaction(animation: .spring(response: 0.5, dampingFraction: 0.65, blendDuration: 0.025)),
content: { renderPhoto(phase: $0, item: item) })
} else {
/// Note: Shows placeholder view
EmptyView()
}
}
}
.background(Color.gray.opacity(0.2))
.padding(.leading, 0)
.padding(.trailing, 16)
.frame(maxWidth: .infinity, minHeight: 65, maxHeight: 65, alignment: .topLeading)
}
}
.padding([.top, .bottom], 150.0)
.presentationDetents([.medium, .large], selection: $panelDetent)
}
}
@ViewBuilder
private func renderPhoto(phase: AsyncImagePhase, item: Foo) -> some View {
switch phase {
case .success(let image):
let _ = print("Success state")
case .failure(let error):
let _ = print("%%% detailed error is - \(error.localizedDescription) %%%%")
case .empty:
let _ = print("Empty state")
@unknown default:
EmptyView()
}
}
}
Is there a way to get detailed error message using AsycnImage? Or will I have to get images using the old URLSession request method? I need to display detailed error message on the UI.
Here is my test code that prints the error
and error.localizedDescription
,
giving very little info to a user, just LoadingError
.
Included is a test fetching the image (which does not exist) using URLSession
.
The data/info your get seems to give you more information (File Not Found),
but this is just because the server sends you this info.
Note, casting the error to URLError
and DecodingError
return nil
, because this is not the
type of error you get this time, but you may well get something next time.
struct Foo {
var title: String
var url: String
var image: Image?
init(title: String, url: String, image: Image? = nil) {
self.title = title
self.url = url
self.image = image
}
}
struct ContentView: View {
@State private var showSheetA: Bool = false
var body: some View {
Text("This is main view")
Button("show sheet") {
showSheetA = true
}.buttonStyle(.bordered)
.sheet(isPresented: $showSheetA) {
SheetViewA()
}
}
}
struct SheetViewA: View {
@State private var data = [
Foo(title: "Image E", url: "https://t3.ftcdn.net/jpg/10/08/34/50/360_F_1008345045_VOe4ziz83vb6d3E3X6KI00qHyLd32D4l.jpg123", image: nil)
]
@State private var panelDetent: PresentationDetent = .medium
var body: some View {
NavigationStack {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 10) {
ForEach(data, id: \.title) { item in
if let urlObject = URL(string: item.url) {
AsyncImage(url: urlObject,
scale: 1.0,
transaction: Transaction(animation: .spring(response: 0.5, dampingFraction: 0.65, blendDuration: 0.025)),
content: { renderPhoto(phase: $0, item: item) })
} else {
/// Note: Shows placeholder view
EmptyView()
}
}
}
.background(Color.gray.opacity(0.2))
.padding(.leading, 0)
.padding(.trailing, 16)
.frame(maxWidth: .infinity, minHeight: 65, maxHeight: 65, alignment: .topLeading)
}
}
.padding([.top, .bottom], 150.0)
.presentationDetents([.medium, .large], selection: $panelDetent)
// for testing
.task {
await getImage()
}
}
}
@ViewBuilder
private func renderPhoto(phase: AsyncImagePhase, item: Foo) -> some View {
switch phase {
case .success(let image):
let _ = print("Success state")
case .failure(let error):
let _ = print("%%% error is - \(error) %%%%")
let _ = print("%%% localizedDescription error is - \(error.localizedDescription)")
let _ = print("%%% ---> URLError error is - \(error as? URLError) %%%%")
let _ = print("%%% ---> DecodingError error is - \(error as? DecodingError) %%%%")
case .empty:
let _ = print("Empty state")
@unknown default:
EmptyView()
}
}
// for testing
func getImage() async {
guard let url = URL(string: "https://t3.ftcdn.net/jpg/10/08/34/50/360_F_1008345045_VOe4ziz83vb6d3E3X6KI00qHyLd32D4l.jpg123") else {
print("bad URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
print("\n---> URLSession data: \(String(data: data, encoding: .utf8) as AnyObject)\n")
} catch {
print("\n---> URLSession error: \(error)\n")
}
}
}
Note, the server returns a html result:
<html>
<head>
</head>
<body>
<h1>File Not Found</h1>
</body>
</html>
When AsyncImage
tries to make sense of this, it concludes that this
cannot be loaded (because this is not an image).