如何在 Swift 中检查 URL 的有效性?

试图让一个应用程序启动默认浏览器到一个 URL,但只有当 URL 输入是有效的,否则它会显示一条消息说 URL 是无效的。

如何使用 Swift 检查有效性?

106740 次浏览

You can use the NSURL type (whose constructor returns an optional type) combined with an if-let statement to check the validity of a given URL. In other words, make use of the NSURL failable initializer, a key feature of Swift:

let stringWithPossibleURL: String = self.textField.text // Or another source of text


if let validURL: NSURL = NSURL(string: stringWithPossibleURL) {
// Successfully constructed an NSURL; open it
UIApplication.sharedApplication().openURL(validURL)
} else {
// Initialization failed; alert the user
let controller: UIAlertController = UIAlertController(title: "Invalid URL", message: "Please try again.", preferredStyle: .Alert)
controller.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))


self.presentViewController(controller, animated: true, completion: nil)
}

If your goal is to verify if your application can open a URL, here is what you can do. Although safari can open the URL, the website might not exist or it might be down.

// Swift 5
func verifyUrl (urlString: String?) -> Bool {
if let urlString = urlString {
if let url = NSURL(string: urlString) {
return UIApplication.shared.canOpenURL(url as URL)
}
}
return false
}

As a side note, this does not check whether or not a URL is valid or complete. For example, a call that passes "https://" returns true.

var url:NSURL = NSURL(string: "tel://000000000000")!
if UIApplication.sharedApplication().canOpenURL(url) {
UIApplication.sharedApplication().openURL(url)
} else {
// Put your error handler code...
}

I found this one clean (In Swift):

func canOpenURL(string: String?) -> Bool {
guard let urlString = string else {return false}
guard let url = NSURL(string: urlString) else {return false}
if !UIApplication.sharedApplication().canOpenURL(url) {return false}


//
let regEx = "((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+"
let predicate = NSPredicate(format:"SELF MATCHES %@", argumentArray:[regEx])
return predicate.evaluateWithObject(string)
}

Usage:

if canOpenURL("abc") {
print("valid url.")
} else {
print("invalid url.")
}

===

for Swift 4.1:

func canOpenURL(_ string: String?) -> Bool {
guard let urlString = string,
let url = URL(string: urlString)
else { return false }


if !UIApplication.shared.canOpenURL(url) { return false }


let regEx = "((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+"
let predicate = NSPredicate(format:"SELF MATCHES %@", argumentArray:[regEx])
return predicate.evaluate(with: string)
}


// Usage
if canOpenURL("abc") {
print("valid url.")
} else {
print("invalid url.") // This line executes
}


if canOpenURL("https://www.google.com") {
print("valid url.") // This line executes
} else {
print("invalid url.")
}

For a swift 3 version of the accepted answer:

func verifyUrl(urlString: String?) -> Bool {
if let urlString = urlString {
if let url = URL(string: urlString) {
return UIApplication.shared.canOpenURL(url)
}
}
return false
}

Or for a more Swifty solution:

func verifyUrl(urlString: String?) -> Bool {
guard let urlString = urlString,
let url = URL(string: urlString) else {
return false
}


return UIApplication.shared.canOpenURL(url)
}

My personal preference is to approach this with an extension, because I like to call the method directly on the string object.

extension String {


private func matches(pattern: String) -> Bool {
let regex = try! NSRegularExpression(
pattern: pattern,
options: [.caseInsensitive])
return regex.firstMatch(
in: self,
options: [],
range: NSRange(location: 0, length: utf16.count)) != nil
}


func isValidURL() -> Bool {
guard let url = URL(string: self) else { return false }
if !UIApplication.shared.canOpenURL(url) {
return false
}


let urlPattern = "^(http|https|ftp)\\://([a-zA-Z0-9\\.\\-]+(\\:[a-zA-Z0-9\\.&%\\$\\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(\\:[0-9]+)*(/($|[a-zA-Z0-9\\.\\,\\?\\'\\\\\\+&%\\$#\\=~_\\-]+))*$"
return self.matches(pattern: urlPattern)
}
}

This way it is also extensible with another use-cases, such as isValidEmail, isValidName or whatever your application requires.

This isn't a regex approach, but it is a naive one that works well for making sure there is a host and an extension if you want a simple and inexpensive approach:

extension String {
var isValidUrlNaive: Bool {
var domain = self
guard domain.count > 2 else {return false}
guard domain.trim().split(" ").count == 1 else {return false}
if self.containsString("?") {
var parts = self.splitWithMax("?", maxSplit: 1)
domain = parts[0]
}
return domain.split(".").count > 1
}
}

