chore: core 多模块骨架
This commit is contained in:
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user