fix: widget iOS 17 deployment target + register FloatingTabState
- Widget extension: 16.0 → 17.0 (containerBackground/widget API require 17) - xcodegen re-run picks up FloatingTabState.swift (was on disk but not registered in project.pbxproj after previous xcodegen run)
This commit is contained in:
52
KisaniCal/Components/FloatingTabState.swift
Normal file
52
KisaniCal/Components/FloatingTabState.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class FloatingTabState: ObservableObject {
|
||||
@Published private(set) var isCollapsed = false
|
||||
private var lastY: CGFloat = 0
|
||||
|
||||
func report(scrollY: CGFloat) {
|
||||
let delta = scrollY - lastY
|
||||
guard abs(delta) > 1.5 else { return }
|
||||
let shouldCollapse = delta < 0 // content Y decreases → user scrolling down
|
||||
lastY = scrollY
|
||||
guard shouldCollapse != isCollapsed else { return }
|
||||
withAnimation(KisaniSpring.snappy) {
|
||||
isCollapsed = shouldCollapse
|
||||
}
|
||||
}
|
||||
|
||||
func resetOffset() {
|
||||
lastY = 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Scroll tracker
|
||||
|
||||
private struct ScrollYKey: PreferenceKey {
|
||||
static let defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
|
||||
}
|
||||
|
||||
private struct TabBarScrollModifier: ViewModifier {
|
||||
@EnvironmentObject private var tabState: FloatingTabState
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(
|
||||
GeometryReader { geo in
|
||||
Color.clear
|
||||
.preference(key: ScrollYKey.self, value: geo.frame(in: .global).minY)
|
||||
}
|
||||
)
|
||||
.onPreferenceChange(ScrollYKey.self) { y in
|
||||
tabState.report(scrollY: y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func trackScrollForTabBar() -> some View {
|
||||
modifier(TabBarScrollModifier())
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ struct ContentView: View {
|
||||
@AppStorage("hasOnboarded") private var hasOnboarded = false
|
||||
@State private var showOnboarding = false
|
||||
@State private var selectedTab = 0
|
||||
@StateObject private var tabState = FloatingTabState()
|
||||
@State private var iPadSelection: Int? = 0
|
||||
@State private var calIconImage: Image = ContentView.buildCalIcon()
|
||||
|
||||
@@ -135,16 +136,19 @@ struct ContentView: View {
|
||||
.tabBarInset()
|
||||
}
|
||||
.tint(AppColors.accent)
|
||||
.onChange(of: selectedTab) { _ in tabState.resetOffset() }
|
||||
|
||||
KisaniTabBar(
|
||||
selection: $selectedTab,
|
||||
calIcon: calIconImage,
|
||||
showMatrix: showMatrixTab,
|
||||
showWorkout: showWorkoutTab
|
||||
showWorkout: showWorkoutTab,
|
||||
isCollapsed: tabState.isCollapsed
|
||||
)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.environmentObject(tabState)
|
||||
.onAppear { UITabBar.appearance().isHidden = true }
|
||||
}
|
||||
|
||||
@@ -191,6 +195,7 @@ struct KisaniTabBar: View {
|
||||
let calIcon: Image
|
||||
var showMatrix: Bool
|
||||
var showWorkout: Bool
|
||||
var isCollapsed: Bool = false
|
||||
@Environment(\.colorScheme) private var cs
|
||||
|
||||
private struct Entry: Identifiable {
|
||||
@@ -224,15 +229,15 @@ struct KisaniTabBar: View {
|
||||
}
|
||||
if let img = entry.custom {
|
||||
img
|
||||
.font(.system(size: 20))
|
||||
.font(.system(size: isCollapsed ? 17 : 20))
|
||||
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text(cs))
|
||||
} else if let icon = entry.sfIcon {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 17, weight: selection == entry.id ? .semibold : .regular))
|
||||
.font(.system(size: isCollapsed ? 15 : 17, weight: selection == entry.id ? .semibold : .regular))
|
||||
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text(cs))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity).frame(height: 50)
|
||||
.frame(maxWidth: .infinity).frame(height: isCollapsed ? 38 : 50)
|
||||
}
|
||||
.buttonStyle(PressButtonStyle(scale: 0.88))
|
||||
}
|
||||
@@ -241,8 +246,9 @@ struct KisaniTabBar: View {
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 28)
|
||||
.fill(AppColors.surface(cs))
|
||||
.shadow(color: .black.opacity(0.10), radius: 20, x: 0, y: 6)
|
||||
.shadow(color: .black.opacity(isCollapsed ? 0.06 : 0.10), radius: 20, x: 0, y: 6)
|
||||
.shadow(color: .black.opacity(0.04), radius: 4, x: 0, y: 1)
|
||||
)
|
||||
.animation(KisaniSpring.snappy, value: isCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ struct QuadrantCard: View {
|
||||
// ── Scrollable task list ──
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Color.clear.frame(height: 0).trackScrollForTabBar()
|
||||
ForEach(tasks) { task in
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
|
||||
@@ -44,6 +44,7 @@ struct SettingsView: View {
|
||||
AppColors.background(cs).ignoresSafeArea()
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
Color.clear.frame(height: 0).trackScrollForTabBar()
|
||||
|
||||
Text("Settings")
|
||||
.font(AppFonts.sans(15, weight: .semibold))
|
||||
|
||||
@@ -18,6 +18,7 @@ struct TodayView: View {
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Color.clear.frame(height: 0).trackScrollForTabBar()
|
||||
|
||||
// ── Header ──
|
||||
HStack {
|
||||
|
||||
@@ -710,6 +710,7 @@ struct ManageWorkoutsSheet: View {
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Color.clear.frame(height: 0).trackScrollForTabBar()
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("PROGRAMS").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user