URL 验证发布者

URL Verification Publisher

我正在尝试在 Swift 中编写一个 URL 验证程序 w/ Combine 并让 SwiftUI 视图订阅它。它似乎在模拟器中运行良好,但在我的开发中崩溃 phone (运行 13.1).

场景... 用户键入 UITextField,它连接到视图模型中的 urlString 变量。当它发生变化时,我清理字符串,创建一个 URL,然后使用 URLSession 进行 HEAD 测试。这一切都在 sim 中工作,但点击设备上的文本字段时,它会使应用程序崩溃,而且我没有得到任何好的堆栈跟踪。有什么想法吗?

static func testURLPublisher(string: String) -> AnyPublisher<URL?, Never> {

        let validatedURL = try? validateURL(string: string)

        guard let urlToCheck = validatedURL else {
            return Just(nil).eraseToAnyPublisher()
        }

        var request = URLRequest(url: urlToCheck)
        request.httpMethod = "HEAD"

        let publisher = URLSession.shared.dataTaskPublisher(for: request)
            .handleEvents(receiveSubscription: { _ in
                networkActivityPublisher.send(true)
            }, receiveCompletion: { _ in
                networkActivityPublisher.send(false)
            }, receiveCancel: {
                networkActivityPublisher.send(false)
            })
            .tryMap { data, response -> URL? in
                // URL Responded - Check Status Code
                guard let urlResponse = response as? HTTPURLResponse, ((urlResponse.statusCode >= 200 && urlResponse.statusCode < 400) || urlResponse.statusCode == 405) else {
                        throw URLValidatorError.serverError("Could not find the a servr at: \(urlToCheck)")
                }
                        return urlResponse.url?.absoluteURL
            }
        .catch { err in
            return Just(nil)
        }
        .eraseToAnyPublisher()
        return publisher
    }

正在使用它的视图如下所示...

class NewSiteViewModel: ObservableObject {

    @Published var validatedURL: URL?

    //@Published var secretKey: String?
    @Published var urlString: String = ""

    @Published var isValidURL: Bool = false

    private var cancellable = Set<AnyCancellable>()

    init() {
        $urlString
        .dropFirst(1)
            .throttle(for: 0.5, scheduler: DispatchQueue(label: "Validator"), latest: true)
            .removeDuplicates()
            .compactMap { string -> AnyPublisher<URL?, Never> in
                return URL.testURLPublisher(string: string)
            }
            .switchToLatest()
            .receive(on: RunLoop.main)
            .sink { recievedURL in
                guard let url = recievedURL else {
                    self.validatedURL = nil
                    self.isValidURL = false
                    return
                }
                self.validatedURL = url
                self.isValidURL = true

            }
            .store(in: &cancellable)
    }
}

13.1beta4 没有像 13.0 中的早期实现那样实现 .throttle。现在给出 DispatchQueue("..") 作为参数在该行崩溃,没有任何可用的错误文本。一个有效的论点是 RunLoop.main.

在这里工作。您可能必须按照 中的描述将 Allow arbitrary Loads 添加到您的 info.plist。它可能不会在 Simulator 中强制执行,但它肯定在设备上。

您可以省略 NSExceptionDomains 部分。

使用 .throttle 参数的示例

import SwiftUI
import Combine

struct URLTesterView: View {
    @ObservedObject var model = NewSiteViewModel()

    @State var networkActivity = false

    var body: some View {
        VStack{
            TextField("url string", text: $model.urlString)
            Text("Is valid: \(model.isValidURL ? "true" : "false")")
            Text("Validated URL: \(model.validatedURL?.absoluteString ?? "")")
            Text("Network activity: \(networkActivity ? "true" : "false")")
        }.onReceive(networkActivityPublisher
            .receive(on: DispatchQueue.main)) {
            self.networkActivity = [=10=]
        }
    }
}

class NewSiteViewModel: ObservableObject {

    @Published var validatedURL: URL?

    //@Published var secretKey: String?
    @Published var urlString: String = ""

    @Published var isValidURL: Bool = false

    private var cancellable = Set<AnyCancellable>()

    init() {
        $urlString
        .dropFirst(1)
            .throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
            .removeDuplicates()
            .compactMap { string -> AnyPublisher<URL?, Never> in
                return URL.testURLPublisher(string: string)
            }
            .switchToLatest()
            .receive(on: RunLoop.main)
            .sink { recievedURL in
                guard let url = recievedURL else {
                    self.validatedURL = nil
                    self.isValidURL = false
                    return
                }
                self.validatedURL = url
                self.isValidURL = true
            }
            .store(in: &cancellable)
    }
}

func validateURL(string: String) throws -> URL {
    guard let url = URL(string: string) else {
        throw URLValidatorError.urlIsInvalid(string)
    }
    return url
}

let networkActivityPublisher = PassthroughSubject<Bool, Never>()

enum URLValidatorError: Error {
    case serverError(_ string: String)
    case urlIsInvalid(_ string: String)
}

extension URL {
    static func testURLPublisher(string: String) -> AnyPublisher<URL?, Never> {

        let validatedURL = try? validateURL(string: string)

        guard let urlToCheck = validatedURL else {
            return Just(nil).eraseToAnyPublisher()
        }

        var request = URLRequest(url: urlToCheck)
        request.httpMethod = "HEAD"

        let publisher = URLSession.shared.dataTaskPublisher(for: request)
            .handleEvents(receiveSubscription: { _ in
                networkActivityPublisher.send(true)
            }, receiveCompletion: { _ in
                networkActivityPublisher.send(false)
            }, receiveCancel: {
                networkActivityPublisher.send(false)
            })
            .tryMap { data, response -> URL? in
                // URL Responded - Check Status Code
                guard let urlResponse = response as? HTTPURLResponse, ((urlResponse.statusCode >= 200 && urlResponse.statusCode < 400) || urlResponse.statusCode == 405) else {
                        throw URLValidatorError.serverError("Could not find the a servr at: \(urlToCheck)")
                }
                        return urlResponse.url?.absoluteURL
            }
        .catch { err in
            return Just(nil)
        }
        .eraseToAnyPublisher()
        return publisher
    }
}