title: iOS APP 添加桌面快捷方式
tags:
- 技术
- iOS
date: 2021-05-06 13:29#
iOS APP 添加桌面快捷方式#
背景#
新接到一个需求,需要 APP 内的某些功能,能够把入口添加到桌面,点击桌面到入口可以直接跳转 APP 对应界面(类似于下面这张示例图),于是就做了一番调研。
其实很多 APP 目前都已经实现了类似的功能,比如支付宝、云闪付等等,其中的每一个独立功能都可以单独添加到桌面,所以网上有很多实现的方法,笔者做的是整理和试错。
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F22%2Fwn137cohlfDUpKN.png&w=3840&q=75)
实现#
首先,添加到桌面功能的操作流程是:
客户端打开 APP -> 进入到对应到 APP 功能模块 -> 点击添加快捷方式到桌面按钮 -> 跳转浏览器,并加载引导页面,点击分享,选择添加到主屏幕 -> 从主屏幕点击刚刚添加到快捷功能,跳转到 APP 的对应界面。
根据笔者了解到的信息,目前实现这种功能,大致可以分为两种实现方式:
-
方法一:H5 提供网页,每个不同的功能提供不同的网页,服务端返回这些网页的 URL,客户端配置打开 URL Scheme,然后使用 Safari 直接加载 URL,加载的网页中根据进入方式的不同,自动重定向打开 APP 的 URL Scheme。
-
方法二:H5 提供通用的网页,客户端替换通用网页中的内容,比如标题、图标等,并转为 DataURI 格式,服务端提供接口 URL,客户端配置打开 URL Scheme,使用 Safari 加载,接口返回强制重定向加载 DataURI 数据
其中方法二还有另一种实现方式,即客户端使用 HttpServer 模拟服务端。但笔者看来,无论是服务端返回 URL 还是客户端使用 HttpServer,其实是服务端的不同实现方式,故而没有单独分类。
准备#
第一步 客户端:iOS 打开已有 Xcode 项目,选中 Target,添加 URL Scheme,这个 URL Scheme 是自己定义的,在这个地方定义了 xxx 之后,可以通过在浏览器中输入 xxx:// 来唤起 APP,比如笔者定义了一个 mkaddtohomescreen,然后在浏览器中输入 mkaddtohomescreen://,就会弹出是否打开对应 APP 的提示
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F23%2FUn47sFZaxi6GJC5.png&w=3840&q=75)
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F23%2FW4zIUYMdHluxS3A.png&w=3840&q=75)
定义好了 Scheme 之后,可以考虑 Scheme 添加参数的问题,通过在 scheme 后添加参数,在 Appdelegate 中applicaiton:open:options:
方法拦截到,根据对应参数跳转不同界面
比如 Scheme 为
mkaddtohomescreen://page/view1
,在applicaiton:open:options:
中,url.absouluteString = mkaddtohomescreen://page/view1,url.host = page,url.path = /view1,
所以可以根据 path 的不同跳转不同的界面。
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if let navController = window?.rootViewController as? UINavigationController,
let topController = navController.topViewController {
// eg: mkaddtohomescreen://page/view1
// url.host = page
// url.path = /view1
if url.absoluteString.hasPrefix("mkaddtohomescreen://") {
// 说明是APP的URL Scheme,处理
let targetVC = targetViewController(from: url.path)
if targetVC != nil {
// 判断当前显示的界面是否是要跳转的界面
if topController.classForCoder == targetVC?.classForCoder {
return true
}
navController.pushViewController(targetVC!, animated: true)
}
else {
return true
}
}
}
return true
}
// 根据URL path返回要跳转的界面
func targetViewController(from path: String) -> UIViewController? {
var targetVC: UIViewController?
switch path {
// 根据URL的path跳转不同路径
case "/view1":
targetVC = Method1ViewController()
break
case "/view2":
targetVC = Method2ViewController()
break
case "/view3":
targetVC = Method3ViewController()
break
default:
targetVC = nil
break
}
return targetVC
}
第二步 H5 参考47. 给 App 的某个功能添加快捷方式
中的 shortcuts.html,其中共有三部分,大致为:
- header 部分定义了网页的标题,以及显示到桌面快捷方式的图标和标题
- body 部分则定义来这个网页的内容,其实是引导用户如何添加到桌面
- script 部分则是做了一个判断,判断是桌面快捷方式进入的情况,自己调用 redirect
代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="\(title)">
<link rel="apple-touch-icon-precomposed" href="data:image/jpeg;base64,\(feature_icon)"/>
<title>\(title)</title>
</head>
<script>document.documentElement.style.fontSize = 100 * document.documentElement.clientWidth / 375 + "px"</script>
<style>
/* css代码见链接 https://github.com/mokong/WKAddToHomeScreen */
</style>
<body>
<a id="redirect" href="\(urlToRedirect.absoluteString)"></a>
<div id="container">
<div class="main">
<div class="subject">添加快捷功能到桌面</div>
</div>
<div class="guide">
<div class="content">
<p class="tips">
点击下方工具栏上的<img class="icon" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/XEbFrgamEdvSxVFOBeuZ.png">
</p>
<p class="tips">
并选择<img class="icon" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/IkKEhyTLQpYtqXMZBYtQ.png">“<strong>添加到主屏幕</strong>”
</p>
<img class="toolbar" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/oFNuXVhPJYvBDJPXJTmt.jpg">
<img class="arrow" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/FlBEnTRnlhMyLyVhlfZT.png">
</div>
</div>
</div>
</body>
</html>
<script type="text/javascript">
if (window.navigator.standalone) {
var element = document.getElementById('container');
element.style.display = "none";
var element = document.getElementById('redirect');
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.body.style.backgroundColor = '#FFFFFF';
setTimeout(function() { element.dispatchEvent(event); }, 25);
} else {
var element = document.getElementById('container');
element.style.display = "inline";
}
</script>
其中有关于涉及到桌面快捷方式图标和标题设置的解释可参考苹果官方的Configuring Web Applications,如下
<!-- Specifying a Webpage Icon for Web Clip -->
<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<!-- apple-touch-icon-precomposed与apple-touch-icon的区别为前者是原图,后者是会被苹果处理的图片,这两个的使用二选一即可
<link rel="apple-touch-icon-precomposed" href="xxx.png"> -->
<!-- Specifying a Launch Screen Image -->
<link rel="apple-touch-startup-image" href="/launch.png">
<!-- Adding a Launch Icon Title -->
<meta name="apple-mobile-web-app-title" content="AppTitle">
<!-- Hiding Safari User Interface Components -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Changing the Status Bar Appearance> -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!-- Linking to Other Native Apps -->
<a href="tel:1-408-555-5555">Call me</a>
方法一#
H5 提供网页,比如上面示例的代码,然后把其中 header 部分的内容设置为固定的
- "apple-mobile-web-app-title" 是桌面快捷方式的标题
- "apple-touch-icon-precomposed" 是桌面快捷方式的图片,其中的格式可以选择使用 DataURI 的这种样式,生成方法可以参考下面的
- "title" 则是页面的标题
- \(urlToRedirect.absoluteString) 是定义好的 URL Scheme 链接
获取图片 DataURI 格式的数据 Swift 的代码如下
// 获取图片DataURI格式的数据
if let iconData = UIImage(named: "homeScreen")?.jpegData(compressionQuality: 0.5) {
let iconDataURI = iconData.base64EncodedString()
}
具体步骤如下:
-
配置好客户端的 URLScheme
-
H5 提供编写好的网页,如果没有 H5,可使用上面的 shortcuts.html 内容,把其中的待替换字段 \(title)、\(feature_icon)、以及 \(urlToRedirect.absoluteString) 设置为自己 APP 的,其中的 apple-touch-icon-precomposed 需要放图标经过 DataURI 后的 String
-
需要服务端提供 URL,返回这个网页,然后客户端打开这个 URL。如果服务端也没有。。。那就跟我一样,使用模拟接口返回,打开mocky,(可能需要注册),Response Content Type 设置为 text/html,HTTP Response Body 中放入下面的网页内容,然后点击底部的 GENERATE MY HTTP RESPONSE,就会生成一个 URL
-
在点击添加快捷方式的地方,直接 openURL 即可
func addMethod1(_ sender: Any) { // 方法一,不需要本地放H5数据,只需要打开指定URL即可 // 可使用mocky来提供模拟接口 let urlStr = "https://run.mocky.io/v3/98baaf4a-edec-4956-8506-7bbfca349d07" UIApplication.shared.open(URL(string: urlStr)!, options: [:], completionHandler: nil) }
方法二#
可参考给 App 的某个功能添加快捷方式,文章中使用的是客户端自建服务器返回 DataURI 数据的方法,具体操作如下:
-
配置好客户端的 URL Schemes
-
客户端使用 Pod 添加 Swifter,用于自建服务器
-
H5 提供编写好的网页,使用上面的 shortcuts.html 内容,其中的待替换字段不要动
-
在点击添加快捷方式时,客户端读取 html 的内容并替换里面指定字段,转为 DataURI,启动本地服务器,并返回 DataURI 数据
func addMethod2(_ sender: Any) { // 定义好的URL Scheme let schemeStr = "mkaddtohomescreen://page/view2" // 要替换的桌面快捷方式图标 let shortcutImageData = UIImage(named: "homescreen")?.jpegData(compressionQuality: 0.5) // 要替换的桌面快捷方式标题 let shortcutTitle = "添加到主屏幕2" guard let schemeURL = URL(string: schemeStr), let shortcutImageStr = shortcutImageData?.base64EncodedString() else { return } // 替换H5中的内容 let htmlStr = htmlFor(title: shortcutTitle, urlToRedirect: schemeURL.absoluteString, icon: shortcutImageStr) guard let base64 = htmlStr.data(using: .utf8)?.base64EncodedString() else { return } // 启动本地服务器,端口号是9081 if let shortcutUrl = URL(string: "http://localhost:9081/s") { // 转为dataURI格式 let server = HttpServer() server["/s"] = { request in return .movedPermanently("data:text/html;base64,\(base64)") } try? server.start(9081) UIApplication.shared.open(shortcutUrl, options: [:], completionHandler: nil) } } func htmlFor(title: String, urlToRedirect: String, icon: String) -> String { let shortcutsPath = Bundle.main.path(forResource: "content2", ofType: "html") var shortcutsContent = try! String(contentsOfFile: shortcutsPath!) as String shortcutsContent = shortcutsContent.replacingOccurrences(of: "\\(title)", with: title) shortcutsContent = shortcutsContent.replacingOccurrences(of: "\\(urlToRedirect.absoluteString)", with: urlToRedirect) shortcutsContent = shortcutsContent.replacingOccurrences(of: "\\(feature_icon)", with: icon) print(shortcutsContent) return shortcutsContent }
还没完#
代码放到了Github,大家下载运行后会发现还有一个问题待解决,即:当使用方法一,添加快捷标签到桌面后,第一次点击桌面的快捷标签打开了 APP;再次点击桌面的快捷标签,显示白屏,原因是第一次打开快捷标签没有关闭,重新打开时没有触发加载,所以也就没有跳转 APP。而使用了 DataURI 加载的方法二,则没有这个问题,每次点击图标均可以直接跳转。
但是对比支付宝的添加到桌面发现支付宝的也是采用的方法一,第一次从桌面添加的快捷打开时自动跳转到支付宝,第二次点击桌面到快捷图标时,发现也是停留在一个页面,但是支付宝在这个页面上放了东西,可以称之为中间页。如下
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F23%2FA9QaIjZ8HN1xD7O.png&w=3840&q=75)
要怎么实现中间页那种效果呢,目前笔者方法一的实现,点击时依赖的是服务端返回的 H5 网页内容,里面的 Script 会根据进入方式的不同,直接自跳转打开 APP 的 URL Scheme;所以想要添加中间页,嗯,想法是:嵌套一层。即:
- 服务端返回的 H5 网页内容,里面的 Script 不直接跳转打开 APP 的 URL Scheme,而是跳转中间页的链接
- 中间页的页面,同样的逻辑,再次跳转打开 APP 的 URLScheme;同时中间页的页面添加按钮,点击也是跳转 APP 的 URLScheme。
这样,第一次点击时,是桌面 - 中间页 - APP 的 URL Scheme;第二次点击时,则是直接显示中间页,然后手动点击中间页上的立即进入,再次打开 APP。
下面来尝试一下:
首先编辑中间页面的 H5,大致内容如下,仅供参考。。。。就是把之前 H5 页面的 body 部分简单修改一下,添加一个 button,事件是点击打开 Scheme,同时自动跳转 Scheme 的逻辑也还存在。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="方法三中间页内容">
<title>"方法三中间页标题"</title>
</head>
<script>document.documentElement.style.fontSize = 100 * document.documentElement.clientWidth / 375 + "px"</script>
<style>
/* css代码见链接 https://github.com/mokong/WKAddToHomeScreen */
</style>
<body>
<a id="redirect" href="mkaddtohomescreen://page/view3"></a>
<div id="B_container" class="backguide" style="display: block; width='100%'; background-color=#cyan">
<div class="tips">你即将进入</div>
<img id="B_icon" class="icon" src=""></img>
<div id="B_appname" class="appname">MKAddToHomeScreen</div>
<button class="enter" onclick="jumpSchema()" style="background-color: #red; widht=100%; height=64px">立即进入</button>
</div>
</body>
</html>
<script type="text/javascript">
function jumpSchema() {
var element = document.getElementById('redirect');
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.body.style.backgroundColor = '#FFFFFF';
setTimeout(function() { element.dispatchEvent(event); }, 25);
}
if (window.navigator.standalone) {
var element = document.getElementById('redirect');
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.body.style.backgroundColor = '#FFFFFF';
setTimeout(function() { element.dispatchEvent(event); }, 25);
} else {
var element = document.getElementById('container');
element.style.display = "inline";
}
</script>
把中间页的网页放到mocky中按照同样的方式 (Response Content Type 设置为 text/html,HTTP Response Body 中放入网页内容),生成一个 URL,然后把这个 URL 放到之前网页要自跳转的 href 中,然后再把之前网页再用mocky生成一个链接,在 APP 中使用 openURL 的方式打开最后生成的这个链接,运行,调试。
发现结果是期望的,即第一次打开直接跳转,第二次打开显示中间页上面有点击跳转按钮;但是中间页的样式看起来确跟支付宝的不一样,这样生成的中间页因为经过了一次跳转,所以顶部和底部都显示了 Safari 二级页面的样式,嗯哼,这个不是笔者所希望的效果,而且体验支付宝的效果之后,发现支付宝的中间页是没有二级页面的那种头部和底部的,所以,那是怎么实现的呢?
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F29%2F2hGU6u1YcgtkApz.jpg&w=3840&q=75)
如果不想要中间页显示为二级页面的形式,就不能采用上面那种经过一次跳转方法。只能采用单一页面的方法,在一个 H5 页面上想办法。所以现在想要的是,在同一个页面上,从 APP 跳转的时候显示 “引导添加到桌面” 的样式,从桌面打开时显示 “中间页” 的样式。
按照这个逻辑来,用两个 div,包括两段样式,根据进入方式的不同,设置两个 div 的显示隐藏是不是就可以了呢?说做就做,把上面第二个 html 中的内容和样式放到第一个 html 中,代码如下:middle_container 是中间页的 div,jump_container 是引导页 div,然后根据 window.navigator.standalone 判断显示哪一个 div,middle_container 中按钮点击是跳转打开 APP,同时再把第一个 html 的跳转由跳转中间页改为打开 APP
Ps:
要检测 Web 应用程序当前是否运行在全屏状态,只要检测 window.navigator.standalone 是否为 true 就可以了,如果这个属性为 true 则表示 Web 应用程序当前运行在全屏状态,否则运行在非全屏状态。可用于检测到 Web 应用程序运行在非全屏状态时提示用户把 Web 应用程序的图标添加到主屏幕。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="方法三标题">
<link rel="apple-touch-icon-precomposed" href="data:image/jpeg;base64,imageData"/>
<title>方法三网页标题</title>
</head>
<script>document.documentElement.style.fontSize = 100 * document.documentElement.clientWidth / 375 + "px"</script>
<style>
/* css代码见链接 https://github.com/mokong/WKAddToHomeScreen */
</style>
<body>
<a id="redirect" href="mkaddtohomescreen://page/view3"></a>
<div id="middle_container" class="backguide">
<div class="middle_tips">你即将进入</div>
<img class="middle_icon" src=""></img>
<div class="middle_appname">MKAddToHomeScreen</div>
<button class="middle_enter" onclick="jumpSchema()" style="background-color: #red; widht=100%; height=64px">立即进入</button>
</div>
<div id="jump_container">
<div class="main">
<div class="subject">添加快捷功能到桌面</div>
</div>
<div class="guide">
<div class="content">
<p class="tips">
点击下方工具栏上的<img class="icon" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/XEbFrgamEdvSxVFOBeuZ.png">
</p>
<p class="tips">
并选择<img class="icon" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/IkKEhyTLQpYtqXMZBYtQ.png">“<strong>添加到主屏幕</strong>”
</p>
<img class="toolbar" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/oFNuXVhPJYvBDJPXJTmt.jpg">
<img class="arrow" src="https://dariel-1256714552.cos.ap-shanghai.myqcloud.com/FlBEnTRnlhMyLyVhlfZT.png">
</div>
</div>
</div>
</body>
</html>
<script type="text/javascript">
function jumpSchema() {
var element = document.getElementById('redirect');
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.body.style.backgroundColor = '#FFFFFF';
setTimeout(function() { element.dispatchEvent(event); }, 25);
}
if (window.navigator.standalone) {
var middle_element = document.getElementById('middle_container');
var jump_element = document.getElementById('jump_container');
middle_element.style.display = "inline";
jump_element.style.display = "none"
var element = document.getElementById('redirect');
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.body.style.backgroundColor = '#FFFFFF';
setTimeout(function() { element.dispatchEvent(event); }, 25);
} else {
var middle_element = document.getElementById('middle_container');
var jump_element = document.getElementById('jump_container');
middle_element.style.display = "none";
jump_element.style.display = "inline";
}
</script>
然后用当前内容放到mocky生成一个链接,在程序中打开这个链接,体验,Binggo。没有了二级界面的样式,而且再次打开,页面显示也不是空白,而是如下样式:
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F29%2FQRFYdJX9N2tSquj.png&w=3840&q=75)
总结#
完整代码在:Github。
笔者感觉两种方式各有优缺点:方法一依赖于网络,因为需要服务端返回的网页内容,加载完成后才能进行下一步跳转。而方法二采用 DataURI 方式的,把数据已经转为 string 放在了本地,点击时直接加载,故而不依赖网络。但方法一实现简单,客户端、H5、和服务端配合虽然有些冗余,但工作量小,很容易实现。方法二的加载采用 DataURI,查看调试数据不方便。根据笔者的观察,支付宝其实采用的是方法二,没网络的时候也可以加载打开主 APP,且在方法二的基础上还加上了中间页。
附图:
![image](/_next/image?url=https%3A%2F%2Fi.loli.net%2F2021%2F04%2F23%2FACd5wtVjEFXWpqD.png&w=3840&q=75)