@@ -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
144186extension 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+ }
0 commit comments