Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
9a938b68c2
commit
a39f2f7bdf
752
AGENTS.md
Normal file
752
AGENTS.md
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Additional Context Files
|
||||||
|
|
||||||
|
Before starting work, read project documentation:
|
||||||
|
|
||||||
|
- `WORKSPACE.md` — (if present) Multi-project workspace overview and project relationships
|
||||||
|
- `README.md` — Project scope, features, and architecture
|
||||||
|
- In multi-project workspaces, each project folder has its own `README.md`
|
||||||
|
|
||||||
|
When making architectural changes, keep documentation files in sync with code changes.
|
||||||
|
|
||||||
|
|
||||||
|
## 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 a min of iOS 17 or later. (Yes, it definitely exists.)
|
||||||
|
- Swift 5 using modern Swift concurrency. Our primary project is not on 6 yet.
|
||||||
|
- SwiftUI backed up by `@Observable` classes for shared data.
|
||||||
|
- **Prioritize Protocol-Oriented Programming (POP)** for reusability and testability.
|
||||||
|
- **Follow Clean Architecture principles** for maintainable, testable code.
|
||||||
|
- Do not introduce third-party frameworks without asking first.
|
||||||
|
- Avoid UIKit unless requested.
|
||||||
|
|
||||||
|
|
||||||
|
## Clean Architecture
|
||||||
|
|
||||||
|
**Separation of concerns is mandatory.** Code should be organized into distinct layers with clear responsibilities and dependencies flowing inward.
|
||||||
|
|
||||||
|
### File Organization Principles
|
||||||
|
|
||||||
|
1. **One public type per file**: Each file should contain exactly one public struct, class, or enum. Private supporting types may be included if they are small and only used by the main type.
|
||||||
|
|
||||||
|
2. **Keep files lean**: Aim for files under 300 lines. If a file exceeds this:
|
||||||
|
- Extract reusable sub-views into separate files in a `Components/` folder
|
||||||
|
- Extract sheets/modals into a `Sheets/` folder
|
||||||
|
- Move complex logic into dedicated types
|
||||||
|
|
||||||
|
3. **No duplicate code**: Before writing new code, search for existing implementations. Extract common patterns into reusable components.
|
||||||
|
|
||||||
|
4. **Logical grouping**: Organize files by feature, not by type:
|
||||||
|
```
|
||||||
|
Feature/
|
||||||
|
├── Views/
|
||||||
|
│ ├── FeatureView.swift
|
||||||
|
│ ├── Components/
|
||||||
|
│ │ ├── FeatureRowView.swift
|
||||||
|
│ │ └── FeatureHeaderView.swift
|
||||||
|
│ └── Sheets/
|
||||||
|
│ └── FeatureEditSheet.swift
|
||||||
|
├── Models/
|
||||||
|
│ └── FeatureModel.swift
|
||||||
|
└── State/
|
||||||
|
└── FeatureStore.swift
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer Responsibilities
|
||||||
|
|
||||||
|
| Layer | Contains | Depends On |
|
||||||
|
|-------|----------|------------|
|
||||||
|
| **Views** | SwiftUI views, UI components | State, Models |
|
||||||
|
| **State** | `@Observable` stores, view models | Models, Services |
|
||||||
|
| **Services** | Business logic, networking, persistence | Models |
|
||||||
|
| **Models** | Data types, entities, DTOs | Nothing |
|
||||||
|
| **Protocols** | Interfaces for services and stores | Models |
|
||||||
|
|
||||||
|
### Architecture Rules
|
||||||
|
|
||||||
|
1. **Views are dumb renderers**: No business logic in views. Views read state and call methods.
|
||||||
|
2. **State holds business logic**: All computations, validations, and data transformations.
|
||||||
|
3. **Services are stateless**: Pure functions where possible. Injected via protocols.
|
||||||
|
4. **Models are simple**: Plain data types. No dependencies on UI or services.
|
||||||
|
|
||||||
|
### Example Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
App/
|
||||||
|
├── Design/ # Design constants, colors, typography
|
||||||
|
├── Localization/ # String helpers
|
||||||
|
├── Models/ # Data models (SwiftData, plain structs)
|
||||||
|
├── Protocols/ # Protocol definitions for DI
|
||||||
|
├── Services/ # Business logic, API clients, persistence
|
||||||
|
├── State/ # Observable stores, app state
|
||||||
|
└── Views/
|
||||||
|
├── Components/ # Reusable UI components
|
||||||
|
├── Sheets/ # Modal presentations
|
||||||
|
└── [Feature]/ # Feature-specific views
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Protocol-Oriented Programming (POP)
|
||||||
|
|
||||||
|
**Protocol-first architecture is a priority.** When designing new features, always think about protocols and composition before concrete implementations.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
1. **Look for duplicated patterns**: Similar logic across files is a candidate for protocol extraction.
|
||||||
|
2. **Identify common interfaces**: Types that expose similar properties/methods should conform to a shared protocol.
|
||||||
|
3. **Check before implementing**: 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., `Shareable`, `DataProviding`, `Persistable`).
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
- **Reusability**: Shared protocols work across 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
|
||||||
|
|
||||||
|
|
||||||
|
## View/State Separation (MVVM-lite)
|
||||||
|
|
||||||
|
**Views should be "dumb" renderers.** All business logic belongs in stores or dedicated view models.
|
||||||
|
|
||||||
|
### What Belongs in State/Store
|
||||||
|
|
||||||
|
- **Business logic**: Calculations, validations, rules
|
||||||
|
- **Computed properties based on data**: Hints, recommendations, derived values
|
||||||
|
- **State checks**: `canSubmit`, `isLoading`, `hasError`
|
||||||
|
- **Data transformations**: Filtering, sorting, aggregations
|
||||||
|
|
||||||
|
### What is Acceptable in Views
|
||||||
|
|
||||||
|
- **Pure UI layout logic**: Adaptive layouts based on size class
|
||||||
|
- **Visual styling**: Color selection based on state
|
||||||
|
- **@ViewBuilder sub-views**: Breaking up complex layouts (keep in same file if small)
|
||||||
|
- **Accessibility labels**: Combining data into accessible descriptions
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BAD - Business logic in view
|
||||||
|
struct MyView: View {
|
||||||
|
@Bindable var state: FeatureState
|
||||||
|
|
||||||
|
private var isValid: Bool {
|
||||||
|
!state.name.isEmpty && state.email.contains("@")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD - Logic in State, view just reads
|
||||||
|
// In FeatureState:
|
||||||
|
var isValid: Bool {
|
||||||
|
!name.isEmpty && email.contains("@")
|
||||||
|
}
|
||||||
|
|
||||||
|
// In View:
|
||||||
|
Button("Save") { state.save() }
|
||||||
|
.disabled(!state.isValid)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- Prefer modern Foundation API (e.g., `URL.documentsDirectory`, `appending(path:)`).
|
||||||
|
- Never use C-style number formatting; use `format:` modifiers instead.
|
||||||
|
- Prefer static member lookup to struct instances (`.circle` not `Circle()`).
|
||||||
|
- Never use old-style GCD; use modern Swift concurrency.
|
||||||
|
- Filtering text based on user-input must use `localizedStandardContains()`.
|
||||||
|
- Avoid force unwraps and force `try` unless 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.
|
||||||
|
- Never use `onChange()` in its 1-parameter variant.
|
||||||
|
- Never use `onTapGesture()` unless you need tap location/count; use `Button`.
|
||||||
|
- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)`.
|
||||||
|
- Never use `UIScreen.main.bounds` to read available space.
|
||||||
|
- Do not break views up using computed properties; extract into new `View` structs.
|
||||||
|
- Do not force specific font sizes; prefer Dynamic Type.
|
||||||
|
- Use `NavigationStack` with `navigationDestination(for:)`.
|
||||||
|
- If using an image for a button label, always specify text alongside.
|
||||||
|
- Prefer `ImageRenderer` to `UIGraphicsImageRenderer`.
|
||||||
|
- Use `bold()` instead of `fontWeight(.bold)`.
|
||||||
|
- Avoid `GeometryReader` if newer alternatives work (e.g., `containerRelativeFrame()`).
|
||||||
|
- When enumerating in `ForEach`, don't convert to Array first.
|
||||||
|
- Hide scroll indicators with `.scrollIndicators(.hidden)`.
|
||||||
|
- Avoid `AnyView` unless absolutely required.
|
||||||
|
- **Never use raw numeric literals** for padding, spacing, opacity, etc.—use Design constants.
|
||||||
|
- **Never use inline colors**—define all colors with semantic names.
|
||||||
|
- Avoid UIKit colors in SwiftUI code.
|
||||||
|
|
||||||
|
|
||||||
|
## watchOS Development (CRITICAL)
|
||||||
|
|
||||||
|
**Read this entire section before implementing any watch functionality.**
|
||||||
|
|
||||||
|
### Creating a Watch Target
|
||||||
|
|
||||||
|
When adding a watchOS target to an existing iOS app:
|
||||||
|
|
||||||
|
1. **File → New → Target → "Watch App for watchOS"**
|
||||||
|
2. Choose **"Watch App for Existing iOS App"** (NOT standalone)
|
||||||
|
3. Name it appropriately (e.g., `AppNameWatch`)
|
||||||
|
4. Xcode creates a folder like `AppNameWatch Watch App/`
|
||||||
|
|
||||||
|
### CRITICAL: Embedding the Watch App
|
||||||
|
|
||||||
|
⚠️ **THIS IS THE #1 CAUSE OF "WATCH APP NOT INSTALLED" ERRORS** ⚠️
|
||||||
|
|
||||||
|
The watch app MUST be embedded in the iOS app for deployment to real devices:
|
||||||
|
|
||||||
|
1. Select the **iOS target** in Xcode
|
||||||
|
2. Go to **Build Phases** tab
|
||||||
|
3. Verify there's an **"Embed Watch Content"** phase
|
||||||
|
4. **CRITICAL**: Ensure **"Code Sign On Copy"** is CHECKED ✓
|
||||||
|
|
||||||
|
If "Embed Watch Content" doesn't exist:
|
||||||
|
1. Click **"+"** → **"New Copy Files Phase"**
|
||||||
|
2. Rename to **"Embed Watch Content"**
|
||||||
|
3. Set **Destination** to **"Products Directory"**
|
||||||
|
4. Set **Subpath** to `$(CONTENTS_FOLDER_PATH)/Watch`
|
||||||
|
5. Add the watch app (e.g., `AppNameWatch Watch App.app`)
|
||||||
|
6. **CHECK "Code Sign On Copy"** ← This is critical!
|
||||||
|
|
||||||
|
Without proper embedding, the iOS app installs but the watch app does NOT install on the paired Apple Watch.
|
||||||
|
|
||||||
|
### Bundle Identifiers
|
||||||
|
|
||||||
|
Watch app bundle IDs MUST be prefixed with the iOS app's bundle ID:
|
||||||
|
|
||||||
|
```
|
||||||
|
iOS app: com.company.AppName
|
||||||
|
Watch app: com.company.AppName.watchkitapp ← MUST start with iOS bundle ID
|
||||||
|
```
|
||||||
|
|
||||||
|
Also verify `WKCompanionAppBundleIdentifier` in the watch target's build settings matches the iOS app's bundle ID exactly.
|
||||||
|
|
||||||
|
### Data Sync: WatchConnectivity (NOT App Groups)
|
||||||
|
|
||||||
|
**DO NOT use App Groups for iPhone ↔ Watch data sharing.**
|
||||||
|
|
||||||
|
App Groups:
|
||||||
|
- ❌ Do NOT work between iPhone and Apple Watch
|
||||||
|
- ❌ Different container paths on each device
|
||||||
|
- ❌ Will waste hours debugging why data isn't syncing
|
||||||
|
- ✅ Only work between an app and its extensions on the SAME device
|
||||||
|
|
||||||
|
**Use WatchConnectivity framework instead:**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// iOS side - WatchConnectivityService.swift
|
||||||
|
import WatchConnectivity
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
|
static let shared = WatchConnectivityService()
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
if WCSession.isSupported() {
|
||||||
|
WCSession.default.delegate = self
|
||||||
|
WCSession.default.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncData(_ data: [String: Any]) {
|
||||||
|
guard WCSession.default.activationState == .activated,
|
||||||
|
WCSession.default.isPaired,
|
||||||
|
WCSession.default.isWatchAppInstalled else { return }
|
||||||
|
|
||||||
|
try? WCSession.default.updateApplicationContext(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WatchConnectivity Methods
|
||||||
|
|
||||||
|
| Method | Use Case |
|
||||||
|
|--------|----------|
|
||||||
|
| `updateApplicationContext` | Latest state that persists (use this for most syncs) |
|
||||||
|
| `sendMessage` | Immediate delivery when counterpart is reachable |
|
||||||
|
| `transferUserInfo` | Queued delivery, guaranteed but not immediate |
|
||||||
|
|
||||||
|
### watchOS Framework Limitations
|
||||||
|
|
||||||
|
These iOS frameworks are NOT available on watchOS:
|
||||||
|
|
||||||
|
- ❌ `CoreImage` - Generate QR codes on iOS, send image data to watch
|
||||||
|
- ❌ `UIKit` (mostly) - Use SwiftUI
|
||||||
|
- ❌ `AVFoundation` (limited)
|
||||||
|
|
||||||
|
### Simulator Limitations
|
||||||
|
|
||||||
|
WatchConnectivity on simulators is **unreliable**:
|
||||||
|
|
||||||
|
- `isWatchAppInstalled` often returns `false` even when running
|
||||||
|
- `isReachable` may be `false` even with both apps running
|
||||||
|
- `updateApplicationContext` may fail with "counterpart not installed"
|
||||||
|
|
||||||
|
**Workarounds for simulator testing:**
|
||||||
|
1. Add `#if targetEnvironment(simulator)` blocks with sample data
|
||||||
|
2. Test real sync functionality on physical devices only
|
||||||
|
|
||||||
|
### Debugging Watch Sync Issues
|
||||||
|
|
||||||
|
If `isWatchAppInstalled` returns `false`:
|
||||||
|
|
||||||
|
1. ✅ Check "Embed Watch Content" build phase exists
|
||||||
|
2. ✅ Check "Code Sign On Copy" is enabled
|
||||||
|
3. ✅ Verify bundle ID is prefixed correctly
|
||||||
|
4. ✅ Clean build folder (⇧⌘K) and rebuild
|
||||||
|
5. ✅ On iPhone, open Watch app → verify app appears under "Installed"
|
||||||
|
|
||||||
|
### NSObject Requirement
|
||||||
|
|
||||||
|
`WCSessionDelegate` is an Objective-C protocol, so conforming classes must inherit from `NSObject`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
|
// NSObject is required for WCSessionDelegate conformance
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## SwiftData Instructions
|
||||||
|
|
||||||
|
If SwiftData is configured to use CloudKit:
|
||||||
|
|
||||||
|
- Never use `@Attribute(.unique)`.
|
||||||
|
- Model properties must have default values or be optional.
|
||||||
|
- All relationships must be marked optional.
|
||||||
|
|
||||||
|
|
||||||
|
## Model Design: Single Source of Truth
|
||||||
|
|
||||||
|
**Computed properties should be the single source of truth for derived data.**
|
||||||
|
|
||||||
|
### Name Fields Pattern
|
||||||
|
|
||||||
|
When a model has multiple name components (prefix, firstName, middleName, lastName, suffix, etc.), use a computed property for the display name:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ✅ GOOD - Computed from individual fields
|
||||||
|
var fullName: String {
|
||||||
|
var parts: [String] = []
|
||||||
|
if !prefix.isEmpty { parts.append(prefix) }
|
||||||
|
if !firstName.isEmpty { parts.append(firstName) }
|
||||||
|
if !lastName.isEmpty { parts.append(lastName) }
|
||||||
|
// ... etc
|
||||||
|
return parts.joined(separator: " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD - Stored displayName that can get out of sync
|
||||||
|
var displayName: String // Never add this
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
- **Always up to date**: Changes to individual fields are immediately reflected
|
||||||
|
- **No sync bugs**: No risk of stored value diverging from component fields
|
||||||
|
- **Simpler code**: No need to update displayName when editing name fields
|
||||||
|
|
||||||
|
### Related Properties
|
||||||
|
|
||||||
|
If you need different formats for different purposes:
|
||||||
|
|
||||||
|
- `fullName` — For display (may include formatting like quotes, parentheses)
|
||||||
|
- `vCardName` — For export (plain format, no special formatting)
|
||||||
|
|
||||||
|
|
||||||
|
## Localization Instructions
|
||||||
|
|
||||||
|
- Use **String Catalogs** (`.xcstrings` files) for localization.
|
||||||
|
- SwiftUI `Text("literal")` views automatically look up strings in the catalog.
|
||||||
|
- For strings outside of `Text` views, use `String(localized:)` or a helper extension.
|
||||||
|
- Store all user-facing strings in the String Catalog.
|
||||||
|
- Support at minimum: English (en), Spanish-Mexico (es-MX), French-Canada (fr-CA).
|
||||||
|
- Never use `NSLocalizedString`; prefer `String(localized:)`.
|
||||||
|
|
||||||
|
|
||||||
|
## Design Constants
|
||||||
|
|
||||||
|
**Never use raw numeric literals or hardcoded colors directly in views.**
|
||||||
|
|
||||||
|
### Values That MUST Be Constants
|
||||||
|
|
||||||
|
- **Spacing & Padding**: `Design.Spacing.medium` not `.padding(12)`
|
||||||
|
- **Corner Radii**: `Design.CornerRadius.large` not `cornerRadius: 16`
|
||||||
|
- **Font Sizes**: `Design.BaseFontSize.body` not `size: 14`
|
||||||
|
- **Opacity Values**: `Design.Opacity.strong` not `.opacity(0.7)`
|
||||||
|
- **Colors**: `Color.Primary.accent` not `Color(red:green:blue:)`
|
||||||
|
- **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.avatar` not `frame(width: 56)`
|
||||||
|
|
||||||
|
### Organization
|
||||||
|
|
||||||
|
- Create a `DesignConstants.swift` file using enums for namespacing.
|
||||||
|
- Extend `Color` with semantic color definitions.
|
||||||
|
- View-specific constants go at the top of the view struct with a comment.
|
||||||
|
- Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`.
|
||||||
|
|
||||||
|
|
||||||
|
## App Identifiers (xcconfig)
|
||||||
|
|
||||||
|
**Centralize all company-specific identifiers** using xcconfig files for true single-source configuration. This enables one-line migration between developer accounts.
|
||||||
|
|
||||||
|
### Why xcconfig?
|
||||||
|
|
||||||
|
- **Single source of truth**: Change one file, everything updates
|
||||||
|
- **Build-time resolution**: Bundle IDs, entitlements, and Swift code all derive from same source
|
||||||
|
- **No manual updates**: Entitlements use variable substitution
|
||||||
|
- **Environment support**: Easy Debug/Release/Staging configurations
|
||||||
|
|
||||||
|
### Setup Instructions
|
||||||
|
|
||||||
|
#### Step 1: Create xcconfig Files
|
||||||
|
|
||||||
|
Create `Configuration/Base.xcconfig`:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Base.xcconfig - Source of truth for all identifiers
|
||||||
|
// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
COMPANY_IDENTIFIER = com.yourcompany
|
||||||
|
APP_NAME = YourAppName
|
||||||
|
DEVELOPMENT_TEAM = YOUR_TEAM_ID
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DERIVED IDENTIFIERS - DO NOT EDIT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)
|
||||||
|
WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp
|
||||||
|
APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip
|
||||||
|
TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests
|
||||||
|
UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests
|
||||||
|
|
||||||
|
APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME)
|
||||||
|
CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME)
|
||||||
|
|
||||||
|
APPCLIP_DOMAIN = yourapp.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `Configuration/Debug.xcconfig`:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Debug.xcconfig
|
||||||
|
#include "Base.xcconfig"
|
||||||
|
// Add debug-specific settings here
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `Configuration/Release.xcconfig`:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Release.xcconfig
|
||||||
|
#include "Base.xcconfig"
|
||||||
|
// Add release-specific settings here
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Configure Xcode Project
|
||||||
|
|
||||||
|
In `project.pbxproj`, add file references and set `baseConfigurationReference` for each build configuration:
|
||||||
|
|
||||||
|
**1. Add xcconfig file references to PBXFileReference section:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/* Use SOURCE_ROOT and full path from project root */
|
||||||
|
EACONFIG001 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Base.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||||
|
EACONFIG002 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||||
|
EACONFIG003 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT**: Use `sourceTree = SOURCE_ROOT` (not `"<group>"`) and include the full path from project root (e.g., `AppName/Configuration/Base.xcconfig`).
|
||||||
|
|
||||||
|
**2. Set `baseConfigurationReference` on project-level Debug/Release configurations:**
|
||||||
|
|
||||||
|
```
|
||||||
|
EA123456 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = EACONFIG002 /* Debug.xcconfig */;
|
||||||
|
buildSettings = { ... };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Replace hardcoded values with variables:**
|
||||||
|
|
||||||
|
```
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
|
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Update Entitlements
|
||||||
|
|
||||||
|
Use variable substitution in `.entitlements` files:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Bridge to Swift via Info.plist
|
||||||
|
|
||||||
|
Add keys to `Info.plist` that bridge xcconfig values to Swift:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>AppGroupIdentifier</key>
|
||||||
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
<key>CloudKitContainerIdentifier</key>
|
||||||
|
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
||||||
|
<key>AppClipDomain</key>
|
||||||
|
<string>$(APPCLIP_DOMAIN)</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 5: Create Swift Interface
|
||||||
|
|
||||||
|
**Why this is needed:** Swift code cannot read xcconfig files directly. The xcconfig values flow through Info.plist, and this Swift file provides a clean API to access them at runtime. Without this file, you'd have to call `Bundle.main.object(forInfoDictionaryKey:)` everywhere you need an identifier.
|
||||||
|
|
||||||
|
**When to use:** Any Swift code that needs App Group identifiers, CloudKit containers, custom domains, or other configuration values must use `AppIdentifiers.*` instead of hardcoding strings.
|
||||||
|
|
||||||
|
Create `Configuration/AppIdentifiers.swift`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum AppIdentifiers {
|
||||||
|
// Read from Info.plist (values come from xcconfig)
|
||||||
|
static let appGroupIdentifier: String = {
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String
|
||||||
|
?? "group.com.yourcompany.AppName"
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let cloudKitContainerIdentifier: String = {
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String
|
||||||
|
?? "iCloud.com.yourcompany.AppName"
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let appClipDomain: String = {
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String
|
||||||
|
?? "yourapp.example.com"
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Derived from bundle identifier
|
||||||
|
static var bundleIdentifier: String {
|
||||||
|
Bundle.main.bundleIdentifier ?? "com.yourcompany.AppName"
|
||||||
|
}
|
||||||
|
|
||||||
|
static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" }
|
||||||
|
static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" }
|
||||||
|
|
||||||
|
static func appClipURL(recordName: String) -> URL? {
|
||||||
|
URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Base.xcconfig (source of truth)
|
||||||
|
↓
|
||||||
|
project.pbxproj (baseConfigurationReference)
|
||||||
|
↓
|
||||||
|
Build Settings → Bundle IDs, Team ID, etc.
|
||||||
|
↓
|
||||||
|
Info.plist (bridges values via $(VARIABLE))
|
||||||
|
↓
|
||||||
|
AppIdentifiers.swift (Swift reads from Bundle.main)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage in Code
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Always use AppIdentifiers instead of hardcoding
|
||||||
|
FileManager.default.containerURL(
|
||||||
|
forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
|
CKContainer(identifier: AppIdentifiers.cloudKitContainerIdentifier)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Targets
|
||||||
|
|
||||||
|
When adding new targets (Widgets, Intents, App Clips, etc.), follow this pattern:
|
||||||
|
|
||||||
|
#### 1. Add Bundle ID Variable to Base.xcconfig
|
||||||
|
|
||||||
|
```
|
||||||
|
// In Base.xcconfig, add new derived identifier
|
||||||
|
WIDGET_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Widget
|
||||||
|
INTENT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Intent
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Set Target to Use xcconfig
|
||||||
|
|
||||||
|
For the new target's Debug/Release configurations in `project.pbxproj`:
|
||||||
|
|
||||||
|
```
|
||||||
|
EA_NEW_TARGET_DEBUG /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = EACONFIG002 /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)";
|
||||||
|
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||||
|
// ... other settings
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Configure Entitlements (if needed)
|
||||||
|
|
||||||
|
If the target needs App Groups or CloudKit access, create an entitlements file using variables:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- WidgetExtension.entitlements -->
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Share Code via App Groups
|
||||||
|
|
||||||
|
Extensions must use App Groups to share data with the main app:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// In extension code
|
||||||
|
let sharedDefaults = UserDefaults(suiteName: AppIdentifiers.appGroupIdentifier)
|
||||||
|
let containerURL = FileManager.default.containerURL(
|
||||||
|
forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Update AppIdentifiers.swift (if needed)
|
||||||
|
|
||||||
|
Add new computed properties for target-specific identifiers:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
static var widgetBundleIdentifier: String { "\(bundleIdentifier).Widget" }
|
||||||
|
static var intentBundleIdentifier: String { "\(bundleIdentifier).Intent" }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Common Target Types and Bundle ID Patterns
|
||||||
|
|
||||||
|
| Target Type | Bundle ID Variable | Example Value |
|
||||||
|
|-------------|-------------------|---------------|
|
||||||
|
| Widget Extension | `WIDGET_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Widget` |
|
||||||
|
| Intent Extension | `INTENT_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Intent` |
|
||||||
|
| App Clip | `APPCLIP_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Clip` |
|
||||||
|
| Watch App | `WATCH_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).watchkitapp` |
|
||||||
|
| Notification Extension | `NOTIFICATION_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).NotificationExtension` |
|
||||||
|
| Share Extension | `SHARE_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).ShareExtension` |
|
||||||
|
|
||||||
|
#### Checklist for New Targets
|
||||||
|
|
||||||
|
- [ ] Add bundle ID variable to `Base.xcconfig`
|
||||||
|
- [ ] Set `baseConfigurationReference` to Debug/Release xcconfig
|
||||||
|
- [ ] Use `$(VARIABLE)` for `PRODUCT_BUNDLE_IDENTIFIER`
|
||||||
|
- [ ] Use `$(DEVELOPMENT_TEAM)` for team
|
||||||
|
- [ ] Create entitlements with `$(APP_GROUP_IDENTIFIER)` if sharing data
|
||||||
|
- [ ] Add to `AppIdentifiers.swift` if Swift code needs the identifier
|
||||||
|
- [ ] Register App ID in Apple Developer Portal (uses same App Group)
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
To migrate to a new developer account, edit **one file** (`Base.xcconfig`):
|
||||||
|
|
||||||
|
```
|
||||||
|
COMPANY_IDENTIFIER = com.newcompany
|
||||||
|
DEVELOPMENT_TEAM = NEW_TEAM_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
Then clean build (⇧⌘K) and rebuild. Everything updates automatically—including all extension targets.
|
||||||
|
|
||||||
|
|
||||||
|
## Dynamic Type Instructions
|
||||||
|
|
||||||
|
- Always support Dynamic Type for accessibility.
|
||||||
|
- Use `@ScaledMetric` to scale custom dimensions.
|
||||||
|
- Choose appropriate `relativeTo` text styles based on semantic purpose.
|
||||||
|
- For constrained UI elements, you may use fixed sizes but document the reason.
|
||||||
|
- Prefer system text styles: `.font(.body)`, `.font(.title)`, `.font(.caption)`.
|
||||||
|
|
||||||
|
|
||||||
|
## VoiceOver Accessibility Instructions
|
||||||
|
|
||||||
|
- All interactive elements must have meaningful `.accessibilityLabel()`.
|
||||||
|
- Use `.accessibilityValue()` for dynamic state.
|
||||||
|
- Use `.accessibilityHint()` to describe what happens on interaction.
|
||||||
|
- Use `.accessibilityAddTraits()` for element type.
|
||||||
|
- Hide decorative elements with `.accessibilityHidden(true)`.
|
||||||
|
- Group related elements to reduce navigation complexity.
|
||||||
|
- Post accessibility announcements for important events.
|
||||||
|
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- Use a consistent project structure organized by feature.
|
||||||
|
- Follow strict naming conventions for types, properties, and methods.
|
||||||
|
- **One public type per file**—break types into separate files.
|
||||||
|
- Write unit tests for core application logic.
|
||||||
|
- Only write UI tests if unit tests are not possible.
|
||||||
|
- Add code comments and documentation as needed.
|
||||||
|
- Never include secrets or API keys in the repository.
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation Instructions
|
||||||
|
|
||||||
|
- **Keep `README.md` files up to date** when adding new functionality.
|
||||||
|
- In multi-project workspaces, update the relevant project's `README.md`.
|
||||||
|
- Document new features, settings, or mechanics in the appropriate README.
|
||||||
|
- Update documentation when modifying existing behavior.
|
||||||
|
- Include configuration options and special interactions.
|
||||||
|
- README updates should be part of the same commit as the feature.
|
||||||
|
|
||||||
|
|
||||||
|
## PR Instructions
|
||||||
|
|
||||||
|
- If installed, ensure SwiftLint returns no warnings or errors.
|
||||||
|
- Verify that documentation reflects any new functionality.
|
||||||
|
- Check for duplicate code before submitting.
|
||||||
|
- Ensure all new files follow the one-type-per-file rule.
|
||||||
20
SecureStorageSample.code-workspace
Normal file
20
SecureStorageSample.code-workspace
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "localPackages/SharedPackage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../_Packages/LocalData"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.enablePersistentSessions": true,
|
||||||
|
"terminal.integrated.persistentSessionReviveProcess": "onExitAndWindowClose",
|
||||||
|
"task.allowAutomaticTasks": "off",
|
||||||
|
"swift.disableAutoResolve": true,
|
||||||
|
"swift.disableSwiftPackageManagerIntegration": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@
|
|||||||
EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; };
|
EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; };
|
||||||
EA65D9452F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; };
|
EA65D9452F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; };
|
||||||
|
EA756CEA2F465F31006196BB /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA756CE92F465F31006196BB /* LocalData */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -122,6 +123,7 @@
|
|||||||
files = (
|
files = (
|
||||||
EA2390152F18361C00AC8894 /* SharedKit in Frameworks */,
|
EA2390152F18361C00AC8894 /* SharedKit in Frameworks */,
|
||||||
EA238DFB2F1832C600AC8894 /* SharedKit in Frameworks */,
|
EA238DFB2F1832C600AC8894 /* SharedKit in Frameworks */,
|
||||||
|
EA756CEA2F465F31006196BB /* LocalData in Frameworks */,
|
||||||
EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */,
|
EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */,
|
||||||
EA179D562F17379800B1D54A /* LocalData in Frameworks */,
|
EA179D562F17379800B1D54A /* LocalData in Frameworks */,
|
||||||
EA238DF82F18328100AC8894 /* LocalData in Frameworks */,
|
EA238DF82F18328100AC8894 /* LocalData in Frameworks */,
|
||||||
@ -241,6 +243,7 @@
|
|||||||
EA238DF72F18328100AC8894 /* LocalData */,
|
EA238DF72F18328100AC8894 /* LocalData */,
|
||||||
EA238DFA2F1832C600AC8894 /* SharedKit */,
|
EA238DFA2F1832C600AC8894 /* SharedKit */,
|
||||||
EA2390142F18361C00AC8894 /* SharedKit */,
|
EA2390142F18361C00AC8894 /* SharedKit */,
|
||||||
|
EA756CE92F465F31006196BB /* LocalData */,
|
||||||
);
|
);
|
||||||
productName = SecureStorageSample;
|
productName = SecureStorageSample;
|
||||||
productReference = EA179D012F1722BB00B1D54A /* SecureStorageSample.app */;
|
productReference = EA179D012F1722BB00B1D54A /* SecureStorageSample.app */;
|
||||||
@ -406,8 +409,8 @@
|
|||||||
mainGroup = EA179CF82F1722BB00B1D54A;
|
mainGroup = EA179CF82F1722BB00B1D54A;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
EA238DF62F18328100AC8894 /* XCLocalSwiftPackageReference "../remotePackages/LocalData" */,
|
|
||||||
EA2390132F18361C00AC8894 /* XCLocalSwiftPackageReference "localPackages/SharedPackage" */,
|
EA2390132F18361C00AC8894 /* XCLocalSwiftPackageReference "localPackages/SharedPackage" */,
|
||||||
|
EA756CE82F465F31006196BB /* XCLocalSwiftPackageReference "../../_Packages/LocalData" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = EA179D022F1722BB00B1D54A /* Products */;
|
productRefGroup = EA179D022F1722BB00B1D54A /* Products */;
|
||||||
@ -579,6 +582,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -643,6 +647,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -979,14 +984,14 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
EA238DF62F18328100AC8894 /* XCLocalSwiftPackageReference "../remotePackages/LocalData" */ = {
|
|
||||||
isa = XCLocalSwiftPackageReference;
|
|
||||||
relativePath = ../remotePackages/LocalData;
|
|
||||||
};
|
|
||||||
EA2390132F18361C00AC8894 /* XCLocalSwiftPackageReference "localPackages/SharedPackage" */ = {
|
EA2390132F18361C00AC8894 /* XCLocalSwiftPackageReference "localPackages/SharedPackage" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = localPackages/SharedPackage;
|
relativePath = localPackages/SharedPackage;
|
||||||
};
|
};
|
||||||
|
EA756CE82F465F31006196BB /* XCLocalSwiftPackageReference "../../_Packages/LocalData" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = ../../_Packages/LocalData;
|
||||||
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@ -1015,6 +1020,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = SharedKit;
|
productName = SharedKit;
|
||||||
};
|
};
|
||||||
|
EA756CE92F465F31006196BB /* LocalData */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = LocalData;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = EA179CF92F1722BB00B1D54A /* Project object */;
|
rootObject = EA179CF92F1722BB00B1D54A /* Project object */;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user