Compare commits

...

10 Commits

Author SHA1 Message Date
AndyChen1024
6fd1df0503 refactor: 迁移 App 入口并接入 IosClientKit 2026-09-14 21:10:44 +08:00
AndyChen1024
ecb6a82f88 chore: 添加 LICENSE 2026-09-14 21:10:44 +08:00
AndyChen1024
ae763ae3a1 docs: 项目文档与协作规范 2026-09-14 21:10:44 +08:00
AndyChen1024
ec19d3bf98 chore: 添加 SwiftLint 配置 2026-09-14 21:10:44 +08:00
AndyChen1024
653caa0b84 refactor: app 组合根与导航 2026-09-14 21:10:44 +08:00
AndyChen1024
b5de8ddfbd feat: feature 多模块骨架 2026-09-14 21:10:44 +08:00
AndyChen1024
4fe0a26432 chore: core 多模块骨架 2026-09-14 21:10:44 +08:00
AndyChen1024
295efac07c build: 引入 IosClientKit SPM 包清单 2026-09-14 21:10:43 +08:00
AndyChen1024
599f1c7f8e chore: 忽略构建产物与 AI 工具目录 2026-09-14 21:10:43 +08:00
AndyChen1024
e29994f49d chore(skills): 引入 agent skills 与锁文件 2026-09-14 21:10:43 +08:00
33 changed files with 971 additions and 43 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# 临时目录(日志/截图等)
tmp/
# macOS
.DS_Store
# Xcode
xcuserdata/
*.xcuserstate
DerivedData/
build/
# Swift Package Manager
.build/
.swiftpm/
# AI 工具本地配置 / 安装的技能(可经 skills-lock.json 重装)
.claude/
.agents/

38
.swiftlint.yml Normal file
View File

@@ -0,0 +1,38 @@
# SwiftLint 配置
disabled_rules:
- trailing_whitespace
opt_in_rules:
- anyobject_protocol
- array_init
- closure_spacing
- empty_count
- empty_string
- redundant_nil_coalescing
- vertical_whitespace_closing_braces
excluded:
- .build
- .swiftpm
- DerivedData
- tmp
- ios-client/Assets.xcassets
line_length:
warning: 120
error: 160
ignores_comments: true
ignores_urls: true
type_name:
min_length: 2
max_length: 60
identifier_name:
min_length: 2
max_length: 60
file_length:
warning: 500
error: 1000

81
CLAUDE.md Normal file
View File

