feat(ios): TabBar 显示/隐藏增加入场出场动画

- TabBarState ObservableObject 管理可见性
- ContentView 用 .toolbar(hidden)+animation 驱动动画
- 子页面 hideTabBarWithAnimation() 替代静态 toolbar hidden
- 0.28s easeInOut 淡入淡出

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-30 10:57:39 +08:00
parent 7f252b48f0
commit f6df01d9ca
18 changed files with 61 additions and 25 deletions

View File

@ -1,7 +1,40 @@
import SwiftUI
// MARK: - TabBar visibility state
class TabBarState: ObservableObject {
@Published var isHidden = false
}
extension View {
func hideTabBarWithAnimation() -> some View {
modifier(HideTabBarModifier())
}
}
struct HideTabBarModifier: ViewModifier {
@EnvironmentObject private var tabBarState: TabBarState
func body(content: Content) -> some View {
content
.onAppear {
withAnimation(.easeInOut(duration: 0.28)) {
tabBarState.isHidden = true
}
}
.onDisappear {
withAnimation(.easeInOut(duration: 0.28)) {
tabBarState.isHidden = false
}
}
}
}
// MARK: - ContentView
struct ContentView: View {
@State private var selectedTab = "study"
@StateObject private var tabBarState = TabBarState()
var body: some View {
TabView(selection: $selectedTab) {
@ -41,7 +74,10 @@ struct ContentView: View {
}
.tag("profile")
}
.environmentObject(tabBarState)
.tint(Color.zxPrimary)
.toolbar(tabBarState.isHidden ? .hidden : .visible, for: .tabBar)
.animation(.easeInOut(duration: 0.28), value: tabBarState.isHidden)
}
}

View File

@ -68,7 +68,7 @@ struct AIChatPage: View {
}
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {

View File

@ -101,7 +101,7 @@ struct AIFeedbackPageView: View {
.transition(.opacity.combined(with: .scale(scale: 0.95)))
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -39,7 +39,7 @@ struct ActiveRecallView: View {
.scrollIndicators(.hidden)
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.task { await viewModel.loadQuestions() }
.overlay {

View File

@ -12,6 +12,6 @@ struct DailyThinkingPage: View {
VStack(alignment:.leading,spacing:8){Text("你的回答").font(.system(size:13,weight:.semibold)).foregroundColor(Color.zxF04);TextEditor(text:$answer).zxFontScaled(size:13).foregroundColor(Color.zxF0).tint(Color.zxPurple).frame(minHeight:160).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1))}
if !submitted{ NavigationLink(value: Route.aiFeedback){ Text("提交回答,获取 AI 反馈").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16)).shadow(color:Color(hex:"#7C6EFA",opacity:0.3),radius:24) }.zxPressable() }
}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,120) }.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden,for:.navigationBar)
}.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden,for:.navigationBar)
}
}

View File

@ -32,7 +32,7 @@ struct RecallTestPage: View {
}
.scrollIndicators(.hidden)
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -16,7 +16,7 @@ struct WeakPointsPage: View {
}
.scrollIndicators(.hidden)
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -133,6 +133,6 @@ struct LibrarySearchView: View {
}
}.padding(.horizontal, 20) }.scrollIndicators(.hidden)
}
}.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
}.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -72,7 +72,7 @@ struct CreateLibraryPage: View {
}
.disabled(isCreating || name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || desc.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()
.photosPicker(isPresented: $showCoverPicker, selection: $coverPhotoItem, matching: .images)
.onChange(of: coverPhotoItem) { _, item in
guard let item else { return }
@ -238,7 +238,7 @@ struct LibraryDetailPage: View {
.zxPullToRefresh { await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) }
}
}
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()
.onChange(of: detailTab) { _, newTab in
if newTab == 1 && sources.isEmpty { Task { await loadSources() } }
}
@ -455,7 +455,7 @@ struct AddKnowledgePage: View {
.disabled(!canSave || isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()
.fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.plainText, .pdf, .image], allowsMultipleSelection: true) { result in
if case .success(let urls) = result { handleFiles(urls) }
}
@ -643,7 +643,7 @@ struct KnowledgeDetailPage: View {
}
}
}.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)}
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()}
}
struct ZXChip: View { let text: String; let color: Color
var body: some View { Text(text).font(.system(size: 10, weight: .semibold)).foregroundColor(color).padding(.horizontal, 8).padding(.vertical, 2).background(color.opacity(0.12)).clipShape(Capsule()) }
@ -683,7 +683,7 @@ struct ImportPage: View {
}
}.padding(.horizontal, 20).padding(.top, 8) }.scrollIndicators(.hidden) }
}
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()
.disabled(isImporting)
.task { do { let kbs = try await KnowledgeBaseService.shared.list(page: 1, limit: 1); kbId = kbs.first?.id } catch {} }
.fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.pdf, .plainText], allowsMultipleSelection: true) { result in
@ -851,7 +851,7 @@ struct ImportReviewPage: View {
.scrollIndicators(.hidden)
}
}}
.navigationTitle("候选审批").navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)
.navigationTitle("候选审批").navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()
.disabled(isProcessing)
.task { await load() }
}
@ -911,6 +911,6 @@ struct EditKnowledgePage: View {
Task { _ = try? await KnowledgeItemService.shared.update(id: item.id, title: title, content: content, summary: nil) }
} label: { Text("保存修改").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16)) }
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).hideTabBarWithAnimation()
}
}

View File

@ -142,7 +142,7 @@ struct EditProfilePage: View {
}
.navigationTitle("编辑资料")
.navigationBarTitleDisplayMode(.inline)
.toolbar(.hidden, for: .tabBar)
.hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images)
.onChange(of: selectedPhotoItem) { _, item in

View File

@ -39,6 +39,6 @@ struct FeedbackFormView: View {
.disabled(isSubmitting)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
}.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -49,6 +49,6 @@ struct GoalSettingDetailView: View {
.disabled(isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
}.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -46,6 +46,6 @@ struct MethodPreferenceView: View {
.disabled(isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
}.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -35,7 +35,7 @@ struct NotificationListView: View {
.zxPullToRefresh { await refresh() }
}
.navigationTitle("消息中心")
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {

View File

@ -115,7 +115,7 @@ struct SettingsView: View {
notificationEnabled = p.notificationEnabled ?? true
reviewReminder = notificationEnabled
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
}
private func sectionHeader(_ text: String) -> some View {

View File

@ -51,7 +51,7 @@ struct QuizListView: View {
}
}
}
.navigationTitle("测验").navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
.navigationTitle("测验").navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
.task { await load() }
}
@ -164,7 +164,7 @@ struct QuizTakerView: View {
}
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
.task { await load() }
}
@ -230,7 +230,7 @@ struct QuizResultView: View {
}.scrollIndicators(.hidden)
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation().toolbarBackground(.hidden, for: .navigationBar)
.task { await load() }
}

View File

@ -36,7 +36,7 @@ struct LearningSessionView: View {
bottomBar
}.ignoresSafeArea(edges: .bottom)
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.onReceive(timer) { _ in
if isRunning { elapsed += 1 }

View File

@ -40,7 +40,7 @@ struct ReviewCardView: View {
.scrollIndicators(.hidden)
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.task { await viewModel.loadDueCards() }
.overlay {