【问题标题】:URL Verification PublisherURL 验证发布者
【发布时间】: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)
    }
}

【问题讨论】:

    标签: swift swiftui combine


    【解决方案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 的计时器,这增加了混乱。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-09
    • 1970-01-01
    • 2010-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-15
    相关资源
    最近更新 更多