@@ -0,0 +1,81 @@
# CLAUDE.md — iOS 客户端项目 AI 协作规范
## 项目概览
SwiftUI + MVVM 单向数据流 + SPM 多 target 模块化的 iOS「打卡/习惯追踪」客户端,架构对齐 Android NIA(`core/*` + `feature/*`)。
## 目录结构
- `ios-client.xcodeproj` / `ios-client/`:App 宿主(薄壳,只含 `@main` 挂载 `AppRootView`)
- `IosClientKit/`:本地 SPM 包,承载所有模块
- `Sources/App`:组合根(`AppContainer` DI + `AppRootView` 导航)
- `Sources/CoreModel`:领域模型(`Sendable` 值类型,叶子)
- `Sources/CoreCommon`:通用工具(叶子)
- `Sources/CoreDomain`:Repository 协议 + UseCase(纯 Swift,不 import SwiftUI)
- `Sources/CoreRepository`:Repository 实现(Fake + 未来 OfflineFirst)
- `Sources/CoreNetwork`:DTO + APIClient(URLSession)
- `Sources/CoreDatabase`:SwiftData(`@Model` + `ModelContainer`)
- `Sources/DesignSystem`:设计 tokens
- `Sources/CoreUI`:可复用 SwiftUI 组件
- `Sources/FeatureHome` / `Sources/FeatureAuth`:Feature 模块
## 依赖方向(硬约束,违反即编译不过)
单向无环:`App → feature → core`;`App` 额外 → `CoreRepository`(装配具体实现)。
- 领域模型只在 `CoreModel`,其余模块只能 import `CoreModel`。
- Repository 协议只定义在 `CoreDomain`,实现在 `CoreRepository`。
- `CoreDomain` 纯 Swift:禁止 `import SwiftUI` / 数据框架。
- DTO 只在 `CoreNetwork`,领域 Model 只在 `CoreModel`,二者分离。
- **禁止**命名任何模块为 `CoreData`(与苹果 `CoreData` 框架撞名,已统一为 `CoreRepository`)。
## 状态管理约定
- ViewModel:`@MainActor @Observable final class`,持有 `uiState`。
- UiState:`enum` 关联值(对应 Kotlin `sealed interface`)。
- Kotlin → Swift 映射:
| Android (Kotlin) | iOS (Swift) |
|---|---|
| `Flow<T>` | `AsyncThrowingStream<T, Error>` |
| `StateFlow` + `stateIn` | `@Observable` + `@MainActor` |
| `flatMapLatest` | `Task` 取消自动 switch-to-latest |
| `sealed interface UiState` | `enum UiState`(关联值)|
| Hilt `@Inject` / AppContainer | 手写 `AppContainer` |
| Retrofit + kotlinx.serialization | URLSession + Codable |
| Room | SwiftData |
| DataStore | `@AppStorage` / UserDefaults |
## 代码风格
- 跨模块符号一律 `public`,并加 `///` 文档注释。
- 领域模型标记 `Sendable`。
- 命名:类型 PascalCase,变量/函数 camelCase;UI 文案与注释可用中文。
## 明确禁止
- ❌ TCA、Swinject 等 DI 框架、Moya 等网络封装、任何第三方大框架
- ❌ UIKit(除非 SwiftUI 确无法覆盖的极少数场景)
- ❌ 循环依赖 / 反向 import
## 构建 / 验证命令
```bash
# 编译包(同时做依赖环检测,SPM 遇环即报错)
swift build --package-path IosClientKit
# 模拟器编译 App
xcodebuild -project ios-client.xcodeproj -scheme ios-client \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO build
# Lint
swiftlint lint --strict
```
## 新增 Feature 清单
1. 在 `IosClientKit/Package.swift` 的 `targets` 加 `FeatureXxx` 及依赖。
2. 新建 `Sources/FeatureXxx/`:Screen + ViewModel + UiState(enum)。
3. UseCase 放 `CoreDomain`,Repository 实现放 `CoreRepository`。
4. 在 `App` 的 `AppContainer` 装配依赖、`AppRootView` 接入导航。

31
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,31 @@
# 贡献指南
## 架构约束(提交前必须遵守)
- 依赖单向:`App → feature → core`,禁止反向 import。
- Repository 协议只在 `CoreDomain`,实现在 `CoreRepository`。
- DTO 与领域 Model 分离(`CoreNetwork` / `CoreModel`)。
- 禁止命名模块 `CoreData`(与苹果框架撞名,统一为 `CoreRepository`)。
## 代码规范
- 跨模块符号 `public` + `///` 文档注释。
- 领域模型标记 `Sendable`。
- ViewModel:`@MainActor @Observable`,UiState 用 `enum`。
- 引入任何第三方依赖前,需在 PR 说明必要性(项目默认零第三方框架)。
## 提交流程
1. 从 `main` 新建分支:`feat/xxx`、`fix/xxx`。
2. 本地跑通:
```bash
swiftlint lint
xcodebuild -project ios-client.xcodeproj -scheme ios-client \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO build
```
3. 发起 PR,至少一位 reviewer 通过后合并。
## Commit 规范
采用 Conventional Commits:`feat:` / `fix:` / `refactor:` / `docs:` / `chore:`。

View File