Use this only if you want a quick way to check on the client side and you have server logic that will do a more rigorous check before saving the data.

Using 'canOpenUrl' was too expensive for my use case, I found this approach to be quicker

func isStringLink(string: String) -> Bool {
let types: NSTextCheckingResult.CheckingType = [.link]
let detector = try? NSDataDetector(types: types.rawValue)
guard (detector != nil && string.characters.count > 0) else { return false }
if detector!.numberOfMatches(in: string, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, string.characters.count)) > 0 {
return true
}
return false
}
extension String {
func isStringLink() -> Bool {
let types: NSTextCheckingResult.CheckingType = [.link]
let detector = try? NSDataDetector(types: types.rawValue)
guard (detector != nil && self.characters.count > 0) else { return false }
if detector!.numberOfMatches(in: self, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.characters.count)) > 0 {
return true
}
return false
}
}


//Usage
let testURL: String = "http://www.google.com"
if testURL.isStringLink() {
//Valid!
} else {
//Not valid.
}

It's advised to use this check only once and then reuse.

P.S. Credits to Shachar for this function.

Try this:

func isValid(urlString: String) -> Bool
{
if let urlComponents = URLComponents.init(string: urlString), urlComponents.host != nil, urlComponents.url != nil
{
return true
}
return false
}

This simply checks for valid URL components and if the host and url components are not nil. Also, you can just add this to an extensions file

In some cases it can be enough to check that the url satisfies RFC 1808. There are several ways to do this. One example:

if let url = URL(string: urlString), url.host != nil {
// This is a correct url
}

This is because .host, as well as .path, .fragment and a few other methods would return nil if url doesn't conform to RFC 1808.

If you don't check, you might have this kind of messages in console log:

Task <DF46917D-1A04-4E76-B54E-876423224DF7>.<72> finished with error - code: -1002

For swift 4 you can use:

class func verifyUrl (urlString: String?) -> Bool {
//Check for nil
if let urlString = urlString {
// create NSURL instance
if let url = URL(string: urlString) {
// check if your application can open the NSURL instance
return UIApplication.shared.canOpenURL(url)
}
}
return false
}

Swift 4 elegant solution using NSDataDetector:

extension String {
var isValidURL: Bool {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) {
// it is a link, if the match covers the whole string
return match.range.length == self.utf16.count
} else {
return false
}
}
}

Usage:

let string = "https://www.fs.blog/2017/02/naval-ravikant-reading-decision-making/"
if string.isValidURL {
// TODO
}

Reasoning behind using NSDataDetector instead of UIApplication.shared.canOpenURL:

I needed a method that would detect whether the user provided an input that is an URL to something. In many cases, users don't include the http:// nor https:// URL scheme in the URL they type in - e.g., instead of "http://www.google.com" they would type in "www.google.com". Without the URL scheme, the UIApplication.shared.canOpenURL will fail to recognize the URL and will return false. NSDataDetector is, compared to UIApplication.shared.canOpenURL, a rather forgiving tool (as @AmitaiB mentioned in comments) - and it can detect even URLs without the http:// scheme. This way I am able to detect a URL without having to try to add the scheme everytime when testing the string.

Sidenote - SFSafariViewController can open only URLs with http:///https://. Thus, if a detected URL does not have a URL scheme specified, and you want to open the link, you will have to prepend the scheme manually.

For Swift 4 version

static func isValidUrl (urlString: String?) -> Bool {
if let urlString = urlString {
if let url = URL(string: urlString) {
return UIApplication.shared.canOpenURL(url)
}
}
return false
}

This is for latest Swift 4, based on Doug Amos answer (for swift 3)

public static func verifyUrl (urlString: String?) -> Bool {
//Check for nil
if let urlString = urlString {
// create NSURL instance
if let url = NSURL(string: urlString) {
// check if your application can open the NSURL instance
return UIApplication.shared.canOpenURL(url as URL)
}
}
return false
}

This will return a boolean for a URL's validity, or nil if an optional URL with a value of nil is passed.

extension URL {


var isValid: Bool {
get {
return UIApplication.shared.canOpenURL(self)
}
}


}

Note that, if you plan to use a Safari view, you should test url.scheme == "http" || url.scheme == "https".

Swift 4.2 Elegant URL construction with verification

import Foundation
import UIKit


extension URL {


init?(withCheck string: String?) {
let regEx = "((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+"
guard
let urlString = string,
let url = URL(string: urlString),
NSPredicate(format: "SELF MATCHES %@", argumentArray: [regEx]).evaluate(with: urlString),
UIApplication.shared.canOpenURL(url)
else {
return nil
}


self = url
}
}

Usage

