Keeping your users informed about the latest version of your application is important for enhancing the user experience. In this post I will outline how I implemented a feature to notify the user of the latest update for you app, providing a link to download it.
It was a “rush” feature, I worked on this for less than two hours, so it’s not perfect by any means, but it’s serviceable. The latest version will be sourced from GitHub releases.
Connecting to the API
The first step I had to take was retrieving the latest version of the app from the GitHub’s API.
To accomplish this, I designed a function that accepts a closure to forward its output. Using a closure provides flexibility in handling the result, whether it’s a successful version retrieval or an error. This design pattern is particularly useful in asynchronous programming, as it allows the application to remain responsive while waiting for the network call to complete.
The GitHub API returns a JSON list of the releases of our application. Each release contains various pieces of information, but for our purpose, I only needed the name of the latest version. This simplifies the data I need to handle.
1import Foundation
2
3struct GithubVersion: Decodable {
4 let name: String
5}
This struct conforms to the Decodable protocol, allowing it to be easily
parsed from the JSON response.
1import Foundation
2
3enum GithubVersionRepositoryError: Error {
4 case noData
5 case noVersionFound
6}
7
8let GITHUB_API_URL: URL = URL(string: "https://api.github.com/repos/")!
9
10struct GithubVersionRepository {
11 static let shared = GithubVersionRepository()
12 func getVersion(completion: @escaping (Result<String, any Error>) -> Void) {
13 URLSession.shared.dataTask(with: getReleasesUrl(repoUrl: "eliseomartelli/Cleeb")) {
14 data, response, error in
15 if let error = error {
16 completion(.failure(error))
17 return
18 }
19 guard data != nil else {
20 completion(.failure(GithubVersionRepositoryError.noData))
21 return
22 }
23 do {
24 let versions = try JSONDecoder().decode([GithubVersion].self, from: data!)
25 guard let latestVersion = versions.first else {
26 completion(.failure(GithubVersionRepositoryError.noVersionFound))
27 return
28 }
29 completion(.success(latestVersion.name))
30 return
31 } catch {
32 completion(.failure(error))
33 return
34 }
35 }.resume()
36 }
37
38 // Obtains the repo URL endpoint for releases.
39 private func getReleasesUrl(repoUrl : String) -> URL {
40 return GITHUB_API_URL
41 .appendingPathComponent(repoUrl)
42 .appendingPathComponent("releases")
43 }
44}
In the getVersion function, I initiate a data task with URLSession to call
the GitHub releases endpoint for my application. If there’s an error during the
request or if no data is returned, the closure is invoked with an appropriate
error message. If the data is successfully retrieved, I decode it into an array
of GithubVersion instances. I extract the latest version name and
call the completion closure with it.
Making it work
To effectively manage and display version information, I created a
VersionViewModel. This class conforms to ObservableObject protocol,
allowing it to notify SwiftUI for any changes.
1import Foundation
2
3class VersionViewModel: ObservableObject {
4 @Published var latestVersion: String = Bundle.main.buildVersion!
5 @Published var errorMessage: String?
6 @Published var isUpdateAvailable: Bool = false
7
8 init() {
9 self.fetchLatestVersion()
10 }
11
12 private func fetchLatestVersion() {
13 GithubVersionRepository.shared.getVersion { result in
14 DispatchQueue.main.async {
15 switch result {
16 case .success(let version):
17 self.latestVersion = version
18 case .failure(let error):
19 self.errorMessage = error.localizedDescription
20 }
21 self.isUpdateAvailable = Bundle.main.buildVersion != self.latestVersion
22 }
23 }
24 }
25}
Updating the UI
1import SwiftUI
2
3struct MainView: View {
4 @EnvironmentObject var versionViewModel: VersionViewModel
5
6 var body: some View {
7 VStack {
8 if versionViewModel.isUpdateAvailable {
9 Button(action: openDownloadPage) {
10 Text(version)
11 .font(.caption)
12 .background(Color.blue)
13 .foregroundColor(.white)
14 .clipShape(Capsule())
15 }
16 .buttonStyle(PlainButtonStyle())
17 }
18 }
19 }
20
21 private func openDownloadPage() {
22 if let url = URL(string: "https://github.com/eliseomartelli/Cleeb/releases") {
23 NSWorkspace.shared.open(url)
24 }
25 }
26}
Integrate Everything
Integrate the VersionViewModel into your app structure to allow your views to
access the latest version data.
1@main
2struct YourApp: App {
3 @StateObject private var versionViewModel = VersionViewModel()
4
5 var body: some Scene {
6 Window("Your App", id: "yourapp") {
7 MainView()
8 .environmentObject(versionViewModel)
9 }
10 }
11}
Now I can notify users of updates to my application!
If you found this interesting, check out Cleeb, the app I made to help you clean your macOS laptop keyboard!