原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

本文将介绍在 SwiftUI 视图中翻开 URL 的若干种办法,其他的内容还包括怎么主动辨认文本中的内容并为其转化为可点击链接,以及怎么自界说翻开 URL 前后的行为等。

本文的典范代码是在 Swift Playgrounds 4.1 ( macOS 版别 )中完结的,可在 此处下载。了解更多有关 Swift Playgrounds 的内容,能够参看 Swift Playgrounds 4 娱乐还是生产力 一文。

在 SwiftUI 视图中打开 URL 的若干方法

SwiftUI 1.0( iOS 13、Catalina )

在视图中,开发者通常需求处理两种不同的翻开 URL 的情况:

  • 点击一个按钮( 或相似的部件 )翻开指定的 URL
  • 将文本中的部分内容变成可点击区域,点击后翻开指定的 URL

惋惜的是,1.0 年代的 SwiftUI 还相当幼嫩,没有供给任何原生的办法来应对上述两种场景。

对于第一种场景,常见的做法为:

// iOS
Button("Wikipedia"){
    UIApplication.shared.open(URL(string:"https://www.wikipedia.org")!)
}
// macOS
Button("Wikipedia"){
    NSWorkspace.shared.open(URL(string:"https://www.wikipedia.org")!)
}

而第二种场景完结起来就相当地麻烦,需求包装 UITextView( 或 UILabel )并配合 NSAttributedString 一起来完结,此时 SwiftUI 仅被当作一个布局东西罢了。

SwiftUI 2.0( iOS 14、Big sur )

SwiftUI 2.0 为第一个场景供给了相当完美的原生方案,但仍无法经过原生的办法来处理第二种场景。

openURL

openURL 是 SwiftUI 2.0 中新增的一个环境值( EnvironmentValue ),它有两个作用:

  • 经过调用它的 callFunction 办法,完结翻开 URL 的动作

此时在 Button 中,咱们能够直接经过 openURL 来完结在 SwiftUI 1.0 版别中经过调用其他结构 API 才能完结的工作。

struct Demo: View {
    @Environment(\.openURL) private var openURL // 引入环境值
    var body: some View {
        Button {
            if let url = URL(string: "https://www.example.com") {
                openURL(url) { accepted in  //  经过设置 completion 闭包,能够检查是否已完结 URL 的敞开。状况由 OpenURLAction 供给
                    print(accepted ? "Success" : "Failure")
                }
            }
        } label: {
            Label("Get Help", systemImage: "person.fill.questionmark")
        }
    }
}
  • 经过供给 OpenURLAction ,自界说经过 openURL 翻开链接的行为(后文中详细阐明)

Link

SwiftUI 2.0 供给了一个结合 Button 和 openURL 的 Link 控件,协助开发者进一步简化代码:

Link(destination: URL(string: "mailto://feedback@fatbobman.com")!, label: {
    Image(systemName: "envelope.fill")
    Text("发邮件")
})

SwiftUI 3.0( iOS 15、Monterey )

3.0 年代,随着 Text 功用的增强和 AttributedString 的呈现,SwiftUI 总算补上了另一个短板 —— 将文本中的部分内容变成可点击区域,点击后翻开指定的 URL。

Text 用例 1 :主动辨认 LocalizedStringKey 中的 URL

经过支撑 LocalizedStringKey 的结构办法创立的 Text ,会主动辨认文本中的网址( 开发者无须做任何设定),点击后会翻开对应的 URL 。

Text("www.wikipedia.org 13900000000 feedback@fatbobman.com") // 默许运用参数类型为 LocalizedStringKey 的结构器

在 SwiftUI 视图中打开 URL 的若干方法

此种办法只能辨认网络地址( 网页地址、邮件地址等 ),因此代码中的电话号码无法主动辨认。

请注意,下面的代码运用的是参数类型为 String 的结构器,因此 Text 将无法主动辨认内容中的 URL :

let text = "www.wikipedia.org 13900000000 feedback@fatbobman.com" // 类型为 String
Text(text) // 参数类型为 String 的结构器不支撑主动辨认

Text 用例 2 :辨认 Markdown 语法中的 URL 符号

SwiftUI 3.0 的 Text ,当内容类型为 LocalizedStringKey 时,Text 能够对部分 Markdown 语法符号进行解析 :

Text("[Wikipedia](https://www.wikipedia.org) ~~Hi~~ [13900000000](tel://13900000000)")

在这种办法下,咱们能够运用任何品种的 URI (不限于网络),比如代码中的拨打电话。

在 SwiftUI 视图中打开 URL 的若干方法

Text 用例 3 :包括 link 信息的 AttributedString

在 WWDC 2021 上,苹果推出了 NSAttributedString 的值类型版别 AttributedString, 而且能够直接运用在 Text 中。经过在 AttributedString 中为不同位置的文字设置不同的特点,然后完结在 Text 中翻开 URL 的功用。

