Издатель проверки URL

Я пытаюсь написать валидатор URL-адресов в Swift w / Combine, и на него подписывается представление SwiftUI. Кажется, он отлично работает в симуляторе, но вылетает на моем телефоне разработчика (работает 13.1).

Сценарий ... Пользователь вводит UITextField, который подключен к переменной urlString в модели представления. Когда это изменяется, я очищаю строку, создаю URL-адрес, а затем выполняю тест HEAD с помощью URLSession. Все это работает в симуляторе, но при нажатии текстового поля на устройстве происходит сбой приложения, и я не получаю никаких хороших трассировок стека. Любые идеи?

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


person someoneAnyone    schedule 17.09.2019    source источник


Ответы (1)


13.1beta4 не реализует .throttle, как более ранняя реализация в 13.0. Теперь использование DispatchQueue("..") в качестве аргумента приводит к сбою в этой строке без какого-либо полезного текста ошибки. Один из эффективных аргументов - RunLoop.main.

Здесь работает. Вероятно, вам придется добавить Allow arbitrary Loads, как описано в Безопасность транспорта заблокировала открытый текст HTTP к вашему 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
    }
}
person Fabian    schedule 18.09.2019
comment
Я пробовал это ... без изменений. Мне интересно, есть ли другая проблема, вызывающая это. Мне придется еще немного покопаться, чтобы увидеть, есть ли проблема с потоком. - person someoneAnyone; 18.09.2019
comment
`` 0x1c2e91f84 ‹+240›: bl 0x1c2e9207c; обрисовал в общих чертах уничтожение Dispatch.DispatchWorkItemFlags '' - person someoneAnyone; 18.09.2019
comment
@someoneAnyone Я добавил рабочий пример. Может, это что-то другое. Это сообщение на форуме указывает способ отследить, где намечено уничтожение это исходит от. Но поскольку DispatchWorkItemFlags, вероятно, не то, к чему вы прикасались, это своего рода тупик. - person Fabian; 18.09.2019
comment
Я в тупике. Я воссоздал самую простую версию, используя ваш образец выше ... (спасибо за улучшения сетевой активности, кстати), но все еще вылетает. Интересно, связано ли это с iOS 13.1. Вскоре я публикую ссылку на свой образец проекта на GitHub. - person someoneAnyone; 18.09.2019
comment
Неважно, я не понимал, что у меня все еще работает 13.0. На iPadOS 13.1 происходит сбой при вводе первого символа в текстовое поле. - person Fabian; 18.09.2019
comment
Пройдя через него, я получил результат: закомментируйте строку .throttle, она не работает на 13.1, по крайней мере, на версии для iPad. Без него здесь проходит. Возможно, они ужесточили его, чтобы разрешить только аргументы типа RunLoop.main или DispatchQueue.main. - person Fabian; 18.09.2019
comment
Удаление .throttle помогает продлить работу приложения, но в конечном итоге оно все равно дает сбой. Я получаю около восьми символов, а затем получаю похожий сбой. Прогресс? - person someoneAnyone; 18.09.2019
comment
Удаление .throttle / замена Dispatch-части на RunLoop.main исправляет это для iPadOS 13.1 навсегда. Пример приложения отлично справляется с этим. Вероятно, стоит сообщать обо всем этом как об ошибках в надежде, что они исправят .throttle к концу бета-версии 13.1. У меня нет устройств iOS, чтобы протестировать его, так как я разрабатываю только для iPad. - person Fabian; 18.09.2019
comment
Теперь работает! Я удалил все ссылки на .throttle все работает в образце. В моем реальном проекте также был таймер с использованием .throttle, что добавляло путаницы. - person someoneAnyone; 19.09.2019
comment
Надеюсь исправят до релиза. Я рада это слышать! - person Fabian; 19.09.2019