繁体   English   中英

如何从 Swift 中的 URLSession.dataTask() 完成处理程序中更新 isLoading state?

[英]How to update isLoading state from within URLSession.dataTask() completion handler in Swift?

我在 Swift 中创建了一个视图(称为AddressInputView ),它应该执行以下操作:

  1. 从用户输入中获取地址
  2. 当用户点击提交时,启动 ProgressView animation 并将地址发送到后端
  3. 调用返回后,切换到 ResultView 并显示结果

我的问题是,一旦用户点击提交,视图就会立即切换到 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM