diff --git a/IosClientKit/Sources/FeatureAuth/AuthScreen.swift b/IosClientKit/Sources/FeatureAuth/AuthScreen.swift new file mode 100644 index 0000000..c1f3cd3 --- /dev/null +++ b/IosClientKit/Sources/FeatureAuth/AuthScreen.swift @@ -0,0 +1,10 @@ +import SwiftUI + +/// FeatureAuth 占位(后续实现登录 / 注册)。 +public struct AuthScreen: View { + public init() {} + + public var body: some View { + ContentUnavailableView("登录", systemImage: "person.crop.circle") + } +} diff --git a/IosClientKit/Sources/FeatureHome/HomeScreen.swift b/IosClientKit/Sources/FeatureHome/HomeScreen.swift new file mode 100644 index 0000000..79aa4a1 --- /dev/null +++ b/IosClientKit/Sources/FeatureHome/HomeScreen.swift @@ -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) + } +} diff --git a/IosClientKit/Sources/FeatureHome/HomeUiState.swift b/IosClientKit/Sources/FeatureHome/HomeUiState.swift new file mode 100644 index 0000000..3926a18 --- /dev/null +++ b/IosClientKit/Sources/FeatureHome/HomeUiState.swift @@ -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) +} diff --git a/IosClientKit/Sources/FeatureHome/HomeViewModel.swift b/IosClientKit/Sources/FeatureHome/HomeViewModel.swift new file mode 100644 index 0000000..ff3b237 --- /dev/null +++ b/IosClientKit/Sources/FeatureHome/HomeViewModel.swift @@ -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? + + 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) + } + } + } +}