Validate URL without scheme
Asked Answered
O

2

3

Swift 5, Xcode 10, iOS 12

My code uses UIApplication.shared.canOpenURL to validate URLs, which unfortunately fails without e.g. "http://".

Example:

print(UIApplication.shared.canOpenURL(URL(string: "stackoverflow.com")!)) //false
print(UIApplication.shared.canOpenURL(URL(string: "http://stackoverflow.com")!)) //true
print(UIApplication.shared.canOpenURL(URL(string: "129.0.0.1")!)) //false
print(UIApplication.shared.canOpenURL(URL(string: "ftp://129.0.0.1")!)) //true

I'm aware of the change with schemes (iOS9+) and I know that I can just add a prefix like "http://" if the String doesn't start with it already, then check this new String but I'm still wondering:

Question: How do I add a "there's no scheme" scheme, so valid URLs like "stackoverflow.com" return true too (is this even possible?)?

Outlaw answered 26/7, 2019 at 10:4 Comment(10)
Why don't you append the scheme if not exists in the URL ?Murray
How do you know that "129.0.0.1" should be prefixed with ftp:// and not http:// or https://? What makes an invalid url valid?Kory
@Murray I'm using a library for sockets and at least one of them isn't able to connect if there's a scheme.Outlaw
@JoakimDanielson I don't. I don't want to check if the URL leads to an actual server that can be accessed, I just need some type of check if the URL is valid theoretically. So e.g. "bla" or "---" won't be valid but "bla.com" will.Outlaw
@Outlaw Suppose http://190.128.0.1 exists and ftp://190.128.0.1 doesn't. In that case is 190.128.0.1 valid or invalid ?Murray
See the huge list of URL Schemes so you can't just guess the valid one.Murray
@Murray Theoretically it's a valid IP. It doesn't matter if it actually exists, I just want to check if it uses the right syntax. I found a couple of regex codes for that (e.g. this one) but they don't support all the cases I have to check.Outlaw
@Outlaw Then you need to validate URL with regex not with if it can be open or not. Like for email we check if its valid or not but not if its reachable or not.Murray
[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$ I found this regex for you which validates like this.Murray
Let us continue this discussion in chat.Murray
M
11

It's not possible to add a valid scheme to URL because no one knows which prefix will be add to which URL. You can just validate a URL with the help of regex.

I searched and modified the regex.

extension String { 
    func isValidUrl() -> Bool { 
        let regex = "((http|https|ftp)://)?((\\w)*|([0-9]*)|([-|_])*)+([\\.|/]((\\w)*|([0-9]*)|([-|_])*))+" 
        let predicate = NSPredicate(format: "SELF MATCHES %@", regex) 
        return predicate.evaluate(with: self) 
    } 
}

I tested it with below urls:

print("http://stackoverflow.com".isValidUrl()) 
print("stackoverflow.com".isValidUrl()) 
print("ftp://127.0.0.1".isValidUrl()) 
print("www.google.com".isValidUrl()) 
print("127.0.0.1".isValidUrl()) 
print("127".isValidUrl()) 
print("hello".isValidUrl())

Output

true 
true 
true 
true 
true 
false 
false

Note: 100% regex is not possible to validate the email and url

Murray answered 26/7, 2019 at 12:1 Comment(4)
It's not possible to add an empty scheme, so upvoted/accepted as an alternative! This regex does however fail for Strings like "10.1..1.9", "10.9" or "stackexchange.com.com", so checking for those would need additional (probably non-regex) code.Outlaw
Logically stackexchange.com.com can not be 100% invalid because url like https://www.news.com.au/ are also valid.Murray
You're right, I asked for syntax and two extensions are possible and also valid in some cases. For a quick check - e.g. to avoid dealing with a timeout every time you try to connect to the server - this regex is more than sufficient. For everything else you will have to write more code (or use a library) anyway.Outlaw
I'm currently doing some more testing and came across a case that should be valid but returns false: If there's a port, e.g. "127.0.0.1:8080". Do you know what to add to also check for an optional port before the first forward slash?Outlaw
T
1

This is the method that I use

extension String {

    /// Return first available URL in the string else nil
    func checkForURL() -> NSRange? {
        guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
            return nil
        }
        let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count))

        for match in matches {
            guard Range(match.range, in: self) != nil else { continue }
            return match.range
        }
        return nil
    }

    func getURLIfPresent() -> String? {
        guard let range = self.checkForURL() else{
            return nil
        }
        guard let stringRange = Range(range,in:self) else {
            return nil
        }
        return String(self[stringRange])
    }
}

Apparently, the method name and the comment in the code are not verbose enough, so here is the explanation.

Used NSDataDetector and provided it the type - NSTextCheckingResult.CheckingType.link to check for links.

This goes through the string provided and returns all the matches for URL type.

This checks for link in the string that you provide, if any, else returns nil.

The method getURLIfPresent return the URL part from that string.

Here are a few examples

print("http://stackoverflow.com".getURLIfPresent())
print("stackoverflow.com".getURLIfPresent())
print("ftp://127.0.0.1".getURLIfPresent())
print("www.google.com".getURLIfPresent())
print("127.0.0.1".getURLIfPresent())
print("127".getURLIfPresent())
print("hello".getURLIfPresent())

Output

Optional("http://stackoverflow.com")
Optional("stackoverflow.com")
Optional("ftp://127.0.0.1")
Optional("www.google.com")
nil
nil
nil

But, this doesn't return true for "127.0.0.1". So I don't think it will fulfil your cause. In your case, going the regex way is better it seems. As you can add more conditions if you come across some more patterns that demand to be considered as URL.

Tubbs answered 26/7, 2019 at 10:16 Comment(2)
As usual Apple's documentation is bad... What are the prerequisites for this? Does it need a scheme to work? Does it work with IPs too? Could you please add a few examples to show how this works.Outlaw
Thanks for adding the examples. Interesting that it's okay with "stackoverflow.com" and "127.0.0.1" but not "127.0.0.1". But yes, unless it supports IPs without scheme too, I can't use it.Outlaw

© 2022 - 2024 — McMap. All rights reserved.