Alejo
Alejo

Reputation: 57

How to detect URLSession Errors from the view and react accordingly?

So Im writing a simple lyrics app that uses an API that receives a song name and artist and returns the lyrics for it. Everything works as expected if I send proper song names and artists, but im having a hard time detecting errors from the view and reacting accordingly such as showing an alert before the sheet gets rendered or something. I mean an error such as the user entering a song or artist name with a typo, which would make the API to not get lyrics for such song. When the lyrics are not found, the API returns a 400 not found HTTP code, maybe I can check for such an error code somewhere on the api method call and check later from the view or similar?

This is my view, simply makes the api call if theres an internet connection available, which toggles the sheet to render:

            HStack {
                Spacer()
                
                Button(action: {
                    if(!NetworkMonitor.shared.isConnected) {
                        self.noConnectionAlert.toggle()
                    } else {
                        viewModel.loadApiSongData(songName: songName, artistName: artistName)
                        //self.showingLyricsSheet = true
                    }
                }, label: {
                    CustomButton(sfSymbolName: "music.note", text: "Search Lyrics!")
                })
                
                .alert(isPresented: $noConnectionAlert) {
                    Alert(title: Text("No internet connection"), message: Text("Oops! It seems you arent connected to the internet. Please connect and try again!"), dismissButton: .default(Text("Got it!")))
                }
                Spacer()
            }
            .padding(.top, 20)
            .sheet(item: $viewModel.apiResponse) { item in
                LyricsView(vm: self.viewModel, songName: songName, artistName: artistName, arrLyrics: item.apiValues)
            }

This is my API calling method:

class ViewModel : ObservableObject {
    @Published var searchedSongs = [SongDetails]() {
        didSet {
            print(searchedSongs)
        }
    }
    @Published var apiResponse : APIResponse?

    func loadApiSongData(songName: String, artistName: String) {
        let rawUrl = "https://api.lyrics.ovh/v1/\(artistName)/\(songName)"
        let fixedUrl = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
        print("Old url: \(rawUrl)")
        print("New url: \(fixedUrl!)")
        
        guard let url = URL(string: fixedUrl!) else {
            print("Invalid URL")
            return
        }
        
        let request = URLRequest(url: url)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                if let decodedResponse = try? JSONDecoder().decode(Song.self, from: data) {
                    // we have good data – go back to the main thread
                    DispatchQueue.main.async {
                        // update our UI
                        print("Found good lyrics")
                        if(!self.songAlreadySearched(songName: songName)) {
                            let song = SongDetails(songName: songName, artistName: artistName, lyrics: decodedResponse.lyrics)
                            self.searchedSongs.append(song)
                        }
                        self.apiResponse = APIResponse(apiValues: [decodedResponse.lyrics])
                    }
                    // everything is good, so we can exit
                    return
                }
            }
            print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
        }.resume()
    }

Upvotes: 0

Views: 625

Answers (1)

achu
achu

Reputation: 329

@Published var apiError: APIError? // Added error enum in ViewModel. Use this in your view. Add message string in enum whatever you want to display in UI


    URLSession.shared.dataTask(with: request) { data, response, error in
        DispatchQueue.main.async {
            self.responseHandler(data, response, error, songName, artistName)
        }
       
    }.resume()

And the handler function

private func responseHandler(_ data: Data?,
                     _ response: URLResponse?,
                     _ error: Error?,
                     _ songName: String,
                     _ artistName: String) {
    if let error = error {
        if error._code == -1009 {
            apiError = .offline
        } else {
            apiError = .sessionError
        }
        return
    }
    guard let response = response as? HTTPURLResponse, let data = data else {
        apiError = .missingDataError
        return
    }
    guard (200..<300).contains(response.statusCode) else {
        switch Status(rawValue: response.statusCode) {
        case .requestTimeout:
            apiError = .timeoutError
        case .internalServerError:
            apiError = .internalServerError
        case .notFound:
            apiError = .notFound
        default:
            apiError = .requestError
        }
        
        return
    }
    do {
        let decodedResponse = try JSONDecoder().decode(Song.self, from: data)
        if(!songAlreadySearched(songName: songName)) {
            let song = SongDetails(songName: songName, artistName: artistName, lyrics: decodedResponse.lyrics)
            searchedSongs.append(song)
        }
        apiResponse = APIResponse(apiValues: decodedResponse.lyrics)
        
    } catch {
        apiError = .parsingError
    }
}

Error enums

enum APIError: Error {
    case sessionError
    case parsingError
    case missingDataError
    case requestError
    case timeoutError
    case offline
    case internalServerError
    case notFound
    case genericError
}
enum Status: Int {
    case multipleChoices = 300
    case badRequest = 400
    case forbidden = 403
    case notFound = 404
    case requestTimeout = 408
    case internalServerError = 500
    case notImplemented = 501
    case badGateway = 502
    case serviceUnavailabe = 503
    case gatewayTimeout = 504
}

Upvotes: 2

Related Questions