macOS 应用内更新检查

孙康

如果要实现应用内新版本检查,需对应用安装包和版本信息进行托管。这里所举例适用 Github 平台,但检查思路大同小异。总体步骤分为菜单配置、版本信息获取、用户窗口提示、在线下载更新。本文展示代码源自开源项目 NuwaStone

配置检查更新菜单

首先需要在菜单栏新增一项菜单,然后关联功能实现方法。如这里在主菜单内新增Check for Updates...菜单。然后在ViewController文件中新增相关响应函数,这里入参类型为NSMenuItem

添加检查更新菜单
添加检查更新菜单

1
2
@IBAction func updateMenuItemSelected(_ sender: NSMenuItem) {
}

然后将新增的菜单和函数进行绑定。找到该菜单右上橘黄色的1图标,它表示First Responder,然后右击,在下拉显示的函数中找到刚刚新增的updateMenuItemSelected,然后鼠标移动到函数右侧圆框,等圆框内显示+,单击+并拖动到新增菜单,即可实现菜单和函数绑定,如下图所示。

绑定菜单和响应方法
绑定菜单和响应方法

获取版本信息

首先需要获取应用的当前版本信息和最新版本信息,两者对比即可明确是否需要更新。获取当前应用的版本非常简单,相关信息都会存储到应用包内的info.plist文件中,开发者可通过主Bundle对象获取当前版本信息,代码如下:

1
2
3
4
@IBAction func updateMenuItemSelected(_ sender: NSMenuItem) {
let infoDict = Bundle.main.infoDictionary
let currentVersion = infoDict!["CFBundleShortVersionString"] as! String
}

应用的最新版本信息需通过发布页面获取,这里以 GitHub 托管网站为例进行说明。在对开源项目配置Releases页面后,开发者可在此发布产品(应用)更新,并对当前发布选择一个tag,一般选取的tag为版本号。而且最新的发布会被置为Latest,这意味着我们可以通过访问https://github.com/UserName/ProjectName/releases/latest查看产品的最新版本,访问该链接时,会自动跳转到https://github.com/UserName/ProjectName/releases/tag/YourLatestTagYourLatestTag即最新的版本号。我们可以据此使用curl -I获取最新的版本信息,命令如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  ~ curl -I https://github.com/ConradSun/NuwaStone/releases/latest
HTTP/1.1 200 Connection established

HTTP/2 302
server: GitHub.com
date: Mon, 17 Jul 2023 07:09:05 GMT
content-type: text/html; charset=utf-8
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, Accept-Encoding, Accept, X-Requested-With
location: https://github.com/ConradSun/NuwaStone/releases/tag/v2.0
cache-control: no-cache
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
......

上面curl -I输出里的location便是跳转后的地址。截取最后一个地址单元,便能获取到最新的版本号,代码如下:

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
@IBAction func updateMenuItemSelected(_ sender: NSMenuItem) {
var latestVersion = String()

guard let result = launchTask(path: "/usr/bin/curl", args: ["-I", "YourUrl"]) else {
return
}

let contentList = result.split(separator: "\r\n")
for contentItem in contentList {
if contentItem.contains("location") {
let tag = contentItem.split(separator: "/").last!
if tag.first == "v" {
Logger(.Info, "The latest version is \(tag).")
latestVersion = tag.dropFirst().lowercased()
}
}
}
}

func launchTask(path: String, args: [String]) -> String? {
let task = Process()
let pipe = Pipe()
task.arguments = args
task.standardOutput = pipe

if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: path)
try? task.run()
} else {
task.launchPath = path
task.launch()
}

var output = Data()
if #available(macOS 10.15.4, *) {
guard let value = try? pipe.fileHandleForReading.readToEnd() else {
return nil
}
output = value
} else {
output = pipe.fileHandleForReading.readDataToEndOfFile()
}

guard let result = String(data: output, encoding: .utf8) else {
return nil
}
return result
}

用户提示弹窗

当前应用的版本号和最新的版本号均获取到了,只需比较两个版本号是否相等,便可知道当前版本是否最新,然后对是否需要更新进行弹窗提示。完整代码如下:

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
@IBAction func updateMenuItemSelected(_ sender: NSMenuItem) {
let infoDict = Bundle.main.infoDictionary
let currentVersion = infoDict!["CFBundleShortVersionString"] as! String
var latestVersion = String()

let infoView = NSAlert()
infoView.alertStyle = .informational

guard let result = launchTask(path: "/usr/bin/curl", args: ["-I", "YourUrl"]) else {
return
}

let contentList = result.split(separator: "\r\n")
for contentItem in contentList {
if contentItem.contains("location") {
let tag = contentItem.split(separator: "/").last!
if tag.first == "v" {
Logger(.Info, "The latest version is \(tag).")
latestVersion = tag.dropFirst().lowercased()
}
}
}

if latestVersion.isEmpty {
infoView.alertStyle = .critical
infoView.messageText = "Error"
infoView.informativeText = "Failed to get latest info for YourAPPName."
} else if latestVersion == currentVersion {
infoView.messageText = "You're up-to-date!"
infoView.informativeText = "YourAPPName \(currentVersion) is the latest version."
} else {
infoView.messageText = "You're out-of-date!"
infoView.informativeText = "Newer version \(currentVersion) is currently avaliable."
infoView.addButton(withTitle: "Ignore")
infoView.addButton(withTitle: "Update")
}
let resp = infoView.runModal()
if resp == .alertSecondButtonReturn {
let siteAddr = URL(string: "YourProductsUrl")
NSWorkspace.shared.open(siteAddr!)
}
}
  • Title: macOS 应用内更新检查
  • Author: 孙康
  • Created at : 2023-07-15 12:00:00
  • Updated at : 2023-08-31 19:56:22
  • Link: https://conradsun.github.io/2023/078d2ace36.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
macOS 应用内更新检查