If you want to use a Result
with your fetch you need to do something like this:
CompanyViewModel
enum CompanyError: Error {
case networkFailed, decodeFailed
}
@MainActor
class CompanyViewModel: ObservableObject {
@Published var companies: [Company] = []
@Published var showingError = false
var errorMessage = ""
func fetchCompanies() async {
let fetchTask = Task { () -> [Company] in
let url = URL(string: "https://fierce-retreat-36855.herokuapp.com/company")!
let (data, _) = try await URLSession.shared.data(from: url)
let companies = try JSONDecoder().decode([Company].self, from: data)
return companies
}
let result = await fetchTask.result
switch result {
case .success(let companies):
self.companies = companies
case .failure(let error):
showError("An error occurred: \(error.localizedDescription).")
}
}
private func showError(_ message: String) {
self.showingError = true
self.errorMessage = message
}
}
If you see:
add @MainActor
to ViewModel
:
The problem with this is you have no way to set the error in the result.
One way to get around this is to throw
errors instead of passing them in the Result
:
func fetchCompanies() async {
let fetchTask = Task { () -> [Company] in
let url = URL(string: "https://fierce-retreat-36855.herokuapp.com/company")!
let (data, _) = try await URLSession.shared.data(from: url)
do {
let companies = try JSONDecoder().decode([Company].self, from: data)
return companies
} catch {
throw CompanyError.decodeFailed
}
}
let result = await fetchTask.result
do {
self.companies = try result.get()
self.showingError = false
} catch CompanyError.decodeFailed {
showError("JSON decoding error occurred.")
} catch {
showError("Unknown error occurred.")
}
}
func fetchCompanies() async {
let fetchTask = Task { () -> [Company] in
let url = URL(string: "https://fierce-retreat-36855.herokuapp.com/company")!
let data: Data
let urlResponse: URLResponse
do {
(data, urlResponse) = try await URLSession.shared.data(from: url)
guard let response = urlResponse as? HTTPURLResponse else { return [Company]() }
if response.statusCode == 200 {
if let companies = try JSONDecoder().decode([Company].self, from: data) {
return companies
} else {
throw CompanyError.decodeFailed
}
}
else {
throw CompanyError.invalidResponse
}
} catch {
throw CompanyError.networkFailed
}
}
let result = await fetchTask.result
do {
self.companies = try result.get()
self.showingError = false
} catch CompanyError.networkFailed {
showError("Unable to fetch the quotes.")
} catch CompanyError.decodeFailed {
showError("Unable to convert quotes to text.")
} catch CompanyError.invalidResponse {
showError("Invalid HTTP response.")
} catch {
showError("Unknown error.")
}
}
And of course everthing is state driven. So if we want to display an alert in the UI, we need a change of state - in this case from the ViewModel
:
CompanyViewModel
@MainActor
class CompanyViewModel: ObservableObject {
@Published var companies: [Company] = []
@Published var showingError = false
var errorMessage = ""
func fetchCompanies() async {
// Change the state
do {
self.companies = try result.get()
self.showingError = false
} catch CompanyError.decodeFailed {
showError("JSON decoding error occurred.")
}
ContentView
struct ContentView: View {
@StateObject var companyVM: CompanyViewModel
@State private var showingAddCompany = false
var body: some View {
NavigationStack {
.task {
await companyVM.fetchCompanies()
}
// Update the display
.alert(companyVM.errorMessage, isPresented: $companyVM.showingError) {
Button("OK", role: .cancel) { }
}
}
}
}
CompanyViewModel
import Foundation
struct Company: Codable, Identifiable, Hashable {
let id: String
let name: String
let employees: [Employee]
}
struct Employee: Codable, Identifiable, Hashable {
let id: String
let name: String
}
let employee1 = Employee(id: "1", name: "Jobs")
let employee2 = Employee(id: "2", name: "Watson")
let employee3 = Employee(id: "3", name: "Gates")
let employees = [employee1, employee2, employee3]
let company1 = Company(id: "1", name: "Apple", employees: employees)
let company2 = Company(id: "2", name: "IBM", employees: employees)
let company3 = Company(id: "3", name: "Microsoft", employees: employees)
enum CompanyError: Error {
case networkFailed, decodeFailed
}
@MainActor
class CompanyViewModel: ObservableObject {
@Published var companies: [Company] = []
@Published var showingError = false
var errorMessage = ""
func fetchCompanies() async {
let fetchTask = Task { () -> [Company] in
let url = URL(string: "https://fierce-retreat-36855.herokuapp.com/company")!
let (data, _) = try await URLSession.shared.data(from: url)
do {
let companies = try JSONDecoder().decode([Company].self, from: data)
return companies
} catch {
throw CompanyError.decodeFailed
}
}
let result = await fetchTask.result
do {
self.companies = try result.get()
self.showingError = false
} catch CompanyError.decodeFailed {
showError("JSON decoding error occurred.")
} catch {
showError("Unknown error occurred.")
}
}
private func showError(_ message: String) {
self.showingError = true
self.errorMessage = message
}
}
ContentView
import SwiftUI
struct ContentView: View {
@StateObject var companyVM: CompanyViewModel
@State private var showingAddCompany = false
var body: some View {
NavigationStack {
List(companyVM.companies) { company in
NavigationLink(value: company) {
Text(company.name)
}
}
.navigationTitle("Companies")
.navigationDestination(for: Company.self) { company in
CompanyView(company: company)
}
.toolbar {
Button(action: {
self.showingAddCompany.toggle()
}) {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showingAddCompany) {
AddCompany(companyVM: self.companyVM)
}
.task {
await companyVM.fetchCompanies()
}
.alert(companyVM.errorMessage, isPresented: $companyVM.showingError) {
Button("OK", role: .cancel) { }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(companyVM: CompanyViewModel())
.preferredColorScheme(.dark)
}
}