首页 > 解决方案 > URL 验证发布者

问题描述

我正在尝试在带有组合的 Swift 中编写一个 URL 验证器,并让一个 SwiftUI 视图订阅它。它似乎在模拟器中工作正常,但在我的开发手机上崩溃(运行 13.1)。

场景...用户输入一个 UITextField,它连接到视图模型中的 urlString var。当发生变化时,我清理字符串,创建一个 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)
    }
}

标签: swiftswiftuicombine

解决方案


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

它在这里工作。您可能必须按照传输安全性已阻止明文 HTTPAllow arbitrary Loads中所述添加到您的. 它可能不会在 中强制执行,但肯定是在设备上。info.plistSimulator

您可以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 = $0
        }
    }
}

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
    }
}

推荐阅读