访问我的博客www.fatbobman.com[1]可以获得更好的阅读体验
本文将介绍在 SwiftUI 视图中打开 URL 的若干种方式,其他的内容还包括如何自动识别文本中的内容并为其转换为可点击链接,以及如何自定义打开 URL 前后的行为等。
本文的范例代码是在 Swift Playgrounds 4.1 ( macOS 版本 )中完成的,可在此处下载[2]。了解更多有关 Swift Playgrounds 的内容,可以参阅Swift Playgrounds 4 娱乐还是生产力[3]一文。
SwiftUI 1.0( iOS 13、Catalina )
在视图中,开发者通常需要处理两种不同的打开 URL 的情况:
点击一个按钮( 或类似的部件 )打开指定的 URL
将文本中的部分内容变成可点击区域,点击后打开指定的 URL
遗憾的是,1.0 时代的 SwiftUI 还相当稚嫩,没有提供任何原生的方法来应对上述两种场景。
对于第一种场景,常见的做法为:
// iOSButton("Wikipedia"){UIApplication.shared.open(URL(string:"https://www.wikipedia.org")!)}// macOSButton("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 才能完成的工作。
structDemo:View{@Environment(\.openURL)privatevaropenURL// 引入环境值varbody: someView{Button{ifleturl =URL(string:"https://www.example.com") {openURL(url) { acceptedin// 通过设置 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 的构造器
此种方法只能识别网络地址( 网页地址、邮件地址等 ),因此代码中的电话号码无法自动识别。
请注意,下面的代码使用的是参数类型为 String 的构造器,因此 Text 将无法自动识别内容中的 URL :
lettext ="www.wikipedia.org 13900000000 feedback@fatbobman.com"// 类型为 StringText(text)// 参数类型为 String 的构造器不支持自动识别
Text 用例 2 :识别 Markdown 语法中的 URL 标记
SwiftUI 3.0 的 Text ,当内容类型为 LocalizedStringKey 时,Text 可以对部分 Markdown 语法标记进行解析 :
Text("[Wikipedia](https://www.wikipedia.org "Wikipedia") ~~Hi~~ [13900000000](tel://13900000000 "13900000000")")
在这种方式下,我们可以使用任何种类的 URI (不限于网络),比如代码中的拨打电话。
Text 用例 3 :包含 link 信息的 AttributedString
在 WWDC 2021 上,苹果推出了 NSAttributedString 的值类型版本 AttributedString, 并且可以直接使用在 Text 中。通过在 AttributedString 中为不同位置的文字设置不同的属性,从而实现在 Text 中打开 URL 的功能。
letattributedString:AttributedString= {varfatbobman =AttributedString("肘子的 Swift 记事本")fatbobman.link =URL(string:"https://www.fatbobman.com")!fatbobman.font = .titlefatbobman.foregroundColor = .green// link 不为 nil 的 Run,将自动屏蔽自定义的前景色和下划线vartel =AttributedString("电话号码")tel.link =URL(string:"tel://13900000000")tel.backgroundColor = .yellowvarand =AttributedString(" and ")and.foregroundColor = .redreturnfatbobman + and + tel}()Text(attributedString)
更多有关 AttributedString 的内容,请参阅AttributedString——不仅仅让文字更漂亮[4]
Text 用例 4 :识别字符串中的 URL 信息,并转换成 AttributedString
上述 3 个用例中,除了用例 1可以自动识别文字中的网络地址外,其他两个用例都需要开发者通过某种方式显式添加 URL 信息。
开发者可以通过使用 NSDataDetector + AttributedString 的组合,从而实现类似系统信息、邮件、微信 app 那样,对文字中的不同类型的内容进行自动识别,并设置对应的 URL。
NSDataDetector[5]是 NSRegularExpression 的子类,它可以检测自然语言文本中的半结构化信息,如日期、地址、链接、电话号码、交通信息等内容,它被广泛应用于苹果提供的各种系统应用中。
lettext ="https://www.wikipedia.org 13900000000 feedback@fatbobman.com"// 设定需要识别的类型lettypes =NSTextCheckingResult.CheckingType.link.rawValue |NSTextCheckingResult.CheckingType.phoneNumber.rawValue// 创建识别器letdetector =try!NSDataDetector(types: types)// 获取识别结果letmatches = detector.matches(in: text, options: [], range:NSRange(location:0, length: text.count))// 逐个处理检查结果formatchinmatches {ifmatch.resultType == .date {...}}
你可以将 NSDataDetector 视为拥有极高复杂度的正则表达式封装套件。
完整的代码如下:
extensionString{functoDetectedAttributedString()->AttributedString{varattributedString =AttributedString(self)lettypes =NSTextCheckingResult.CheckingType.link.rawValue |NSTextCheckingResult.CheckingType.phoneNumber.rawValueguardletdetector =try?NSDataDetector(types: types)else{returnattributedString}letmatches = detector.matches(in:self, options: [], range:NSRange(location:0, length:count))formatchinmatches {letrange = match.rangeletstartIndex = attributedString.index(attributedString.startIndex, offsetByCharacters: range.lowerBound)letendIndex = attributedString.index(startIndex, offsetByCharacters: range.length)// 为 link 设置 urlifmatch.resultType == .link,leturl = match.url {attributedString[startIndex..// 如果是邮件,设置背景色ifurl.scheme =="mailto"{attributedString[startIndex..0.3)}}// 为 电话号码 设置 urlifmatch.resultType == .phoneNumber,letphoneNumber = match.phoneNumber {leturl =URL(string:"tel:\(phoneNumber)")attributedString[startIndex..returnattributedString}}Text("https://www.wikipedia.org 13900000000 feedback@fatbobman.com".toDetectedAttributedString())
自定义 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)
相较 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)))})
自定义 openURL 的行为
在 Button 中,我们可以通过在闭包中添加逻辑代码,自定义开启 URL 之前与之后的行为。
Button("打开网页") {ifleturl =URL(string:"https://www.example.com") {// 开启 URL 前的行为print(url)openURL(url) { acceptedin// 通过设置 completion 闭包,定义点击 URL 后的行为print(accepted ?"Open success":"Open failure")}}}
但在 Link 和 Text 中,我们则需要通过为环境值 openURL 提供 OpenURLAction 处理代码的方式来实现自定义打开链接的行为。
Text("Visit [Example Company](https://www.example.com "ExampleCompany") for details.").environment(\.openURL,OpenURLAction{ urlinhandleURL(url)return.handled})
OpenURLAction 的结构如下:
publicstructOpenURLAction{@available(iOS15.0, macOS12.0, tvOS15.0, watchOS8.0, *)publicinit(handler: @escaping (URL) ->OpenURLAction.Result)publicstructResult{publicstaticlethandled:OpenURLAction.Result// 当前的代码已处理该 URL ,调用行为不会再向下传递publicstaticletdiscarded:OpenURLAction.Result// 当前的处理代码将丢弃该 URL ,调用行为不会再向下传递publicstaticletsystemAction:OpenURLAction.Result// 当前代码不处理,调用行为向下传递( 如果外层没有用户的自定义 OpenURLAction ,则使用系统默认的实现)publicstaticfuncsystemAction(_url: URL)->OpenURLAction.Result// 当前代码不处理,将新的 URL 向下传递( 如果外层没有用户的自定义 OpenURLAction ,则使用系统默认的实现)}}
比如:
Text("www.fatbobman.com feedback@fatbobman.com 13900000000".toDetectedAttributedString())// 创建三个链接 https mailto tel.environment(\.openURL,OpenURLAction{ urlinswitchurl.scheme {case"mailto":return.discarded// 邮件将直接丢弃,不处理default:return.systemAction// 其他类型的 URI 传递到下一层(外层)}}).environment(\.openURL,OpenURLAction{ urlinswitchurl.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 进阶 —— 事件、焦点、键盘[6]。
handler 的返回结果handled
和discarded
都将阻止 url 继续向下传递,它们之间的不同只有在显式调用 openURL 时才会表现出来。
// callAsFunction 的定义publicstructOpenURLAction{publicfunccallAsFunction(_url: URL, completion: @escaping(_accepted: Bool)->Void)}// handled 时 accepted 为 true , discarded 时 accepted 为 falseopenURL(url) { acceptedinprint(accepted ?"Success":"Failure")}
结合上面的介绍,下面的代码将实现:在点击链接后,用户可以选择是打开链接还是将链接复制在粘贴板上:
structContentView:View{@Environment(\.openURL)varopenURL@Statevarurl:URL?varshow:Binding<Bool>{Binding<Bool>(get: { url !=nil},set: {_inurl =nil})}letattributedString:AttributedString= {varfatbobman =AttributedString("肘子的 Swift 记事本")fatbobman.link =URL(string:"https://www.fatbobman.com")!fatbobman.font = .titlevartel =AttributedString("电话号码")tel.link =URL(string:"tel://13900000000")tel.backgroundColor = .yellowvarand =AttributedString(" and ")and.foregroundColor = .redreturnfatbobman + and + tel}()varbody: someView{Form{Section("NSDataDetector + AttributedString"){// 使用 NSDataDetector 进行转换Text("https://www.fatbobman.com 13900000000 feedback@fatbobman.com".toDetectedAttributedString())}}.environment(\.openURL, .init(handler: { urlinswitchurl.scheme {case"tel","http","https","mailto":self.url = urlreturn.handleddefault:return.systemAction}})).confirmationDialog("", isPresented: show){ifleturl = url {Button("复制到剪贴板"){letstr:Stringswitchurl.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 三年来的不断进步,相信不久后的 WWDC 2022 会为开发者带来更多的惊喜。
希望本文能够对你有所帮助。
参考资料
[1]
www.fatbobman.com:https://www.fatbobman.com
[2]此处下载:https://github.com/fatbobman/BlogCodes/tree/main/openURLinSwiftUI
[3]Swift Playgrounds 4 娱乐还是生产力:https://www.fatbobman.com/posts/swiftPlaygrounds4/
[4]AttributedString——不仅仅让文字更漂亮:https://www.fatbobman.com/posts/attributedString/
[5]NSDataDetector:https://developer.apple.com/documentation/foundation/nsdatadetector
[6]SwiftUI TextField 进阶 —— 事件、焦点、键盘:https://fatbobman.com/posts/textfield-event-focus-keyboard/
声明:本文部分素材转载自互联网,如有侵权立即删除 。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别
丞旭猿论坛
暂无评论内容