var imageUrl: URL? {
if let url = URL(withCheck: imageString) {
return url
}
if let url = URL(withCheck: image2String) {
return url
}
return nil
}

Helium having to deal with various schemes:

struct UrlHelpers {
// Prepends `http://` if scheme isn't `https?://` unless "file://"
static func ensureScheme(_ urlString: String) -> String {
if !(urlString.lowercased().hasPrefix("http://") || urlString.lowercased().hasPrefix("https://")) {
return urlString.hasPrefix("file://") ? urlString : "http://" + urlString
} else {
return urlString
}
}


// https://mathiasbynens.be/demo/url-regex
static func isValid(urlString: String) -> Bool {
// swiftlint:disable:next force_try
if urlString.lowercased().hasPrefix("file:"), let url = URL.init(string: urlString) {
return FileManager.default.fileExists(atPath:url.path)
}


let regex = try! NSRegularExpression(pattern: "^(https?://)[^\\s/$.?#].[^\\s]*$")
return (regex.firstMatch(in: urlString, range: urlString.nsrange) != nil)
}
}

Version that works with Swift 4.2 and has reliable URL pattern matching ...

func matches(pattern: String) -> Bool
{
do
{
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
return regex.firstMatch(in: self, options: [], range: NSRange(location: 0, length: utf16.count)) != nil
}
catch
{
return false
}
}




func isValidURL() -> Bool
{
guard let url = URL(string: self) else { return false }
if !UIApplication.shared.canOpenURL(url) { return false }


let urlPattern = "(http|ftp|https):\\/\\/([\\w+?\\.\\w+])+([a-zA-Z0-9\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?"
return self.matches(pattern: urlPattern)
}

This accepted answer doesn't work in my case with wrong url without data https://debug-cdn.checkit4team.com/5/image/Y29tbW9uL2RlZmF1bHRfYXZhdGFyLnBuZw==

So I write extension to solve

extension String {
var verifyUrl: Bool {
get {
let url = URL(string: self)


if url == nil || NSData(contentsOf: url!) == nil {
return false
} else {
return true
}
}
}
}

Use it:

if string. verifyUrl {
// do something
}

Hope this help!

2020, I was tasked on fixing a bug of a method for underlining string links within a string. Most of the answers here don't work properly (try: aaa.com.bd or aaa.bd) These links should be valid. And then I stumbled upon the regex string for this.

Ref: https://urlregex.com/

So based on that regex

"((?:http|https)://)?(?:www\\.)?[\\w\\d\\-_]+\\.\\w{2,3}(\\.\\w{2})?(/(?<=/)(?:[\\w\\d\\-./_]+)?)?"

We can write a function.

SWIFT 5.x:

extension String {
var validURL: Bool {
get {
let regEx = "((?:http|https)://)?(?:www\\.)?[\\w\\d\\-_]+\\.\\w{2,3}(\\.\\w{2})?(/(?<=/)(?:[\\w\\d\\-./_]+)?)?"
let predicate = NSPredicate(format: "SELF MATCHES %@", argumentArray: [regEx])
return predicate.evaluate(with: self)
}
}
}

OBJECTIVE-C (write this however you want, category or not).

- (BOOL)stringIsValidURL:(NSString *)string
{
NSString *regEx = @"((?:http|https)://)?(?:www\\.)?[\\w\\d\\-_]+\\.\\w{2,3}(\\.\\w{2})?(/(?<=/)(?:[\\w\\d\\-./_]+)?)?";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@" argumentArray:@[regEx]];
return [predicate evaluateWithObject:string];
}

Swift 5.1 Solution

extension String {
func canOpenUrl() -> Bool {
guard let url = URL(string: self), UIApplication.shared.canOpenURL(url) else { return false }
let regEx = "((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+"
let predicate = NSPredicate(format:"SELF MATCHES %@", argumentArray:[regEx])
return predicate.evaluate(with: self)
}
}

Most of the answer here doesn't address my issue so I'm posting here how I resolved it:

static func isValidDomain(domain: String?) -> Bool {
guard let domain = domain else {
return false
}
// Modified version of https://stackoverflow.com/a/49072718/2304737
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let domainWithScheme = "https://\(domain)"
if let url = URL(string: domainWithScheme),
let match = detector.firstMatch(in: domainWithScheme, options: [], range: NSRange(location: 0, length: domainWithScheme.utf16.count)) {
// it is a link, if the match covers the whole string
return match.range.length == domainWithScheme.utf16.count && url.host != nil
} else {
return false
}
}

What lacks on Milan Nosáľ's answer is, it doesn't address this particular input:

https://#view-?name=post&post.post_id=519&message.message_id=349

So I just add host check existence and unschemed "URL".