I created a view (called AddressInputView
) in Swift which should do the following:
- Get an address from user input
- When user hits submit, start ProgressView animation and send the address to backend
- Once the call has returned, switch to a ResultView and show results
My problem is that once the user hits submit, then the view switches to the ResultView immediately without waiting for the API call to return. Therefore, the ProgressView animation is only visible for a split second.
This is my code:
AddressInputView
struct AddressInputView: View {
@State var buttonSelected = false
@State var radius = 10_000 // In meters
@State var isLoading = false
@State private var address: String = ""
@State private var results: [Result] = []
func onSubmit() {
if !address.isEmpty {
fetch()
}
}
func fetch() {
results.removeAll()
isLoading = true
let backendUrl = Bundle.main.object(forInfoDictionaryKey: "BACKEND_URL") as? String ?? ""
let escapedAddress = address.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
let params = "address=(escapedAddress)&radius=(radius)"
let fullUrl = "(backendUrl)/results?(params)"
var request = URLRequest(url: URL(string: fullUrl)!)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, _, _ in
if data != nil {
do {
let serviceResponse = try JSONDecoder().decode(ResultsServiceResponse.self, from: data!)
self.results = serviceResponse.results
} catch let jsonError as NSError {
print("JSON decode failed: ", String(describing: jsonError))
}
}
isLoading = false
})
buttonSelected = true
task.resume()
}
var body: some View {
NavigationStack {
if isLoading {
ProgressView()
} else {
VStack {
TextField(
"",
text: $address,
prompt: Text("Search address").foregroundColor(.gray)
)
.onSubmit {
onSubmit()
}
Button(action: onSubmit) {
Text("Submit")
}
.navigationDestination(
isPresented: $buttonSelected,
destination: { ResultView(
address: $address,
results: $results
)
}
)
}
}
}
}
}
So, I tried to move buttonSelected = true
right next to isLoading = false
within the completion handler for session.dataTask
but if I do that ResultView won’t be shown. Could it be that state updates are not possible from within completionHandler
? If yes, why is that so and what’s the fix?
Main Question: How can I change the code above so that the ResultView won’t be shown until the API call has finished? (While the API call has not finished yet, I want the ProgressView to be shown).
2
Answers
I think the problem is that the completion handler of
URLSession
is executed on a background thread. You have to dispatch the UI related API to the main thread.But I recommend to take advantage of
async/await
and rather than building the URL with String Interpolation useURLComponents/URLQueryItem
. It handles the necessary percent encoding on your behalfThe
URLRequest
is not needed, GET is the default.And you can force unwrap the value of the Info.plist dictionary. If it doesn’t exist you made a design mistake.
Your
fetch()
function calls an asynchronous functionsession.dataTask
which returns immediately, before the data task is complete.The easiest way to resolve this these days is to switch to using
async
functions, e.g.In the code above, the
fetch()
func is suspended whilesession.data(for: request)
is called, and only resumes once it’s complete.From the
.navigationDestination
documentation:so add a
@State var path
to your view and use this.navigationDestination
initialiser:then at the end of your
fetch()
func, just setExample putting it all together