feat: Tab(role: .search) with context-dependent action per tab
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

Single separated circle in tab bar (like Photos search icon):
- Home/Fitness: shows + icon, taps opens food assistant
- Reader idle: shows play icon, taps starts auto-scroll
- Reader playing: shows pause icon, taps stops auto-scroll

Icon updates dynamically via computed actionIcon property.
handleActionTap() routes the tap based on selectedTab.
After action, selectedTab returns to the previous tab (doesn't
stay on the invisible "action" tab).

Speed controls still appear as glass capsule overlay when playing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 07:55:50 -05:00
parent 8a8f865702
commit e2fc87b6aa

View File

@@ -35,6 +35,14 @@ struct MainTabView: View {
auth.currentUser?.id != 4 auth.currentUser?.id != 4
} }
// Dynamic icon for the search-role tab based on context
private var actionIcon: String {
if selectedTab == 2 {
return isAutoScrolling ? "pause.fill" : "play.fill"
}
return "plus"
}
var body: some View { var body: some View {
ZStack { ZStack {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
@@ -51,55 +59,37 @@ struct MainTabView: View {
ReaderTabView(vm: readerVM, isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed) ReaderTabView(vm: readerVM, isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed)
} }
} }
// Action button separated circle on trailing side of tab bar
// Home/Fitness: quick add food (+)
// Reader: play/pause auto-scroll
Tab(value: 3, role: .search) {
// This view shows briefly when tapped immediately redirect
Color.clear
.onAppear {
handleActionTap()
}
} label: {
Label("Action", systemImage: actionIcon)
}
} }
.tint(Color.accentWarm) .tint(Color.accentWarm)
.tabBarMinimizeBehavior(.onScrollDown) .tabBarMinimizeBehavior(.onScrollDown)
// Floating action button context-dependent // Feedback button (not on Reader)
VStack { if selectedTab != 2 {
Spacer() VStack {
HStack(alignment: .bottom) { Spacer()
if selectedTab != 2 { HStack {
FeedbackButton() FeedbackButton()
.padding(.leading, 20) .padding(.leading, 20)
Spacer()
} }
.padding(.bottom, 70)
Spacer()
if selectedTab == 2 && showReader {
// Reader: play/pause auto-scroll
Button {
withAnimation(.spring(duration: 0.3)) {
isAutoScrolling.toggle()
}
} label: {
Image(systemName: isAutoScrolling ? "pause.fill" : "play.fill")
.font(.system(size: 18))
.foregroundStyle(.white)
.frame(width: 56, height: 56)
.background(isAutoScrolling ? Color.red.opacity(0.85) : Color.accentWarm)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 8, y: 4)
}
.padding(.trailing, 20)
} else {
// Home/Fitness: FAB (+) for food
Button { showAssistant = true } label: {
Image(systemName: "plus")
.font(.title2.weight(.semibold))
.foregroundStyle(.white)
.frame(width: 56, height: 56)
.background(Color.accentWarm)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 8, y: 4)
}
.padding(.trailing, 20)
}
} }
.padding(.bottom, 70)
} }
// Auto-scroll speed controls (overlay when playing) // Auto-scroll speed controls (overlay when playing on Reader)
if isAutoScrolling && selectedTab == 2 { if isAutoScrolling && selectedTab == 2 {
VStack { VStack {
Spacer() Spacer()
@@ -130,7 +120,7 @@ struct MainTabView: View {
.padding(.vertical, 8) .padding(.vertical, 8)
.background(.ultraThinMaterial, in: Capsule()) .background(.ultraThinMaterial, in: Capsule())
.shadow(color: .black.opacity(0.1), radius: 8, y: 2) .shadow(color: .black.opacity(0.1), radius: 8, y: 2)
.padding(.bottom, 140) .padding(.bottom, 90)
.transition(.move(edge: .bottom).combined(with: .opacity)) .transition(.move(edge: .bottom).combined(with: .opacity))
} }
.animation(.spring(duration: 0.3), value: isAutoScrolling) .animation(.spring(duration: 0.3), value: isAutoScrolling)
@@ -157,11 +147,25 @@ struct MainTabView: View {
await readerVM.loadInitial() await readerVM.loadInitial()
} }
.onChange(of: selectedTab) { _, newTab in .onChange(of: selectedTab) { _, newTab in
// Stop auto-scroll when leaving Reader
if newTab != 2 { isAutoScrolling = false } if newTab != 2 { isAutoScrolling = false }
} }
} }
private func handleActionTap() {
if selectedTab == 2 {
// Reader: toggle auto-scroll, stay on Reader
withAnimation(.spring(duration: 0.3)) {
isAutoScrolling.toggle()
}
selectedTab = 2 // stay on Reader (don't switch to the "action" tab)
} else {
// Home/Fitness: open food assistant, return to previous tab
let returnTab = selectedTab
showAssistant = true
selectedTab = returnTab
}
}
private func foodAdded() { private func foodAdded() {
showAssistant = false showAssistant = false
selectedTab = 1 selectedTab = 1