@@ -0,0 +1,52 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "IosClientKit",
defaultLocalization: "en",
platforms: [
.iOS("17.0"),
.macOS(.v14),
],
products: [
// 组合根:App 宿主只链接这一个产品,其余模块经依赖传递带入。
.library(name: "App", targets: ["App"]),
],
targets: [
// 叶子模块
.target(name: "CoreModel"),
.target(name: "CoreCommon"),
.target(name: "DesignSystem"),
// 领域层(纯 Swift:不 import SwiftUI / 数据框架)
.target(name: "CoreDomain", dependencies: ["CoreModel"]),
// 基础设施层
.target(name: "CoreNetwork", dependencies: ["CoreModel", "CoreCommon"]),
.target(name: "CoreDatabase", dependencies: ["CoreModel"]),
.target(
name: "CoreRepository",
dependencies: ["CoreDomain", "CoreNetwork", "CoreDatabase", "CoreModel", "CoreCommon"]
),
// UI 层
.target(name: "CoreUI", dependencies: ["DesignSystem", "CoreCommon"]),
// Feature 层
.target(
name: "FeatureHome",
dependencies: ["CoreDomain", "CoreUI", "CoreModel", "CoreCommon"]
),
.target(name: "FeatureAuth", dependencies: ["CoreDomain", "CoreUI", "CoreModel"]),
// 组合根(依赖一切,用于 DI 装配)
.target(
name: "App",
dependencies: [
"FeatureHome", "FeatureAuth", "CoreRepository",
"CoreUI", "DesignSystem", "CoreDomain", "CoreModel",
]
),
],
swiftLanguageModes: [.v5]
)

View File

@@ -0,0 +1,14 @@
import CoreDomain
import CoreRepository
import FeatureHome
/// 手写组合根 / DI 容器(对应 Android Hilt 的 AppContainer)。
@MainActor
public enum AppContainer {
/// 装配 FeatureHome 依赖并返回 ViewModel。
public static func makeHomeViewModel() -> HomeViewModel {
let repository: HomeRepository = FakeHomeRepository()
let useCase = HomeUseCase(repository: repository)
return HomeViewModel(homeUseCase: useCase)
}
}

View File

@@ -0,0 +1,36 @@
import SwiftUI
import CoreUI
import FeatureAuth
import FeatureHome
public struct AppRootView: View {
@State private var homeViewModel: HomeViewModel
public init() {
_homeViewModel = State(initialValue: AppContainer.makeHomeViewModel())
}
public var body: some View {
TabView {
NavigationStack {
HomeScreen(viewModel: homeViewModel)
}
.tabItem { Label("首页", systemImage: "house.fill") }
NavigationStack {
PlaceholderView("计划", systemImage: "calendar")
}
.tabItem { Label("计划", systemImage: "calendar") }
NavigationStack {
PlaceholderView("打卡", systemImage: "checkmark.seal.fill")
}
.tabItem { Label("打卡", systemImage: "checkmark.seal.fill") }
NavigationStack {
AuthScreen()
}
.tabItem { Label("我的", systemImage: "person.crop.circle") }
}
}
}

View File

@@ -0,0 +1,14 @@
/// 全局日志工具(Android 端 Timber 的轻量替代)。
public enum Logger {
public static func info(_ message: @autoclosure () -> String) {
#if DEBUG
print("[INFO] \(message())")
#endif
}
public static func error(_ message: @autoclosure () -> String) {
#if DEBUG
print("[ERROR] \(message())")
#endif
}
}

View File

@@ -0,0 +1,28 @@
import Foundation
import SwiftData
/// 打卡习惯的持久化实体(对应 Android Room 的 @Entity)。
@Model
public final class HabitEntity {
public var id: String
public var title: String
public var detail: String
public var streakDays: Int
public var createdAt: Date
public init(id: String, title: String, detail: String, streakDays: Int, createdAt: Date) {
self.id = id
self.title = title
self.detail = detail
self.streakDays = streakDays
self.createdAt = createdAt
}
}
/// SwiftData 容器工厂(对应 Android RoomDatabase)。
public enum PersistenceController {
public static func makeModelContainer(inMemory: Bool = false) throws -> ModelContainer {
let configuration = ModelConfiguration(isStoredInMemoryOnly: inMemory)
return try ModelContainer(for: HabitEntity.self, configurations: configuration)
}
}

View File

@@ -0,0 +1,8 @@
import CoreModel
/// Home 数据仓库协议(定义在 domain 层,实现放 CoreRepository)。
/// Kotlin: interface HomeRepository { fun observeHome(): Flow<HomeData> }
public protocol HomeRepository: Sendable {
/// Kotlin: Flow<HomeData> → Swift: AsyncThrowingStream<HomeData, Error>
func observeHome() -> AsyncThrowingStream<HomeData, Error>
}

