Neph
Neph

Reputation: 2001

Validate URL without scheme

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

Upvotes: 3

Views: 5011

Answers (2)

TheTiger
TheTiger

Reputation: 13354

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

Upvotes: 12

AjinkyaSharma
AjinkyaSharma

Reputation: 1979

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.

Upvotes: 1

Related Questions