Documentation: - Update README.md with complete feature list and SelfieCam branding - Update AI_Implementation.md with current architecture and branding details - Add SelfieCam-specific sections to AGENTS.md (premium, branding, camera) Features: - Add branding debug section to SettingsView (icon generator, preview) - Add BrandingConfig.swift with app colors and launch screen config - Add LaunchBackground.colorset for seamless launch experience - Wrap app in AppLaunchView for animated launch screen
663 lines
26 KiB
Markdown
663 lines
26 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 18.0 or later.
|
|
- Swift 6 or later, using modern Swift concurrency.
|
|
- SwiftUI backed up by `@Observable` classes for shared data.
|
|
- **Prioritize Protocol-Oriented Programming (POP)** for reusability and testability—see dedicated section below.
|
|
- Avoid UIKit unless requested.
|
|
|
|
|
|
## Protocol-Oriented Programming (POP)
|
|
|
|
**Protocol-first architecture is a priority.** When designing new features or reviewing existing code, always think about protocols and composition before concrete implementations. This enables code reuse across modules, easier testing, and cleaner architecture.
|
|
|
|
### When architecting new code:
|
|
|
|
1. **Start with the protocol**: Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol.
|
|
2. **Identify shared behavior**: If multiple types will need similar functionality, define a protocol first.
|
|
3. **Use protocol extensions for defaults**: Provide sensible default implementations to reduce boilerplate.
|
|
4. **Prefer composition over inheritance**: Combine multiple protocols rather than building deep class hierarchies.
|
|
|
|
### When reviewing existing code for reuse:
|
|
|
|
1. **Look for duplicated patterns**: If you see similar logic across modules, extract a protocol to a shared location.
|
|
2. **Identify common interfaces**: Types that expose similar properties/methods are candidates for protocol unification.
|
|
3. **Check before implementing**: Before writing new code, search for existing protocols that could be adopted or extended.
|
|
4. **Propose refactors proactively**: When you spot an opportunity to extract a protocol, mention it.
|
|
|
|
### Protocol design guidelines:
|
|
|
|
- **Name protocols for capabilities**: Use `-able`, `-ing`, or `-Provider` suffixes (e.g., `Searchable`, `DataLoading`, `ContentProvider`).
|
|
- **Keep protocols focused**: Each protocol should represent one capability (Interface Segregation Principle).
|
|
- **Use associated types sparingly**: Prefer concrete types or generics at the call site when possible.
|
|
- **Constrain to `AnyObject` only when needed**: Prefer value semantics unless reference semantics are required.
|
|
|
|
### Examples
|
|
|
|
**❌ BAD - Concrete implementations without protocols:**
|
|
```swift
|
|
// Features/Users/UserListViewModel.swift
|
|
@Observable @MainActor
|
|
class UserListViewModel {
|
|
var items: [User] = []
|
|
var isLoading: Bool = false
|
|
func load() async { ... }
|
|
func refresh() async { ... }
|
|
}
|
|
|
|
// Features/Products/ProductListViewModel.swift - duplicates the same pattern
|
|
@Observable @MainActor
|
|
class ProductListViewModel {
|
|
var items: [Product] = []
|
|
var isLoading: Bool = false
|
|
func load() async { ... }
|
|
func refresh() async { ... }
|
|
}
|
|
```
|
|
|
|
**✅ GOOD - Protocol in shared module, adopted by features:**
|
|
```swift
|
|
// Shared/Protocols/DataLoading.swift
|
|
protocol DataLoading: AnyObject {
|
|
associatedtype Item: Identifiable
|
|
var items: [Item] { get set }
|
|
var isLoading: Bool { get set }
|
|
|
|
func load() async
|
|
func refresh() async
|
|
}
|
|
|
|
extension DataLoading {
|
|
func refresh() async {
|
|
items = []
|
|
await load()
|
|
}
|
|
}
|
|
|
|
// Features/Users/UserListViewModel.swift - adopts protocol
|
|
@Observable @MainActor
|
|
class UserListViewModel: DataLoading {
|
|
var items: [User] = []
|
|
var isLoading: Bool = false
|
|
|
|
func load() async { ... }
|
|
// refresh() comes from protocol extension
|
|
}
|
|
```
|
|
|
|
**❌ BAD - View only works with one concrete type:**
|
|
```swift
|
|
struct ItemListView: View {
|
|
@Bindable var viewModel: UserListViewModel
|
|
// Tightly coupled to Users
|
|
}
|
|
```
|
|
|
|
**✅ GOOD - View works with any DataLoading type:**
|
|
```swift
|
|
struct ItemListView<ViewModel: DataLoading & Observable>: View {
|
|
@Bindable var viewModel: ViewModel
|
|
// Reusable across all features
|
|
}
|
|
```
|
|
|
|
### Common protocols to consider extracting:
|
|
|
|
| Capability | Protocol Name | Shared By |
|
|
|------------|---------------|-----------|
|
|
| Loading data | `DataLoading` | All list features |
|
|
| Search/filter | `Searchable` | Features with search |
|
|
| Settings/config | `Configurable` | Features with settings |
|
|
| Pagination | `Paginating` | Large data sets |
|
|
| Form validation | `Validatable` | Input forms |
|
|
| Persistence | `Persistable` | Cached data |
|
|
|
|
### Refactoring checklist:
|
|
|
|
When you encounter code that could benefit from POP:
|
|
|
|
- [ ] Is this logic duplicated across multiple features?
|
|
- [ ] Could this type conform to an existing protocol in the shared module?
|
|
- [ ] Would extracting a protocol make this code testable in isolation?
|
|
- [ ] Can views be made generic over a protocol instead of a concrete type?
|
|
- [ ] Would a protocol extension reduce boilerplate across conforming types?
|
|
|
|
### Benefits:
|
|
|
|
- **Reusability**: Shared protocols work across all features
|
|
- **Testability**: Mock types can conform to protocols for unit testing
|
|
- **Flexibility**: New features can adopt existing protocols immediately
|
|
- **Maintainability**: Fix a bug in a protocol extension, fix it everywhere
|
|
- **Discoverability**: Protocols document the expected interface clearly
|
|
|
|
|
|
## 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.
|
|
- Avoid `AnyView` unless it is absolutely required.
|
|
- **Never use raw numeric literals** for padding, spacing, opacity, font sizes, dimensions, corner radii, shadows, or animation durations—always use Design constants (see "No magic numbers" section).
|
|
- **Never use inline `Color(red:green:blue:)` or hex colors**—define all colors in a `Color` extension with semantic names.
|
|
- Avoid using UIKit colors in SwiftUI code.
|
|
|
|
|
|
## View/State separation (MVVM-lite)
|
|
|
|
**Views should be "dumb" renderers.** All business logic belongs in dedicated view models or state objects.
|
|
|
|
### What belongs in the State/ViewModel:
|
|
- **Business logic**: Calculations, validations, business rules
|
|
- **Computed properties based on data**: recommendations, derived values
|
|
- **State checks**: `isLoading`, `canSubmit`, `isFormValid`, `hasUnsavedChanges`
|
|
- **Data transformations**: filtering, sorting, aggregations
|
|
|
|
### What is acceptable in Views:
|
|
- **Pure UI layout logic**: `isIPad`, `maxContentWidth` based on size class
|
|
- **Visual styling**: color selection based on state (`statusColor`, `errorColor`)
|
|
- **@ViewBuilder sub-views**: breaking up complex layouts
|
|
- **Accessibility labels**: combining data into accessible descriptions
|
|
|
|
### Examples
|
|
|
|
**❌ BAD - Business logic in view:**
|
|
```swift
|
|
struct MyView: View {
|
|
@Bindable var viewModel: FormViewModel
|
|
|
|
private var isFormValid: Bool {
|
|
!viewModel.email.isEmpty && viewModel.email.contains("@")
|
|
}
|
|
|
|
private var formattedPrice: String? {
|
|
guard let price = viewModel.price else { return nil }
|
|
return viewModel.formatter.string(from: price)
|
|
}
|
|
}
|
|
```
|
|
|
|
**✅ GOOD - Logic in ViewModel, view just reads:**
|
|
```swift
|
|
// In ViewModel:
|
|
var isFormValid: Bool {
|
|
!email.isEmpty && email.contains("@") && password.count >= 8
|
|
}
|
|
|
|
var formattedPrice: String? {
|
|
guard let price = price else { return nil }
|
|
return formatter.string(from: price)
|
|
}
|
|
|
|
// In View:
|
|
Button("Submit", action: submit)
|
|
.disabled(!viewModel.isFormValid)
|
|
if let price = viewModel.formattedPrice { Text(price) }
|
|
```
|
|
|
|
### Benefits:
|
|
- **Testable**: ViewModel logic can be unit tested without UI
|
|
- **Single source of truth**: No duplicated logic across views
|
|
- **Cleaner views**: Views focus purely on layout and presentation
|
|
- **Easier debugging**: Logic is centralized, not scattered
|
|
|
|
|
|
## 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., "Items: %@"), 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.
|
|
- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API.
|
|
|
|
|
|
## No magic numbers or hardcoded values
|
|
|
|
**Never use raw numeric literals or hardcoded colors directly in views.** All values must be extracted to named constants, enums, or variables. This applies to:
|
|
|
|
### Values that MUST be constants:
|
|
- **Spacing & Padding**: `.padding(Design.Spacing.medium)` not `.padding(12)`
|
|
- **Corner Radii**: `Design.CornerRadius.large` not `cornerRadius: 16`
|
|
- **Font Sizes**: `Design.FontSize.body` not `size: 14`
|
|
- **Opacity Values**: `Design.Opacity.strong` not `.opacity(0.7)`
|
|
- **Colors**: `Color.Primary.accent` not `Color(red: 0.8, green: 0.6, blue: 0.2)`
|
|
- **Line Widths**: `Design.LineWidth.medium` not `lineWidth: 2`
|
|
- **Shadow Values**: `Design.Shadow.radiusLarge` not `radius: 10`
|
|
- **Animation Durations**: `Design.Animation.quick` not `duration: 0.3`
|
|
- **Component Sizes**: `Design.Size.iconMedium` not `frame(width: 32)`
|
|
|
|
### What to do when you see a magic number:
|
|
1. Check if an appropriate constant already exists in your design constants file
|
|
2. If not, add a new constant with a semantic name
|
|
3. Use the constant in place of the raw value
|
|
4. If it's truly view-specific and used only once, extract to a `private let` at the top of the view struct
|
|
|
|
### Examples of violations:
|
|
```swift
|
|
// ❌ BAD - Magic numbers everywhere
|
|
.padding(16)
|
|
.opacity(0.6)
|
|
.frame(width: 80, height: 52)
|
|
.shadow(radius: 10, y: 5)
|
|
Color(red: 0.25, green: 0.3, blue: 0.45)
|
|
|
|
// ✅ GOOD - Named constants
|
|
.padding(Design.Spacing.large)
|
|
.opacity(Design.Opacity.accent)
|
|
.frame(width: Design.Size.cardWidth, height: Design.Size.cardHeight)
|
|
.shadow(radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
|
|
Color.Primary.background
|
|
```
|
|
|
|
|
|
## Design constants instructions
|
|
|
|
- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing:
|
|
```swift
|
|
enum Design {
|
|
enum Spacing {
|
|
static let xxSmall: CGFloat = 2
|
|
static let xSmall: CGFloat = 4
|
|
static let small: CGFloat = 8
|
|
static let medium: CGFloat = 12
|
|
static let large: CGFloat = 16
|
|
static let xLarge: CGFloat = 20
|
|
}
|
|
enum CornerRadius {
|
|
static let small: CGFloat = 8
|
|
static let medium: CGFloat = 12
|
|
static let large: CGFloat = 16
|
|
}
|
|
enum FontSize {
|
|
static let small: CGFloat = 10
|
|
static let body: CGFloat = 14
|
|
static let large: CGFloat = 18
|
|
static let title: CGFloat = 24
|
|
}
|
|
enum Opacity {
|
|
static let subtle: Double = 0.1
|
|
static let hint: Double = 0.2
|
|
static let light: Double = 0.3
|
|
static let medium: Double = 0.5
|
|
static let accent: Double = 0.6
|
|
static let strong: Double = 0.7
|
|
static let heavy: Double = 0.8
|
|
static let almostFull: Double = 0.9
|
|
}
|
|
enum LineWidth {
|
|
static let thin: CGFloat = 1
|
|
static let medium: CGFloat = 2
|
|
static let thick: CGFloat = 3
|
|
}
|
|
enum Shadow {
|
|
static let radiusSmall: CGFloat = 2
|
|
static let radiusMedium: CGFloat = 6
|
|
static let radiusLarge: CGFloat = 10
|
|
static let offsetSmall: CGFloat = 1
|
|
static let offsetMedium: CGFloat = 3
|
|
}
|
|
enum Animation {
|
|
static let quick: Double = 0.3
|
|
static let springDuration: Double = 0.4
|
|
static let staggerDelay1: Double = 0.1
|
|
static let staggerDelay2: Double = 0.25
|
|
}
|
|
}
|
|
```
|
|
- 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)
|
|
}
|
|
enum Button {
|
|
static let primaryLight = Color(red: 1.0, green: 0.85, blue: 0.3)
|
|
static let primaryDark = Color(red: 0.9, green: 0.7, blue: 0.2)
|
|
}
|
|
}
|
|
```
|
|
- Within each view, extract view-specific magic numbers to private constants at the top of the struct with a comment explaining why they're local:
|
|
```swift
|
|
struct MyView: View {
|
|
// Layout: fixed dimensions for consistent appearance
|
|
private let thumbnailSize: CGFloat = 45
|
|
// Typography: constrained space requires fixed size
|
|
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, FontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
|
|
- When adding new features, check existing constants first before creating new ones.
|
|
- Name constants semantically (what they represent) not literally (their value): `accent` not `pointSix`, `large` not `sixteen`.
|
|
|
|
|
|
## 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 captionSize: 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 (icons, badges, compact layouts) where overflow would break the design, you may use fixed sizes but document the reason:
|
|
```swift
|
|
// Fixed size: badge has strict space constraints
|
|
private let badgeFontSize: 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, selectable items) must have meaningful `.accessibilityLabel()`.
|
|
- Use `.accessibilityValue()` to communicate dynamic state (e.g., current selection, count, progress).
|
|
- Use `.accessibilityHint()` to describe what will happen when interacting with an element:
|
|
```swift
|
|
Button("Submit", action: submit)
|
|
.accessibilityHint("Submits the form and creates your account")
|
|
```
|
|
- 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
|
|
DecorationView()
|
|
.accessibilityHidden(true) // Decorative element
|
|
```
|
|
- Group related elements to reduce VoiceOver navigation complexity:
|
|
```swift
|
|
VStack {
|
|
titleLabel
|
|
subtitleLabel
|
|
statusIndicator
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel("Item details")
|
|
.accessibilityValue("Title: \(title). Status: \(status)")
|
|
```
|
|
- 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: "Upload complete!")
|
|
}
|
|
```
|
|
- Provide accessibility names for model types that appear in UI:
|
|
```swift
|
|
enum Status {
|
|
var accessibilityName: String {
|
|
switch self {
|
|
case .pending: return String(localized: "Pending")
|
|
case .complete: return String(localized: "Complete")
|
|
// ...
|
|
}
|
|
}
|
|
}
|
|
```
|
|
- 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.
|
|
|
|
|
|
## Documentation instructions
|
|
|
|
- **Always keep documentation up to date** when adding new functionality or making changes that users or developers need to know about.
|
|
- Document new features, settings, or behaviors in the appropriate documentation files.
|
|
- Update documentation when modifying existing behavior.
|
|
- Include any configuration options, keyboard shortcuts, or special interactions.
|
|
- Documentation updates should be part of the same commit as the feature/change they document.
|
|
|
|
|
|
## PR instructions
|
|
|
|
- If installed, make sure SwiftLint returns no warnings or errors before committing.
|
|
- Verify that documentation reflects any new functionality or behavioral changes.
|
|
|
|
|
|
---
|
|
|
|
# SelfieCam-Specific Guidelines
|
|
|
|
The following sections are specific to this app's architecture and features.
|
|
|
|
|
|
## App Architecture
|
|
|
|
SelfieCam uses the following architectural patterns:
|
|
|
|
### Dependencies
|
|
- **Bedrock**: Local Swift package for design system, branding, and cloud sync
|
|
- **MijickCamera**: SwiftUI camera framework for capture and preview
|
|
- **RevenueCat**: Subscription management for premium features
|
|
|
|
### Key Protocols
|
|
|
|
| Protocol | Purpose | Conforming Types |
|
|
|----------|---------|------------------|
|
|
| `RingLightConfigurable` | Ring light settings (size, color, opacity) | `SettingsViewModel` |
|
|
| `CaptureControlling` | Capture actions (timer, flash, shutter) | `SettingsViewModel` |
|
|
| `PremiumManaging` | Subscription state and purchases | `PremiumManager` |
|
|
|
|
|
|
## Premium Features
|
|
|
|
### Adding a New Premium Feature
|
|
|
|
1. **Add setting to `SyncedSettings`** with an appropriate default value
|
|
2. **Use `PremiumGate.get()`** in the getter:
|
|
```swift
|
|
var myPremiumFeature: Bool {
|
|
get { PremiumGate.get(cloudSync.data.myFeature, default: false, isPremium: isPremiumUnlocked) }
|
|
set {
|
|
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
|
|
updateSettings { $0.myFeature = newValue }
|
|
}
|
|
}
|
|
```
|
|
3. **Add crown icon** in the UI to indicate premium status
|
|
4. **Wire up paywall** trigger when non-premium users tap the control
|
|
|
|
### Current Premium Features
|
|
|
|
- Custom ring light colors
|
|
- Premium color presets (Ice Blue, Soft Pink, Warm Amber, Cool Lavender)
|
|
- Flash sync with ring light color
|
|
- HDR mode
|
|
- High quality photos
|
|
- True mirror mode
|
|
- Skin smoothing
|
|
- Center Stage
|
|
- Extended timers (5s, 10s)
|
|
- Video and Boomerang capture modes
|
|
|
|
|
|
## Settings & iCloud Sync
|
|
|
|
### How Settings Work
|
|
|
|
1. All settings are stored in `SyncedSettings` struct
|
|
2. `CloudSyncManager<SyncedSettings>` handles iCloud synchronization
|
|
3. `SettingsViewModel` exposes properties that read/write through the sync manager
|
|
4. Slider values use debounced saves (300ms) to prevent excessive writes
|
|
|
|
### Adding a New Setting
|
|
|
|
1. Add property to `SyncedSettings` with default value
|
|
2. Add corresponding property in `SettingsViewModel`
|
|
3. For premium settings, use `PremiumGate` utilities
|
|
4. Add UI in `SettingsView`
|
|
|
|
|
|
## Branding System
|
|
|
|
### Overview
|
|
|
|
The app uses Bedrock's branding system for:
|
|
- Animated launch screen
|
|
- App icon generation
|
|
- Consistent color scheme
|
|
|
|
### Key Files
|
|
|
|
- `Shared/BrandingConfig.swift` - App icon and launch screen configuration
|
|
- `Resources/Assets.xcassets/LaunchBackground.colorset/` - Launch screen background color
|
|
- `App/SelfieCamApp.swift` - Wraps ContentView with AppLaunchView
|
|
|
|
### Modifying Branding
|
|
|
|
1. Update colors in `BrandingConfig.swift` → `Color.Branding`
|
|
2. Update `LaunchBackground.colorset` to match primary color
|
|
3. Adjust icon/launch screen config as needed
|
|
4. Use Icon Generator in Settings → Debug to create new app icon
|
|
|
|
### Documentation
|
|
|
|
See `Bedrock/Sources/Bedrock/Branding/BRANDING_GUIDE.md` for complete branding documentation.
|
|
|
|
|
|
## Camera Integration
|
|
|
|
### MijickCamera
|
|
|
|
The app uses MijickCamera for camera functionality:
|
|
|
|
```swift
|
|
import MijickCamera
|
|
|
|
// Camera position
|
|
var cameraPosition: CameraPosition // .front or .back
|
|
|
|
// Flash modes
|
|
var flashMode: CameraFlashMode // .off, .on, .auto
|
|
```
|
|
|
|
### Camera Features
|
|
|
|
- Front/back camera switching
|
|
- Pinch-to-zoom
|
|
- Photo capture with quality settings
|
|
- Video recording (premium)
|
|
- HDR mode (premium)
|
|
|
|
|
|
## Ring Light System
|
|
|
|
### How It Works
|
|
|
|
The ring light is a colored overlay (`RingLightOverlay`) that surrounds the camera preview:
|
|
|
|
- **Size**: Adjustable border width (10-120pt)
|
|
- **Color**: Preset colors or custom color picker
|
|
- **Opacity**: Adjustable brightness (10%-100%)
|
|
- **Toggle**: Can be enabled/disabled
|
|
|
|
### Color Presets
|
|
|
|
| Color | ID | Premium |
|
|
|-------|-----|---------|
|
|
| Pure White | `pureWhite` | No |
|
|
| Warm Cream | `warmCream` | No |
|
|
| Ice Blue | `iceBlue` | Yes |
|
|
| Soft Pink | `softPink` | Yes |
|
|
| Warm Amber | `warmAmber` | Yes |
|
|
| Cool Lavender | `coolLavender` | Yes |
|
|
| Custom | `custom` | Yes |
|
|
|
|
|
|
## Documentation Files
|
|
|
|
When making changes, update the appropriate documentation:
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `README.md` | User-facing app overview, setup instructions |
|
|
| `AI_Implementation.md` | Technical architecture, implementation details |
|
|
| `AGENTS.md` | Development guidelines (this file) |
|
|
|
|
Always commit documentation updates with the related code changes.
|