Skip to content

Commit 0bf75d0

Browse files
committed
feat: Enhance update feedback in DeveloperUpdateButton with last check result display
1 parent 831fe90 commit 0bf75d0

File tree

2 files changed

+93
-23
lines changed

2 files changed

+93
-23
lines changed

Sources/ClickIt/Core/Update/UpdaterManager.swift

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ class UpdaterManager: NSObject, ObservableObject {
1313
@Published var isCheckingForUpdates: Bool = false
1414
@Published var lastUpdateCheck: Date?
1515
@Published var updateError: String?
16+
@Published var lastCheckResult: String?
1617

1718
// Note: Direct SUAppcastItem storage omitted due to sendability constraints
1819

1920
// MARK: - Private Properties
20-
private let updaterController: SPUStandardUpdaterController
21+
private var updaterController: SPUStandardUpdaterController
2122
private let userDefaults = UserDefaults.standard
2223

2324
// MARK: - Settings
@@ -39,15 +40,30 @@ class UpdaterManager: NSObject, ObservableObject {
3940

4041
// MARK: - Initialization
4142
override init() {
42-
// Initialize Sparkle updater controller
43+
// Initialize without starting
4344
self.updaterController = SPUStandardUpdaterController(
44-
startingUpdater: true,
45+
startingUpdater: false,
4546
updaterDelegate: nil,
4647
userDriverDelegate: nil
4748
)
4849

4950
super.init()
5051

52+
// Only start updater if not in manual-only mode
53+
let shouldStartUpdater: Bool
54+
#if DEBUG
55+
shouldStartUpdater = !AppConstants.DeveloperUpdateConfig.manualCheckOnly
56+
#else
57+
shouldStartUpdater = true
58+
#endif
59+
60+
// Recreate with self as delegate after super.init()
61+
self.updaterController = SPUStandardUpdaterController(
62+
startingUpdater: shouldStartUpdater,
63+
updaterDelegate: self,
64+
userDriverDelegate: nil
65+
)
66+
5167
setupUpdater()
5268
configureAutomaticChecks()
5369
}
@@ -58,16 +74,23 @@ class UpdaterManager: NSObject, ObservableObject {
5874
func checkForUpdates() {
5975
guard !isCheckingForUpdates else { return }
6076

77+
print("🔄 Starting manual update check...")
6178
isCheckingForUpdates = true
6279
updateError = nil
80+
lastCheckResult = nil
6381

6482
updaterController.updater.checkForUpdates()
6583
userDefaults.set(Date(), forKey: AppConstants.lastUpdateCheckKey)
6684
lastUpdateCheck = Date()
6785

6886
// Reset checking state after a timeout to handle cases where delegate isn't called
69-
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
70-
self.isCheckingForUpdates = false
87+
// This handles scenarios like empty appcast feeds
88+
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
89+
if self.isCheckingForUpdates {
90+
self.isCheckingForUpdates = false
91+
self.lastCheckResult = "Current version \(self.currentVersionDetailed) is up to date"
92+
print("⏰ Update check timed out - assuming up to date")
93+
}
7194
}
7295
}
7396

@@ -97,6 +120,13 @@ class UpdaterManager: NSObject, ObservableObject {
97120
return AppConstants.appVersion
98121
}
99122

123+
/// Get the current app version with build number
124+
var currentVersionDetailed: String {
125+
let version = AppConstants.appVersion
126+
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
127+
return version != buildNumber ? "\(version) (\(buildNumber))" : version
128+
}
129+
100130
/// Get time since last update check
101131
var timeSinceLastCheck: TimeInterval? {
102132
guard let lastCheck = userDefaults.object(forKey: AppConstants.lastUpdateCheckKey) as? Date else {
@@ -108,18 +138,22 @@ class UpdaterManager: NSObject, ObservableObject {
108138
// MARK: - Private Methods
109139

110140
private func setupUpdater() {
111-
// Configure updater settings
112-
updaterController.updater.automaticallyChecksForUpdates = autoUpdateEnabled
141+
// Configure updater settings (will be set by configureAutomaticChecks)
113142
updaterController.updater.updateCheckInterval = AppConstants.updateCheckInterval
114143

115-
// Set appcast URL if not configured in Info.plist
116-
if updaterController.updater.feedURL == nil {
117-
updaterController.updater.feedURL = URL(string: AppConstants.appcastURL)
118-
}
144+
// Log feed URL configuration (will be provided by delegate)
145+
print("✅ Sparkle configured with delegate feed URL: \(AppConstants.appcastURL)")
119146

120147
// Set initial defaults if not set
121148
if !userDefaults.bool(forKey: "hasSetDefaultUpdateSettings") {
122-
userDefaults.set(true, forKey: AppConstants.autoUpdateEnabledKey)
149+
// In manual-only mode, disable auto-updates by default
150+
#if DEBUG
151+
let defaultAutoUpdate = !AppConstants.DeveloperUpdateConfig.manualCheckOnly
152+
#else
153+
let defaultAutoUpdate = true
154+
#endif
155+
156+
userDefaults.set(defaultAutoUpdate, forKey: AppConstants.autoUpdateEnabledKey)
123157
userDefaults.set(false, forKey: AppConstants.checkForBetaUpdatesKey)
124158
userDefaults.set(true, forKey: "hasSetDefaultUpdateSettings")
125159
}
@@ -131,7 +165,15 @@ class UpdaterManager: NSObject, ObservableObject {
131165
}
132166

133167
private func configureAutomaticChecks() {
134-
updaterController.updater.automaticallyChecksForUpdates = autoUpdateEnabled
168+
// Disable automatic checks in manual-only mode (debug builds)
169+
let enableAutomaticChecks: Bool
170+
#if DEBUG
171+
enableAutomaticChecks = !AppConstants.DeveloperUpdateConfig.manualCheckOnly && autoUpdateEnabled
172+
#else
173+
enableAutomaticChecks = autoUpdateEnabled
174+
#endif
175+
176+
updaterController.updater.automaticallyChecksForUpdates = enableAutomaticChecks
135177
updaterController.updater.updateCheckInterval = AppConstants.updateCheckInterval
136178
}
137179

@@ -143,13 +185,39 @@ class UpdaterManager: NSObject, ObservableObject {
143185

144186
extension UpdaterManager: SPUUpdaterDelegate {
145187

188+
/// Provide the feed URL if not configured in Info.plist
189+
nonisolated func feedURLString(for updater: SPUUpdater) -> String? {
190+
print("🔍 Sparkle requesting feed URL: \(AppConstants.appcastURL)")
191+
return AppConstants.appcastURL
192+
}
193+
194+
/// Called when update check begins
195+
nonisolated func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
196+
print("📦 Sparkle will install update: \(item.displayVersionString)")
197+
}
198+
199+
/// Called when update check starts
200+
nonisolated func updater(_ updater: SPUUpdater, userDidSkipThisVersion item: SUAppcastItem) {
201+
print("⏭️ User skipped version: \(item.displayVersionString)")
202+
}
203+
204+
/// Called when appcast download finishes
205+
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
206+
print("📥 Appcast loaded with \(appcast.items.count) items")
207+
if appcast.items.isEmpty {
208+
print("⚠️ Empty appcast - no releases available")
209+
}
210+
}
211+
146212
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
213+
print("✅ Sparkle: No updates found")
147214
DispatchQueue.main.async {
148215
self.isCheckingForUpdates = false
149216
self.isUpdateAvailable = false
150217
self.updateVersion = nil
151218
self.updateBuildNumber = nil
152219
self.updateReleaseNotes = nil
220+
self.lastCheckResult = "Current version \(self.currentVersionDetailed) is up to date"
153221
}
154222
}
155223

@@ -158,21 +226,26 @@ extension UpdaterManager: SPUUpdaterDelegate {
158226
let version = item.displayVersionString
159227
let buildNumber = item.versionString
160228
let releaseNotesURL = item.releaseNotesURL?.absoluteString
229+
let currentVersion = AppConstants.appVersion
161230

231+
print("🆕 Sparkle: Found update \(version)")
162232
DispatchQueue.main.async {
163233
self.isCheckingForUpdates = false
164234
self.isUpdateAvailable = true
165235
self.updateVersion = version
166236
self.updateBuildNumber = buildNumber
167237
self.updateReleaseNotes = releaseNotesURL
238+
self.lastCheckResult = "Update available: \(self.currentVersionDetailed)\(version)"
168239
// Note: currentUpdateItem is omitted due to sendability constraints
169240
}
170241
}
171242

172243
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
244+
print("❌ Sparkle error: \(error.localizedDescription)")
173245
DispatchQueue.main.async {
174246
self.isCheckingForUpdates = false
175247
self.updateError = error.localizedDescription
248+
self.lastCheckResult = "Failed to check for updates: \(error.localizedDescription)"
176249
}
177250
}
178251
}
@@ -208,4 +281,4 @@ extension UpdaterManager {
208281
var isCurrentVersionBeta: Bool {
209282
return currentVersion.contains("beta") || currentVersion.contains("rc")
210283
}
211-
}
284+
}

Sources/ClickIt/UI/Components/DeveloperUpdateButton.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,17 @@ struct DeveloperUpdateButton: View {
3131
.font(.caption)
3232
.foregroundColor(.secondary)
3333
}
34+
} else if let result = updaterManager.lastCheckResult {
35+
Text(result)
36+
.font(.caption)
37+
.foregroundColor(updaterManager.isUpdateAvailable ? .green :
38+
updaterManager.updateError != nil ? .red : .secondary)
39+
.multilineTextAlignment(.center)
3440
} else if let error = updaterManager.updateError {
3541
Text("Error: \(error)")
3642
.font(.caption)
3743
.foregroundColor(.red)
3844
.multilineTextAlignment(.center)
39-
} else if updaterManager.isUpdateAvailable {
40-
Text("Update available: \(updaterManager.formatVersionInfo())")
41-
.font(.caption)
42-
.foregroundColor(.green)
43-
.multilineTextAlignment(.center)
44-
} else if updaterManager.lastUpdateCheck != nil {
45-
Text("No updates available")
46-
.font(.caption)
47-
.foregroundColor(.secondary)
4845
}
4946
}
5047
.padding(12)

0 commit comments

Comments
 (0)