11 KiB
11 KiB
完成页迭代 — 完整代码(仅交付,禁止自动应用)
以下为审查修正后的完整代码。关键修正:底部「回到我的内容」改为 navStore.switchToGrowthTab(),不再使用 navigationPath = NavigationPath(),以保证从任意 Tab 进入都能回到技能页 Tab。
1. CompletionView.swift(整文件替换)
import SwiftUI
// MARK: - 🏆 课程完结页 (Gesture Flow Edition)
// 交互:左滑进入(上级控制),右滑返回(系统原生),底部点击回技能页 Tab
struct CompletionView: View {
let courseId: String
let courseTitle: String?
let completedLessonCount: Int
@EnvironmentObject private var navStore: NavigationStore
@Environment(\.dismiss) private var dismiss
@State private var isSealed = false
@State private var breathingOpacity: Double = 0.3
@State private var contentOpacity: Double = 0
var body: some View {
ZStack {
Color.bgPaper.ignoresSafeArea()
VStack(spacing: 0) {
Spacer().frame(height: 60)
Spacer()
ZStack {
Circle()
.strokeBorder(
isSealed ?
AnyShapeStyle(
LinearGradient(
colors: [Color.brandVital, Color.cyberIris],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
) :
AnyShapeStyle(Color.inkSecondary.opacity(0.3)),
style: StrokeStyle(lineWidth: isSealed ? 4 : 1, dash: isSealed ? [] : [5, 5])
)
.frame(width: 220, height: 220)
.shadow(
color: isSealed ? Color.brandVital.opacity(0.4) : .clear,
radius: 20, x: 0, y: 0
)
.scaleEffect(isSealed ? 1.0 : 0.95)
.opacity(isSealed ? 1.0 : breathingOpacity)
.animation(.easeInOut(duration: 0.5), value: isSealed)
if isSealed {
VStack(spacing: 8) {
Text("已完成的")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.inkSecondary)
Text("第 \(completedLessonCount) 节")
.font(.system(size: 36, weight: .bold, design: .serif))
.foregroundStyle(
LinearGradient(
colors: [Color.brandVital, Color.cyberIris],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
.transition(.scale(scale: 0.5).combined(with: .opacity))
} else {
Text("完")
.font(.system(size: 80, weight: .light, design: .serif))
.foregroundColor(.inkPrimary.opacity(0.2))
.opacity(breathingOpacity)
}
}
.contentShape(Circle())
.onTapGesture {
triggerInteraction()
}
Spacer()
Button {
handleReturnToRoot()
} label: {
HStack(spacing: 4) {
Text("回到我的内容")
.font(.system(size: 15, weight: .medium))
Image(systemName: "arrow.right")
.font(.system(size: 12))
}
.foregroundColor(.brandVital)
.padding(.vertical, 12)
.padding(.horizontal, 32)
.background(Capsule().fill(Color.brandVital.opacity(0.05)))
}
.opacity(contentOpacity)
.padding(.bottom, 80)
}
}
.toolbar(.hidden, for: .navigationBar)
.modifier(SwipeBackEnablerModifier())
.onAppear {
startBreathing()
}
}
private func startBreathing() {
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
breathingOpacity = 0.8
}
}
private func triggerInteraction() {
guard !isSealed else { return }
let generator = UIImpactFeedbackGenerator(style: .heavy)
generator.impactOccurred()
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
isSealed = true
}
withAnimation(.easeOut.delay(0.5)) {
contentOpacity = 1.0
}
}
/// 回到技能页 Tab 根目录(与当前「继续学习」一致)
private func handleReturnToRoot() {
navStore.switchToGrowthTab()
}
}
// MARK: - 侧滑返回:隐藏导航栏时仍可右滑 pop
struct SwipeBackEnablerModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(SwipeBackEnabler())
}
}
private struct SwipeBackEnabler: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController { UIViewController() }
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
DispatchQueue.main.async {
if let nc = uiViewController.navigationController {
nc.interactivePopGestureRecognizer?.delegate = nil
nc.interactivePopGestureRecognizer?.isEnabled = true
}
}
}
}
2. VerticalScreenPlayerView.swift(仅改动部分)
2.1 保持现有 init 与属性
navigationPath: Binding<NavigationPath>?保持可选,NoteTreeView / NoteListView 不传时仍不 push 完成页。isLastNode,courseTitle等保持不变。
2.2 替换「加载成功」分支:去掉占位页,仅用左滑手势 push
原逻辑(约 146–186 行):
TabView 内 ForEach(allCourseNodes) + CompletionPlaceholderPage().tag("wg://completion"),外加 .onChange(of: currentNodeId) 在 newId == "wg://completion" 时 append completion。
新逻辑:
TabView内仅ForEach(allCourseNodes),每页在最后一节上挂LastPageSwipeModifier,左滑时调用triggerCompletionNavigation。- 删除
CompletionPlaceholderPage及其 tag、删除.onChange(of: currentNodeId)中与"wg://completion"相关的逻辑。 - 新增
@State private var isNavigatingToCompletion = false(防抖),以及triggerCompletionNavigation()、LastPageSwipeModifier。
替换后的内容区代码(直接替换原 else if !allCourseNodes.isEmpty { ... } 整块):
} else if !allCourseNodes.isEmpty {
TabView(selection: $currentNodeId) {
ForEach(allCourseNodes, id: \.id) { node in
let isFirst = isFirstNodeInChapter(nodeId: node.id)
let chapterTitle = getChapterTitle(for: node.id)
LessonPageView(
courseId: courseId,
nodeId: node.id,
currentGlobalNodeId: $currentNodeId,
initialScrollIndex: node.id == initialNodeId ? initialScrollIndex : nil,
headerConfig: HeaderConfig(
showChapterTitle: isFirst,
chapterTitle: chapterTitle
),
courseTitle: courseTitle,
navigationPath: navigationPath
)
.tag(node.id)
.modifier(LastPageSwipeModifier(
isLastPage: node.id == allCourseNodes.last?.id,
onSwipeLeft: triggerCompletionNavigation
))
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(.container, edges: .bottom)
}
2.3 新增状态与函数(放在 Logic Helpers 区域)
@State private var isNavigatingToCompletion = false
private func triggerCompletionNavigation() {
guard !isNavigatingToCompletion, let path = navigationPath else { return }
isNavigatingToCompletion = true
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
let count = UserManager.shared.studyStats.lessons
path.wrappedValue.append(CourseNavigation.completion(
courseId: courseId,
courseTitle: courseTitle,
completedLessonCount: count
))
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
isNavigatingToCompletion = false
}
}
2.4 删除 onAppear 中「从完成页返回切回最后一节」逻辑
删除:
if currentNodeId == "wg://completion", let lastId = allCourseNodes.last?.id {
currentNodeId = lastId
}
(去掉占位页后不再存在 "wg://completion" 选中的情况。)
2.5 删除 CompletionPlaceholderPage,新增 LastPageSwipeModifier
删除 CompletionPlaceholderPage 结构体。
在 // MARK: - 📄 单页课程视图 之前 新增:
// MARK: - 最后一页左滑:仅在最后一节挂载,左滑 push 完成页
private struct LastPageSwipeModifier: ViewModifier {
let isLastPage: Bool
let onSwipeLeft: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(
DragGesture(minimumDistance: 20, coordinateSpace: .local)
.onEnded { value in
guard isLastPage else { return }
if value.translation.width < -60,
abs(value.translation.width) > abs(value.translation.height) {
onSwipeLeft()
}
}
)
}
}
3. 调用方(GrowthView / ProfileView / DiscoveryView)
无需改动。CompletionView 仍为三参:courseId, courseTitle, completedLessonCount,依赖 @EnvironmentObject navStore,底部用 switchToGrowthTab() 回到技能页 Tab。
4. 小结
- CompletionView:无顶部返回、无打字机;赛博印章交互;右滑依赖
SwipeBackEnablerModifier;底部「回到我的内容」=navStore.switchToGrowthTab()。 - VerticalScreenPlayerView:去掉占位页与
onChange完成页逻辑;最后一节左滑用LastPageSwipeModifier+triggerCompletionNavigationpush 完成页;保留可选navigationPath,笔记流不变。 - 不修改 GrowthView / ProfileView / DiscoveryView 的
CompletionView(...)调用。