View File

@@ -0,0 +1,16 @@
import CoreModel
/// 获取 Home 数据的用例。
/// Kotlin: class HomeUseCase(...) { suspend operator fun invoke(): HomeData }
public struct HomeUseCase: Sendable {
private let repository: HomeRepository
public init(repository: HomeRepository) {
self.repository = repository
}
/// Kotlin: Flow<HomeData> → Swift: AsyncThrowingStream<HomeData, Error>
public func observeHome() -> AsyncThrowingStream<HomeData, Error> {
repository.observeHome()
}
}

View File

@@ -0,0 +1,27 @@
import Foundation
/// 一个打卡习惯(对应 Android 端 Habit)。
public struct Habit: Identifiable, Hashable, Sendable {
public let id: String
public let title: String
/// 备注 / 目标描述。
public let detail: String
/// 今日是否已打卡。
public let isCheckedInToday: Bool
/// 连续打卡天数。
public let streakDays: Int
public init(
id: String,
title: String,
detail: String,
isCheckedInToday: Bool,
streakDays: Int
) {
self.id = id
self.title = title
self.detail = detail
self.isCheckedInToday = isCheckedInToday
self.streakDays = streakDays
}
}

View File

@@ -0,0 +1,19 @@
import Foundation
/// Home 页面的聚合数据(对应 Android 端 HomeData)。
public struct HomeData: Identifiable, Hashable, Sendable {
public let id: String
/// 顶部问候语,例如「早上好,Andy」。
public let greeting: String
/// 今日打卡摘要,例如「已完成 2 / 4」。
public let summary: String
/// 今日待打卡的习惯列表。
public let habits: [Habit]
public init(id: String, greeting: String, summary: String, habits: [Habit]) {
self.id = id
self.greeting = greeting
self.summary = summary
self.habits = habits
}
}

View File

@@ -0,0 +1,37 @@
import Foundation
/// 网络错误。
public enum NetworkError: Error, Sendable {
case invalidResponse
case http(statusCode: Int)
case decoding(String)
}
/// 通用 HTTP 客户端(对应 Android Retrofit 的最小抽象)。
public protocol APIClient: Sendable {
func get<T: Decodable & Sendable>(_ url: URL) async throws -> T
}
/// 基于 URLSession 的 APIClient 实现。
public struct URLSessionAPIClient: APIClient {
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
public func get<T: Decodable & Sendable>(_ url: URL) async throws -> T {
let (data, response) = try await session.data(from: url)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(http.statusCode) else {
throw NetworkError.http(statusCode: http.statusCode)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.decoding(error.localizedDescription)
}
}
}

View File

@@ -0,0 +1,15 @@
import Foundation
/// 示例网络 DTO(与领域 Model 分离,对应 Kotlin @Serializable 的 data class)。
/// 注意:DTO 只承载接口字段,不直接进入 UI;领域层用 CoreModel.HomeData。
public struct HomeDTO: Codable, Sendable {
public let id: String
public let greeting: String
public let habits: [HabitDTO]
}
public struct HabitDTO: Codable, Sendable {
public let id: String
public let title: String
public let checkedIn: Bool
}

View File

@@ -0,0 +1,40 @@
import CoreCommon
import CoreDomain
import CoreModel
/// Fake 仓库实现(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。
public struct FakeHomeRepository: HomeRepository {
public init() {}
public func observeHome() -> AsyncThrowingStream<HomeData, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
// 模拟网络延迟。
try await Task.sleep(for: .milliseconds(400))
try Task.checkCancellation()
Logger.info("FakeHomeRepository 返回假数据")
continuation.yield(Self.fakeData)
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
private static let fakeData = HomeData(
id: "home-1",
greeting: "早上好,Andy",
summary: "今日已完成 2 / 4",
habits: [
Habit(id: "habit-1", title: "晨跑 5 公里", detail: "保持运动习惯", isCheckedInToday: true, streakDays: 12),
Habit(id: "habit-2", title: "阅读 30 分钟", detail: "每天进步一点", isCheckedInToday: true, streakDays: 8),
Habit(id: "habit-3", title: "喝水 8 杯", detail: "健康补水", isCheckedInToday: false, streakDays: 0),
Habit(id: "habit-4", title: "写日记", detail: "记录今日所思", isCheckedInToday: false, streakDays: 3),
]
)
}

