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