228 lines
11 KiB
Markdown
228 lines
11 KiB
Markdown
# 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.
|
||
- SwiftUI backed up by `@Observable` classes for shared data.
|
||
- Do not introduce third-party frameworks without asking first.
|
||
- Avoid UIKit unless requested.
|
||
|
||
|
||
## Swift instructions
|
||
|
||
- Always mark `@Observable` classes with `@MainActor`.
|
||
- 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.
|
||
|
||
|
||
## 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.
|
||
- 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.
|
||
|
||
|
||
## Localization instructions
|
||
|
||
- Use **String Catalogs** (`.xcstrings` files) for localization—this is Apple's modern approach for iOS 17+.
|
||
- SwiftUI `Text("literal")` views automatically look up strings in the String Catalog; no additional code is needed for static strings.
|
||
- For strings outside of `Text` views or with dynamic content, use `String(localized:)` or create a helper extension:
|
||
```swift
|
||
extension String {
|
||
static func localized(_ key: String) -> String {
|
||
String(localized: String.LocalizationValue(key))
|
||
}
|
||
static func localized(_ key: String, _ arguments: CVarArg...) -> String {
|
||
let format = String(localized: String.LocalizationValue(key))
|
||
return String(format: format, arguments: arguments)
|
||
}
|
||
}
|
||
```
|
||
- For format strings with interpolation (e.g., "Balance: $%@"), define a key in the String Catalog and use `String.localized("key", value)`.
|
||
- Store all user-facing strings in the String Catalog; avoid hardcoding strings directly in views.
|
||
- Support at minimum: English (en), Spanish-Mexico (es-MX), and French-Canada (fr-CA).
|
||
- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API.
|
||
|
||
|
||
## Design constants instructions
|
||
|
||
- Avoid magic numbers for layout values (padding, spacing, corner radii, font sizes, etc.).
|
||
- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing:
|
||
```swift
|
||
enum Design {
|
||
enum Spacing {
|
||
static let small: CGFloat = 8
|
||
static let medium: CGFloat = 12
|
||
static let large: CGFloat = 16
|
||
}
|
||
enum CornerRadius {
|
||
static let small: CGFloat = 8
|
||
static let medium: CGFloat = 12
|
||
}
|
||
enum BaseFontSize {
|
||
static let body: CGFloat = 14
|
||
static let title: CGFloat = 24
|
||
}
|
||
}
|
||
```
|
||
- For colors used across the app, extend `Color` with semantic color definitions:
|
||
```swift
|
||
extension Color {
|
||
enum Primary {
|
||
static let background = Color(red: 0.1, green: 0.2, blue: 0.3)
|
||
static let accent = Color(red: 0.8, green: 0.6, blue: 0.2)
|
||
}
|
||
}
|
||
```
|
||
- Within each view, extract view-specific magic numbers to private constants at the top of the struct:
|
||
```swift
|
||
struct MyView: View {
|
||
private let cardWidth: CGFloat = 45
|
||
private let headerFontSize: CGFloat = 18
|
||
// ...
|
||
}
|
||
```
|
||
- Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`.
|
||
- Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
|
||
|
||
|
||
## Dynamic Type instructions
|
||
|
||
- Always support Dynamic Type for accessibility; never use fixed font sizes without scaling.
|
||
- Use `@ScaledMetric` to scale custom font sizes and dimensions based on user accessibility settings:
|
||
```swift
|
||
struct MyView: View {
|
||
@ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = 14
|
||
@ScaledMetric(relativeTo: .title) private var titleFontSize: CGFloat = 24
|
||
@ScaledMetric(relativeTo: .caption) private var chipTextSize: CGFloat = 11
|
||
|
||
var body: some View {
|
||
Text("Hello")
|
||
.font(.system(size: bodyFontSize, weight: .medium))
|
||
}
|
||
}
|
||
```
|
||
- Choose the appropriate `relativeTo` text style based on the semantic purpose:
|
||
- `.largeTitle`, `.title`, `.title2`, `.title3` for headings
|
||
- `.headline`, `.subheadline` for emphasized content
|
||
- `.body` for main content
|
||
- `.callout`, `.footnote`, `.caption`, `.caption2` for smaller text
|
||
- For constrained UI elements (chips, cards, badges) where overflow would break the design, you may use fixed sizes but document the reason:
|
||
```swift
|
||
// Fixed size: chip face has strict space constraints
|
||
private let chipValueFontSize: CGFloat = 11
|
||
```
|
||
- Prefer system text styles when possible: `.font(.body)`, `.font(.title)`, `.font(.caption)`.
|
||
- Test with accessibility settings: Settings > Accessibility > Display & Text Size > Larger Text.
|
||
|
||
|
||
## VoiceOver accessibility instructions
|
||
|
||
- All interactive elements (buttons, betting zones, selectable items) must have meaningful `.accessibilityLabel()`.
|
||
- Use `.accessibilityValue()` to communicate dynamic state (e.g., current bet amount, selection state, hand value).
|
||
- Use `.accessibilityHint()` to describe what will happen when interacting with an element:
|
||
```swift
|
||
Button("Deal", action: deal)
|
||
.accessibilityHint("Deals cards and starts the round")
|
||
```
|
||
- Use `.accessibilityAddTraits()` to communicate element type:
|
||
- `.isButton` for tappable elements that aren't SwiftUI Buttons
|
||
- `.isHeader` for section headers
|
||
- `.isModal` for modal overlays
|
||
- `.updatesFrequently` for live-updating content
|
||
- Hide purely decorative elements from VoiceOver:
|
||
```swift
|
||
TableBackgroundView()
|
||
.accessibilityHidden(true) // Decorative element
|
||
```
|
||
- Group related elements to reduce VoiceOver navigation complexity:
|
||
```swift
|
||
VStack {
|
||
handLabel
|
||
cardStack
|
||
valueDisplay
|
||
}
|
||
.accessibilityElement(children: .ignore)
|
||
.accessibilityLabel("Player hand")
|
||
.accessibilityValue("Ace of Hearts, King of Spades. Value: 1")
|
||
```
|
||
- For complex elements, use `.accessibilityElement(children: .contain)` to allow navigation to children while adding context.
|
||
- Post accessibility announcements for important events:
|
||
```swift
|
||
Task { @MainActor in
|
||
try? await Task.sleep(for: .milliseconds(500))
|
||
UIAccessibility.post(notification: .announcement, argument: "Player wins!")
|
||
}
|
||
```
|
||
- Provide accessibility names for model types that appear in UI:
|
||
```swift
|
||
enum Suit {
|
||
var accessibilityName: String {
|
||
switch self {
|
||
case .hearts: return String(localized: "Hearts")
|
||
// ...
|
||
}
|
||
}
|
||
}
|
||
```
|
||
- Test with VoiceOver enabled: Settings > Accessibility > VoiceOver.
|
||
|
||
|
||
## 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.
|
||
|
||
|
||
## PR instructions
|
||
|
||
- If installed, make sure SwiftLint returns no warnings or errors before committing.
|
||
|