Displaying State of an Async API Call in Swiftui

Displaying State of an Async Api call in SwiftUI

I would go a step further and add idle and failed states.

Then instead of throwing an error change the state to failed and pass the error description. I removed the Double value from the loading state to just show a spinning ProgressView

@MainActor
class GoogleBooksApi: ObservableObject {

enum LoadingState {
case idle
case loading
case loaded(GoogleBook)
case failed(Error)
}

@Published var state: LoadingState = .idle

func fetchBook(id identifier: String) async {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { state = .failed(URLError(.badURL)); return }
self.state = .loading
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(GoogleBook.self, from: data)
self.state = .loaded(response)
} catch {
state = .failed(error)
}
}
}

In the view you have to switch on the state and show different views.
And – very important – you have to declare the observable object as @StateObject. This is a very simple implementation

struct ContentView: View {
@State var code = "ISBN"

@StateObject var api = GoogleBooksApi()

var body: some View {
VStack {
switch api.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let books):
if let info = books.items.first?.volumeInfo {
Text("Name: \(info.title)")
Text("Author: \(info.authors?.joined(separator: ", ") ?? "")")
Text("total: \(books.totalItems)")
}
case .failed(let error):
if error is DecodingError {
Text(error.description)
} else {
Text(error.localizedDescription)
}
}

Button(action: {
code = "978-0441013593"
Task {
await api.fetchBook(id: code)
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}

Show result of an asynchronous call in a SwiftUI view

As I told you in comments, I'd use an observable object and subscribe to a property of it:

class Geocoder : ObservableObject {
@Published var address = "Hello World"
func addressFor(_ location: CLLocation) -> Void {
let geocoder: CLGeocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placeMark, error) in
if error == nil {
if let firstPlaceMark = placeMark?.first {
self.address = (firstPlaceMark.name ?? "") + " " + (firstPlaceMark.locality ?? "")
}
} else {
print(error)
}
}
}
}

struct ContentView: View {

init(location: CLLocation) {
self.location = location
self.geocoder.addressFor(location)
}
var location: CLLocation
@ObservedObject var geocoder = Geocoder()
var body: some View {
Text(geocoder.address)
}

}

in SwiftUI how do I determine view to show based on async call

You have the right idea, but generally with async calls, you're probably going to want to move them into an ObservableObject rather than have them in your View code itself, since views are transient in SwiftUI (although the exception may be the top level App like you have).

class SignInManager : ObservableObject {
@Published var loginStatus: LoginStatus = .undetermined

public init() {
getLoginStatus()
}

func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
//may need to use DispatchQueue.main.async {} to make sure you're on the main thread
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
@main
struct SignUpApp: App {

@StateObject var signInManager = SignInManager()

var body: some Scene {
WindowGroup {
switch signInManager.loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
}

How to call function after async requests finish in SwiftUI?

I tried using this snippet of code within my second api request and it solved my issue, although i do not understand why I need to do this

DispatchQueue.main.async {
self.venueData.venuesdataarray.append(RESPONSE_DETAILS_HERE)
}

Originally I had asked, Does anyone know how I can update my @EnvironmentObject after all the requests complete?

Does anyone know why the snippet I have above makes everything work? Id just like to understand what im doing and maybe someone could learn something if they find this

Display API call data to new View

Your suspicion was about right:

when you do:

foodName = foodApi.foodDescription

the network call did not finish yet.

Restructure your code. there is no need for the foodName variable as you have allready one in your FoodApiSearch. Pass that on to your subview and access it there. As soon as the network call completes it should update your view and show the result.

struct testA: View {
//Textfield
@State var userFoodInput = ""
@State private var didUserSearch = false
//will store api var of foodName
@StateObject private var foodApi = FoodApiSearch()
//send to food results view
//@State private var foodName = ""
var body: some View {
VStack{
TextField("enter first name", text: $userFoodInput)
.onSubmit {
didUserSearch = true
foodApi.searchFood(userItem: userFoodInput)
// foodName = foodApi.foodDescription
}

FoodSearchResultsView(userSearch: $didUserSearch)
.environmentObject(foodApi)
}
}
}

struct FoodSearchResultsView: View {
//calls API
@EnvironmentObject private var foodApi: FoodApiSearch
@State private var searchResultsItem = ""
// @Binding var foodName: String
//if toggled, will display, binded to search bar
@Binding var userSearch: Bool

var body: some View {
if userSearch{
VStack{
Text("Best Match")
HStack{
VStack(alignment: .leading){
Text(foodApi.foodDescription)
Text("1 Cup")
.font(.caption)
.offset(y:8)
}
.foregroundColor(.black)
Spacer()
Text("72 Cals")
}

.frame(width:225, height:50)
.padding([.leading, .trailing], 45)
.padding([.top, .bottom], 10)
.background(RoundedRectangle(
cornerRadius:20).fill(Color("LightWhite")))
.foregroundColor(.black)

Button("Add Food"){
userSearch = false
}
.padding()
}
.frame(maxWidth:.infinity, maxHeight: 700)
}
}
}

Displaying activity indicator while loading async request in SwiftUI

Your view model should have loading state, like the following

@Published var loading = false
private func fetchSources() {
self.loading = true
NetworkManager.shared.getSourceData { (sources) in
self.sources = sources.map(SourcesViewModel.init)
self.loading = false
}
}

and activity indicator should be bound to it, like

ActivityIndicatorView(isShowing: $model.loading) {

Accessing Google API data from within 3 async callbacks and a function in SwiftUI

Solved my own problem.

It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.

So using that code allows you to run the Google Identity token refresh with async/await.

private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>

return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
user.authentication.do { authentication, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: authentication)
}
}
}
}

static func search(user: GIDGoogleUser) async throws {

// some code

guard let authentication = try await auth(user) else { ... }

// some code
}

I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).

let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value

return response.values[0]

Changing views via conditional statements inside of async commands in SwiftUI IOS15

SwiftUI renders your user interface in response to changes in state. So instead of trying to render Dashboard or ContentView within your Task, instead you need to set some form of state higher up that determines which view to show to the user.

For example, if you had a @State variable recording your logged in status, your code would start to look like

struct ContentView: View {
@State var loggedIn = false

var body: some View {
if loggedIn {
Dashboard()
} else {
// login form
Button {
Task {
let logInStatus = try await network.LoginFunction(userName: username, passWord: password)
self.loggedIn = logInStatus
}
}
}
}
}

When your async tasks changes the ContentView's state, SwiftUI's rendering subsystem will pick up that loggedIn has changed, and re-render body so that the login form is replaced by a call to Dashboard().



Related Topics



Leave a reply



Submit