Skip to content

Modernize: Swift 6 language mode, Xcode 26 settings, Sparkle 2.9.6, CI build workflow - #981

Open
keithadler wants to merge 4 commits into
jordanbaird:mainfrom
keithadler:modernize
Open

keithadler wants to merge 4 commits into
jordanbaird:mainfrom
keithadler:modernize

Conversation

@keithadler

Copy link
Copy Markdown

Summary

Brings the project up to the current toolchain: Swift 6 language mode, Xcode 26 recommended settings, a current Sparkle, and CI that actually catches problems.

  • Swift 6 language mode with strict concurrency checking, plus the MemberImportVisibility upcoming feature. Debug and Release both build with zero warnings.
  • SwiftLint: fixes the 9 violations that SwiftLint 0.65 reports, and pins the CI lint job to the official ghcr.io/realm/swiftlint:0.65.1 image. The previous third-party action bundled an old SwiftLint, so these violations were never reported on PRs.
  • Build workflow: compiles the Release configuration on macos-26 for every push and PR, and uploads the unsigned app as an artifact so changes can be tried without a local build.
  • Sparkle 2.6.4 → 2.9.6 (already allowed by the version requirement; only the pin moves).
  • Handles the .extraLarge control size in CustomColorPicker, which was the only compiler warning before this change.

How the concurrency migration was approached

Nearly all of the Swift 6 diagnostics were about main-thread-only code that had no isolation annotation. The changes make those assumptions explicit rather than changing where code runs:

  • @MainActor on main-thread-only helpers: HotkeyRegistry, IceBarColorManager, MenuBarItemImageCache, the color picker and window reader coordinators, BindingExposable, SystemAppearance.current, NSStatusItem.showMenu.
  • C callbacks that Core Graphics and Carbon deliver on the main thread (event taps, run loop observers, hotkey handlers) are bridged into the main actor with MainActor.assumeIsolated. Each site has a comment explaining why that is sound.
  • Unsynchronized global mutable state is replaced: HotkeyRegistry uses an instance counter, ScreenCapture.cachedCheckPermissions uses an OSAllocatedUnfairLock, and MenuBarManager owns its own SystemWideElement instead of using AXSwift's global var.
  • MenuBarItemImageCache is now @MainActor, but the actual screen capture stays off the main thread: it moved into a nonisolated static function that takes only Sendable inputs. The cache's images, screen and menuBarHeight are now only mutated on the main actor (previously screen/menuBarHeight were written from a background task).
  • MenuBarItemSpacingManager.applyOffset collects relaunch failures through task group results instead of a captured array.
  • HotkeyRegistry.register takes a KeyCombination instead of a Hotkey; it only ever read the key combination.

Two deliberate behavior notes:

  1. RunLoopLocalEventMonitor now always observes the main run loop instead of the current one. It drives NSApp.nextEvent / NSApp.postEvent, which are only valid on the main thread, so observing any other run loop could never have worked.
  2. SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor (Xcode 26's "approachable concurrency" default) was evaluated and not enabled. With it, the whole project compiles with only two errors, but it silently moves the menu bar item image capture onto the main thread. Explicit isolation keeps the existing threading and documents it.

Test plan

  • xcodebuild Debug and Release: zero warnings, zero errors (Xcode 26.6)
  • swiftlint --strict with SwiftLint 0.65.1: clean
  • Launched the Release build locally; runs without trapping (all assumeIsolated sites are exercised at startup by the event taps, hotkeys and image cache)
  • Reviewer: exercise hotkeys, the Ice Bar, menu bar search, item spacing and the appearance editor on a real setup

Commits are split by concern (lint fixes, Swift 6, Sparkle, CI) so any one of them can be dropped independently.

🤖 Generated with Claude Code

keithadler and others added 4 commits September 2, 2026 22:47
Replace `aspectRatio(contentMode:)` with `scaledToFit()`/`scaledToFill()`
(legacy_swiftui_aspect_ratio) and drop two `swiftlint:disable` comments that
no longer suppress anything, since `URL(string:)!` with a literal is no
longer flagged by `force_unwrapping`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Switch the target to the Swift 6 language mode (strict concurrency checking)
and enable the MemberImportVisibility upcoming feature, matching the settings
Xcode 26 recommends. The project builds with zero warnings in both Debug and
Release.

Most changes make existing main-thread assumptions explicit rather than
changing behavior:

- Annotate main-thread-only helpers (`HotkeyRegistry`, `IceBarColorManager`,
  `MenuBarItemImageCache`, the color picker and window reader coordinators,
  `BindingExposable`, `SystemAppearance.current`, `NSStatusItem.showMenu`)
  with `@MainActor`.
- Bridge C callbacks that Core Graphics and Carbon deliver on the main thread
  (event taps, run loop observers, hotkey handlers) into the main actor with
  `MainActor.assumeIsolated`, documenting why that is sound at each site.
- Replace unsynchronized global mutable state with an instance property
  (`HotkeyRegistry.nextID`), a lock (`ScreenCapture.cachedCheckPermissions`)
  and an instance-owned `SystemWideElement` instead of AXSwift's global.
- Keep the image capture in `MenuBarItemImageCache` off the main thread by
  moving it into a `nonisolated` static function that takes only `Sendable`
  inputs, so the cache's state is now mutated exclusively on the main actor.
- Collect relaunch failures in `MenuBarItemSpacingManager` through task group
  results instead of a captured array.
- `RunLoopLocalEventMonitor` now always observes the main run loop; it drives
  `NSApp.nextEvent` and `NSApp.postEvent`, which are only valid there.
- Add the imports that MemberImportVisibility requires, and handle the
  `.extraLarge` control size in `CustomColorPicker`.

Default main-actor isolation (`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`)
was evaluated and deliberately not enabled: it would silently move the menu
bar item image capture onto the main thread.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The version requirement already allowed it; this only moves the pin.
`SPUUpdaterDelegate` is now annotated for concurrency upstream, so the
`@preconcurrency` on the conformance is no longer needed (and warns).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The lint workflow used a third-party action bundling an old SwiftLint, which
let violations that current releases report slip through. Run the official
SwiftLint container image instead, pinned to 0.65.1.

Add a build workflow that compiles the Release configuration with Xcode 26 on
every push and pull request, and uploads the unsigned app as an artifact so
changes can be tried without a local build.

Co-Authored-By: Claude Fable 5.1 <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