let attributedString:AttributedString = {
    var fatbobman = AttributedString("肘子的 Swift 记事本")
    fatbobman.link = URL(string: "https://www.fatbobman.com")!
    fatbobman.font = .title
    fatbobman.foregroundColor = .green // link 不为 nil 的 Run,将主动屏蔽自界说的前景色和下划线
    var tel = AttributedString("电话号码")
    tel.link = URL(string:"tel://13900000000")
    tel.backgroundColor = .yellow
    var and = AttributedString(" and ")
    and.foregroundColor = .red
    return fatbobman + and + tel
}()
Text(attributedString)

在 SwiftUI 视图中打开 URL 的若干方法

更多有关 AttributedString 的内容,请参看 AttributedString——不仅仅让文字更美丽

Text 用例 4 :辨认字符串中的 URL 信息,并转化成 AttributedString

上述 3 个用例中,除了用例 1能够主动辨认文字中的网络地址外,其他两个用例都需求开发者经过某种办法显式增加 URL 信息。

开发者能够经过运用 NSDataDetector + AttributedString 的组合,然后完结相似体系信息、邮件、微信 app 那样,对文字中的不同类型的内容进行主动辨认,并设置对应的 URL。

NSDataDetector 是 NSRegularExpression 的子类,它能够检测自然语言文本中的半结构化信息,如日期、地址、链接、电话号码、交通信息等内容,它被广泛应用于苹果供给的各种体系应用中。

let text = "https://www.wikipedia.org 13900000000 feedback@fatbobman.com"
// 设定需求辨认的类型
let types = NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.phoneNumber.rawValue
// 创立辨认器
let detector = try! NSDataDetector(types: types)
// 获取辨认成果
let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.count))
// 逐个处理检查成果
for match in matches {
    if match.resultType == .date {
        ...
    }
}

你能够将 NSDataDetector 视为拥有极高复杂度的正则表达式封装套件。

完好的代码如下:

extension String {
    func toDetectedAttributedString() -> AttributedString {
        var attributedString = AttributedString(self)
        let types = NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.phoneNumber.rawValue
        guard let detector = try? NSDataDetector(types: types) else {
            return attributedString
        }
        let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: count))
        for match in matches {
            let range = match.range
            let startIndex = attributedString.index(attributedString.startIndex, offsetByCharacters: range.lowerBound)
            let endIndex = attributedString.index(startIndex, offsetByCharacters: range.length)
            // 为 link 设置 url
            if match.resultType == .link, let url = match.url {
                attributedString[startIndex..<endIndex].link = url
                // 假如是邮件,设置背景色
                if url.scheme == "mailto" {
                attributedString[startIndex..<endIndex].backgroundColor = .red.opacity(0.3)
                }
            }
            // 为 电话号码 设置 url
            if match.resultType == .phoneNumber, let phoneNumber = match.phoneNumber {
                let url = URL(string: "tel:\(phoneNumber)")
                attributedString[startIndex..<endIndex].link = url
            }
        }
        return attributedString
    }
}
Text("https://www.wikipedia.org 13900000000 feedback@fatbobman.com".toDetectedAttributedString())

在 SwiftUI 视图中打开 URL 的若干方法

自界说 Text 中链接的颜色

惋惜的是,即使咱们已经为 AttributedString 设置了前景色,但当某段文字的 link 特点非 nil 时,Text 将主动疏忽它的前景色和下划线设定,运用体系默许的 link 烘托设定来显示。

目前能够经过设置上色来改动 Text 中悉数的 link 颜色:

Text("www.wikipedia.org 13900000000 feedback@fatbobman.com")
    .tint(.green)
Link("Wikipedia", destination: URL(string: "https://www.wikipedia.org")!)
    .tint(.pink)

在 SwiftUI 视图中打开 URL 的若干方法

相较 Text 中链接的固定款式,能够用 Button 或 Link 创立能够自在定制外观的链接按钮:

Button(action: {
    openURL(URL(string: "https://www.wikipedia.org")!)
}, label: {
    Circle().fill(.angularGradient(.init(colors: [.red,.orange,.pink]), center: .center, startAngle: .degrees(0), endAngle: .degrees(360)))
})

在 SwiftUI 视图中打开 URL 的若干方法

自界说 openURL 的行为

在 Button 中,咱们能够经过在闭包中增加逻辑代码,自界说敞开 URL 之前与之后的行为。

Button("翻开网页") {
            if let url = URL(string: "https://www.example.com") {
                // 敞开 URL 前的行为
                print(url)
                openURL(url) { accepted in  //  经过设置 completion 闭包,界说点击 URL 后的行为
                    print(accepted ? "Open success" : "Open failure")
                }
            }
}

但在 Link 和 Text 中,咱们则需求经过为环境值 openURL 供给 OpenURLAction 处理代码的办法来完结自界说翻开链接的行为。

