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 验证发布者的主要内容,如果未能解决你的问题,请参考以下文章