[英]How to update isLoading state from within URLSession.dataTask() completion handler in Swift?
我在 Swift 中创建了一个视图(称为AddressInputView
),它应该执行以下操作:
我的问题是,一旦用户点击提交,视图就会立即切换到 ResultView,而无需等待 API 调用返回。 因此,ProgressView animation 仅在一瞬间可见。
这是我的代码:
地址输入视图
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
)
}
)
}
}
}
}
}
因此,我尝试在 session.dataTask 的完成处理程序session.dataTask
buttonSelected = true
移动到isLoading = false
旁边,但如果我这样做,则不会显示 ResultView。 难道 state 更新不可能从completionHandler
中进行吗? 如果是,为什么会这样,解决方法是什么?
主要问题:如何更改上面的代码,以便在 API 调用完成之前不显示 ResultView? (虽然 API 调用尚未完成,但我希望显示 ProgressView)。
我认为问题在于URLSession
的完成处理程序是在后台线程上执行的。 您必须将与 API 相关的 UI 分派到主线程。
但我建议利用async/await
,而不是使用URLComponents/URLQueryItem
。 它代表您处理必要的百分比编码
func fetch() {
results.removeAll()
isLoading = true
Task {
let backendUrlString = Bundle.main.object(forInfoDictionaryKey: "BACKEND_URL") as! String
var components = URLComponents(string: backendUrlString)!
components.path = "/results"
components.queryItems = [
URLQueryItem(name: "address", value: address),
URLQueryItem(name: "radius", value: "\(radius)")
]
do {
let (data, _ ) = try await URLSession.shared.data(from: components.url!)
let serviceResponse = try JSONDecoder().decode(ResultsServiceResponse.self, from: data)
self.results = serviceResponse.results
isLoading = false
buttonSelected = true
} catch {
print(error)
// show something to the user
}
}
}
不需要URLRequest
,GET 是默认值。
您可以强制解包 Info.plist 字典的值。 如果它不存在,你就犯了设计错误。
您的fetch()
function 调用异步 function session.dataTask
,它会在数据任务完成之前立即返回。
这些天解决这个问题的最简单方法是切换到使用async
函数,例如
func onSubmit() {
if !address.isEmpty {
Task {
do {
try await fetch()
} catch {
print("Error \(error.localizedDescription)")
}
}
}
}
func fetch() async throws {
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 (data, _) = try await session.data(for: request)
let serviceResponse = try JSONDecoder().decode(ResultsServiceResponse.self, from: data)
self.results = serviceResponse.results
isLoading = false
buttonSelected = true
}
在上面的代码中, fetch()
函数在session.data(for: request)
被调用时被挂起,只有在它完成后才会恢复。
来自.navigationDestination
文档:
通常,倾向于将路径绑定到导航堆栈以进行编程导航。
因此,将@State var path
添加到您的视图并使用此.navigationDestination
初始化程序:
enum Destination {
case result
}
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
if isLoading {
ProgressView()
} else {
VStack {
TextField("", text: $address, prompt: Text("Search address").foregroundColor(.gray))
.onSubmit {
onSubmit()
}
Button(action: onSubmit) {
Text("Submit")
}
.navigationDestination(for: Destination.self, destination: { destination in
switch destination {
case .result:
ResultView(address: $address, results: $results)
}
})
}
}
}
}
然后在你的fetch()
函数的末尾,只需设置
isLoading = false
path.append(Destination.result)
把它们放在一起的例子
struct Result: Decodable {
}
struct ResultsServiceResponse: Decodable {
let results: [Result]
}
struct ResultView: View {
@Binding var address: String
@Binding var results: [Result]
var body: some View {
Text(address)
}
}
enum Destination {
case result
}
struct ContentView: View {
@State var radius = 10_000 // In meters
@State var isLoading = false
@State private var address: String = ""
@State private var results: [Result] = []
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
if isLoading {
ProgressView()
} else {
VStack {
TextField("", text: $address, prompt: Text("Search address").foregroundColor(.gray))
.onSubmit {
onSubmit()
}
Button(action: onSubmit) {
Text("Submit")
}
.navigationDestination(for: Destination.self, destination: { destination in
switch destination {
case .result:
ResultView(address: $address, results: $results)
}
})
}
}
}
}
func onSubmit() {
if !address.isEmpty {
Task {
do {
try await fetch()
} catch {
print("Error \(error.localizedDescription)")
}
}
}
}
func fetch() async throws {
results.removeAll()
isLoading = true
try await Task.sleep(nanoseconds: 2_000_000_000)
self.results = [Result()]
isLoading = false
path.append(Destination.result)
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.