蜂鸟Pro v2.0.1 - 基础框架版本 (待完善)
## 当前状态 - 插件界面已完成重命名 (cursorpro → hummingbird) - 双账号池 UI 已实现 (Auto/Pro 卡片) - 后端已切换到 MySQL 数据库 - 添加了 Cursor 官方用量 API 文档 ## 已知问题 (待修复) 1. 激活时检查账号导致无账号时激活失败 2. 未启用无感换号时不应获取账号 3. 账号用量模块不显示 (seamless 未启用时应隐藏) 4. 积分显示为 0 (后端未正确返回) 5. Auto/Pro 双密钥逻辑混乱,状态不同步 6. 账号添加后无自动分析功能 ## 下一版本计划 - 重构数据模型,优化账号状态管理 - 实现 Cursor API 自动分析账号 - 修复激活流程,不依赖账号 - 启用无感时才分配账号 - 完善账号用量实时显示 ## 文件说明 - docs/系统设计文档.md - 完整架构设计 - cursor 官方用量接口.md - Cursor API 文档 - 参考计费/ - Vibeviewer 开源项目参考 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
47
参考计费/.cursor/commands/release_version.md
Normal file
47
参考计费/.cursor/commands/release_version.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Release Version Command
|
||||
|
||||
## Description
|
||||
Automatically bump version number, build DMG package, create GitHub PR and release with English descriptions.
|
||||
|
||||
## Usage
|
||||
```
|
||||
@release_version [version_type]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
- `version_type` (optional): Type of version bump
|
||||
- `patch` (default): 1.1.1 → 1.1.2
|
||||
- `minor`: 1.1.1 → 1.2.0
|
||||
- `major`: 1.1.1 → 2.0.0
|
||||
|
||||
## Examples
|
||||
```
|
||||
@release_version
|
||||
@release_version patch
|
||||
@release_version minor
|
||||
@release_version major
|
||||
```
|
||||
|
||||
## What it does
|
||||
1. **Version Bump**: Updates version in `Scripts/create_dmg.sh` and `Derived/InfoPlists/Vibeviewer-Info.plist`
|
||||
2. **Build DMG**: Runs `make dmg` to create installation package
|
||||
3. **Git Operations**: Commits changes and pushes to current branch
|
||||
4. **Create PR**: Creates GitHub PR with English description
|
||||
5. **Create Release**: Creates GitHub release with DMG attachment and English release notes
|
||||
|
||||
## Prerequisites
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Current branch pushed to remote
|
||||
- Make sure you're in the project root directory
|
||||
|
||||
## Output
|
||||
- Updated version files
|
||||
- Built DMG package
|
||||
- GitHub PR link
|
||||
- GitHub Release link
|
||||
|
||||
## Notes
|
||||
- The command will automatically detect the current version and increment accordingly
|
||||
- All descriptions will be in English
|
||||
- The DMG file will be automatically attached to the release
|
||||
- Make sure you have write permissions to the repository
|
||||
18
参考计费/.cursor/mcp.json
Normal file
18
参考计费/.cursor/mcp.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"tuist": {
|
||||
"command": "/opt/homebrew/bin/tuist",
|
||||
"args": [
|
||||
"mcp",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"XcodeBuildMCP": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"xcodebuildmcp@latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
149
参考计费/.cursor/rules/api_guideline.mdc
Normal file
149
参考计费/.cursor/rules/api_guideline.mdc
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
# API Authoring Guidelines (VibeviewerAPI)
|
||||
|
||||
## Goals
|
||||
- Unify API naming, directories, abstractions, dependency injection, and decoding patterns
|
||||
- Keep all APIs in a single module `VibeviewerAPI` to enforce isolation and modularity
|
||||
- Standardize `DecodableTargetType` and the `HttpClient.decodableRequest(_:)` usage (async/await only) on top of Moya/Alamofire
|
||||
|
||||
## Hard rules
|
||||
- API targets must be declared with `struct` (no `enum`/case-style targets)
|
||||
- Use async/await-only decoding; callback-based styles are forbidden
|
||||
- Separate API declarations from model declarations:
|
||||
- API Targets/Services → `VibeviewerAPI`
|
||||
- Data models/aggregations → `VibeviewerModel`
|
||||
- Views/upper layers must use `Service` protocols via dependency injection, and must not call API targets or `HttpClient` directly
|
||||
- The API module only exposes `Service` protocols and default implementations; API targets, networking details, and common header configuration remain internal
|
||||
|
||||
## Dependencies & imports
|
||||
- API module imports only:
|
||||
- `Foundation`
|
||||
- `Moya`
|
||||
- `Alamofire` (used via `HttpClient`)
|
||||
- `VibeviewerModel`
|
||||
- Never import UI frameworks in the API module (`SwiftUI`/`AppKit`/`UIKit`)
|
||||
|
||||
## Naming conventions
|
||||
- Targets: Feature name + `API`, e.g., `YourFeatureAPI`
|
||||
- Protocols: `YourFeatureService`
|
||||
- Default implementations: `DefaultYourFeatureService`
|
||||
- Models: `YourFeatureResponse`, `YourFeatureDetail`, etc.
|
||||
|
||||
## Directory structure (VibeviewerAPI)
|
||||
```text
|
||||
VibeviewerAPI/
|
||||
Sources/VibeviewerAPI/
|
||||
Mapping/
|
||||
... DTOs & Mappers
|
||||
Plugins/
|
||||
RequestHeaderConfigurationPlugin.swift
|
||||
RequestErrorHandlingPlugin.swift
|
||||
SimpleNetworkLoggerPlugin.swift
|
||||
Service/
|
||||
MoyaProvider+DecodableRequest.swift
|
||||
HttpClient.swift # Unified Moya provider & session wrapper
|
||||
HttpClientError.swift
|
||||
Targets/
|
||||
CursorGetMeAPI.swift # internal target
|
||||
CursorUsageAPI.swift # internal target
|
||||
CursorTeamSpendAPI.swift # internal target
|
||||
CursorService.swift # public protocol + default implementation (service only)
|
||||
```
|
||||
|
||||
## Target and decoding conventions
|
||||
- Targets conform to `DecodableTargetType`:
|
||||
- `associatedtype ResultType: Decodable`
|
||||
- `var decodeAtKeyPath: String? { get }` (default `nil`)
|
||||
- Implement `baseURL`, `path`, `method`, `task`, `headers`, `sampleData`
|
||||
- Avoid overriding `validationType` unless necessary
|
||||
|
||||
Example:
|
||||
```swift
|
||||
import Foundation
|
||||
import Moya
|
||||
import VibeviewerModel
|
||||
|
||||
struct UserProfileDetailAPI: DecodableTargetType {
|
||||
typealias ResultType = UserProfileResponse
|
||||
|
||||
let userId: String
|
||||
|
||||
var baseURL: URL { APIConfig.baseURL }
|
||||
var path: String { "/users/\(userId)" }
|
||||
var method: Moya.Method { .get }
|
||||
var task: Task { .requestPlain }
|
||||
var headers: [String: String]? { APIHeadersBuilder.basicHeaders(cookieHeader: nil) }
|
||||
var sampleData: Data { Data("{\"id\":\"1\",\"name\":\"foo\"}".utf8) }
|
||||
}
|
||||
```
|
||||
|
||||
## Service abstraction & dependency injection
|
||||
- Expose protocol + default implementation (expose services only; hide networking details)
|
||||
- The default `public init(decoding:)` must not leak internal protocol types; provide `internal init(network:decoding:)` for test injection
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import Moya
|
||||
import VibeviewerModel
|
||||
|
||||
public protocol UserProfileService {
|
||||
func fetchDetail(userId: String) async throws -> UserProfileResponse
|
||||
}
|
||||
|
||||
public struct DefaultUserProfileService: UserProfileService {
|
||||
private let network: NetworkClient
|
||||
private let decoding: JSONDecoder.KeyDecodingStrategy
|
||||
|
||||
// Business-facing: do not expose internal NetworkClient abstraction
|
||||
public init(decoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) {
|
||||
self.network = DefaultNetworkClient()
|
||||
self.decoding = decoding
|
||||
}
|
||||
|
||||
// Test injection: available within the API module (same package or @testable)
|
||||
init(network: any NetworkClient, decoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) {
|
||||
self.network = network
|
||||
self.decoding = decoding
|
||||
}
|
||||
|
||||
public func fetchDetail(userId: String) async throws -> UserProfileResponse {
|
||||
try await network.decodableRequest(
|
||||
UserProfileDetailAPI(userId: userId),
|
||||
decodingStrategy: decoding
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note: `DefaultNetworkClient`, the `NetworkClient` protocol, and the concrete `HttpClient` implementation details remain `internal` and are not exposed.
|
||||
|
||||
## View usage (dependency injection)
|
||||
Views must not call API targets or `HttpClient` directly. Use injected services instead:
|
||||
```swift
|
||||
import VibeviewerAPI
|
||||
import VibeviewerModel
|
||||
|
||||
let service: UserProfileService = DefaultUserProfileService()
|
||||
let model = try await service.fetchDetail(userId: "1")
|
||||
```
|
||||
|
||||
## Error handling & logging
|
||||
- Enable `SimpleNetworkLoggerPlugin` by default to log requests/responses
|
||||
- Enable `RequestErrorHandlingPlugin` by default:
|
||||
- Timeouts/offline → unified handling
|
||||
- Customizable via strategy protocols
|
||||
|
||||
## Testing & mock conventions
|
||||
- Within the `VibeviewerAPI` module, inject a `FakeNetworkClient` via `internal init(network:decoding:)` to replace real networking
|
||||
- Provide `sampleData` for each target; prefer minimal realistic JSON to ensure robust decoding
|
||||
- Use `@testable import VibeviewerAPI` to access internal symbols when external tests are required
|
||||
|
||||
## Alignment with modular architecture (architecture.mdc)
|
||||
- Do not import UI frameworks in the API module
|
||||
- Expose only `Service` protocols and default implementations; hide targets and networking details
|
||||
- Dependency direction: `VibeviewerModel` ← `VibeviewerAPI` ← `VibeviewerFeature`
|
||||
- Strict “one file, one type/responsibility”; clear feature aggregation; one-way dependencies
|
||||
|
||||
|
||||
108
参考计费/.cursor/rules/architecture.mdc
Normal file
108
参考计费/.cursor/rules/architecture.mdc
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
title: Vibeviewer Architecture Guidelines
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
- The project uses a layered, modular Swift Package architecture with goals: minimal public surface, one-way dependencies, single responsibility, testability, and replaceability.
|
||||
- Layers and dependency direction (top-down only):
|
||||
- Core/Shared → common utilities and extensions (no business-layer dependencies)
|
||||
- Model → pure data/DTO/domain entities (may depend on Core)
|
||||
- API/Service → networking/IO/3rd-party orchestration and DTO→domain mapping (depends on Model + 3rd-party)
|
||||
- Feature/UI → SwiftUI views and interactions (depends on API-exposed service protocols and domain models; must not depend on networking libraries)
|
||||
- Architectural style: Native SwiftUI MV (not MVVM). State via @State/@Observable; dependency injection via @Environment; concurrency with async/await and @MainActor.
|
||||
|
||||
## Do (Recommended)
|
||||
|
||||
- Module placement & responsibilities
|
||||
- Before adding code, decide whether it belongs to UI/Service/Model/Core and place it in the corresponding package/directory; one type/responsibility per file.
|
||||
- The API layer exposes only “service protocol + default implementation”; networking library/targets/plugins are encapsulated internally.
|
||||
- Service functions return domain models (Model-layer entities) or clear error types; avoid leaking DTOs to the UI.
|
||||
|
||||
- Domain models & mapping
|
||||
- Abstract API response DTOs into domain entities (e.g., UserProfile / UsageOverview / TeamSpendOverview / UsageEvent / FilteredUsageHistory).
|
||||
- Perform DTO→domain mapping in the API layer; UI consumes domain-only.
|
||||
|
||||
- Dependencies & visibility
|
||||
- One-way: Core ← Model ← API ← Feature.
|
||||
- Default to internal; use public only for cross-package use; prefer protocols over concrete types.
|
||||
|
||||
- SwiftUI & concurrency
|
||||
- Inject services via @Environment; place side effects in .task / .onChange so they automatically cancel with the view lifecycle.
|
||||
- UI updates occur on @MainActor; networking/IO on background using async/await; cross-isolation types must be Sendable.
|
||||
|
||||
- Testing & replaceability
|
||||
- Provide an injectable network client interface for services; separate default implementation from testable construction paths.
|
||||
- Put utilities/algorithms into Core; prefer pure functions for unit testing and reuse.
|
||||
|
||||
- Troble Shooting
|
||||
- if you facing an lint error by "can't not found xxx in scope" when you edit/new/delete some interface on Package, that means you need to call XCodeBuildMCP to rebuild that package, so that other package can update the codebase to fix that error
|
||||
|
||||
## Don't (Avoid)
|
||||
|
||||
- UI directly depending on networking libraries, triggering requests, or being exposed to backend error details.
|
||||
- Feature depending on API internals (e.g., Targets/Plugins/concrete networking implementations).
|
||||
- Exposing API DTOs directly to the UI (causes global coupling and fragility).
|
||||
- Reverse dependencies (e.g., Model depends on Feature; API depends on UI).
|
||||
- Introducing MVVM/ViewModel as the default; or using Task { } in onAppear (use .task instead).
|
||||
- Overusing public types/initializers; placing multiple unrelated types in one file.
|
||||
|
||||
## Review checklist
|
||||
|
||||
1) Quadrant self-check (placement)
|
||||
- UI/interaction/rendering → Feature/UI
|
||||
- Networking/disk/auth/3rd-party → API/Service
|
||||
- Pure data/DTO/state aggregation → Model
|
||||
- Utilities/extensions/algorithms → Core
|
||||
|
||||
2) Surface area & replaceability
|
||||
- Can it be exposed via protocol to hide details? Is internal sufficient by default?
|
||||
- Do services return only domain models/error enums? Is it easy to replace/mock?
|
||||
|
||||
3) Dependency direction & coupling
|
||||
- Any violation of Core ← Model ← API ← Feature one-way dependency?
|
||||
- Does the UI still reference DTOs or networking implementations? If yes, move mapping/abstraction to the API layer.
|
||||
|
||||
4) Concurrency & thread safety
|
||||
- Are UI updates on @MainActor? Are cross-isolation types Sendable? Are we using async/await?
|
||||
- Should serialization-required persistence/cache be placed within an Actor boundary?
|
||||
|
||||
5) File organization & naming
|
||||
- Clear directories (Feature/Views, API/Service, API/Targets, API/Plugins, Model/Entities, Core/Extensions).
|
||||
- One type per file; names reflect layer and responsibility (e.g., FeatureXView, FeatureXService, GetYAPI, ZResponse).
|
||||
- Package directory structure: Sources/<PackageName>/ organized by feature subfolders; avoid dumping all source at one level.
|
||||
- Suggested subfolders:
|
||||
- API: Service / Targets / Plugins / Mapping (DTO→Domain mapping)
|
||||
- Feature: Views / Components / Scenes / Modifiers
|
||||
- Model: Entities
|
||||
- Core: Extensions / Utils
|
||||
- Consistent naming: use a shared prefix/suffix for similar features for discoverability.
|
||||
- Suffix examples: …Service, …API, …Response, …Request, …View, …Section, …Window, …Plugin, …Mapper.
|
||||
- Use a consistent domain/vendor prefix where needed (e.g., Cursor…).
|
||||
- File name equals type name: each file contains only one primary type; exact case-sensitive match.
|
||||
- Protocol/implementation convention: protocol uses FooService; default implementation uses DefaultFooService (or LiveFooService). Expose only protocols and inject implementations.
|
||||
|
||||
- Model-layer naming (Entities vs DTOs):
|
||||
- Entities (exposed to business/UI):
|
||||
- Use domain-oriented neutral nouns; avoid vendor prefixes by default (e.g., UserProfile, UsageOverview, TeamSpendOverview, UsageEvent, FilteredUsageHistory, AppSettings, Credentials, DashboardSnapshot).
|
||||
- If source domain must be shown (e.g., “Cursor”), use a consistent prefix within that domain (e.g., CursorCredentials, CursorDashboardSnapshot) for consistency and discoverability.
|
||||
- Suggested suffixes: …Overview, …Snapshot, …History, …Event, …Member, …RoleCount.
|
||||
- Prefer struct, value semantics, and Sendable; expose public types/members only when needed cross-package.
|
||||
- File name equals type name; single-type files.
|
||||
- DTOs (API layer only, under API/Mapping/DTOs):
|
||||
- Use vendor/source prefix + semantic suffix: e.g., Cursor…Request, Cursor…Response, Cursor…Event.
|
||||
- Default visibility is internal; do not expose to Feature/UI; map to domain in the API layer only.
|
||||
- File name equals type name; single-type files; field names mirror backend responses (literal), adapted to domain naming via mapping.
|
||||
- Mapping lives in the API layer (Service/Mapping); UI/Feature must never depend on DTOs.
|
||||
|
||||
## Pre-PR checks
|
||||
- Remove unnecessary public modifiers; check for reverse dependencies across layers.
|
||||
- Ensure UI injects services via @Environment and contains no networking details.
|
||||
- Ensure DTO→domain mapping is complete, robust, and testable.
|
||||
|
||||
Note: When using iOS 26 features, follow availability checks and progressive enhancement; ensure reasonable fallbacks for older OS versions.
|
||||
|
||||
## FAQ
|
||||
- After adding/removing module code, if lint reports a missing class but you are sure it exists, rebuild the package with XcodeBuild MCP and try again.
|
||||
|
||||
738
参考计费/.cursor/rules/project.mdc
Normal file
738
参考计费/.cursor/rules/project.mdc
Normal file
@@ -0,0 +1,738 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# Project Overview
|
||||
|
||||
> 参见 Tuist/模块化细节与常见问题排查:`.cursor/rules/tuist.mdc`
|
||||
|
||||
This is a native **MacOS MenuBar application** built with **Swift 6.1+** and **SwiftUI**. The codebase targets **iOS 18.0 and later**, allowing full use of modern Swift and iOS APIs. All concurrency is handled with **Swift Concurrency** (async/await, actors, @MainActor isolation) ensuring thread-safe code.
|
||||
|
||||
- **Frameworks & Tech:** SwiftUI for UI, Swift Concurrency with strict mode, Swift Package Manager for modular architecture
|
||||
- **Architecture:** Model-View (MV) pattern using pure SwiftUI state management. We avoid MVVM and instead leverage SwiftUI's built-in state mechanisms (@State, @Observable, @Environment, @Binding)
|
||||
- **Testing:** Swift Testing framework with modern @Test macros and #expect/#require assertions
|
||||
- **Platform:** iOS (Simulator and Device)
|
||||
- **Accessibility:** Full accessibility support using SwiftUI's accessibility modifiers
|
||||
|
||||
## Project Structure
|
||||
|
||||
The project follows a **workspace + SPM package** architecture:
|
||||
|
||||
```
|
||||
YourApp/
|
||||
├── Config/ # XCConfig build settings
|
||||
│ ├── Debug.xcconfig
|
||||
│ ├── Release.xcconfig
|
||||
│ ├── Shared.xcconfig
|
||||
│ └── Tests.xcconfig
|
||||
├── YourApp.xcworkspace/ # Workspace container
|
||||
├── YourApp.xcodeproj/ # App shell (minimal wrapper)
|
||||
├── YourApp/ # App target - just the entry point
|
||||
│ ├── Assets.xcassets/
|
||||
│ ├── YourAppApp.swift # @main entry point only
|
||||
│ └── YourApp.xctestplan
|
||||
├── YourAppPackage/ # All features and business logic
|
||||
│ ├── Package.swift
|
||||
│ ├── Sources/
|
||||
│ │ └── YourAppFeature/ # Feature modules
|
||||
│ └── Tests/
|
||||
│ └── YourAppFeatureTests/ # Swift Testing tests
|
||||
└── YourAppUITests/ # UI automation tests
|
||||
```
|
||||
|
||||
**Important:** All development work should be done in the **YourAppPackage** Swift Package, not in the app project. The app project is merely a thin wrapper that imports and launches the package features.
|
||||
|
||||
# Code Quality & Style Guidelines
|
||||
|
||||
## Swift Style & Conventions
|
||||
|
||||
- **Naming:** Use `UpperCamelCase` for types, `lowerCamelCase` for properties/functions. Choose descriptive names (e.g., `calculateMonthlyRevenue()` not `calcRev`)
|
||||
- **Value Types:** Prefer `struct` for models and data, use `class` only when reference semantics are required
|
||||
- **Enums:** Leverage Swift's powerful enums with associated values for state representation
|
||||
- **Early Returns:** Prefer early return pattern over nested conditionals to avoid pyramid of doom
|
||||
|
||||
## Optionals & Error Handling
|
||||
|
||||
- Use optionals with `if let`/`guard let` for nil handling
|
||||
- Never force-unwrap (`!`) without absolute certainty - prefer `guard` with failure path
|
||||
- Use `do/try/catch` for error handling with meaningful error types
|
||||
- Handle or propagate all errors - no empty catch blocks
|
||||
|
||||
# Modern SwiftUI Architecture Guidelines (2025)
|
||||
|
||||
### No ViewModels - Use Native SwiftUI Data Flow
|
||||
**New features MUST follow these patterns:**
|
||||
|
||||
1. **Views as Pure State Expressions**
|
||||
```swift
|
||||
struct MyView: View {
|
||||
@Environment(MyService.self) private var service
|
||||
@State private var viewState: ViewState = .loading
|
||||
|
||||
enum ViewState {
|
||||
case loading
|
||||
case loaded(data: [Item])
|
||||
case error(String)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// View is just a representation of its state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Environment Appropriately**
|
||||
- **App-wide services**: Router, Theme, CurrentAccount, Client, etc. - use `@Environment`
|
||||
- **Feature-specific services**: Timeline services, single-view logic - use `let` properties with `@Observable`
|
||||
- Rule: Environment for cross-app/cross-feature dependencies, let properties for single-feature services
|
||||
- Access app-wide via `@Environment(ServiceType.self)`
|
||||
- Feature services: `private let myService = MyObservableService()`
|
||||
|
||||
3. **Local State Management**
|
||||
- Use `@State` for view-specific state
|
||||
- Use `enum` for view states (loading, loaded, error)
|
||||
- Use `.task(id:)` and `.onChange(of:)` for side effects
|
||||
- Pass state between views using `@Binding`
|
||||
|
||||
4. **No ViewModels Required**
|
||||
- Views should be lightweight and disposable
|
||||
- Business logic belongs in services/clients
|
||||
- Test services independently, not views
|
||||
- Use SwiftUI previews for visual testing
|
||||
|
||||
5. **When Views Get Complex**
|
||||
- Split into smaller subviews
|
||||
- Use compound views that compose smaller views
|
||||
- Pass state via bindings between views
|
||||
- Never reach for a ViewModel as the solution
|
||||
|
||||
# iOS 26 Features (Optional)
|
||||
|
||||
**Note**: If your app targets iOS 26+, you can take advantage of these cutting-edge SwiftUI APIs introduced in June 2025. These features are optional and should only be used when your deployment target supports iOS 26.
|
||||
|
||||
## Available iOS 26 SwiftUI APIs
|
||||
|
||||
When targeting iOS 26+, consider using these new APIs:
|
||||
|
||||
#### Liquid Glass Effects
|
||||
- `glassEffect(_:in:isEnabled:)` - Apply Liquid Glass effects to views
|
||||
- `buttonStyle(.glass)` - Apply Liquid Glass styling to buttons
|
||||
- `ToolbarSpacer` - Create visual breaks in toolbars with Liquid Glass
|
||||
|
||||
#### Enhanced Scrolling
|
||||
- `scrollEdgeEffectStyle(_:for:)` - Configure scroll edge effects
|
||||
- `backgroundExtensionEffect()` - Duplicate, mirror, and blur views around edges
|
||||
|
||||
#### Tab Bar Enhancements
|
||||
- `tabBarMinimizeBehavior(_:)` - Control tab bar minimization behavior
|
||||
- Search role for tabs with search field replacing tab bar
|
||||
- `TabViewBottomAccessoryPlacement` - Adjust accessory view content based on placement
|
||||
|
||||
#### Web Integration
|
||||
- `WebView` and `WebPage` - Full control over browsing experience
|
||||
|
||||
#### Drag and Drop
|
||||
- `draggable(_:_:)` - Drag multiple items
|
||||
- `dragContainer(for:id:in:selection:_:)` - Container for draggable views
|
||||
|
||||
#### Animation
|
||||
- `@Animatable` macro - SwiftUI synthesizes custom animatable data properties
|
||||
|
||||
#### UI Components
|
||||
- `Slider` with automatic tick marks when using step parameter
|
||||
- `windowResizeAnchor(_:)` - Set window anchor point for resizing
|
||||
|
||||
#### Text Enhancements
|
||||
- `TextEditor` now supports `AttributedString`
|
||||
- `AttributedTextSelection` - Handle text selection with attributed text
|
||||
- `AttributedTextFormattingDefinition` - Define text styling in specific contexts
|
||||
- `FindContext` - Create find navigator in text editing views
|
||||
|
||||
#### Accessibility
|
||||
- `AssistiveAccess` - Support Assistive Access in iOS scenes
|
||||
|
||||
#### HDR Support
|
||||
- `Color.ResolvedHDR` - RGBA values with HDR headroom information
|
||||
|
||||
#### UIKit Integration
|
||||
- `UIHostingSceneDelegate` - Host and present SwiftUI scenes in UIKit
|
||||
- `NSGestureRecognizerRepresentable` - Incorporate gesture recognizers from AppKit
|
||||
|
||||
#### Immersive Spaces (if applicable)
|
||||
- `manipulable(coordinateSpace:operations:inertia:isEnabled:onChanged:)` - Hand gesture manipulation
|
||||
- `SurfaceSnappingInfo` - Snap volumes and windows to surfaces
|
||||
- `RemoteImmersiveSpace` - Render stereo content from Mac to Apple Vision Pro
|
||||
- `SpatialContainer` - 3D layout container
|
||||
- Depth-based modifiers: `aspectRatio3D(_:contentMode:)`, `rotation3DLayout(_:)`, `depthAlignment(_:)`
|
||||
|
||||
## iOS 26 Usage Guidelines
|
||||
- **Only use when targeting iOS 26+**: Ensure your deployment target supports these APIs
|
||||
- **Progressive enhancement**: Use availability checks if supporting multiple iOS versions
|
||||
- **Feature detection**: Test on older simulators to ensure graceful fallbacks
|
||||
- **Modern aesthetics**: Leverage Liquid Glass effects for cutting-edge UI design
|
||||
|
||||
```swift
|
||||
// Example: Using iOS 26 features with availability checks
|
||||
struct ModernButton: View {
|
||||
var body: some View {
|
||||
Button("Tap me") {
|
||||
// Action
|
||||
}
|
||||
.buttonStyle({
|
||||
if #available(iOS 26.0, *) {
|
||||
.glass
|
||||
} else {
|
||||
.bordered
|
||||
}
|
||||
}())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SwiftUI State Management (MV Pattern)
|
||||
|
||||
- **@State:** For all state management, including observable model objects
|
||||
- **@Observable:** Modern macro for making model classes observable (replaces ObservableObject)
|
||||
- **@Environment:** For dependency injection and shared app state
|
||||
- **@Binding:** For two-way data flow between parent and child views
|
||||
- **@Bindable:** For creating bindings to @Observable objects
|
||||
- Avoid ViewModels - put view logic directly in SwiftUI views using these state mechanisms
|
||||
- Keep views focused and extract reusable components
|
||||
|
||||
Example with @Observable:
|
||||
```swift
|
||||
@Observable
|
||||
class UserSettings {
|
||||
var theme: Theme = .light
|
||||
var fontSize: Double = 16.0
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct SettingsView: View {
|
||||
@State private var settings = UserSettings()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Direct property access, no $ prefix needed
|
||||
Text("Font Size: \(settings.fontSize)")
|
||||
|
||||
// For bindings, use @Bindable
|
||||
@Bindable var settings = settings
|
||||
Slider(value: $settings.fontSize, in: 10...30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sharing state across views
|
||||
@MainActor
|
||||
struct ContentView: View {
|
||||
@State private var userSettings = UserSettings()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
MainView()
|
||||
.environment(userSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct MainView: View {
|
||||
@Environment(UserSettings.self) private var settings
|
||||
|
||||
var body: some View {
|
||||
Text("Current theme: \(settings.theme)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example with .task modifier for async operations:
|
||||
```swift
|
||||
@Observable
|
||||
class DataModel {
|
||||
var items: [Item] = []
|
||||
var isLoading = false
|
||||
|
||||
func loadData() async throws {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
// Simulated network call
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
items = try await fetchItems()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct ItemListView: View {
|
||||
@State private var model = DataModel()
|
||||
|
||||
var body: some View {
|
||||
List(model.items) { item in
|
||||
Text(item.name)
|
||||
}
|
||||
.overlay {
|
||||
if model.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
// This task automatically cancels when view disappears
|
||||
do {
|
||||
try await model.loadData()
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
// Pull to refresh also uses async/await
|
||||
try? await model.loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Concurrency
|
||||
|
||||
- **@MainActor:** All UI updates must use @MainActor isolation
|
||||
- **Actors:** Use actors for expensive operations like disk I/O, network calls, or heavy computation
|
||||
- **async/await:** Always prefer async functions over completion handlers
|
||||
- **Task:** Use structured concurrency with proper task cancellation
|
||||
- **.task modifier:** Always use .task { } on views for async operations tied to view lifecycle - it automatically handles cancellation
|
||||
- **Avoid Task { } in onAppear:** This doesn't cancel automatically and can cause memory leaks or crashes
|
||||
- No GCD usage - Swift Concurrency only
|
||||
|
||||
### Sendable Conformance
|
||||
|
||||
Swift 6 enforces strict concurrency checking. All types that cross concurrency boundaries must be Sendable:
|
||||
|
||||
- **Value types (struct, enum):** Usually Sendable if all properties are Sendable
|
||||
- **Classes:** Must be marked `final` and have immutable or Sendable properties, or use `@unchecked Sendable` with thread-safe implementation
|
||||
- **@Observable classes:** Automatically Sendable when all properties are Sendable
|
||||
- **Closures:** Mark as `@Sendable` when captured by concurrent contexts
|
||||
|
||||
```swift
|
||||
// Sendable struct - automatic conformance
|
||||
struct UserData: Sendable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
}
|
||||
|
||||
// Sendable class - must be final with immutable properties
|
||||
final class Configuration: Sendable {
|
||||
let apiKey: String
|
||||
let endpoint: URL
|
||||
|
||||
init(apiKey: String, endpoint: URL) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
// @Observable with Sendable
|
||||
@Observable
|
||||
final class UserModel: Sendable {
|
||||
var name: String = ""
|
||||
var age: Int = 0
|
||||
// Automatically Sendable if all stored properties are Sendable
|
||||
}
|
||||
|
||||
// Using @unchecked Sendable for thread-safe types
|
||||
final class Cache: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var storage: [String: Any] = [:]
|
||||
|
||||
func get(_ key: String) -> Any? {
|
||||
lock.withLock { storage[key] }
|
||||
}
|
||||
}
|
||||
|
||||
// @Sendable closures
|
||||
func processInBackground(completion: @Sendable @escaping (Result<Data, Error>) -> Void) {
|
||||
Task {
|
||||
// Processing...
|
||||
completion(.success(data))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
- Keep functions focused on a single responsibility
|
||||
- Break large functions (>50 lines) into smaller, testable units
|
||||
- Use extensions to organize code by feature or protocol conformance
|
||||
- Prefer `let` over `var` - use immutability by default
|
||||
- Use `[weak self]` in closures to prevent retain cycles
|
||||
- Always include `self.` when referring to instance properties in closures
|
||||
|
||||
# Testing Guidelines
|
||||
|
||||
We use **Swift Testing** framework (not XCTest) for all tests. Tests live in the package test target.
|
||||
|
||||
## Swift Testing Basics
|
||||
|
||||
```swift
|
||||
import Testing
|
||||
|
||||
@Test func userCanLogin() async throws {
|
||||
let service = AuthService()
|
||||
let result = try await service.login(username: "test", password: "pass")
|
||||
#expect(result.isSuccess)
|
||||
#expect(result.user.name == "Test User")
|
||||
}
|
||||
|
||||
@Test("User sees error with invalid credentials")
|
||||
func invalidLogin() async throws {
|
||||
let service = AuthService()
|
||||
await #expect(throws: AuthError.self) {
|
||||
try await service.login(username: "", password: "")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Swift Testing Features
|
||||
|
||||
- **@Test:** Marks a test function (replaces XCTest's test prefix)
|
||||
- **@Suite:** Groups related tests together
|
||||
- **#expect:** Validates conditions (replaces XCTAssert)
|
||||
- **#require:** Like #expect but stops test execution on failure
|
||||
- **Parameterized Tests:** Use @Test with arguments for data-driven tests
|
||||
- **async/await:** Full support for testing async code
|
||||
- **Traits:** Add metadata like `.bug()`, `.feature()`, or custom tags
|
||||
|
||||
## Test Organization
|
||||
|
||||
- Write tests in the package's Tests/ directory
|
||||
- One test file per source file when possible
|
||||
- Name tests descriptively explaining what they verify
|
||||
- Test both happy paths and edge cases
|
||||
- Add tests for bug fixes to prevent regression
|
||||
|
||||
# Entitlements Management
|
||||
|
||||
This template includes a **declarative entitlements system** that AI agents can safely modify without touching Xcode project files.
|
||||
|
||||
## How It Works
|
||||
|
||||
- **Entitlements File**: `Config/MyProject.entitlements` contains all app capabilities
|
||||
- **XCConfig Integration**: `CODE_SIGN_ENTITLEMENTS` setting in `Config/Shared.xcconfig` points to the entitlements file
|
||||
- **AI-Friendly**: Agents can edit the XML file directly to add/remove capabilities
|
||||
|
||||
## Adding Entitlements
|
||||
|
||||
To add capabilities to your app, edit `Config/MyProject.entitlements`:
|
||||
|
||||
## Common Entitlements
|
||||
|
||||
| Capability | Entitlement Key | Value |
|
||||
|------------|-----------------|-------|
|
||||
| HealthKit | `com.apple.developer.healthkit` | `<true/>` |
|
||||
| CloudKit | `com.apple.developer.icloud-services` | `<array><string>CloudKit</string></array>` |
|
||||
| Push Notifications | `aps-environment` | `development` or `production` |
|
||||
| App Groups | `com.apple.security.application-groups` | `<array><string>group.id</string></array>` |
|
||||
| Keychain Sharing | `keychain-access-groups` | `<array><string>$(AppIdentifierPrefix)bundle.id</string></array>` |
|
||||
| Background Modes | `com.apple.developer.background-modes` | `<array><string>mode-name</string></array>` |
|
||||
| Contacts | `com.apple.developer.contacts.notes` | `<true/>` |
|
||||
| Camera | `com.apple.developer.avfoundation.audio` | `<true/>` |
|
||||
|
||||
# XcodeBuildMCP Tool Usage
|
||||
|
||||
To work with this project, build, test, and development commands should use XcodeBuildMCP tools instead of raw command-line calls.
|
||||
|
||||
## Project Discovery & Setup
|
||||
|
||||
```javascript
|
||||
// Discover Xcode projects in the workspace
|
||||
discover_projs({
|
||||
workspaceRoot: "/path/to/YourApp"
|
||||
})
|
||||
|
||||
// List available schemes
|
||||
list_schems_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace"
|
||||
})
|
||||
```
|
||||
|
||||
## Building for Simulator
|
||||
|
||||
```javascript
|
||||
// Build for iPhone simulator by name
|
||||
build_sim_name_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace",
|
||||
scheme: "YourApp",
|
||||
simulatorName: "iPhone 16",
|
||||
configuration: "Debug"
|
||||
})
|
||||
|
||||
// Build and run in one step
|
||||
build_run_sim_name_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace",
|
||||
scheme: "YourApp",
|
||||
simulatorName: "iPhone 16"
|
||||
})
|
||||
```
|
||||
|
||||
## Building for Device
|
||||
|
||||
```javascript
|
||||
// List connected devices first
|
||||
list_devices()
|
||||
|
||||
// Build for physical device
|
||||
build_dev_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace",
|
||||
scheme: "YourApp",
|
||||
configuration: "Debug"
|
||||
})
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```javascript
|
||||
// Run tests on simulator
|
||||
test_sim_name_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace",
|
||||
scheme: "YourApp",
|
||||
simulatorName: "iPhone 16"
|
||||
})
|
||||
|
||||
// Run tests on device
|
||||
test_device_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace",
|
||||
scheme: "YourApp",
|
||||
deviceId: "DEVICE_UUID_HERE"
|
||||
})
|
||||
|
||||
// Test Swift Package
|
||||
swift_package_test({
|
||||
packagePath: "/path/to/YourAppPackage"
|
||||
})
|
||||
```
|
||||
|
||||
## Simulator Management
|
||||
|
||||
```javascript
|
||||
// List available simulators
|
||||
list_sims({
|
||||
enabled: true
|
||||
})
|
||||
|
||||
// Boot simulator
|
||||
boot_sim({
|
||||
simulatorUuid: "SIMULATOR_UUID"
|
||||
})
|
||||
|
||||
// Install app
|
||||
install_app_sim({
|
||||
simulatorUuid: "SIMULATOR_UUID",
|
||||
appPath: "/path/to/YourApp.app"
|
||||
})
|
||||
|
||||
// Launch app
|
||||
launch_app_sim({
|
||||
simulatorUuid: "SIMULATOR_UUID",
|
||||
bundleId: "com.example.YourApp"
|
||||
})
|
||||
```
|
||||
|
||||
## Device Management
|
||||
|
||||
```javascript
|
||||
// Install on device
|
||||
install_app_device({
|
||||
deviceId: "DEVICE_UUID",
|
||||
appPath: "/path/to/YourApp.app"
|
||||
})
|
||||
|
||||
// Launch on device
|
||||
launch_app_device({
|
||||
deviceId: "DEVICE_UUID",
|
||||
bundleId: "com.example.YourApp"
|
||||
})
|
||||
```
|
||||
|
||||
## UI Automation
|
||||
|
||||
```javascript
|
||||
// Get UI hierarchy
|
||||
describe_ui({
|
||||
simulatorUuid: "SIMULATOR_UUID"
|
||||
})
|
||||
|
||||
// Tap element
|
||||
tap({
|
||||
simulatorUuid: "SIMULATOR_UUID",
|
||||
x: 100,
|
||||
y: 200
|
||||
})
|
||||
|
||||
// Type text
|
||||
type_text({
|
||||
simulatorUuid: "SIMULATOR_UUID",
|
||||
text: "Hello World"
|
||||
})
|
||||
|
||||
// Take screenshot
|
||||
screenshot({
|
||||
simulatorUuid: "SIMULATOR_UUID"
|
||||
})
|
||||
```
|
||||
|
||||
## Log Capture
|
||||
|
||||
```javascript
|
||||
// Start capturing simulator logs
|
||||
start_sim_log_cap({
|
||||
simulatorUuid: "SIMULATOR_UUID",
|
||||
bundleId: "com.example.YourApp"
|
||||
})
|
||||
|
||||
// Stop and retrieve logs
|
||||
stop_sim_log_cap({
|
||||
logSessionId: "SESSION_ID"
|
||||
})
|
||||
|
||||
// Device logs
|
||||
start_device_log_cap({
|
||||
deviceId: "DEVICE_UUID",
|
||||
bundleId: "com.example.YourApp"
|
||||
})
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
```javascript
|
||||
// Get bundle ID from app
|
||||
get_app_bundle_id({
|
||||
appPath: "/path/to/YourApp.app"
|
||||
})
|
||||
|
||||
// Clean build artifacts
|
||||
clean_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace"
|
||||
})
|
||||
|
||||
// Get app path for simulator
|
||||
get_sim_app_path_name_ws({
|
||||
workspacePath: "/path/to/YourApp.xcworkspace",
|
||||
scheme: "YourApp",
|
||||
platform: "iOS Simulator",
|
||||
simulatorName: "iPhone 16"
|
||||
})
|
||||
```
|
||||
|
||||
# Development Workflow
|
||||
|
||||
1. **Make changes in the Package**: All feature development happens in YourAppPackage/Sources/
|
||||
2. **Write tests**: Add Swift Testing tests in YourAppPackage/Tests/
|
||||
3. **Build and test**: Use XcodeBuildMCP tools to build and run tests
|
||||
4. **Run on simulator**: Deploy to simulator for manual testing
|
||||
5. **UI automation**: Use describe_ui and automation tools for UI testing
|
||||
6. **Device testing**: Deploy to physical device when needed
|
||||
|
||||
# Best Practices
|
||||
|
||||
## SwiftUI & State Management
|
||||
|
||||
- Keep views small and focused
|
||||
- Extract reusable components into their own files
|
||||
- Use @ViewBuilder for conditional view composition
|
||||
- Leverage SwiftUI's built-in animations and transitions
|
||||
- Avoid massive body computations - break them down
|
||||
- **Always use .task modifier** for async work tied to view lifecycle - it automatically cancels when the view disappears
|
||||
- Never use Task { } in onAppear - use .task instead for proper lifecycle management
|
||||
|
||||
## Performance
|
||||
|
||||
- Use .id() modifier sparingly as it forces view recreation
|
||||
- Implement Equatable on models to optimize SwiftUI diffing
|
||||
- Use LazyVStack/LazyHStack for large lists
|
||||
- Profile with Instruments when needed
|
||||
- @Observable tracks only accessed properties, improving performance over @Published
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Always provide accessibilityLabel for interactive elements
|
||||
- Use accessibilityIdentifier for UI testing
|
||||
- Implement accessibilityHint where actions aren't obvious
|
||||
- Test with VoiceOver enabled
|
||||
- Support Dynamic Type
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
- Never log sensitive information
|
||||
- Use Keychain for credential storage
|
||||
- All network calls must use HTTPS
|
||||
- Request minimal permissions
|
||||
- Follow App Store privacy guidelines
|
||||
|
||||
## Data Persistence
|
||||
|
||||
When data persistence is required, always prefer **SwiftData** over CoreData. However, carefully consider whether persistence is truly necessary - many apps can function well with in-memory state that loads on launch.
|
||||
|
||||
### When to Use SwiftData
|
||||
|
||||
- You have complex relational data that needs to persist across app launches
|
||||
- You need advanced querying capabilities with predicates and sorting
|
||||
- You're building a data-heavy app (note-taking, inventory, task management)
|
||||
- You need CloudKit sync with minimal configuration
|
||||
|
||||
### When NOT to Use Data Persistence
|
||||
|
||||
- Simple user preferences (use UserDefaults)
|
||||
- Temporary state that can be reloaded from network
|
||||
- Small configuration data (consider JSON files or plist)
|
||||
- Apps that primarily display remote data
|
||||
|
||||
### SwiftData Best Practices
|
||||
|
||||
```swift
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Task {
|
||||
var title: String
|
||||
var isCompleted: Bool
|
||||
var createdAt: Date
|
||||
|
||||
init(title: String) {
|
||||
self.title = title
|
||||
self.isCompleted = false
|
||||
self.createdAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
// In your app
|
||||
@main
|
||||
struct MyProjectApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.modelContainer(for: Task.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In your views
|
||||
struct TaskListView: View {
|
||||
@Query private var tasks: [Task]
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
var body: some View {
|
||||
List(tasks) { task in
|
||||
Text(task.title)
|
||||
}
|
||||
.toolbar {
|
||||
Button("Add") {
|
||||
let newTask = Task(title: "New Task")
|
||||
context.insert(newTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Never use CoreData for new projects. SwiftData provides a modern, type-safe API that's easier to work with and integrates seamlessly with SwiftUI.
|
||||
|
||||
---
|
||||
|
||||
Remember: This project prioritizes clean, simple SwiftUI code using the platform's native state management. Keep the app shell minimal and implement all features in the Swift Package.
|
||||
198
参考计费/.cursor/rules/tuist.mdc
Normal file
198
参考计费/.cursor/rules/tuist.mdc
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Tuist 集成与模块化拆分(Vibeviewer)
|
||||
|
||||
本规则记录项目接入 Tuist、按 Feature 拆分为独立 SPM 包、UI 层依赖注入,以及常见问题排查与修复。
|
||||
|
||||
## 标准方案(Single Source of Truth)
|
||||
- 仅在 `Project.swift` 的 `packages` 节点声明本地包,保持“单一来源”。
|
||||
- 不使用 `Tuist/Dependencies.swift` 声明本地包,避免与 `Project.swift` 重复导致解析冲突。
|
||||
- App 目标依赖统一使用 `.package(product:)`。
|
||||
- 生成工程:`make generate`;清理:`make clear`(仅清当前项目 DerivedData 与项目级 Tuist 缓存)。
|
||||
|
||||
示例(节选,自 `Project.swift`)
|
||||
```swift
|
||||
packages: [
|
||||
.local(path: "Packages/VibeviewerCore"),
|
||||
.local(path: "Packages/VibeviewerModel"),
|
||||
.local(path: "Packages/VibeviewerAPI"),
|
||||
.local(path: "Packages/VibeviewerLoginUI"),
|
||||
.local(path: "Packages/VibeviewerMenuUI"),
|
||||
.local(path: "Packages/VibeviewerSettingsUI")
|
||||
],
|
||||
```
|
||||
|
||||
## UI 层依赖注入(遵循 project.mdc)
|
||||
- 不使用 MVVM;视图内部用 `@State` 管理轻量状态。
|
||||
- 使用 Environment 注入跨模块依赖:
|
||||
- 在 `VibeviewerModel` 暴露 `EnvironmentValues.cursorStorage`。
|
||||
- 在 `VibeviewerMenuUI` 暴露 `EnvironmentValues.cursorService`、`loginWindowManager`、`settingsWindowManager`。
|
||||
- App 注入:
|
||||
```swift
|
||||
MenuPopoverView()
|
||||
.environment(\.cursorService, DefaultCursorService())
|
||||
.environment(\.cursorStorage, CursorStorage.shared)
|
||||
.environment(\.loginWindowManager, LoginWindowManager.shared)
|
||||
.environment(\.settingsWindowManager, SettingsWindowManager.shared)
|
||||
```
|
||||
- 视图使用:
|
||||
```swift
|
||||
@Environment(\.cursorService) private var service
|
||||
@Environment(\.cursorStorage) private var storage
|
||||
@Environment(\.loginWindowManager) private var loginWindow
|
||||
@Environment(\.settingsWindowManager) private var settingsWindow
|
||||
```
|
||||
|
||||
## Feature 拆包规范
|
||||
- 单一职责:
|
||||
- `VibeviewerLoginUI`:登录视图与窗口
|
||||
- `VibeviewerMenuUI`:菜单视图与业务触发
|
||||
- `VibeviewerSettingsUI`:设置视图与窗口
|
||||
- 每个包必须包含测试目录 `Tests/<TargetName>Tests/`(即便是占位),否则会出现测试路径报错。
|
||||
|
||||
## 常见问题与排查
|
||||
- 包在 Xcode 里显示为“文件夹 + ?”,不是 SPM 包:
|
||||
- 原因:`Project.swift` 与 `Tuist/Dependencies.swift` 同时声明了本地包(重复来源),或 SwiftPM/Xcode 缓存脏。
|
||||
- 处理:删除 `Tuist/Dependencies.swift` 的本地包声明(本项目直接删除该文件);删除各包 `.swiftpm/`;`make clear` 后再 `make generate`。
|
||||
|
||||
### 修复步骤(示例:VibeviewerAppEnvironment 未作为包加载/显示为文件夹)
|
||||
1. 确认 Single Source of Truth:仅在 `Project.swift` 的 `packages` 节点保留本地包声明。
|
||||
- 保持如下形式(节选):
|
||||
```swift
|
||||
packages: [
|
||||
.local(path: "Packages/VibeviewerCore"),
|
||||
.local(path: "Packages/VibeviewerModel"),
|
||||
.local(path: "Packages/VibeviewerAPI"),
|
||||
.local(path: "Packages/VibeviewerLoginUI"),
|
||||
.local(path: "Packages/VibeviewerMenuUI"),
|
||||
.local(path: "Packages/VibeviewerSettingsUI"),
|
||||
.local(path: "Packages/VibeviewerAppEnvironment"),
|
||||
]
|
||||
```
|
||||
2. 清空 `Tuist/Dependencies.swift` 的本地包声明,避免与 `Project.swift` 重复:
|
||||
```swift
|
||||
let dependencies = Dependencies(
|
||||
swiftPackageManager: .init(
|
||||
packages: [ /* 留空,统一由 Project.swift 管理 */ ],
|
||||
baseSettings: .settings(base: [:], configurations: [/* 省略 */])
|
||||
),
|
||||
platforms: [.macOS]
|
||||
)
|
||||
```
|
||||
- 注:也可直接删除该文件;两者目标一致——移除重复来源。
|
||||
3. 可选清理缓存(若仍显示为文件夹或解析异常):
|
||||
- 删除各包下残留的 `.swiftpm/` 目录(若存在)。
|
||||
4. 重新生成工程:
|
||||
```bash
|
||||
make clear && make generate
|
||||
```
|
||||
5. 验证:
|
||||
- Xcode 的 Project Navigator 中,`VibeviewerAppEnvironment` 以 Swift Package 方式展示(非普通文件夹)。
|
||||
- App 目标依赖通过 `.package(product: "VibeviewerAppEnvironment")` 引入。
|
||||
- “Couldn't load project at …/.swiftpm/xcode”:
|
||||
- 原因:加载了过期的 `.swiftpm/xcode` 子工程缓存。
|
||||
- 处理:删除对应包 `.swiftpm/` 后重新生成。
|
||||
- `no such module 'X'`:
|
||||
- 原因:缺少包/目标依赖或未在 `packages` 声明路径。
|
||||
- 处理:在包的 `Package.swift` 增加依赖;在 `Project.swift` 的 `packages` 增加 `.local(path:)`;再生成。
|
||||
- 捕获列表语法错误(如 `[weak _ = service]`):
|
||||
- Swift 不允许匿名弱引用捕获。移除该语法,使用受控任务生命周期(持有 `Task` 并适时取消)。
|
||||
|
||||
## Make 命令
|
||||
- 生成:
|
||||
```bash
|
||||
make generate
|
||||
```
|
||||
- 清理(当前项目):
|
||||
```bash
|
||||
make clear
|
||||
```
|
||||
|
||||
## 新增 Feature 包 Checklist
|
||||
1. 在 `Packages/YourFeature/` 创建 `Package.swift`、`Sources/YourFeature/`、`Tests/YourFeatureTests/`。
|
||||
2. 在 `Package.swift` 写入 `.package(path: ...)` 与 `targets.target.dependencies`。
|
||||
3. 在 `Project.swift` 的 `packages` 增加 `.local(path: ...)`,并在 App 目标依赖加 `.package(product: ...)`。
|
||||
4. `make generate` 重新生成。
|
||||
|
||||
> 经验:保持“单一来源”(只在 `Project.swift` 声明本地包)显著降低 Tuist/SwiftPM 解析歧义与缓存问题。# Tuist 集成与模块化拆分(Vibeviewer)
|
||||
|
||||
本规则记录项目接入 Tuist、按 Feature 拆分为独立 SPM 包、UI 层依赖注入,以及常见问题排查与修复。
|
||||
|
||||
## 标准方案(Single Source of Truth)
|
||||
- 仅在 `Project.swift` 的 `packages` 节点声明本地包,保持“单一来源”。
|
||||
- 不使用 `Tuist/Dependencies.swift` 声明本地包,避免与 `Project.swift` 重复导致解析冲突。
|
||||
- App 目标依赖统一使用 `.package(product:)`。
|
||||
- 生成工程:`make generate`;清理:`make clear`(仅清当前项目 DerivedData 与项目级 Tuist 缓存)。
|
||||
|
||||
示例(节选,自 `Project.swift`)
|
||||
```swift
|
||||
packages: [
|
||||
.local(path: "Packages/VibeviewerCore"),
|
||||
.local(path: "Packages/VibeviewerModel"),
|
||||
.local(path: "Packages/VibeviewerAPI"),
|
||||
.local(path: "Packages/VibeviewerLoginUI"),
|
||||
.local(path: "Packages/VibeviewerMenuUI"),
|
||||
.local(path: "Packages/VibeviewerSettingsUI")
|
||||
],
|
||||
```
|
||||
|
||||
## UI 层依赖注入(遵循 project.mdc)
|
||||
- 不使用 MVVM;视图内部用 `@State` 管理轻量状态。
|
||||
- 使用 Environment 注入跨模块依赖:
|
||||
- 在 `VibeviewerModel` 暴露 `EnvironmentValues.cursorStorage`。
|
||||
- 在 `VibeviewerMenuUI` 暴露 `EnvironmentValues.cursorService`、`loginWindowManager`、`settingsWindowManager`。
|
||||
- App 注入:
|
||||
```swift
|
||||
MenuPopoverView()
|
||||
.environment(\.cursorService, DefaultCursorService())
|
||||
.environment(\.cursorStorage, CursorStorage.shared)
|
||||
.environment(\.loginWindowManager, LoginWindowManager.shared)
|
||||
.environment(\.settingsWindowManager, SettingsWindowManager.shared)
|
||||
```
|
||||
- 视图使用:
|
||||
```swift
|
||||
@Environment(\.cursorService) private var service
|
||||
@Environment(\.cursorStorage) private var storage
|
||||
@Environment(\.loginWindowManager) private var loginWindow
|
||||
@Environment(\.settingsWindowManager) private var settingsWindow
|
||||
```
|
||||
|
||||
## Feature 拆包规范
|
||||
- 单一职责:
|
||||
- `VibeviewerLoginUI`:登录视图与窗口
|
||||
- `VibeviewerMenuUI`:菜单视图与业务触发
|
||||
- `VibeviewerSettingsUI`:设置视图与窗口
|
||||
- 每个包必须包含测试目录 `Tests/<TargetName>Tests/`(即便是占位),否则会出现测试路径报错。
|
||||
|
||||
## 常见问题与排查
|
||||
- 包在 Xcode 里显示为“文件夹 + ?”,不是 SPM 包:
|
||||
- 原因:`Project.swift` 与 `Tuist/Dependencies.swift` 同时声明了本地包(重复来源),或 SwiftPM/Xcode 缓存脏。
|
||||
- 处理:删除 `Tuist/Dependencies.swift` 的本地包声明(本项目直接删除该文件);删除各包 `.swiftpm/`;`make clear` 后再 `make generate`。
|
||||
- “Couldn't load project at …/.swiftpm/xcode”:
|
||||
- 原因:加载了过期的 `.swiftpm/xcode` 子工程缓存。
|
||||
- 处理:删除对应包 `.swiftpm/` 后重新生成。
|
||||
- `no such module 'X'`:
|
||||
- 原因:缺少包/目标依赖或未在 `packages` 声明路径。
|
||||
- 处理:在包的 `Package.swift` 增加依赖;在 `Project.swift` 的 `packages` 增加 `.local(path:)`;再生成。
|
||||
- 捕获列表语法错误(如 `[weak _ = service]`):
|
||||
- Swift 不允许匿名弱引用捕获。移除该语法,使用受控任务生命周期(持有 `Task` 并适时取消)。
|
||||
|
||||
## Make 命令
|
||||
- 生成:
|
||||
```bash
|
||||
make generate
|
||||
```
|
||||
- 清理(当前项目):
|
||||
```bash
|
||||
make clear
|
||||
```
|
||||
|
||||
## 新增 Feature 包 Checklist
|
||||
1. 在 `Packages/YourFeature/` 创建 `Package.swift`、`Sources/YourFeature/`、`Tests/YourFeatureTests/`。
|
||||
2. 在 `Package.swift` 写入 `.package(path: ...)` 与 `targets.target.dependencies`。
|
||||
3. 在 `Project.swift` 的 `packages` 增加 `.local(path: ...)`,并在 App 目标依赖加 `.package(product: ...)`。
|
||||
4. `make generate` 重新生成。
|
||||
|
||||
> 经验:保持“单一来源”(只在 `Project.swift` 声明本地包)显著降低 Tuist/SwiftPM 解析歧义与缓存问题。
|
||||
Reference in New Issue
Block a user