URL 验证发布者

Posted

技术标签:

【中文标题】URL 验证发布者【英文标题】:URL Verification Publisher 【发布时间】:2019-09-17 21:33:09 【问题描述】:

我正在尝试在 Swift 中使用 Combine 编写一个 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)
    


【问题讨论】:

【参考方案1】:

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

在这里有效。您可能必须按照Transport security has blocked a cleartext HTTP 中的说明将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 = $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
    

【讨论】:

我试过这个......没有变化。我想知道是否还有其他问题导致它。我将不得不再挖掘一下,看看是否存在线程问题。 没关系,我没有意识到我还在运行 13.0。它在 iPadOS 13.1 上在将第一个字符打入文本字段时崩溃。 单步执行我得到了一个结果:注释掉.throttle-line,它在13.1上不起作用,至少在iPad版本上不起作用。没有它,它就在这里运行。也许他们收紧了它,只允许 RunLoop.mainDispatchQueue.main 这样的参数。 Removing .throttle /replaceing the Dispatch-part with RunLoop.main 在这里为 iPadOS 13.1 修复了它。示例应用程序通过它运行良好。可能值得将整个事情报告为错误,希望他们在 13.1 beta 结束时修复 .throttle。我没有 ios 设备可以测试它,因为我只为 iPad 开发 现在可以使用了!我删除了对.throttle 的所有引用,让示例中的东西正常工作。在我的真实项目中,还有一个使用 .throttle 的计时器,这增加了混乱。

以上是关于URL 验证发布者的主要内容,如果未能解决你的问题,请参考以下文章

尝试使用 JavaScript 验证 URL

Javascript 图像 URL 验证

codeigniter 表单验证规则 url

在 Java 中验证 URL

Grails URL 验证

Gradle + Sonar 主机 URL 和基本身份验证