Conversation
Add @published mirrors of SPUUpdater settings so the Settings UI can bind to them via SwiftUI: - automaticallyChecksForUpdates (read-only @published; backed by KVO) - automaticallyDownloadsUpdates (read-only @published; backed by KVO) - canCheckForUpdates (read-only @published; backed by KVO) - pendingReleaseNotesURL (read/write @published; captured from SUAppcastItem.releaseNotesURL) The mirrors are populated by KVO observers in setupObservers(), which is called after updater.start() so the .initial flag pulls the user's saved NSUserDefaults preference from the previous session. This means the Settings toggle starts in the right state on every launch without us having to read the value ourselves at init. Two new setters, setAutomaticallyChecksForUpdates() and setAutomaticallyDownloadsUpdates(), are the only path the UI uses to change Sparkle settings. They write through SPUUpdater, which is what persists the change to NSUserDefaults — the Sparkle docs require this ("Only set this property if the user wants to change the default via a user settings option"). Writing through the @published mirror would silently drop the change on next launch. openReleaseNotes() opens the cached URL via NSWorkspace.shared so the "View changelog" link in Settings works even when the Sparkle UI was dismissed (i.e. when the user has auto-update disabled). pendingReleaseNotesURL is captured in didFindValidUpdate and cleared in updaterDidNotFindUpdate and the no-update branch of didAbortWithError, so it never points at a stale release-notes URL after a successful "no updates" check. import AppKit added for NSWorkspace.
SilentUserDriver currently responds .install to every showUpdateFound
callback, meaning Dayflow installs every Sparkle update without any
user interaction. With the new Settings toggle, we want the driver to
honor the user's preference:
- if automaticallyChecksForUpdates == true: reply .install
(the existing silent-install behavior — used when the user has
opted into auto-update)
- if automaticallyChecksForUpdates == false: reply .dismiss
(keep the update cached, surface the release-notes URL via the
"View changelog" link in Settings, but don't push the install)
The automaticallyChecksForUpdates property is mirrored from
SPUUpdater.automaticallyChecksForUpdates. The mirror is kept in sync
in two places: the KVO observer in UpdaterManager.setupObservers()
(picks up Sparkle-internal changes, e.g. the 2nd-launch permission
prompt) and the user-facing setters in UpdaterManager
(picks up user-initiated changes immediately).
The KVO observer change in UpdaterManager is a behavioral change for
SilentUserDriver, so it's bundled into this commit rather than the
first one — both changes are required for the toggle to actually
take effect.
Wires UpdaterManager into SettingsOtherTabView as a new "Updates"
section. Follows the same pattern as the existing toggles in the
"App preferences" section: SettingsRow + SettingsToggle bound via a
custom Binding(get:set:) that delegates to UpdaterManager.
Layout (top to bottom):
- Toggle: "Automatically check for updates"
Binds to updaterManager.automaticallyChecksForUpdates via
setAutomaticallyChecksForUpdates(). When off, Dayflow only checks
for updates when the user clicks "Check now".
- Sub-toggle: "Automatically download and install updates"
Binds to updaterManager.automaticallyDownloadsUpdates. Disabled
when the auto-check toggle is off (mirroring the Sparkle reference
UI in the official docs).
- Button: "Check now"
Calls updaterManager.checkForUpdates(showUI: true), which routes
through the SPUStandardUpdaterController (not SilentUserDriver) so
the user always gets the standard release-notes UI even when
auto-update is off. Disabled while isChecking or while
canCheckForUpdates is false. Replaced with a ProgressView while
isChecking.
- Status subtitle
Shows updaterManager.statusText — empty unless a check is
in progress ("Checking…"), just completed ("Latest version"
or "Update available: vX.Y.Z"), or errored ("Update needs
authorization" / "Update check failed").
- Conditional row: "Latest release notes"
Only rendered when pendingReleaseNotesURL is non-nil, i.e. an
update has been found and its feed entry included a notes URL.
Subtitle is the URL host so the user can see where the link
points before clicking. Tapping "View changelog" calls
updaterManager.openReleaseNotes(), which opens the URL in the
user's default browser via NSWorkspace.
Tested: builds clean on Dayflow v2.1.0 (Xcode 26.6, arm64).
6 tasks
Author
|
Closing in favor of #347 — the auto-update toggle commits were cherry-picked into the fix branch so the new feature ships alongside the other v2.1.0 fixes in a single PR. Same 3 commits (cherry-pick of |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Dayflow currently ships with
SilentUserDriverconfigured so Sparklesilently auto-installs every update — no prompt, no opt-out, no
changelog. Combined with
SUAutomaticallyUpdate = truein Info.plist,the user has zero control over when the app upgrades itself.
This PR adds a user-controllable update preference, following the
pattern of the other toggles in the Settings "App preferences"
section: a
SettingsTogglebound to an@Publishedmirror of aSPUUpdaterproperty, written back through an explicit setter(the only path the UI uses — see the Sparkle docs note below).
What changes
3 commits, all on
feature/auto-update-toggle(branched frommain @ 8944d27):1.
feat(updater): expose Sparkle update controls via UpdaterManager@Published private(set) var automaticallyChecksForUpdates(mirror of
SPUUpdater.automaticallyChecksForUpdates)@Published private(set) var automaticallyDownloadsUpdates@Published private(set) var canCheckForUpdates@Published var pendingReleaseNotesURL: URL?— captured fromSUAppcastItem.releaseNotesURLindidFindValidUpdatesetupObservers(),called after
updater.start()so the.initialflag pulls theuser's saved NSUserDefaults preference from the previous session
setAutomaticallyChecksForUpdates(_:)andsetAutomaticallyDownloadsUpdates(_:)are the only path the UIuses to mutate Sparkle settings. They write through
SPUUpdater,which is what persists the change — the Sparkle docs are explicit
that you should only set these properties in response to a user
setting change
openReleaseNotes()opens the cached URL viaNSWorkspace.sharedso the "View changelog" link works even when the Sparkle UI was
dismissed
2.
fix(updater): respect user update preference in SilentUserDrivervar automaticallyChecksForUpdates: Bool = trueto
SilentUserDriver, mirrored fromSPUUpdatershowUpdateFoundnow branches on this flag:true→.install(existing silent-install behavior)false→.dismiss(keeps the update cached, surfaces therelease-notes URL via Settings, doesn't push the install)
UpdaterManager.setupObservers()is updatedto write the new value to
SilentUserDriver.automaticallyChecksForUpdateson every change, so both Sparkle-internal updates (e.g. the
2nd-launch permission prompt) and user toggle changes take effect
on the very next update check
3.
feat(settings): add update controls to Other tabSettingsOtherTabView, between"App preferences" and "Output language"
only checks when the user clicks "Check now"
disabled when the auto-check toggle is off, matching the
Sparkle reference UI from the official docs
updaterManager.checkForUpdates(showUI: true),which routes through the
SPUStandardUpdaterController(notSilentUserDriver) so the user always gets the standardrelease-notes UI even when auto-update is off. Replaced with a
ProgressViewwhile the check is in flightupdaterManager.statusText(e.g."Latest version" / "Update available: v2.2.0" / "Checking…")
pendingReleaseNotesURLis non-nil. Subtitle is the URL hostso the user sees where the link points. "View changelog" button
calls
updaterManager.openReleaseNotes()Test plan
NSUserDefaults storage)
(no regression)
Sparkle update UI with release notes
Notes
Info.plistkeys (SUEnableAutomaticChecks,SUAutomaticallyUpdate,SUScheduledCheckInterval) are unchanged. The user preference isapplied at runtime via the
SPUUpdatersetters, which is thepattern the Sparkle docs recommend
SilentUserDriverwithSPUStandardUserDriveroutright, but kept the silent driver sousers who never visit Settings keep the existing behavior
(and so the diff is smaller / easier to review)
pendingReleaseNotesURLis cleared inupdaterDidNotFindUpdateand the no-update branch of
didAbortWithErrorso it never pointsat a stale URL after a "no updates" check