Search code examples
iosswiftswiftui

Detailed error from AsyncImage in SwiftUI?


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.


Solution

  • 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).