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

Neph
  • 1,823
  • 2
  • 31
  • 69
  • Why don't you append the `scheme` if not exists in the `URL` ? – TheTiger Jul 26 '19 at 10:20
  • 2
    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? – Joakim Danielson Jul 26 '19 at 10:23
  • @TheTiger I'm using a library for sockets and at least one of them isn't able to connect if there's a scheme. – Neph Jul 26 '19 at 10:29
  • @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. – Neph Jul 26 '19 at 10:31
  • @Neph 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 ? – TheTiger Jul 26 '19 at 10:33
  • [See the huge list of URL Schemes](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml) so you can't just guess the valid one. – TheTiger Jul 26 '19 at 10:36
  • @TheTiger 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](https://stackoverflow.com/a/51821182/2016165) one) but they don't support all the cases I have to check. – Neph Jul 26 '19 at 10:38
  • @Neph 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. – TheTiger Jul 26 '19 at 10:39
  • `[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$` I found this regex for you which validates [like this](https://i.imgur.com/SPWDFo0.png). – TheTiger Jul 26 '19 at 10:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/197042/discussion-between-thetiger-and-neph). – TheTiger Jul 26 '19 at 10:47

2 Answers2

10

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

TheTiger
  • 13,264
  • 3
  • 57
  • 82
  • 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. – Neph Jul 26 '19 at 12:07
  • 1
    Logically `stackexchange.com.com` can not be 100% invalid because url like `https://www.news.com.au/` are also valid. – TheTiger Jul 26 '19 at 12:14
  • 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. – Neph Jul 26 '19 at 12:21
  • 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? – Neph Apr 21 '23 at 10:25
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.

AjinkyaSharma
  • 1,870
  • 1
  • 16
  • 26
  • 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. – Neph Jul 26 '19 at 12:29
  • Thanks for adding the examples. Interesting that it's okay with "stackoverflow.com" and "ftp://127.0.0.1" but not "127.0.0.1". But yes, unless it supports IPs without scheme too, I can't use it. – Neph Jul 26 '19 at 12:47