Compare commits
11 Commits
a67045fc52
...
559844a4a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
559844a4a4 | ||
|
|
6fd1df0503 | ||
|
|
ecb6a82f88 | ||
|
|
ae763ae3a1 | ||
|
|
ec19d3bf98 | ||
|
|
653caa0b84 | ||
|
|
b5de8ddfbd | ||
|
|
4fe0a26432 | ||
|
|
295efac07c | ||
|
|
599f1c7f8e | ||
|
|
e29994f49d |
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal 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
38
.swiftlint.yml
Normal 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
81
CLAUDE.md
Normal 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
31
CONTRIBUTING.md
Normal 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:`。
|
||||||
52
IosClientKit/Package.swift
Normal file
52
IosClientKit/Package.swift
Normal 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]
|
||||||
|
)
|
||||||
14
IosClientKit/Sources/App/AppContainer.swift
Normal file
14
IosClientKit/Sources/App/AppContainer.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
IosClientKit/Sources/App/AppRootView.swift
Normal file
36
IosClientKit/Sources/App/AppRootView.swift
Normal 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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
IosClientKit/Sources/CoreCommon/Logger.swift
Normal file
14
IosClientKit/Sources/CoreCommon/Logger.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
IosClientKit/Sources/CoreDomain/HomeRepository.swift
Normal file
8
IosClientKit/Sources/CoreDomain/HomeRepository.swift
Normal 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>
|
||||||
|
}
|
||||||
16
IosClientKit/Sources/CoreDomain/HomeUseCase.swift
Normal file
16
IosClientKit/Sources/CoreDomain/HomeUseCase.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
27
IosClientKit/Sources/CoreModel/Habit.swift
Normal file
27
IosClientKit/Sources/CoreModel/Habit.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
19
IosClientKit/Sources/CoreModel/HomeData.swift
Normal file
19
IosClientKit/Sources/CoreModel/HomeData.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
37
IosClientKit/Sources/CoreNetwork/APIClient.swift
Normal file
37
IosClientKit/Sources/CoreNetwork/APIClient.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
IosClientKit/Sources/CoreNetwork/HomeDTO.swift
Normal file
15
IosClientKit/Sources/CoreNetwork/HomeDTO.swift
Normal 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
|
||||||
|
}
|
||||||
40
IosClientKit/Sources/CoreRepository/FakeHomeRepository.swift
Normal file
40
IosClientKit/Sources/CoreRepository/FakeHomeRepository.swift
Normal 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),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
16
IosClientKit/Sources/CoreUI/EmptyStateView.swift
Normal file
16
IosClientKit/Sources/CoreUI/EmptyStateView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
IosClientKit/Sources/CoreUI/ErrorView.swift
Normal file
22
IosClientKit/Sources/CoreUI/ErrorView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
IosClientKit/Sources/CoreUI/LoadingView.swift
Normal file
11
IosClientKit/Sources/CoreUI/LoadingView.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// 加载中占位视图。
|
||||||
|
public struct LoadingView: View {
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ProgressView("加载中…")
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
IosClientKit/Sources/CoreUI/PlaceholderView.swift
Normal file
16
IosClientKit/Sources/CoreUI/PlaceholderView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
IosClientKit/Sources/DesignSystem/DesignTokens.swift
Normal file
25
IosClientKit/Sources/DesignSystem/DesignTokens.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
10
IosClientKit/Sources/FeatureAuth/AuthScreen.swift
Normal file
10
IosClientKit/Sources/FeatureAuth/AuthScreen.swift
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// FeatureAuth 占位(后续实现登录 / 注册)。
|
||||||
|
public struct AuthScreen: View {
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ContentUnavailableView("登录", systemImage: "person.crop.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
77
IosClientKit/Sources/FeatureHome/HomeScreen.swift
Normal file
77
IosClientKit/Sources/FeatureHome/HomeScreen.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
IosClientKit/Sources/FeatureHome/HomeUiState.swift
Normal file
9
IosClientKit/Sources/FeatureHome/HomeUiState.swift
Normal 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)
|
||||||
|
}
|
||||||
34
IosClientKit/Sources/FeatureHome/HomeViewModel.swift
Normal file
34
IosClientKit/Sources/FeatureHome/HomeViewModel.swift
Normal 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
21
LICENSE
Normal 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
57
README.md
Normal 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)
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
);
|
);
|
||||||
name = "ios-client";
|
name = "ios-client";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
A10000000000000000000002 /* App */,
|
||||||
);
|
);
|
||||||
productName = "ios-client";
|
productName = "ios-client";
|
||||||
productReference = A1890A533057E82C003D5BE4 /* ios-client.app */;
|
productReference = A1890A533057E82C003D5BE4 /* ios-client.app */;
|
||||||
@@ -94,6 +95,9 @@
|
|||||||
);
|
);
|
||||||
mainGroup = A1890A4A3057E82C003D5BE4;
|
mainGroup = A1890A4A3057E82C003D5BE4;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
A10000000000000000000001 /* XCLocalSwiftPackageReference "IosClientKit" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = A1890A543057E82C003D5BE4 /* Products */;
|
productRefGroup = A1890A543057E82C003D5BE4 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -246,6 +250,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 972VPTZ33X;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
@@ -270,7 +275,7 @@
|
|||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -288,6 +293,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 972VPTZ33X;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
@@ -312,7 +318,7 @@
|
|||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -325,6 +331,20 @@
|
|||||||
};
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* 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 */
|
/* Begin XCConfigurationList section */
|
||||||
A1890A4E3057E82C003D5BE4 /* Build configuration list for PBXProject "ios-client" */ = {
|
A1890A4E3057E82C003D5BE4 /* Build configuration list for PBXProject "ios-client" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
11
ios-client/IOSClientApp.swift
Normal file
11
ios-client/IOSClientApp.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import App
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct IOSClientApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
AppRootView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
89
skills-lock.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user