Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
f347bea
video checkpoint
cwbooth5 Feb 2, 2026
7ac7bdf
fix(a11y): add accessibility labels to all camera controls
cwbooth5 May 26, 2026
3f2a12c
fix(a11y): correct camera switch and gallery accessibility labels per…
cwbooth5 May 26, 2026
72c9b52
fix(a11y): add accessibility labels to PIN entry screens
cwbooth5 May 26, 2026
573a054
fix(a11y): add accessibility labels to gallery cells and actions
cwbooth5 May 26, 2026
f32c82b
fix(a11y): hide decorative icons from VoiceOver in security overlays …
cwbooth5 May 26, 2026
43928e7
fix(a11y): replace hardcoded icon font sizes with .title3 in photo tools
cwbooth5 May 26, 2026
6c19871
fix(a11y): replace hardcoded font sizes with Dynamic Type styles in s…
cwbooth5 May 26, 2026
b60fef3
fix(a11y): replace hardcoded font sizes in PIN and onboarding screens
cwbooth5 May 26, 2026
62d6fe7
fix(a11y): replace remaining hardcoded font sizes with Dynamic Type s…
cwbooth5 May 26, 2026
387a4b5
fix(a11y): replace remaining camera and detail hardcoded font sizes
cwbooth5 May 26, 2026
ba71a60
fix(ux): add haptic feedback to shutter, recording, and PIN entry
cwbooth5 May 26, 2026
5fecb68
fix(swiftui): replace foregroundColor→foregroundStyle and cornerRadiu…
cwbooth5 May 26, 2026
939ce1f
fix(swiftui): replace deprecated NavigationView with NavigationStack
cwbooth5 May 26, 2026
a65b420
fix(swiftui): replace UIImpactFeedbackGenerator with .sensoryFeedback…
cwbooth5 May 26, 2026
6ac95df
fix(nav): remove nested NavigationStack from wizard, ImageInfo, and P…
cwbooth5 May 26, 2026
b50d740
fix(nav): avoid double-dismiss black screen after decoy save
cwbooth5 May 30, 2026
6ff0cf9
fix(security): destroy non-decoy videos on poison pill activation
cwbooth5 May 30, 2026
6e1598e
test: clean up orphaned test files; fold valid suites into the target
cwbooth5 May 30, 2026
75b5237
feat(security): support decoy videos so non-decoy videos are destroyed
cwbooth5 May 30, 2026
e533a76
feat(gallery): show progress spinner while saving decoy media
cwbooth5 May 30, 2026
31cfa89
feat(gallery): show real thumbnails for videos
cwbooth5 May 30, 2026
2701f37
refactor(gallery): remove dead SecureGalleryViewModel
cwbooth5 May 30, 2026
9219ac1
ci: guard that every test source file is a member of its test target
cwbooth5 May 30, 2026
d5bc094
docs(readme): document fastlane build/test/release and the test-membe…
cwbooth5 May 30, 2026
1fa859b
fix(security): decoy videos were silently never created -> poison pil…
cwbooth5 May 30, 2026
ecd97ea
fix(video): decrypt over-read the final chunk -> fileIOError (broke s…
cwbooth5 May 30, 2026
2394621
fix(security): keep decoy video thumbnails through the poison pill
cwbooth5 May 31, 2026
e443b08
feat(gallery): import videos from the photo library (encrypted)
cwbooth5 May 31, 2026
f74798f
style(camera): Liquid Glass control buttons
cwbooth5 May 31, 2026
ad4f544
feat(gallery): swipe through photos and videos together in detail view
cwbooth5 May 31, 2026
90828e7
feat(video): glass inline player on the detail pager with reliable co…
cwbooth5 May 31, 2026
02aff67
fix(security): add file protection to wrapped DEK files
cwbooth5 May 31, 2026
5421de7
fix(security): propagate key-derivation errors instead of crashing
cwbooth5 Jun 1, 2026
4b7b1f1
fix(compiler): audioInput is nonisolated
cwbooth5 Jun 1, 2026
1b72cc3
fix(security): delete hardware keys and await eviction in security reset
cwbooth5 Jun 1, 2026
d736305
fix(security): use monotonic clock for session timeout and PIN backoff
cwbooth5 Jun 1, 2026
ab82d5e
fix(security): short-circuit poison-pill PIN verification
cwbooth5 Jun 1, 2026
94c3af1
fix(security): delete dead stub-cipher-key plumbing
cwbooth5 Jun 1, 2026
1fddf1a
fix(security): bind the PIN cryptographically to DEK unwrap (C1)
cwbooth5 Jun 1, 2026
6966ce0
fix(security): single-writer failed-attempt counter (M1)
cwbooth5 Jun 1, 2026
67348b3
fix(security): complete file protection on the settings file (H2, par…
cwbooth5 Jun 1, 2026
6442415
feat(camera): add pure orientation-angle mapping and OrientationObserver
cwbooth5 Jun 2, 2026
946bbf0
fix(camera): lock camera UI to portrait and remove dead landscape layout
cwbooth5 Jun 2, 2026
44bffa6
feat(camera): rotate camera control glyphs to match device orientation
cwbooth5 Jun 2, 2026
4afa788
fix(ipad): require full screen so the camera portrait lock is honored
cwbooth5 Jun 2, 2026
0e8d1a1
fix(orientation): drop unsupported UIDevice.orientation hack
cwbooth5 Jun 2, 2026
67be183
fix(camera): stop controls shifting into the preview on device rotation
cwbooth5 Jun 3, 2026
f0f7a7c
Fix small view update warning
cwbooth5 Jun 3, 2026
2d9f894
fix(ui): simplify glyph rotation
cwbooth5 Jun 3, 2026
51d1418
fix(ui): adjust spacing on zoom capsule
cwbooth5 Jun 3, 2026
18348bb
fix(camera): let control taps through the preview's focus gesture
cwbooth5 Jun 3, 2026
50848ff
feat(video): add auto play video toggle
cwbooth5 Jun 3, 2026
8e4ddb2
feat(video): add replay button + mute toggle to inline player
cwbooth5 Jun 6, 2026
18ba2a6
refactor(detail-pager): add counter auto-hide and consistent layout
cwbooth5 Jun 6, 2026
36734a9
style(ui): use system colors for consistency with glass backgrounds
cwbooth5 Jun 6, 2026
1cae3d0
refactor(obfuscation): use MediaToolbarButton for consistent controls
cwbooth5 Jun 6, 2026
a4e7342
fix(auth): isolate AuthorizationRepository to @MainActor
cwbooth5 Jun 6, 2026
afddfd9
fix(video): tie encrypted-video delegate lifetime to its asset
cwbooth5 Jun 6, 2026
e989a21
fix(auth): make failed-attempt increment atomic in the data source
cwbooth5 Jun 6, 2026
fafd0e2
fix(video): use built-in KVO publisher for AVPlayerItem status
cwbooth5 Jun 6, 2026
4705954
views updates
cwbooth5 Jun 11, 2026
ec6f93b
Add fastlane screenshot
cwbooth5 Jun 12, 2026
f9eaaa5
fix(viewer): latch dismiss-drag direction once and track both axes
cwbooth5 Jun 12, 2026
a247f09
feat(viewer): fade all chrome during the dismiss drag
cwbooth5 Jun 12, 2026
e1ef2c9
feat(video): pinch-zoom on video pages via ZoomableScrollView
cwbooth5 Jun 12, 2026
90725ac
fix(camera): preview container matches capture aspect; max-res stills
cwbooth5 Jun 12, 2026
f60d7e6
fix(video): fade action bar with opacity so dismiss-drag doesn't refl…
cwbooth5 Jun 12, 2026
bb78969
fix(camera): pin max-res stills to the feed aspect; observe device se…
cwbooth5 Jun 12, 2026
5d41e72
fix(tests): register drag-latch tests with the SnapSafeTests target
cwbooth5 Jun 12, 2026
e0b81d1
docs(spec): gallery landscape support design
cwbooth5 Jun 13, 2026
ca817cf
fix(orientation): drop portrait-reset on disappear
cwbooth5 Jun 13, 2026
128f295
feat(gallery): support landscape orientation
cwbooth5 Jun 13, 2026
e57d441
periphery fixes, video fixes
cwbooth5 Jun 13, 2026
abef27d
Fix various security scan issues
cwbooth5 Jun 13, 2026
b9b7a8d
refactor(secureimage): add ImageProcessing namespace + tests
cwbooth5 Jun 13, 2026
0c01443
fix(secureimage): guard image context in rotateImage; broaden ImagePr…
cwbooth5 Jun 13, 2026
c4bb396
refactor(secureimage): route image work through ImageProcessing
cwbooth5 Jun 13, 2026
14bb3e8
refactor(secureimage): move readImageMetadata + metadata types out of…
cwbooth5 Jun 13, 2026
ede87a6
refactor(secureimage): extract PhotoStorageDataSource (directories + …
cwbooth5 Jun 13, 2026
2c27069
refactor(secureimage): move file path + decoy enumeration helpers to …
cwbooth5 Jun 14, 2026
336be46
refactor(secureimage): add ImageProcessing.createThumbnailData + gene…
cwbooth5 Jun 14, 2026
cac2917
fix(secureimage): guard scale>0 in createThumbnailData; log cache mis…
cwbooth5 Jun 14, 2026
0058b94
refactor(secureimage): ThumbnailCache → @unchecked Sendable final class
cwbooth5 Jun 14, 2026
394299b
refactor(secureimage): SecureImageRepository → actor; read APIs retur…
cwbooth5 Jun 14, 2026
dcaa758
fix(gallery): guard decoy-selection loops against cancellation during…
cwbooth5 Jun 14, 2026
87c6b87
refactor(gallery): route cell thumbnails through view model to repopu…
cwbooth5 Jun 14, 2026
37fbdf8
fix(gallery): clear decrypted thumbnails on deauth and on delete
cwbooth5 Jun 14, 2026
0e27e50
refactor(decoy): route decoy-photo removal through RemoveDecoyPhotoUs…
cwbooth5 Jun 14, 2026
be87754
chore(deps): bump faraday, jwt, aws-sdk-s3, addressable to patch CVEs
cwbooth5 Jun 14, 2026
cfceb34
chore(periphery): fix 3 unused-code findings from fresh scan
cwbooth5 Jun 14, 2026
e398fca
fix(gallery): make full toolbar button frame tappable in photo detail
cwbooth5 Jun 14, 2026
15ddbe2
fix(decoy): correct decoy state/limit handling in detail and gallery
cwbooth5 Jun 14, 2026
a5f6c68
fix(pin): make PIN field auto-focus reliable on appear
cwbooth5 Jun 14, 2026
cb7f926
fix(decoy): clear decoy videos when poison pill is removed
cwbooth5 Jun 14, 2026
21e8b76
feat(video): add AVMetadataItemFactory for capture metadata and ISO 6709
cwbooth5 Jun 14, 2026
5ad929d
feat(video): add VideoMetaData model and display formatting
cwbooth5 Jun 14, 2026
82e7b39
feat(video): add SecureImageRepository.getVideoMetaData read path
cwbooth5 Jun 14, 2026
f542d37
feat(video): embed location and creation date at record time
cwbooth5 Jun 14, 2026
2c5f313
feat(video): add VideoInfoViewModel
cwbooth5 Jun 14, 2026
818adf1
feat(video): add VideoInfoView sheet
cwbooth5 Jun 14, 2026
3822f79
feat(video): add videoInfo navigation case
cwbooth5 Jun 15, 2026
5aa4743
feat(video): add Info button to video detail toolbar
cwbooth5 Jun 15, 2026
88d0e35
chore(i18n): extract video info sheet strings into the catalog
cwbooth5 Jun 15, 2026
ee81c34
fix(pin): resolve PIN field focus deadlock from duplicate auth screens
cwbooth5 Jun 15, 2026
82d57d4
Update workflows to latest versions
cwbooth5 Jun 15, 2026
7a427d2
Remove more leftover docs
cwbooth5 Jun 15, 2026
44867bc
Change session timeout to 5min
cwbooth5 Jun 15, 2026
b86e021
Length warning on PIN setup
cwbooth5 Jun 15, 2026
a96b72d
feat: add PINType enum and embed it in HashedPin
cwbooth5 Jun 15, 2026
6f95e59
feat: thread pinType through PinRepository and PIN-creation use cases
cwbooth5 Jun 15, 2026
2c8ffa6
feat: add alphanumeric strength rules to PinStrengthCheckUseCase
cwbooth5 Jun 15, 2026
d213c70
feat: add pinType to PINEntryField for dynamic keyboard and character…
cwbooth5 Jun 15, 2026
5794639
feat: add alphanumeric toggle to PIN setup
cwbooth5 Jun 15, 2026
7d48a10
feat: load and apply pinType at PIN verification
cwbooth5 Jun 15, 2026
8b29150
feat: add independent alphanumeric toggle to poison pill PIN setup
cwbooth5 Jun 15, 2026
e1af6a2
Alphanumeric pins, gallery order, location precision
cwbooth5 Jun 17, 2026
a383368
Pin checksums in actions
cwbooth5 Jul 11, 2026
a5185f9
Bump version to 1.4.0 and document release process
cwbooth5 Oct 9, 2026
c65a7f5
Pin CI to Xcode 26.5
cwbooth5 Oct 9, 2026
a0628a6
Fix CodeQL Swift build on macos-26 runners
cwbooth5 Oct 9, 2026
b64fe87
Speed up CodeQL Swift job and ignore superpowers docs
cwbooth5 Oct 12, 2026
4c1c266
Bump json to 2.21.2 for CVE-2026-71847
cwbooth5 Oct 12, 2026
ca345ec
Move CodeQL workflow to Node 24 actions
cwbooth5 Oct 12, 2026
b1bf18c
Scope CodeQL arm64-only build to the simulator SDK
cwbooth5 Oct 12, 2026
33694b0
Remove CodeQL package pre-resolve step
cwbooth5 Oct 12, 2026
b5078d4
Raise max PIN length to 16 and add alphanumeric PIN help
cwbooth5 Oct 12, 2026
5a061d6
Fix typo in poison pill warning text
cwbooth5 Oct 12, 2026
df94dd5
Stop gallery video playback when swiping to an adjacent page
cwbooth5 Oct 12, 2026
cdc6efb
Add alphanumeric PIN help strings and fix poison pill warning key
cwbooth5 Oct 12, 2026
d046ca0
Apply securityManaged to decoy selection full-screen cover
cwbooth5 Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Set Xcode version
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app
run: sudo xcode-select -s /Applications/Xcode_26.5.app

- name: Use the exact SPM lockfile
run: |
Expand All @@ -32,7 +32,7 @@ jobs:
-scheme SnapSafe

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1
with:
bundler-cache: true
ruby-version: '3.4'
Expand All @@ -50,7 +50,7 @@ jobs:

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results
path: |
Expand All @@ -64,7 +64,7 @@ jobs:

- name: Publish test results
if: always()
uses: EnricoMi/publish-unit-test-result-action/macos@v2
uses: EnricoMi/publish-unit-test-result-action/macos@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2
with:
files: |
fastlane/test_output/*.junit
Expand Down
24 changes: 18 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,26 @@ jobs:
build-mode: manual
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1

- name: Set Xcode version 16.2
- name: Set Xcode version
if: ${{ matrix.language == 'swift' }}
run: sudo xcode-select -s /Applications/Xcode_16.2.app
run: sudo xcode-select -s /Applications/Xcode_26.5.app

- name: Disable macro validation
if: ${{ matrix.language == 'swift' }}
run: |
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
Expand All @@ -83,9 +88,16 @@ jobs:
- if: matrix.build-mode == 'manual'
shell: bash
run: |
xcodebuild -project SnapSafe.xcodeproj/ -scheme SnapSafe -configuration Debug -sdk iphonesimulator18.4 -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' -derivedDataPath ./build CODE_SIGNING_ALLOWED=NO clean build
# Build the simulator slice for arm64 only. Scoped to the simulator SDK
# so macro host tools (Mockable/swift-syntax) keep their default archs;
# a global ARCHS=arm64 breaks the MockableMacro build.
echo 'ARCHS[sdk=iphonesimulator*] = arm64' > "$RUNNER_TEMP/codeql-arm64.xcconfig"
xcodebuild -project SnapSafe.xcodeproj -scheme SnapSafe -configuration Debug \
-destination 'generic/platform=iOS Simulator' -derivedDataPath ./build \
-xcconfig "$RUNNER_TEMP/codeql-arm64.xcconfig" \
-skipMacroValidation CODE_SIGNING_ALLOWED=NO clean build

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: "/language:${{matrix.language}}"
2 changes: 1 addition & 1 deletion .github/workflows/notify-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ jobs:
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 # master
with:
args: '**New SnapSafe iOS release:** ${{ github.event.release.name }} \n\n **Download Here:** \n\n ${{ github.event.release.html_url }}'
14 changes: 7 additions & 7 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

- name: Set Xcode version
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app
run: sudo xcode-select -s /Applications/Xcode_26.5.app

- name: Show Xcode version
run: xcodebuild -version

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1
with:
ruby-version: '3.4'
bundler-cache: true
Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
name: Release ${{ env.VERSION_NAME }}
draft: false
Expand All @@ -88,16 +88,16 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

- name: Set Xcode version
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app
run: sudo xcode-select -s /Applications/Xcode_26.5.app

- name: Show Xcode version
run: xcodebuild -version

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1
with:
ruby-version: '3.4'
bundler-cache: true
Expand Down
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,13 @@ Configs/LocalOverrides.xcconfig
vendor/

# fastlane snapshot
screenshots/
screenshots/

SecureCameraAndroid/

# Local TODO scratch
TODO.md

.claude/worktrees
.superpowers/
docs/superpowers/
6 changes: 6 additions & 0 deletions .periphery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
project: SnapSafe.xcworkspace
schemes:
- SnapSafe
clean_build: true
build_arguments:
- -skipMacroValidation
98 changes: 98 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Agent guide for Swift and SwiftUI

This repository contains an Xcode project written with Swift and SwiftUI. Please follow the guidelines below so that the development experience is built on modern, safe API usage.


## Role

You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and related frameworks. Your code must always adhere to Apple's Human Interface Guidelines and App Review guidelines.


## Core instructions

- Target iOS 26.0 or later. (Yes, it definitely exists.)
- Swift 6.2 or later, using modern Swift concurrency. Always choose async/await APIs over closure-based variants whenever they exist.
- SwiftUI backed up by `@Observable` classes for shared data.
- Do not introduce third-party frameworks without asking first.
- Avoid UIKit unless requested.


## Swift instructions

- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Flag any `@Observable` class missing this annotation.
- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing).
- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated.
- Assume strict Swift concurrency rules are being applied.
- Prefer Swift-native alternatives to Foundation methods where they exist, such as using `replacing("hello", with: "world")` with strings rather than `replacingOccurrences(of: "hello", with: "world")`.
- Prefer modern Foundation API, for example `URL.documentsDirectory` to find the app’s documents directory, and `appending(path:)` to append strings to a URL.
- Never use C-style number formatting such as `Text(String(format: "%.2f", abs(myNumber)))`; always use `Text(abs(change), format: .number.precision(.fractionLength(2)))` instead.
- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`.
- Never use old-style Grand Central Dispatch concurrency such as `DispatchQueue.main.async()`. If behavior like this is needed, always use modern Swift concurrency.
- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()`.
- Avoid force unwraps and force `try` unless it is unrecoverable.
- Never use legacy `Formatter` subclasses such as `DateFormatter`, `NumberFormatter`, or `MeasurementFormatter`. Always use the modern `FormatStyle` API instead. For example, to format a date, use `myDate.formatted(date: .abbreviated, time: .shortened)`. To parse a date from a string, use `Date(inputString, strategy: .iso8601)`. For numbers, use `myNumber.formatted(.number)` or custom format styles.

## SwiftUI instructions

- Always use `foregroundStyle()` instead of `foregroundColor()`.
- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`.
- Always use the `Tab` API instead of `tabItem()`.
- Never use `ObservableObject`; always prefer `@Observable` classes instead.
- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none.
- Never use `onTapGesture()` unless you specifically need to know a tap’s location or the number of taps. All other usages should use `Button`.
- Never use `Task.sleep(nanoseconds:)`; always use `Task.sleep(for:)` instead.
- Never use `UIScreen.main.bounds` to read the size of the available space.
- Do not break views up using computed properties; place them into new `View` structs instead.
- Do not force specific font sizes; prefer using Dynamic Type instead.
- Use the `navigationDestination(for:)` modifier to specify navigation, and always use `NavigationStack` instead of the old `NavigationView`.
- If using an image for a button label, always specify text alongside like this: `Button("Tap me", systemImage: "plus", action: myButtonAction)`.
- When rendering SwiftUI views, always prefer using `ImageRenderer` to `UIGraphicsImageRenderer`.
- Don’t apply the `fontWeight()` modifier unless there is good reason. If you want to make some text bold, always use `bold()` instead of `fontWeight(.bold)`.
- Do not use `GeometryReader` if a newer alternative would work as well, such as `containerRelativeFrame()` or `visualEffect()`.
- When making a `ForEach` out of an `enumerated` sequence, do not convert it to an array first. So, prefer `ForEach(x.enumerated(), id: \.element.id)` instead of `ForEach(Array(x.enumerated()), id: \.element.id)`.
- When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer.
- Use the newest ScrollView APIs for item scrolling and positioning (e.g. `ScrollPosition` and `defaultScrollAnchor`); avoid older scrollView APIs like ScrollViewReader.
- Place view logic into view models or similar, so it can be tested.
- Avoid `AnyView` unless it is absolutely required.
- Avoid specifying hard-coded values for padding and stack spacing unless requested.
- Avoid using UIKit colors in SwiftUI code.


## SwiftData instructions

If SwiftData is configured to use CloudKit:

- Never use `@Attribute(.unique)`.
- Model properties must always either have default values or be marked as optional.
- All relationships must be marked optional.


## Project structure

- Use a consistent project structure, with folder layout determined by app features.
- Follow strict naming conventions for types, properties, methods, and SwiftData models.
- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file.
- Write unit tests for core application logic.
- Only write UI tests if unit tests are not possible.
- Add code comments and documentation comments as needed.
- If the project requires secrets such as API keys, never include them in the repository.
- If the project uses Localizable.xcstrings, prefer to add user-facing strings using symbol keys (e.g. helloWorld) in the string catalog with `extractionState` set to "manual", accessing them via generated symbols such as `Text(.helloWorld)`. Offer to translate new keys into all languages supported by the project.


## PR instructions

- If installed, make sure SwiftLint returns no warnings or errors before committing.


## Xcode MCP

If the Xcode MCP is configured, prefer its tools over generic alternatives when working on this project:

- `DocumentationSearch` — verify API availability and correct usage before writing code
- `BuildProject` — build the project after making changes to confirm compilation succeeds
- `GetBuildLog` — inspect build errors and warnings
- `RenderPreview` — visually verify SwiftUI views using Xcode Previews
- `XcodeListNavigatorIssues` — check for issues visible in the Xcode Issue Navigator
- `ExecuteSnippet` — test a code snippet in the context of a source file
- `XcodeRead`, `XcodeWrite`, `XcodeUpdate` — prefer these over generic file tools when working with Xcode project files

Loading
Loading