View File

@@ -0,0 +1,16 @@
import SwiftUI
/// 空状态占位视图。
public struct EmptyStateView: View {
private let title: String
private let systemImage: String
public init(title: String, systemImage: String = "tray") {
self.title = title
self.systemImage = systemImage
}
public var body: some View {
ContentUnavailableView(title, systemImage: systemImage)
}
}

View File

@@ -0,0 +1,22 @@
import SwiftUI
/// 错误占位视图(带重试)。
public struct ErrorView: View {
private let message: String
private let retry: () -> Void
public init(message: String, retry: @escaping () -> Void) {
self.message = message
self.retry = retry
}
public var body: some View {
ContentUnavailableView {
Label("出错了", systemImage: "exclamationmark.triangle")
} description: {
Text(message)
} actions: {
Button("重试", action: retry)
}
}
}

View File

@@ -0,0 +1,11 @@
import SwiftUI
/// 加载中占位视图。
public struct LoadingView: View {
public init() {}
public var body: some View {
ProgressView("加载中…")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

View File

@@ -0,0 +1,16 @@
import SwiftUI
/// 尚未实现的 Tab 占位视图。
public struct PlaceholderView: View {
private let title: String
private let systemImage: String
public init(_ title: String, systemImage: String) {
self.title = title
self.systemImage = systemImage
}
public var body: some View {
ContentUnavailableView(title, systemImage: systemImage)
}
}

View File

@@ -0,0 +1,25 @@
import SwiftUI
/// 设计 tokens(颜色 / 间距 / 圆角),对应 Android DesignSystem。
public enum DS {
public enum Color {
public static let accent = SwiftUI.Color.accentColor
public static let success = SwiftUI.Color.green
public static let danger = SwiftUI.Color.red
public static let warning = SwiftUI.Color.orange
}
public enum Spacing {
public static let xs: CGFloat = 4
public static let sm: CGFloat = 8
public static let md: CGFloat = 16
public static let lg: CGFloat = 24
public static let xl: CGFloat = 32
}
public enum Radius {
public static let sm: CGFloat = 8
public static let md: CGFloat = 12
public static let lg: CGFloat = 16
}
}

View File

@@ -0,0 +1,10 @@
import SwiftUI
/// FeatureAuth 占位(后续实现登录 / 注册)。
public struct AuthScreen: View {
public init() {}
public var body: some View {
ContentUnavailableView("登录", systemImage: "person.crop.circle")
}
}

View File

@@ -0,0 +1,77 @@
import SwiftUI
import CoreModel
import CoreUI
import DesignSystem
public struct HomeScreen: View {
private let viewModel: HomeViewModel
public init(viewModel: HomeViewModel) {
self.viewModel = viewModel
}
public var body: some View {
content
.navigationTitle("首页")
.task { viewModel.load() }
}
@ViewBuilder
private var content: some View {
switch viewModel.uiState {
case .loading:
LoadingView()
case .empty:
EmptyStateView(title: "今天还没有待打卡的习惯", systemImage: "checkmark.circle")
case .success(let data):
successContent(data)
case .error(let message):
ErrorView(message: message) { viewModel.load() }
}
}
private func successContent(_ data: HomeData) -> some View {
List {
Section {
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
Text(data.greeting)
.font(.largeTitle.bold())
Text(data.summary)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.vertical, DS.Spacing.sm)
}
Section("今日打卡") {
ForEach(data.habits) { habit in
HabitRow(habit: habit)
}
}
}
}
}
private struct HabitRow: View {
let habit: Habit
var body: some View {
HStack {
Image(systemName: habit.isCheckedInToday ? "checkmark.circle.fill" : "circle")
.foregroundStyle(habit.isCheckedInToday ? DS.Color.success : DS.Color.warning)
.font(.title3)
VStack(alignment: .leading, spacing: 2) {
Text(habit.title)
.font(.headline)
Text(habit.detail)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text("🔥 \(habit.streakDays) 天")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, DS.Spacing.xs)
}
}

View File

@@ -0,0 +1,9 @@
import CoreModel
/// Kotlin: sealed interface HomeUiState → Swift: enum 关联值
public enum HomeUiState {
case loading
case success(HomeData)
case empty
case error(String)
}

View File

@@ -0,0 +1,34 @@
import CoreDomain
import CoreModel
import Observation
@MainActor
@Observable
public final class HomeViewModel {
public private(set) var uiState: HomeUiState = .loading
private let homeUseCase: HomeUseCase
private var loadTask: Task<Void, Never>?
public init(homeUseCase: HomeUseCase) {
self.homeUseCase = homeUseCase
}
public func load() {
// flatMapLatest 的等价:新 load 前先取消旧的观察。
loadTask?.cancel()
loadTask = Task { [weak self] in
guard let self else { return }
self.uiState = .loading
do {
for try await data in self.homeUseCase.observeHome() {
self.uiState = data.habits.isEmpty ? .empty : .success(data)
}
} catch is CancellationError {
// 被更新的 load() 取代,保持当前状态即可。
} catch {
self.uiState = .error(error.localizedDescription)
}
}
}
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Andy Chen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# ios-client
SwiftUI + MVVM 单向数据流的 iOS「打卡/习惯追踪」客户端,采用 SPM 多 target 模块化,架构对齐 Android NIA(`core/*` + `feature/*`)。
## 架构
```
App(组合根)──► feature/* ──► core/*
│
└──► CoreRepository ──► CoreNetwork / CoreDatabase / CoreDomain
```
依赖单向无环,由 SPM target 依赖天然强制(import 不到即编不过)。
## 模块
| 模块 | 职责 |
|---|---|
| CoreModel | 领域模型(叶子,`Sendable` 值类型)|
| CoreCommon | 通用工具(日志等)|
| CoreDomain | Repository 协议 + UseCase(纯 Swift)|
| CoreRepository | Repository 实现(Fake / OfflineFirst)|
| CoreNetwork | DTO + URLSession APIClient |
| CoreDatabase | SwiftData(`@Model` + `ModelContainer`)|
| DesignSystem | 设计 tokens(颜色/间距/圆角)|
| CoreUI | 可复用 SwiftUI 组件 |
| FeatureHome | 首页(打卡列表)|
| FeatureAuth | 登录 / 我的(占位)|
| App | 组合根(DI + 导航)|
## 环境要求
- Xcode 26.6+
- iOS 17.0+(`@Observable` / SwiftData 下限)
## 构建
```bash
xcodebuild -project ios-client.xcodeproj -scheme ios-client \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO build
```
## Lint
```bash
brew install swiftlint # 首次
swiftlint lint
```
## 目录与协作规范
见 [CLAUDE.md](CLAUDE.md) 与 [CONTRIBUTING.md](CONTRIBUTING.md)。
## License
[MIT](LICENSE)

View File

@@ -65,6 +65,7 @@
);
name = "ios-client";
packageProductDependencies = (
A10000000000000000000002 /* App */,
);
productName = "ios-client";
productReference = A1890A533057E82C003D5BE4 /* ios-client.app */;
@@ -94,6 +95,9 @@
);
mainGroup = A1890A4A3057E82C003D5BE4;
minimizedProjectReferenceProxies = 1;
packageReferences = (
A10000000000000000000001 /* XCLocalSwiftPackageReference "IosClientKit" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = A1890A543057E82C003D5BE4 /* Products */;
projectDirPath = "";
@@ -246,6 +250,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 972VPTZ33X;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
@@ -270,7 +275,7 @@
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -288,6 +293,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 972VPTZ33X;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
@@ -312,7 +318,7 @@
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -325,6 +331,20 @@
};
/* End XCBuildConfiguration section */
/* Begin XCLocalSwiftPackageReference section */
A10000000000000000000001 /* XCLocalSwiftPackageReference "IosClientKit" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = IosClientKit;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A10000000000000000000002 /* App */ = {
isa = XCSwiftPackageProductDependency;
productName = App;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCConfigurationList section */
A1890A4E3057E82C003D5BE4 /* Build configuration list for PBXProject "ios-client" */ = {
isa = XCConfigurationList;

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A1890A523057E82C003D5BE4"
BuildableName = "ios-client.app"
BlueprintName = "ios-client"
ReferencedContainer = "container:ios-client.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A1890A523057E82C003D5BE4"
BuildableName = "ios-client.app"
BlueprintName = "ios-client"
ReferencedContainer = "container:ios-client.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A1890A523057E82C003D5BE4"
BuildableName = "ios-client.app"
BlueprintName = "ios-client"
ReferencedContainer = "container:ios-client.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,24 +0,0 @@
//
// ContentView.swift
// ios-client
//
// Created by Andy on 2026/9/14.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}

View File

@@ -0,0 +1,11 @@
import SwiftUI
import App
@main
struct IOSClientApp: App {
var body: some Scene {
WindowGroup {
AppRootView()
}
}
}

View File

@@ -1,17 +0,0 @@
//
// ios_clientApp.swift
// ios-client
//
// Created by Andy on 2026/9/14.
//
import SwiftUI
@main
struct ios_clientApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

89
skills-lock.json Normal file
View File

@@ -0,0 +1,89 @@
{
"version": 1,
"skills": {
"brainstorming": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/brainstorming/SKILL.md",
"computedHash": "b58d55b64742ecd37923985ec7bdfbe678ffbcb9895562abd28c0a32dcacd7fb"
},
"dispatching-parallel-agents": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/dispatching-parallel-agents/SKILL.md",
"computedHash": "b011107f17188e0b2898bb1856b5bac8b49537cc86e737c82126558a235bd62a"
},
"executing-plans": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/executing-plans/SKILL.md",
"computedHash": "78a07800ee99a5861ae10be59d79fad085fdca10444e8aaeb2691597e357cff8"
},
"finishing-a-development-branch": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/finishing-a-development-branch/SKILL.md",
"computedHash": "4f571e7befb8b0946d9d7770232c818812412e9ab1dfb811f847edfba1fc6275"
},
"receiving-code-review": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/receiving-code-review/SKILL.md",
"computedHash": "59361179fc9225173e0c145ffd9bafbf9fdbf8b43ce3dc308497a0b4f83a6063"
},
"requesting-code-review": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/requesting-code-review/SKILL.md",
"computedHash": "004f340ae65811095d6a9bb0a759c87d0a8085c5e705dbb019b011626293d2e9"
},
"subagent-driven-development": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/subagent-driven-development/SKILL.md",
"computedHash": "367cc1d3216d6672b947c8c6583dbd6440ea407beb6da75a97d3a8ee1a23640a"
},
"systematic-debugging": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/systematic-debugging/SKILL.md",
"computedHash": "a2750dfbaed1690da73b120254892c4f926b4576cb82eb16fc877783941fe39b"
},
"test-driven-development": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/test-driven-development/SKILL.md",
"computedHash": "a4126a9c5f17be3c228aa9e5755898ade283448b86099f56f40852e8c14532be"
},
"using-git-worktrees": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/using-git-worktrees/SKILL.md",
"computedHash": "48c8a1040e378e0cb7208107294e3afa9067b9370c3f33b49a443eb6b7528b0d"
},
"using-superpowers": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/using-superpowers/SKILL.md",
"computedHash": "c7abe8bdc2099bbe4f245454d32c7563ccd2bb41dcc218b02b461c2acc3c6ae6"
},
"verification-before-completion": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/verification-before-completion/SKILL.md",
"computedHash": "b529822bdac4b197b180fe12225cff86b930b65910435103c10622df7f041651"
},
"writing-plans": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/writing-plans/SKILL.md",
"computedHash": "27c75044e444a4fc68798c3f799dc23eaff3a6bfabff32e6ad75bb20f881c094"
},
"writing-skills": {
"source": "obra/superpowers",
"sourceType": "github",
"skillPath": "skills/writing-skills/SKILL.md",
"computedHash": "e5dfc1a942f56dc757421b0d87ae1f38ab693ebc71138e65268fd408bb5c93d3"
}
}
}