This project is a SwiftPM macOS app. swift run Switcher is useful for local development, but a distributable release should be packaged as a signed .app bundle and notarized before sharing.
- Full Xcode with Swift 6.2 or newer.
- An Apple Developer account with a Developer ID Application certificate.
- Accessibility permission tested on the target macOS version.
- A clean changelog entry in
CHANGELOG.md.
Verify the active toolchain:
xcode-select -p
xcodebuild -version
swift --versionIf Xcode is not selected:
sudo xcode-select -s /Applications/Xcode.app/Contents/DeveloperCreate a release executable:
swift build -c releaseThe executable is written under:
swift build -c release --show-bin-pathThe repository does not currently define an Xcode archive target, so create a .app bundle from the release executable when preparing a distributable artifact:
APP_NAME=Switcher
BUNDLE_ID=com.kshitiz.Switcher
VERSION=1.1.0
BUILD_DIR="$(swift build -c release --show-bin-path)"
APP_DIR=".build/release/${APP_NAME}.app"
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
cp "$BUILD_DIR/$APP_NAME" "$APP_DIR/Contents/MacOS/$APP_NAME"
cat > "$APP_DIR/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
PLISTUse the final bundle identifier chosen for the release channel. Keep it stable between releases so macOS permissions and login-item state remain predictable.
Unsigned development builds can run locally, but launch-at-login and Gatekeeper behavior are not representative until the app is signed and notarized.
Sign the bundle with a Developer ID Application identity:
codesign --force --deep --options runtime \
--sign "Developer ID Application: Your Name (TEAMID)" \
".build/release/Switcher.app"Validate the signature:
codesign --verify --deep --strict --verbose=2 ".build/release/Switcher.app"
spctl --assess --type execute --verbose ".build/release/Switcher.app"Create a zip for notarization:
ditto -c -k --keepParent ".build/release/Switcher.app" ".build/release/Switcher.zip"Submit and staple:
xcrun notarytool submit ".build/release/Switcher.zip" \
--keychain-profile "SwitcherNotaryProfile" \
--wait
xcrun stapler staple ".build/release/Switcher.app"Validate the stapled app:
spctl --assess --type execute --verbose ".build/release/Switcher.app"- Update
AppVersion.current. - Update
CHANGELOG.md. - Run
swift testwith full Xcode selected. - Run
swift build -c release. - Package the
.app. - Sign and notarize the
.app. - Manually test launch, Accessibility permission flow, pinning, resizing, launch-at-login, global hotkey, multiple Spaces, and at least one multi-display setup.
- Tag the release and attach the notarized artifact.