Skip to content

fix: notify on new Xcode version identity, not array-count growth - #842

Open
YuriNachos wants to merge 1 commit into
XcodesOrg:mainfrom
YuriNachos:YuriNachos/w9-xcodes
Open

fix: notify on new Xcode version identity, not array-count growth#842
YuriNachos wants to merge 1 commit into
XcodesOrg:mainfrom
YuriNachos:YuriNachos/w9-xcodes

Conversation

@YuriNachos

Copy link
Copy Markdown

Problem

The "New Xcode version available" notification fired whenever the availableXcodes array count grew (newValue.count > availableXcodes.count), not when a genuinely new version identity appeared. This caused two user-visible bugs:

  • False negative — when a new version appears in the same refresh that an old one is removed (e.g. a data source drops an obsolete beta row the moment it adds the new release), the count is unchanged, so the user is never told a new Xcode landed (the core reason to run the app).
  • False positive — a data-source switch or a duplicate row grows the count without a new version identity, firing a spurious banner.

Solution

Extract a pure helper that compares version identity, not array length:

static func newlyAvailableXcodes(oldXcodes: [AvailableXcode], newXcodes: [AvailableXcode]) -> [AvailableXcode] {
    guard !oldXcodes.isEmpty else { return [] }
    let oldIDs = Set(oldXcodes.map(\.xcodeID))
    return newXcodes.filter { !oldIDs.contains($0.xcodeID) }
}

availableXcodes.willSet now notifies only when the helper's result is non-empty:

if !Self.newlyAvailableXcodes(oldXcodes: availableXcodes, newXcodes: newValue).isEmpty { ... }
  • AvailableXcode.xcodeID (already Hashable) is the identity, so the same version with a different architecture correctly stays distinct and does NOT re-notify.
  • The initial load (empty → populated) is suppressed: oldXcodes.isEmpty[], so first-run does not banner every Xcode.
  • No Localizable.xcstrings change (title/body keys unchanged).

Testing

New XcodesTests/NewVersionNotificationTests.swift (own file to avoid the AppStateTests.swift conflict with PR #840) asserts directly on the pure helper:

  • initial-load suppression (empty → populated = []);
  • true positive (one genuinely new version);
  • false negative (new added + old removed, count unchanged → the new version IS reported);
  • false-negative variant (new appears while list shrinks);
  • false positive ([A][A, A][], no spurious notify);
  • identical list (no notify);
  • architecture-distinct same version (no re-notify).

Real red-before-green: on main's count predicate, the equal-count cases (new+removed) notify nothing, so the test expecting the new version's identity fails; after the fix it passes.

xcodebuild test -scheme Xcodes -destination 'platform=macOS' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -only-testing:XcodesTests/NewVersionNotificationTestsTEST SUCCEEDED.

Checklist


Authored by @YuriNachos. Implementation written by a cccc (Claude Code, GLM-5.2) worker under orchestrator acceptance; an independent adversarial review returned APPROVE with no blocking findings.

The "New Xcode version available" notification was triggered by an
array-count increase rather than by the appearance of a genuinely new
version identity (AvailableXcode.xcodeID). This caused two user-visible
bugs:

- False negative: when a new version is added and an old one removed in
  the same refresh (count unchanged, or even shrunk), no notification
  fired, so a genuinely new version landed silently.
- False positive: when the array grew without a new identity (a duplicate
  row, or a data-source switch returning an already-known xcodeID), a
  spurious "new version" banner appeared.

Extract the decision into a pure static helper
AppState.newlyAvailableXcodes(oldXcodes:newXcodes:) that computes the
xcodeID set difference, and notify iff it is non-empty. The empty-old
guard is preserved so the initial cache load (empty -> populated) does
not notify; the scheduleNotification arguments are unchanged.

Adds XcodesTests/NewVersionNotificationTests.swift with a red-before-green
regression suite (the false-negative and false-positive cases fail against
a buggy count-based mirror and pass against the identity-based body).

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant