refactor: 移除重复导航头
This commit is contained in:
parent
5cc91eca51
commit
feadfd92a7
93
specs/refactor_spec.md
Normal file
93
specs/refactor_spec.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Wake iOS 重构与性能优化规格说明
|
||||
|
||||
版本: v0.1
|
||||
创建时间: 2025-09-08 15:41 +08
|
||||
|
||||
## 背景与目标
|
||||
- 现状问题:代码组织结构一般、页面间切换卡顿(特别是在重动画/媒体加载/网络日志时)。
|
||||
- 目标:
|
||||
- 提升导航一致性与可维护性(仅保留顶层 NavigationStack + Router)。
|
||||
- 降低页面切换卡顿(减少主线程压力、控制刷新频率、优化媒体与动画负载、收敛网络日志)。
|
||||
- 推动 Feature-Oriented 结构与 MVVM,降低视图体量与重绘范围。
|
||||
|
||||
## 架构调整总览
|
||||
- 统一导航:顶层 `NavigationStack(path: $router.path)`(见 `wake/WakeApp.swift`),子页面不再嵌套 `NavigationView`;使用 `Router.shared.navigate/pop/popToRoot`。
|
||||
- MVVM:优先对 `BlindBoxView` 引入 `BlindBoxViewModel`,将轮询、计时器、媒体预处理、会员信息等迁至 VM。
|
||||
- 并发与取消:轮询改 `AsyncSequence`/`Task` 可取消;倒计时改 Combine/AsyncTimer;统一在 `onDisappear`/路由变化处取消。
|
||||
- 媒体与动画:GIF 优先替换为 Lottie 或仅在可见态播放;模糊与缩放动画范围与时机控制;媒体元数据后台计算。
|
||||
- 网络日志:Debug 可控、限流;Release 关闭大段打印;使用 `os_log/Logger` 分类。
|
||||
- 工程结构:Feature-Oriented(`Core/`、`Features/*`、`SharedUI/`);延续 Theme/Typography/Spacing 设计系统。
|
||||
|
||||
## 导航设计规范
|
||||
- 顶层:`WakeApp` 中唯一 `NavigationStack`;其它页面不使用 `NavigationView`。
|
||||
- 路由:统一通过 `Router.shared.navigate(to:)`、`Router.shared.pop()`、`Router.shared.popToRoot()`。
|
||||
- 返回按钮:子页面通过 `Router.shared.pop()` 而非 `presentationMode.dismiss()`。
|
||||
|
||||
## BlindBox 模块重构要点
|
||||
- `BlindBoxViewModel`(@MainActor):
|
||||
- 状态:盲盒列表/单盒数据、会员信息、计时与轮询状态、媒体 URL/尺寸/播放器句柄。
|
||||
- 行为:`loadBlindBox()`、`start/stopPolling()`、`startCountdown()`、`prepareVideo/Image()`、资源清理。
|
||||
- 视图拆分:
|
||||
- `BlindBoxHeader`、`BlindBoxAnimationArea`(Loading/Ready/Opening/None)、`BlindBoxActionButton`、`BlindBoxScalingOverlay`。
|
||||
- 视图仅订阅少量 `@Published`,降低 body 重绘。
|
||||
|
||||
## 并发与轮询规范
|
||||
- 轮询:使用 `Task { for await ... in pollSequence }` + `task.cancel()`,严禁无 cancel 的 `while + Task.sleep`。
|
||||
- 倒计时:优先 0.25s–0.5s 频率;必要时“毫秒展示”不落地 state;严格在主线程更新 UI 状态。
|
||||
|
||||
## 媒体与动画规范
|
||||
- GIF -> Lottie 优先;若保留 GIF:仅在可见态播放,避免与大范围模糊+缩放并发。
|
||||
- 媒体预热与尺寸探测走后台,回主线程赋值。
|
||||
- 播放器生命周期集中管理,页面切换前暂停并释放。
|
||||
|
||||
## 网络日志策略
|
||||
- Debug:按需与限长打印;错误优先;可通过开关关闭详细日志。
|
||||
- Release:关闭大段请求/响应体打印。
|
||||
|
||||
## 工程结构规划(建议)
|
||||
- `Core/`:`Utils/`、`Network/`、`Auth/`、`Router/`、`Theme/`、`Typography/`
|
||||
- `Features/BlindBox/`:`View/`、`ViewModel/`、`Models/`、`API/`、`Components/`
|
||||
- `Features/Subscribe/`:含 `CreditsInfoCard`、`PlanCompare` 等
|
||||
- `Features/Memories/`
|
||||
- `SharedUI/`:Buttons、LottieView、SVGImage、SheetModal 等
|
||||
|
||||
## 实施计划(分阶段)
|
||||
- 第一阶段(1–2 天,先解卡顿):
|
||||
1) 统一导航:移除子页面 `NavigationView`,使用顶层 `NavigationStack + Router`。
|
||||
2) 计时器降频:0.25–0.5s;如非必要移除毫秒级显示。
|
||||
3) GIF 限制播放或替换为 Lottie;关/收敛网络大日志。
|
||||
- 第二阶段(2–4 天):
|
||||
4) 为 `BlindBox` 引入 ViewModel,迁移副作用与状态。
|
||||
5) 轮询改为可取消的异步序列;媒体预热与尺寸探测后台化。
|
||||
6) 视图拆分与体量控制。
|
||||
- 第三阶段(持续):
|
||||
7) 目录重组;ViewModel 标注 `@MainActor`;保留 `os_signpost` 监测关键路径。
|
||||
|
||||
## 验收标准(DoD)
|
||||
- 导航:仅顶层 `NavigationStack`;子页面无 `NavigationView`。
|
||||
- 性能:转场掉帧率明显下降;主界面进入/退出动画流畅。
|
||||
- 结构:`BlindBoxView` < 300 行,主要状态/副作用位于 ViewModel。
|
||||
- 资源:GIF 仅在可见时播放或替换为 Lottie;网络日志按需输出。
|
||||
|
||||
## 任务清单(同步 todo)
|
||||
- [x] nav-1 统一导航(移除子页面 NavigationView,Router 返回)
|
||||
- [ ] mvvm-1 BlindBox 引入 ViewModel,迁移逻辑
|
||||
- [ ] timer-1 计时器降频与取消
|
||||
- [ ] polling-1 轮询可取消化
|
||||
- [ ] media-1 媒体与动画优化(GIF->Lottie/可见播放)
|
||||
- [ ] concurrency-1 @MainActor 与线程安全
|
||||
- [ ] netlog-1 网络日志开关与限流
|
||||
- [ ] structure-1 目录重组
|
||||
- [ ] perf-1 性能埋点与基线
|
||||
|
||||
## 进度记录(每次执行后更新)
|
||||
- 2025-09-08 15:41 +08: 创建 specs 目录与本说明(v0.1)。
|
||||
- 2025-09-08 15:41 +08: 完成 nav-1(第一步):移除 `wake/View/Blind/BlindOutCome.swift` 与 `wake/View/Memories/MemoriesView.swift` 中的 `NavigationView`,改为使用 `Router.shared.pop()` 返回;依赖顶层 `NavigationStack`。
|
||||
- 2025-09-08 15:51 +08: 继续完成 nav-1:
|
||||
- 移除 `wake/View/Credits/CreditsDetailView.swift`、`wake/View/Welcome/SplashView.swift`、`wake/View/Owner/SettingsView.swift`、`wake/View/Components/Upload/MediaUpload.swift`(示例 `MediaUploadExample`)、`wake/View/Examples/MediaDemo.swift` 中的 `NavigationView`。
|
||||
- 将 `wake/View/Feedback.swift` 中 `FeedbackView` 与 `FeedbackDetailView` 的返回行为从 `dismiss()` 统一为 `Router.shared.pop()`。
|
||||
- 保留预览(Preview)中的 `NavigationView`,运行时代码已全部依赖顶层 `NavigationStack + Router`。
|
||||
|
||||
## 决策记录
|
||||
- 采用顶层 `NavigationStack + Router`,子页面取消 `NavigationView`。
|
||||
- `BlindBox` 优先落地 MVVM 重构,其它模块随后跟进。
|
||||
@ -7,7 +7,7 @@ struct BlindOutcomeView: View {
|
||||
let time: String?
|
||||
let description: String?
|
||||
let isMember: Bool
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
// Removed presentationMode; use Router.shared.pop() for back navigation
|
||||
@State private var isFullscreen = false
|
||||
@State private var isPlaying = false
|
||||
@State private var showControls = true
|
||||
@ -22,15 +22,14 @@ struct BlindOutcomeView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
ZStack {
|
||||
Color.themeTextWhiteSecondary.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 自定义导航栏
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
Router.shared.pop()
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "chevron.left")
|
||||
@ -159,9 +158,6 @@ struct BlindOutcomeView: View {
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.statusBar(hidden: isFullscreen)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.navigationBarHidden(true)
|
||||
.overlay(
|
||||
JoinModal(isPresented: $showIPListModal)
|
||||
)
|
||||
|
||||
@ -261,8 +261,7 @@ struct MediaUploadExample: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 20) {
|
||||
VStack(spacing: 20) {
|
||||
// 选择媒体按钮
|
||||
Button(action: { showMediaPicker = true }) {
|
||||
Label("选择媒体", systemImage: "photo.on.rectangle")
|
||||
@ -307,9 +306,8 @@ struct MediaUploadExample: View {
|
||||
.disabled(uploadManager.selectedMedia.isEmpty)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("媒体上传")
|
||||
.sheet(isPresented: $showMediaPicker) {
|
||||
}
|
||||
.sheet(isPresented: $showMediaPicker) {
|
||||
MediaPicker(
|
||||
selectedMedia: Binding(
|
||||
get: { self.uploadManager.selectedMedia },
|
||||
@ -324,7 +322,6 @@ struct MediaUploadExample: View {
|
||||
videoSelectionLimit: videoSelectionLimit,
|
||||
onDismiss: { showMediaPicker = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ struct CreditTransaction {
|
||||
|
||||
// MARK: - 积分详情页面
|
||||
struct CreditsDetailView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
// Removed presentationMode; use Router.shared.pop() for back navigation
|
||||
@State private var showRules = false
|
||||
|
||||
// 示例数据
|
||||
@ -77,30 +77,28 @@ struct CreditsDetailView: View {
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// 导航栏
|
||||
navigationHeader
|
||||
|
||||
// 主积分卡片
|
||||
mainCreditsCard
|
||||
|
||||
// 积分历史
|
||||
creditsHistorySection
|
||||
|
||||
Spacer(minLength: 100)
|
||||
}
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// 导航栏
|
||||
navigationHeader
|
||||
|
||||
// 主积分卡片
|
||||
mainCreditsCard
|
||||
|
||||
// 积分历史
|
||||
creditsHistorySection
|
||||
|
||||
Spacer(minLength: 100)
|
||||
}
|
||||
.background(Theme.Colors.background)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.background(Theme.Colors.background)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
// MARK: - 导航栏
|
||||
private var navigationHeader: some View {
|
||||
NaviHeader(title: "Credits") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
Router.shared.pop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,8 +7,7 @@ struct MediaUploadDemo: View {
|
||||
@State private var isUploading = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 20) {
|
||||
VStack(spacing: 20) {
|
||||
// 上传按钮
|
||||
Button(action: {
|
||||
showMediaPicker = true
|
||||
@ -125,7 +124,6 @@ struct MediaUploadDemo: View {
|
||||
showUploadAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FeedbackView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
// Use Router for navigation instead of dismiss
|
||||
@EnvironmentObject private var router: Router
|
||||
|
||||
@State private var selectedFeedback: FeedbackType? = FeedbackType.allCases.first
|
||||
@ -29,7 +29,7 @@ struct FeedbackView: View {
|
||||
// Custom Navigation Bar
|
||||
HStack {
|
||||
// Back Button
|
||||
Button(action: { dismiss() }) {
|
||||
Button(action: { router.pop() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
@ -140,14 +140,13 @@ struct FeedbackDetailView: View {
|
||||
let feedbackType: FeedbackView.FeedbackType
|
||||
@State private var feedbackText = ""
|
||||
@State private var contactInfo = ""
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Navigation Bar
|
||||
HStack {
|
||||
// Back Button
|
||||
Button(action: { dismiss() }) {
|
||||
Button(action: { Router.shared.pop() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
@ -248,8 +247,8 @@ struct FeedbackDetailView: View {
|
||||
print("Contact: \(contactInfo)")
|
||||
}
|
||||
|
||||
// Dismiss back to feedback type selection
|
||||
dismiss()
|
||||
// Navigate back to feedback type selection
|
||||
Router.shared.pop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ enum MemoryMediaType: Equatable {
|
||||
}
|
||||
|
||||
struct MemoriesView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
// Removed dismiss environment; use Router.shared.pop() for back navigation
|
||||
@State private var memories: [MemoryItem] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@ -74,13 +74,12 @@ struct MemoriesView: View {
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
// Top navigation bar
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.dismiss()
|
||||
Router.shared.pop()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
@ -121,7 +120,6 @@ struct MemoriesView: View {
|
||||
.zIndex(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.onAppear {
|
||||
fetchMemories()
|
||||
|
||||
@ -23,8 +23,7 @@ struct SettingsView: View {
|
||||
// MARK: - 主体视图
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
ZStack {
|
||||
// Theme background color
|
||||
Color.themeTextWhiteSecondary.edgesIgnoringSafeArea(.all)
|
||||
|
||||
@ -84,7 +83,6 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
@ -6,28 +6,25 @@ struct SplashView: View {
|
||||
@EnvironmentObject private var authState: AuthState
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
// 背景渐变
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Theme.Colors.primary, // Primary color with some transparency
|
||||
Theme.Colors.primaryDark, // Darker shade of the primary color
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
VStack(spacing: 50) {
|
||||
// FilmAnimation()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
isAnimating = true
|
||||
ZStack {
|
||||
// 背景渐变
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Theme.Colors.primary, // Primary color with some transparency
|
||||
Theme.Colors.primaryDark, // Darker shade of the primary color
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
VStack(spacing: 50) {
|
||||
// FilmAnimation()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
isAnimating = true
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user