refactor: 移除重复导航头

This commit is contained in:
Junhui Chen 2025-09-08 16:09:56 +08:00
parent 5cc91eca51
commit feadfd92a7
9 changed files with 142 additions and 68 deletions

93
specs/refactor_spec.md Normal file
View 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.25s0.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 等
## 实施计划(分阶段)
- 第一阶段12 天,先解卡顿):
1) 统一导航:移除子页面 `NavigationView`,使用顶层 `NavigationStack + Router`
2) 计时器降频0.250.5s;如非必要移除毫秒级显示。
3) GIF 限制播放或替换为 Lottie关/收敛网络大日志。
- 第二阶段24 天):
4) 为 `BlindBox` 引入 ViewModel迁移副作用与状态。
5) 轮询改为可取消的异步序列;媒体预热与尺寸探测后台化。
6) 视图拆分与体量控制。
- 第三阶段(持续):
7) 目录重组ViewModel 标注 `@MainActor`;保留 `os_signpost` 监测关键路径。
## 验收标准DoD
- 导航:仅顶层 `NavigationStack`;子页面无 `NavigationView`
- 性能:转场掉帧率明显下降;主界面进入/退出动画流畅。
- 结构:`BlindBoxView` < 300 主要状态/副作用位于 ViewModel
- 资源GIF 仅在可见时播放或替换为 Lottie网络日志按需输出。
## 任务清单(同步 todo
- [x] nav-1 统一导航(移除子页面 NavigationViewRouter 返回)
- [ ] 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 重构,其它模块随后跟进。

View File

@ -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)
)

View File

@ -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 }
)
}
}
}
}

View File

@ -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
ScrollView {
VStack(spacing: 0) {
//
navigationHeader
//
mainCreditsCard
//
mainCreditsCard
//
creditsHistorySection
//
creditsHistorySection
Spacer(minLength: 100)
}
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()
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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: -

View File

@ -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())
}
}