How to make alert instead of eternal ProgressView appear after pushing an empty query in SwiftUI?

Viewed 25

I am trying to make an app that will allow you to view the repo list of the user by providing their login name and I get quite a major bug I can't figure our how to get rid of.

How to make the bug appear?

  1. Advance from the initial view to the ListView by typing the correct user login (e.g. jacobtoye) name in the TextField
  2. Remove the content of TextField in the ListView and press "View repos"
  3. At least I don't get an error alert then, but an eternal ProgressView()

How to make an empty query pushed from the ListView show an "invalid username error" alert rather than Eternal ProgressView()?

The code:

CONTENTVIEW FILE:

       import SwiftUI
  
  struct ContentView: View {
      @StateObject var viewModel = ContentViewModel()
      
      var body: some View {
          
          if viewModel.repos.isEmpty {

//Initial View code here
              
              NavigationView{
                  VStack(alignment: .leading){
                      HStack{
                              TextField("Enter GitHub username", text: $viewModel.userName)
                              Button("View repos") {
                                      viewModel.fetchRepos()
                              }
                              .alert("This user doesn't own any repositories", isPresented: $viewModel.showingSecondAlert){
                                  Button("Try again", action: viewModel.flushRepos)
                              }
                              .padding()
                          }
                      .alert("Invalid GitHub username", isPresented: $viewModel.showingMainAlert){
                          Button("Try again", action: viewModel.flushRepos)
                  }
                  .navigationTitle("GH Repo Language")
                  
                      Spacer()
                  }
                  }
              
              }
          
              else if viewModel.reposAreLoading {
                      ProgressView()
                  
              } else {

//ListView code here
                  
              NavigationView{
                  VStack{
                      HStack{
                              TextField("Enter GitHub username", text: $viewModel.userName)
                              Button("View repos") {
                                      viewModel.fetchRepos()
                              }
                              .alert("This user doesn't own any repositories", isPresented: $viewModel.showingSecondAlert){
                                  Button("Try again", action: viewModel.flushRepos)
                              }
                              .padding()
                      }
                      .alert("Invalid GitHub username", isPresented: $viewModel.showingMainAlert){
                          Button("Try again", action: viewModel.flushRepos)
                          }
                    
                      List {
                              ForEach(viewModel.repos) { repo in
                                VStack {
                                    Text("\(repo.fullName)")
                                    }
                                }
                          }
                      }
                  
                  }
                  .navigationTitle("\(viewModel.repos.first?.owner.login ?? "no name")'s repos")
                  }
              }
          }

CONTENTVIEW MODEL:

import Foundation

final class ContentViewModel: ObservableObject {
    @Published var repos = [Repository]()
    @Published var reposAreLoading = false
    @Published var userName = ""
    @Published var showingMainAlert = false
    @Published var showingSecondAlert = false
    
    
    func fetchRepos() {
        
        reposAreLoading = true
        
        guard let url = URL(string: "https://api.github.com/users/\(userName)/repos") else { return }
        URLSession.shared.dataTask(with: url) {(data, _, _) in
            guard let data = data else { return }
            DispatchQueue.main.async {
                do {
                    self.repos = try JSONDecoder().decode([Repository].self, from: data)
                    self.reposAreLoading = false
                    if self.repos.isEmpty {
                        self.showingSecondAlert = true
                    }
                } catch {
                    self.showingMainAlert = true
                }
            }
        }.resume()
        
    }
    
    func flushRepos() {
        self.repos = [Repository]()
    }
}

JSON STRUCTS:

import Foundation

struct Repository: Decodable, Identifiable {
    let id: Int
    let name: String
    let fullName: String
    let owner: Owner
    
    enum CodingKeys: String, CodingKey {
        case id, owner
        case name
        case fullName = "full_name"
    }
}

struct Owner : Decodable, Identifiable {
    let id: Int
    let login: String
    let reposUrl : String

    enum CodingKeys: String, CodingKey, CaseIterable {
        case id
        case login
        case reposUrl = "repos_url"
    }
}

For me it seems that somehow Xcode gets stuck in realising the fetchRepos() function after pushing an empty query from the ListView, but I don't know how to find out what to fix. Please help.

1 Answers

Right now, you don't test for error codes on your URL response. An invalid user name returns a 404, so you'll want to check for that (note that you could also check for an empty user name before even trying to fetch the results).

URLSession.shared.dataTask(with: url) {(data, response, error) in
    DispatchQueue.main.async {
        self.reposAreLoading = false
        
        // may want to check `error` here, too

        if let response = response as? HTTPURLResponse, response.statusCode == 404 {
            self.showingMainAlert = true
            return
        }

        //consider handling other error codes?

        guard let data = data else { return }
        
        do {
            self.repos = try JSONDecoder().decode([Repository].self, from: data)
            if self.repos.isEmpty {
                self.showingSecondAlert = true
            }
        } catch {
            self.showingMainAlert = true
        }
    }
}.resume()

See inline comments for a couple of other notes

Also probably worth noting that you can modernize this with async/await which would simplify your code. See https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-codable-data-with-urlsession-and-swiftui

Related