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
How to Prompt for Accessibility Features in a MACos App (From the Appdelegate)
How to Wait for Http Requests to Finish
Swift Realm: After Writing Transaction Reference Set to Nil
Saving Highscores with Nsuserdefaults
Drag a Cgrect Using Uipangesturerecognizer
Inline Kvo of a Property in Another View Controller
Remove \\U{E2} Characters from String
Swift Callkit Sometimes Can't Activate Loudspeaker After Received Call (Only Incoming Call)
Data Ranged Subscribe Strange Behavior
Displaying State of an Async API Call in Swiftui
Hovering a Modelentity in Front of Arcamera
Swift Delegate Beetween Two Vc Without Segue
Swift Firestore Search for Users
Skscene Becomes Unresponsive While Being Idle
Use Quick Look Inside a Swift Cocoa Application to Preview Audio Files
Swift - Encode and Decode a Dictionary [String:Any] into Plist