2

I'm trying to write a URL Validator in Swift w/ Combine and having a SwiftUI view subscribe to it. It seems to work fine in the simulator but crashes on my dev phone (running 13.1).

Scenario... User types into a UITextField which is connected to the urlString var in the view model. When that changes I clean the string, create a URL, and then do a HEAD test with URLSession. This all works in the sim, but on tap of the text field on the device it crashes the app and I'm not getting any good stack traces. Any ideas?

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
    }

The view that is using it looks like this...

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

someoneAnyone
  • 278
  • 2
  • 6

1 Answers1

3

13.1beta4 does not implement .throttle like the earlier implementation in 13.0. Now giving DispatchQueue("..") as an argument crashes on that line without any usable error text. One argument that works is RunLoop.main.

It works over here. You probably have to add Allow arbitrary Loads as described in Transport security has blocked a cleartext HTTP to your info.plist. It might not be enforced in Simulator, but it definitely is on devices.

You can leave out the NSExceptionDomains-part.

Example with working .throttle argument

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
    }
}
Fabian
  • 5,040
  • 2
  • 23
  • 35
  • I tried this... no change. I'm wondering if there is another problem causing it. I'm going to have to dig around a bit more to see if there is a thread issue. – someoneAnyone Sep 18 '19 at 15:18
  • ``` 0x1c2e91f84 <+240>: bl 0x1c2e9207c ; outlined destroy of Dispatch.DispatchWorkItemFlags ``` – someoneAnyone Sep 18 '19 at 15:19
  • @someoneAnyone I added the working example. Maybe it is sth different. [This forum post](https://forums.developer.apple.com/thread/115715) indicates a way to track where _outlined destroy of_ this is coming from. But since `DispatchWorkItemFlags` is probably not something you touched it's kinda a dead end. – Fabian Sep 18 '19 at 15:26
  • I'm stumped. I recreated the simplest version using your sample above... (thank you for the improvements on network activity btw) and still a crash. I wonder if it has anything to do with iOS 13.1. I post a GitHub link shortly to my sample project. – someoneAnyone Sep 18 '19 at 17:17
  • 1
    Nevermind, I didn't realize I'm still running 13.0. It crashes on iPadOS 13.1 on tipping in the first character into the text field. – Fabian Sep 18 '19 at 18:16
  • 1
    Stepping through it I got a result: comment out the `.throttle`-line, it does not work on 13.1, at least not on the iPad version. Without it it's running through here. Maybe they tightened it to allow only arguments like `RunLoop.main` or `DispatchQueue.main`. – Fabian Sep 18 '19 at 18:39
  • Removing `.throttle` helps keep the app alive for longer, but eventually, it still crashes. I get about eight characters in then I get a similar crash. Progress? – someoneAnyone Sep 18 '19 at 20:43
  • 1
    Removing `.throttle` /replacing the Dispatch-part with `RunLoop.main` fixes it for iPadOS 13.1 for good here. The example app runs fine through it. It's probably worth reporting the whole thing as bugs in the hope they fix `.throttle` for the end of 13.1 beta. I have no iOS devices to test it with since I'm only developing for iPad – Fabian Sep 18 '19 at 20:56
  • 1
    It's working now! I removed all references to `.throttle` got things working in the sample. In my real project, also had a timer using .throttle which was adding to the confusion. – someoneAnyone Sep 18 '19 at 21:01
  • I hope they fix it until release. I'm happy to hear it! – Fabian Sep 18 '19 at 21:03