Text("Visit [Example Company](https://www.example.com) for details.")
    .environment(\.openURL, OpenURLAction { url in
        handleURL(url)
        return .handled
    })

OpenURLAction 的结构如下:

public struct OpenURLAction {
    @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
    public init(handler: @escaping (URL) -> OpenURLAction.Result)
    public struct Result {
        public static let handled: OpenURLAction.Result  // 当时的代码已处理该 URL ,调用行为不会再向下传递
        public static let discarded: OpenURLAction.Result  // 当时的处理代码将丢弃该 URL ,调用行为不会再向下传递
        public static let systemAction: OpenURLAction.Result  // 当时代码不处理,调用行为向下传递( 假如外层没有用户的自界说 OpenURLAction ,则运用体系默许的完结)
        public static func systemAction(_ url: URL) -> OpenURLAction.Result  // 当时代码不处理,将新的 URL 向下传递( 假如外层没有用户的自界说 OpenURLAction ,则运用体系默许的完结)
    }
}

比如:

Text("www.fatbobman.com feedback@fatbobman.com 13900000000".toDetectedAttributedString()) // 创立三个链接 https mailto tel
    .environment(\.openURL, OpenURLAction { url in
        switch url.scheme {
        case "mailto":
            return .discarded // 邮件将直接丢弃,不处理
        default:
            return .systemAction // 其他类型的 URI 传递到下一层(外层)
        }
    })
    .environment(\.openURL, OpenURLAction { url in
        switch url.scheme {
        case "tel":
            print("call number \(url.absoluteString)") // 打印电话号码
            return .handled  // 告知已经处理完毕,将不会持续传递到下一层
        default:
            return .systemAction // 其他类型的 URI 当时代码不处理,直接传递到下一层
        }
    })
    .environment(\.openURL, OpenURLAction { _ in
        .systemAction(URL(string: "https://www.apple.com")!) // 由于在本层之后咱们没有持续设定 OpenURLAction , 因此最终会调用体系的完结翻开苹果官网
    })

这种经过环境值层层设定的处理办法,给了开发者非常大的自在度。在 SwiftUI 中,采用相似逻辑的还有 onSubmit ,有关 onSubmit 的信息,请参看 SwiftUI TextField 进阶 —— 事情、焦点、键盘。

handler 的回来成果 handleddiscarded 都将阻挠 url 持续向下传递,它们之间的不同只要在显式调用 openURL 时才会表现出来。

// callAsFunction 的界说
public struct OpenURLAction {
  public func callAsFunction(_ url: URL, completion: @escaping (_ accepted: Bool) -> Void)
}
// handled 时  accepted 为 true , discarded 时 accepted 为 false
openURL(url) { accepted in
      print(accepted ? "Success" : "Failure")
}

结合上面的介绍,下面的代码将完结:在点击链接后,用户能够挑选是翻开链接还是将链接仿制在粘贴板上:

struct ContentView: View {
    @Environment(\.openURL) var openURL
    @State var url:URL?
    var show:Binding<Bool>{
        Binding<Bool>(get: { url != nil }, set: {_ in url = nil})
    }
    let attributedString:AttributedString = {
        var fatbobman = AttributedString("肘子的 Swift 记事本")
        fatbobman.link = URL(string: "https://www.fatbobman.com")!
        fatbobman.font = .title
        var tel = AttributedString("电话号码")
        tel.link = URL(string:"tel://13900000000")
        tel.backgroundColor = .yellow
        var and = AttributedString(" and ")
        and.foregroundColor = .red
        return fatbobman + and + tel
    }()
    var body: some View {
        Form {
            Section("NSDataDetector + AttributedString"){
                // 运用 NSDataDetector 进行转化
                Text("https://www.fatbobman.com 13900000000 feedback@fatbobman.com".toDetectedAttributedString())
            }
        }
        .environment(\.openURL, .init(handler: { url in
            switch url.scheme {
            case "tel","http","https","mailto":
                self.url = url
                return .handled
            default:
                return .systemAction
            }
        }))
        .confirmationDialog("", isPresented: show){
            if let url = url {
                Button("仿制到剪贴板"){
                    let str:String
                    switch url.scheme {
                    case "tel":
                        str = url.absoluteString.replacingOccurrences(of: "tel://", with: "")
                    default:
                        str = url.absoluteString
                    }
                    UIPasteboard.general.string = str
                }
                Button("翻开 URL"){openURL(url)}
            }
        }
        .tint(.cyan)
    }
}

在 SwiftUI 视图中打开 URL 的若干方法

总结

虽然本文的主要目的是介绍在 SwiftUI 视图中翻开 URL 的几种办法,不过读者应该也能从中感受到 SwiftUI 三年来的不断进步,信任不久后的 WWDC 2022 会为开发者带来更多的惊喜。

希望本文能够对你有所协助。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】