我经常通过链接调用本地应用,比如支付宝的扫码支付,向特定用户发送短信。甚至一些隐藏的功能也以链接作为入口,例如Jellow现在的综合搜索。IOS上有两种实现调用的方法,分别是URL Scheme和Universal Links。本文实现了一个简单的Swift项目,实现并介绍这两种功能。

本文需要基本的Swift基础,接触过SwiftUI,大概就是官方例子大致看过的水平即可。

URL Scheme

简介

这种链接是scheme://data的形式,IOS会根据本地安装的应用搜索scheme,找到后询问你是否要通过相关应用打开该链接。很常用的一个链接是微信的扫码weixin://scanqrcode,通过这个链接直接打开微信的扫码。当然,首先需要本地安装了微信。它让类似捷径等一些自动化软件的功能大大增加。

有部分的Scheme是被保留的,可以通过这些保留的Scheme调用一些系统功能,例如短信、FaceTime,这里介绍了有哪些以及具体怎么使用这些内容,有兴趣可以尝试一下。

如果想要了解应用对应的scheme是什么,解压缩ipa,然后在Info.plist文件的CFBundleURLSchemes字段就是设置的scheme。但要了解scanqrcode之类的具体应用,就麻烦的多,网上如果没有现成的内容就需要反编译去找了。

展示收到的内容

我试了试用SwiftUI来写这个小东西,是个很简单的过程。

新建一个项目以后,在ContentView.swift里面加上基本的显示UI即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import SwiftUI

class LinkData: ObservableObject {
@Published var links :[Link]
init() {
self.links = [Link]()
}
func append(link: Link) {
self.links.append(link)
}
}

struct Link {
var time: String
var data: String
var fromApp: String
var linkType: LinkType

enum LinkType: String {
case urlScheme = "URL Scheme"
case universal = "Universal link"
}
}

struct NavigateList: View {
@ObservedObject var linkData: LinkData

var body: some View {
VStack {
NavigationView {
List{
ForEach(linkData.links, id: \.time) { link in
Row(link: link)
}
}
.navigationBarTitle("链接列表")
}
}
}
}

struct Row: View {
var link: Link

var body: some View {
VStack {
HStack {
Text(link.fromApp)
Spacer()
Text(link.time)
}
HStack {
Text(link.linkType.rawValue)
Spacer()
Text(link.data)
}
}
.padding()
}
}

struct ContentView: View {
@ObservedObject var linkData: LinkData

var body: some View {
NavigateList(linkData: linkData)
}
}

这里可以加一个按钮试一试添加是否成功,那么就把NavigateList加一点东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct NavigateList: View {
@ObservedObject var linkData: LinkData

var body: some View {
VStack {
NavigationView {
List{
ForEach(linkData.links, id: \.time) { link in
Row(link: link)
}
}
.navigationBarTitle("Deep links")
}
Button(action: {
self.linkData.append(
link: Link(
time: "now",data: "data",
fromApp: "Demo App", linkType: .urlScheme
)
)
}) {
Image(systemName: "square.and.arrow.up")
}
}
}
}

按一下按钮,我们就假装收到了一条链接。

响应调用

应用需要决定自己响应什么scheme,这个在XCode > Project > Info > URL Types里面设置。

按照下面的图完成设置就行了。

设置Scheme

这里完成设置以后,通过测试机子的Safari,地址栏填写geoffscheme://就已经能打开应用了。

获取链接内容

这边通过修改SceneDelegate的方式接收数据,这里可能有两种情况。

不过首先,先把URL转换成显示用的Link的方法作为SceneDelegate的类方法写好。

1
2
3
4
5
6
7
8
9
10
11
func parseUrl(_ urlContext: UIOpenURLContext) -> Link {
let fromApp = urlContext.options.sourceApplication ?? "Unknown"
let url = urlContext.url.absoluteString
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyy-MM-dd HH:mm:ss"

return Link(
time: dateFormatter.string(from: Date()), data: url,
fromApp: fromApp, linkType: .urlScheme
)
}

第一种情况,如果应用没有打开,也就是后台也没有该应用,那么链接会调用scene(_:willConnectTo:options)方法。所以就给SceneDelegate添加一个属性和一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var rootView: ContentView?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
self.rootView = ContentView(linkData: LinkData())

if let urlContext = connectionOptions.urlContexts.first {
self.rootView?.linkData.append(link: parseUrl(urlContext))
}

if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: self.rootView)
self.window = window
window.makeKeyAndVisible()
}
}

另一种情况,如果应用被打开了或者在后台,那么链接会调用scene(_:openURLContexts)方法。所以再添加一个方法。

1
2
3
4
5
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let urlContext = URLContexts.first {
self.rootView?.linkData.append(link: parseUrl(urlContext))
}
}

然后通过在Safari中键入geoffscheme://demo或者一些别的geoffscheme://开头的内容,应用都可以被正确唤起并处理链接的内容。

这种链接是https://yourwebsite的形式,它需要有一个支持HTTPS的域名并进行相关的配置。由于其安全性高,支付宝扫码支付、购买转跳等都是用的这种方式。

需要注意的是,这个方法需要一个加入苹果开发者的账号,所以这个内容还属于年费$99付费解锁。如果手头没有这样的账号的话,其实自己玩的应用URL Scheme也够用了。

设置AASA

以我的网站为例,如果我的应用要通过https://geofftools.cn调用,那么我需要提供一个这样的文件:

1
2
3
4
5
6
7
8
9
10
11
{
"applinks": {
"apps": [],
"details": [
{
"appID": "teamid.geoff.myapp",
"paths": ["*"],
}
]
}
}

这个文件可以放在两个位置,https://geofftools.cn/apple-app-site-association或者 https://geofftools.cn/.well-known/apple-app-site-association

其中Apps按要求留空(官方要求),appID是TeamId和BundleId合起来,path的路径里可以用* ? NOT

响应调用

应用需要设置响应的网站,这个在XCode > Project > Capability > Associated Domains里面设置。没有加入付费的苹果开发者的话,这就没办法设置了。

这边填<service>:<fully qualified domain>,例如applinks:geofftools.cn

获取链接内容

和上面的操作类似,这里添加这样一个类方法。

1
2
3
4
5
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let urlContext = userActivity.webpageURL.first {
self.rootView?.linkData.append(link: parseUrl(urlContext))
}
}

对比

Universal Link更复杂也有更多的应用,URL Scheme则很容易配置也能满足基本需求,区别做成表格的话大概是这样一些不同。

Safari二次确认 需要本地安装 调用争抢 需要后端配合 需要加入开发者
Universal Link × ×
URL Scheme × ×

如果考虑安全性和易用性,肯定是Universal Link更合适,他调用应用不必须经过Safari,调用的名称也不会被其他应用占用。但如果只是做个小彩蛋,或者自己实现一些简单的功能,URL Scheme也是完全可以使用的选择。