feat(ios): TabBar 隐藏/出现增加平滑动画

- ContentView 新增 TabBarVisibleKey 环境值 + AnimatedTabBarHide modifier
- TabView 使用 .toolbar(visible/hidden) + .animation 驱动动画
- 子页面 onAppear 隐藏 → onDisappear 出现,带 easeInOut 0.28s
- 替换所有静态 .toolbar(.hidden, for: .tabBar) 为 .animatedTabBarHide()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-27 22:16:22 +08:00
parent f288421493
commit 42b60a21ec
17 changed files with 65 additions and 21 deletions

View File

@ -1,7 +1,48 @@
import SwiftUI import SwiftUI
// MARK: - TabBar hide animation key
struct TabBarVisibleKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var isTabBarHidden: Binding<Bool> {
get { self[TabBarVisibleKey.self] }
set { self[TabBarVisibleKey.self] = newValue }
}
}
/// modifier TabBar /
struct AnimatedTabBarHide: ViewModifier {
@Environment(\.isTabBarHidden) private var isTabBarHidden
func body(content: Content) -> some View {
content
.onAppear {
withAnimation(.easeInOut(duration: 0.28)) {
isTabBarHidden.wrappedValue = true
}
}
.onDisappear {
withAnimation(.easeInOut(duration: 0.28)) {
isTabBarHidden.wrappedValue = false
}
}
}
}
extension View {
func animatedTabBarHide() -> some View {
modifier(AnimatedTabBarHide())
}
}
// MARK: - ContentView
struct ContentView: View { struct ContentView: View {
@State private var selectedTab = "study" @State private var selectedTab = "study"
@State private var tabBarHidden = false
var body: some View { var body: some View {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
@ -42,6 +83,9 @@ struct ContentView: View {
.tag("profile") .tag("profile")
} }
.tint(Color.zxPrimary) .tint(Color.zxPrimary)
.toolbar(tabBarHidden ? .hidden : .visible, for: .tabBar)
.animation(.easeInOut(duration: 0.28), value: tabBarHidden)
.environment(\.isTabBarHidden, $tabBarHidden)
} }
} }

View File

@ -38,7 +38,7 @@ struct AIChatPage: View {
.padding(.horizontal, 20).padding(.bottom, 34) .padding(.horizontal, 20).padding(.bottom, 34)
} }
} }
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar) .navigationBarTitleDisplayMode(.inline).animatedTabBarHide()
.toolbarBackground(.hidden, for: .navigationBar) .toolbarBackground(.hidden, for: .navigationBar)
} }

View File

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

View File

@ -39,7 +39,7 @@ struct ActiveRecallView: View {
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
} }
} }
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar) .navigationBarTitleDisplayMode(.inline).animatedTabBarHide()
.toolbarBackground(.hidden, for: .navigationBar) .toolbarBackground(.hidden, for: .navigationBar)
.task { await viewModel.loadQuestions() } .task { await viewModel.loadQuestions() }
.overlay { .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))} 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() } 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) }.padding(.horizontal,20).padding(.top, 8).padding(.bottom,120) }.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden,for:.navigationBar) }.navigationBarTitleDisplayMode(.inline).animatedTabBarHide().toolbarBackground(.hidden,for:.navigationBar)
} }
} }

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ struct CreateLibraryPage: View {
} }
.disabled(isCreating || name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) .disabled(isCreating || name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) } }.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)} }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
} }
struct LibraryDetailPage: View { struct LibraryDetailPage: View {
@ -68,7 +68,7 @@ struct LibraryDetailPage: View {
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
.zxPullToRefresh { await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) } } .zxPullToRefresh { await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) } }
} }
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar) .navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) { NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) {
@ -226,7 +226,7 @@ struct AddKnowledgePage: View {
.disabled(!canSave || isSaving) .disabled(!canSave || isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80) }.scrollIndicators(.hidden) } }.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).animatedTabBarHide()
.fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.plainText, .pdf, .image], allowsMultipleSelection: false) { result in .fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.plainText, .pdf, .image], allowsMultipleSelection: false) { result in
if case .success(let urls) = result, let url = urls.first { handleFile(url) } if case .success(let urls) = result, let url = urls.first { handleFile(url) }
} }
@ -382,7 +382,7 @@ struct KnowledgeDetailPage: View {
} }
} }
}.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) } }.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)} }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
} }
struct ZXChip: View { let text: String; let color: Color 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()) } 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()) }
@ -397,7 +397,7 @@ struct ImportPage: View {
ZXImportOption(icon: "link", title: "链接导入", desc: "粘贴网页链接,自动提取内容") ZXImportOption(icon: "link", title: "链接导入", desc: "粘贴网页链接,自动提取内容")
ZXImportOption(icon: "photo.on.rectangle", title: "相册导入", desc: "从相册选择截图或图片") ZXImportOption(icon: "photo.on.rectangle", title: "相册导入", desc: "从相册选择截图或图片")
}.padding(.horizontal, 20).padding(.top, 8) }.scrollIndicators(.hidden) } }.padding(.horizontal, 20).padding(.top, 8) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar)} }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
} }
struct ZXImportOption: View { let icon: String; let title: String; let desc: String struct ZXImportOption: View { let icon: String; let title: String; let desc: String
var body: some View { Button { } label: { HStack(spacing: 14) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(Color.zxPurple).frame(width: 48, height: 48).background(Color.zxPurpleBG(0.1)).clipShape(RoundedRectangle(cornerRadius: 14)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(16).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) }.foregroundColor(.primary) } var body: some View { Button { } label: { HStack(spacing: 14) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(Color.zxPurple).frame(width: 48, height: 48).background(Color.zxPurpleBG(0.1)).clipShape(RoundedRectangle(cornerRadius: 14)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(16).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) }.foregroundColor(.primary) }
@ -421,5 +421,5 @@ struct EditKnowledgePage: View {
Task { _ = try? await KnowledgeItemService.shared.update(id: item.id, title: title, content: content, summary: nil) } 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)) } } 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) } }.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).animatedTabBarHide()}
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ struct NotificationListView: View {
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
.zxPullToRefresh { await refresh() } .zxPullToRefresh { await refresh() }
} }
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar) .navigationBarTitleDisplayMode(.inline).animatedTabBarHide()
.toolbarBackground(.hidden, for: .navigationBar) .toolbarBackground(.hidden, for: .navigationBar)
.task { await loadNotifications() } .task { await loadNotifications() }
} }

View File

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

View File

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

View File

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