如何使用 Swift 完成处理程序的结果?

How to use results from Swift completion handler?

我是 Swift 和 SwiftUI 的新手。

在我的 macOS SwiftUI 项目中,我试图验证 URL 是否可以访问,以便我可以有条件地呈现两个视图之一。一种加载图像 URL 的视图,另一种视图在 URL 无法访问时显示错误图像。

这是我的 URL 完成的扩展:

import Foundation

extension URL {
    func isReachable(completion: @escaping (Bool) -> Void) {
        var request = URLRequest(url: self)
        request.httpMethod = "HEAD"
        request.timeoutInterval = 1.0
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if error != nil {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
            if let httpResp: HTTPURLResponse = response as? HTTPURLResponse {
                DispatchQueue.main.async {
                    completion(httpResp.statusCode == 200)
                }
                return
            } else {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
        }.resume()
    }
}

在其他地方,我正在尝试在模型视图中使用它:

var imageURL: URL? {
    if let url = self.book.image_url {
        return URL(string: url)
    } else {
        return nil
    }
}

var imageURLIsReachable: Bool {
    if let url = self.imageURL {
        url.isReachable { result in
            return result  // Error: Cannot convert value of type 'Bool' to closure result type 'Void'
        }
    } else {
        return false
    }
}

尽管 Xcode 显示此错误:

Cannot convert value of type 'Bool' to closure result type 'Void'

我做错了什么?

正如 Xcode 告诉您的那样,问题确实存在于行 return result 中。当您创建函数 func isReachable(completion: @escaping (Bool) -> Void) 时,您是在告诉 Xcode 您将输入 (Bool) -> Void 类型的内容,应该类似于 func someFunction(input: Bool) -> Void.

但是当您使用闭包输入完成处理程序时,您输入的是 Bool -> Bool 类型的函数。删除行 return result,或更改 func isReachable(completion:).

中的完成类型

编辑:

事实上,我不建议在计算 属性 中返回异步结果,那会导致一些其他问题。

我会将其更改为:

func isReachable(completion: @esacping (Bool) -> Void) {
    ...
}

func showResultView() {
    guard let url = imageURL else { 
        // handling if the imageURL is nil
        return
    }
    url.isReachable { result in
        // do something with the result
        if result {
            // show viewController A
        } else {
            // show viewController B
        }
    }
}

// call showResultView anywhere you want, lets say you want to show it whenever the viewController appear
override func viewDidAppear() {
    ...
    showResultView()
}

在阅读了此处的一些评论并做了更多 research/experimentation 后,我开始使用它。这是我更改的内容:

在 URL 扩展中,我保留了几乎相同的内容,因为我发现这样更易读。我确实将 timeoutInterval 推送到参数:

// Extensions/URL.swift


import Foundation

extension URL {
    func isReachable(timeoutInterval: Double, completion: @escaping (Bool) -> Void) {
        var request = URLRequest(url: self)
        request.httpMethod = "HEAD"
        request.timeoutInterval = timeoutInterval
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if error != nil {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
            if let httpResp: HTTPURLResponse = response as? HTTPURLResponse {
                DispatchQueue.main.async {
                    completion(httpResp.statusCode == 200)
                }
                return
            } else {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
        }.resume()
    }
}

我修改了我的 BookViewModel 使两个属性成为 @Published 并在那里使用了 URL 扩展:

// View Models/BookViewModel.swift

import Foundation

class BookViewModel: ObservableObject {
    @Published var book: Book
    @Published var imageURLIsReachable: Bool
    @Published var imageURL: URL?
    
    init(book: Book) {
        self.book = book
        self.imageURL = nil
        self.imageURLIsReachable = false
        if let url = book.image_url {
            self.imageURL = URL(string: url)
            self.imageURL!.isReachable(timeoutInterval: 1.0) { result in
                self.imageURLIsReachable = result
            }
        }
    }
    
    // Rest of properties...
}

现在我的 BookThumbnailView 可以正确显示条件视图:

// Views/BookThumbnailView.swift

import SwiftUI
import Foundation
import KingfisherSwiftUI

struct BookThumbnailView: View {
    @ObservedObject var viewModel: BookViewModel
        
    private var book: Book {
        viewModel.book
    }
    
    @ViewBuilder
    var body: some View {
        if let imageURL = self.viewModel.imageURL {
            if self.viewModel.imageURLIsReachable {
                KFImage(imageURL)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(maxWidth: 70)
                        .cornerRadius(8)
            } else {
                ErrorBookThumbnailView()
            }
        } else {
            DefaultBookThumbnailView()
        }
    }
}

哇,那真是一次学习经历。感谢所有发表评论并提供建议的人!