Skip to content

Commit e82488b

Browse files
committed
✨ check for updates upon app startup
1 parent 03de614 commit e82488b

File tree

4 files changed

+113
-0
lines changed

4 files changed

+113
-0
lines changed

ui/iPortForwarder.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
960312BC2A52E69000B79937 /* ShowErrorDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 960312BB2A52E69000B79937 /* ShowErrorDialog.swift */; };
1111
962F6D502B4FDAD100961137 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962F6D4F2B4FDAD100961137 /* SettingsView.swift */; };
12+
9630309B2D3F85D40030771F /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9630309A2D3F85D00030771F /* AppUpdater.swift */; };
1213
967344D92A1DE03400141A9E /* iPortForwarderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967344D82A1DE03400141A9E /* iPortForwarderApp.swift */; };
1314
967344DB2A1DE03400141A9E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967344DA2A1DE03400141A9E /* ContentView.swift */; };
1415
967344DD2A1DE03800141A9E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 967344DC2A1DE03800141A9E /* Assets.xcassets */; };
@@ -23,6 +24,7 @@
2324
/* Begin PBXFileReference section */
2425
960312BB2A52E69000B79937 /* ShowErrorDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowErrorDialog.swift; sourceTree = "<group>"; };
2526
962F6D4F2B4FDAD100961137 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
27+
9630309A2D3F85D00030771F /* AppUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = "<group>"; };
2628
967344D52A1DE03400141A9E /* iPortForwarder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iPortForwarder.app; sourceTree = BUILT_PRODUCTS_DIR; };
2729
967344D82A1DE03400141A9E /* iPortForwarderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPortForwarderApp.swift; sourceTree = "<group>"; };
2830
967344DA2A1DE03400141A9E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -70,6 +72,7 @@
7072
967344D72A1DE03400141A9E /* iPortForwarder */ = {
7173
isa = PBXGroup;
7274
children = (
75+
9630309A2D3F85D00030771F /* AppUpdater.swift */,
7376
9673452D2A1F8FED00141A9E /* Info.plist */,
7477
967344E92A1DEA1300141A9E /* Views */,
7578
967344D82A1DE03400141A9E /* iPortForwarderApp.swift */,
@@ -200,6 +203,7 @@
200203
96E31ACA2A68EF1A004DAF36 /* iPortForwarderCommands.swift in Sources */,
201204
967344EB2A1DEAFD00141A9E /* ForwardedItemRow.swift in Sources */,
202205
967344E82A1DE8DF00141A9E /* ForwardedItem.swift in Sources */,
206+
9630309B2D3F85D40030771F /* AppUpdater.swift in Sources */,
203207
);
204208
runOnlyForDeploymentPostprocessing = 0;
205209
};

ui/iPortForwarder/AppUpdater.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import AppKit
2+
3+
struct GitHubRelease: Codable {
4+
let tagName: String
5+
let name: String
6+
let body: String
7+
let htmlUrl: String
8+
9+
enum CodingKeys: String, CodingKey {
10+
case tagName = "tag_name"
11+
case name
12+
case body
13+
case htmlUrl = "html_url"
14+
}
15+
}
16+
17+
enum AppUpdater {
18+
static private let owner = "hronro"
19+
static private let repo = "iPortForwarder"
20+
21+
static func checkForUpdates(explicitly: Bool = false) async {
22+
let urlString = "https://api.github.com/repos/\(owner)/\(repo)/releases/latest"
23+
24+
let url = URL(string: urlString)!
25+
26+
let data: Data, response: URLResponse
27+
do {
28+
let r = try await URLSession.shared.data(from: url)
29+
(data, response) = r
30+
} catch {
31+
if explicitly {
32+
await showErrorDialog(error)
33+
}
34+
return
35+
}
36+
37+
guard let status = (response as? HTTPURLResponse)?.statusCode, status == 200 else {
38+
if explicitly {
39+
await showErrorDialog("Failed to obtain information about the latest release.")
40+
}
41+
42+
return
43+
}
44+
45+
let release: GitHubRelease
46+
do {
47+
release = try JSONDecoder().decode(GitHubRelease.self, from: data)
48+
} catch {
49+
if explicitly {
50+
await showErrorDialog("Failed to decode GitHub release JSON.")
51+
await showErrorDialog(error)
52+
}
53+
54+
return
55+
}
56+
let latestVersion = release.tagName.replacingOccurrences(of: "v", with: "")
57+
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
58+
59+
if self.compareVersions(latestVersion, currentVersion) {
60+
await self.presentUpdate(latestVersion)
61+
}
62+
}
63+
64+
private static func compareVersions(_ version1: String, _ version2: String) -> Bool {
65+
let v1Components = version1.split(separator: ".").compactMap { Int($0) }
66+
let v2Components = version2.split(separator: ".").compactMap { Int($0) }
67+
68+
let maxLength = max(v1Components.count, v2Components.count)
69+
70+
for i in 0..<maxLength {
71+
let v1 = i < v1Components.count ? v1Components[i] : 0
72+
let v2 = i < v2Components.count ? v2Components[i] : 0
73+
74+
if v1 > v2 {
75+
return true
76+
} else if v1 < v2 {
77+
return false
78+
}
79+
}
80+
81+
return false
82+
}
83+
84+
@MainActor private static func presentUpdate(_ newVersion: String) {
85+
let alert = NSAlert()
86+
alert.messageText = "iPortForwarder v\(newVersion) is available!"
87+
alert.addButton(withTitle: "View Release Page")
88+
alert.addButton(withTitle: "Not Now")
89+
switch alert.runModal() {
90+
case .alertFirstButtonReturn:
91+
NSWorkspace.shared.open(URL(string: "https://github.com/\(owner)/\(repo)/releases")!)
92+
default:
93+
break
94+
}
95+
}
96+
}

ui/iPortForwarder/ShowErrorDialog.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@ import Libipf
1515
alert.addButton(withTitle: "OK")
1616
alert.runModal()
1717
}
18+
19+
@MainActor func showErrorDialog(_ errorText: String) {
20+
let alert = NSAlert()
21+
alert.messageText = "Error"
22+
alert.informativeText = errorText
23+
alert.alertStyle = NSAlert.Style.critical
24+
alert.addButton(withTitle: "OK")
25+
alert.runModal()
26+
}

ui/iPortForwarder/iPortForwarderApp.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ struct iPortForwarderApp: App {
3030
}
3131
}
3232
}
33+
34+
Task.detached {
35+
await AppUpdater.checkForUpdates()
36+
}
3337
}
3438

3539
var body: some Scene {

0 commit comments

Comments
 (0)