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

101

尝试通过应用启动默认浏览器到一个URL,但仅在输入的URL有效时,否则显示一条消息,指示该URL无效。

我该如何使用Swift检查有效性?


发送一个NSURLRequest并检查返回? - chris
使用字符串来形成一个NSURL,然后检查它是否为nil? - matt
25个回答

150

如果您的目标是验证应用程序是否能够打开URL,可以这样做。虽然Safari可以打开URL,但该网站可能不存在或者已经关闭。

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

需要注意的是,这并不会检查 URL 是否有效或完整。例如,传递"https://" 的调用将返回true。


4
如果let urlString = urlString,url = NSURL(string: urlString),并且UIApplication.sharedApplication().canOpenURL(url)返回true,则返回true。 - Dipu Rajak
当我在Swift2.0上进行测试时,它无法正常工作 if let url = NSURL(string: urlString) { - Vijay Singh Rana
1
@VijaySinghRana 这是一个有效的NSURL,该函数不会检查URL是否存在。 在答案中注意到:“如果您的目标是验证应用程序是否可以打开URL,这是您可以执行的操作”。 - 3366784
如果URL无效,此方法会在调试器中打印此日志“-canOpenURL: failed for URL:”。如何删除它? - Prajeet Shrestha
1
请记住,对于不以协议://开头的URL(如http://或https://),此方法会失败。 - Rizwan Ahmed
显示剩余3条评论

99

使用NSDataDetector的Swift 4优雅解决方案:

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

使用方法:

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

使用NSDataDetector而不是UIApplication.shared.canOpenURL的原因:

我需要一种方法来检测用户输入的内容是否为URL。在许多情况下,用户在输入URL时不包括http://或者https:// URL协议 - 例如,他们可能输入"www.google.com"而不是"http://www.google.com"。如果没有URL协议,UIApplication.shared.canOpenURL将无法识别URL并返回false。与UIApplication.shared.canOpenURL相比,NSDataDetector是一个相对宽容的工具(正如@AmitaiB在评论中提到的),它甚至可以检测没有http://协议的URL。这样,我就能够在不必每次测试字符串时尝试添加协议的情况下检测URL。

附注 - SFSafariViewController只能打开带有http:///https://协议的URL。因此,如果检测到的URL没有指定URL协议,并且您想要打开链接,则必须手动添加协议。


1
请注意,NSDataDetector是一个相当宽容的检测工具。考虑以下内容:*isValidURL? true *UIApplication.shared.canOpenURL? false gskinner.com/products/spl *iVU? T *canOpen? F https://google.com *iVU? T *canOpen? T https:google.com *iVU? T *canOpen? T www.cool.com.au *iVU? T *canOpen? F http://www.cool.com.au *iVU? T *canOpen? T http://www.cool.com.au/ersdfs *iVU? T *canOpen? T http://www.cool.com.au/ersdfs?dfd=dfgd@s=1 *iVU? T *canOpen? T http://www.cool.com:81/index.html *iVU? T *canOpen? T - AmitaiB
啊,这很有道理。 - AmitaiB
1
我认为当字符串以http://(或任何其他有效协议)开头时,canOpen会返回true。 - amsmath
返回 true,用于 https://twitter(没有空格,但为了可读性而这样做)。 - Joshua Wolff
我认为这个代码没有涵盖足够多的边缘情况,例如像"https://#view-?name=post&post.post_id=519&message.message_id=349".isValidUrl这样的输入,它会返回true。 - mr5
显示剩余5条评论

26

以下是接受答案的Swift 3版本:

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

或者采用更具Swift特色的解决方案:

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

    return UIApplication.shared.canOpenURL(url)
}

虽然它忠实于之前的版本,但你可以将 if let.. 组合成单个检查,然后还可以将 if 翻转为 guard ... else { return false },以使 return false 成为失败状态,而 return UIApplication... 则是(可能的)成功状态。 - ReactiveRaven
1
@ReactiveRaven 是的,我同意。我已经按建议添加了一个版本。 - Doug Amos
这会为每个失败的URL添加日志,有点烦人。 - Lakshith Nishshanke

20

"canOpenUrl"在我的使用场景中太昂贵了,我发现这种方法更快

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
}

1
这对我来说非常完美,可以在UIApplication.shared不可用的扩展中使用。谢谢! - Simon
4
Swift 4中,string.characters.count 变成了 string.count - Benjamin Schmidt

16

我发现这个代码很简洁(用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)
}

用法:

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

===

针对 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.")
}

好的解决方案!谢谢你! - Eugene Butkevich
是的,上述逻辑只是验证,不会转换原始字符串! - Ashok

13

2020年,我被指派修复一个方法的漏洞,用于在字符串中下划线标记字符串链接。这里大多数答案都不能正常工作(尝试:aaa.com.bd 或 aaa.bd)这些链接应该是有效的。后来我发现了适用于此的正则表达式字符串。

参考:https://urlregex.com/

所以基于那个正则表达式

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

我们可以编写一个函数。

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(可以按您喜欢的方式书写,可以是类别或其他)。

- (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];
}

1
不错的解决方案和使用方法,但我注意到它由于大小写敏感性而失败:Https://google.com或https://www.computerhope.com/JARGON.HTM作为JARGON.HTM和jargon.htm是两个不同的URL。然而,只需在测试之前将字符串转换为小写即可解决这个问题。 - Shawn Frank
1
对我不起作用(Swift 5.5.2):我的URL看起来像这样:https://you.test.com/0003-example/ - iKK

6

个人偏好是使用扩展来实现,因为我喜欢直接在字符串对象上调用方法。

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\\.&amp;%\\$\\-]+)*@)*((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\\.\\,\\?\\'\\\\\\+&amp;%\\$#\\=~_\\-]+))*$"
        return self.matches(pattern: urlPattern)
    }
}

这样可以使其与其他用例(如isValidEmailisValidName或您的应用程序需要的任何内容)进行扩展。


这似乎在google.com上不起作用。它只接受像http://google.com这样的东西。 - lwdthe1
@lwdthe1 是的,正则表达式有一些漏洞。不过要表达的重点是解决方案的架构(采用扩展方式)。在您的应用程序中完全可以自由地替换正则表达式为您自己的表达式。或者如果您有更好的表达式 - 欢迎改进我的答案! :) - Michal

6

Swift 5.1 解决方案

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

警告,它不支持查询参数(?key=value)。 - Jonathan

5

在某些情况下,只需检查URL是否符合RFC 1808即可。有几种方法可以做到这一点。以下是一个例子:

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

这是因为如果url不符合RFC 1808规范,.host、.path、.fragment等方法将返回nil。如果您没有检查,可能会在控制台日志中看到这种消息:
Task <DF46917D-1A04-4E76-B54E-876423224DF7>.<72> finished with error - code: -1002

在我看来,这应该是被接受的答案。它远比那些脆弱的正则表达式或昂贵的UIApplication调用更简单、优雅,且没有必要去破坏它。 - mluisbrown

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接