chore: core 多模块骨架

This commit is contained in:
AndyChen1024
2026-09-14 21:10:44 +08:00
parent 295efac07c
commit 4fe0a26432
14 changed files with 294 additions and 0 deletions

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
}
}