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