diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index 9216fb1..02210a5 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; }; C1B3FC29820FCAF069D2C6FE /* KisaniCalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */; }; C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; }; + CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */; }; CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */; }; D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; }; EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; }; @@ -70,6 +71,7 @@ 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = ""; }; 825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalWidget.swift; sourceTree = ""; }; 8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBridge.swift; sourceTree = ""; }; + 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = ""; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = ""; }; @@ -95,6 +97,7 @@ 21B93C269F283F11B415B18C /* Components */ = { isa = PBXGroup; children = ( + 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */, 23A4491BFA50721082024756 /* SharedComponents.swift */, ); path = Components; @@ -292,6 +295,7 @@ 13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */, A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */, 1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */, + CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */, D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */, BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */, EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */, @@ -420,7 +424,7 @@ CODE_SIGN_STYLE = Automatic; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSExtensionPointIdentifier = "com.apple.widgetkit-extension"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.widget; SDKROOT = iphoneos; @@ -437,7 +441,7 @@ CODE_SIGN_STYLE = Automatic; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSExtensionPointIdentifier = "com.apple.widgetkit-extension"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.widget; SDKROOT = iphoneos; diff --git a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..077aa32 Binary files /dev/null and b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/KisaniCal/Components/FloatingTabState.swift b/KisaniCal/Components/FloatingTabState.swift new file mode 100644 index 0000000..f1591d6 --- /dev/null +++ b/KisaniCal/Components/FloatingTabState.swift @@ -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()) + } +} diff --git a/KisaniCal/ContentView.swift b/KisaniCal/ContentView.swift index 9024147..7001d7a 100644 --- a/KisaniCal/ContentView.swift +++ b/KisaniCal/ContentView.swift @@ -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) } } diff --git a/KisaniCal/Views/MatrixView.swift b/KisaniCal/Views/MatrixView.swift index 281c99c..0a6f5e6 100644 --- a/KisaniCal/Views/MatrixView.swift +++ b/KisaniCal/Views/MatrixView.swift @@ -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) diff --git a/KisaniCal/Views/SettingsView.swift b/KisaniCal/Views/SettingsView.swift index 0fbf864..685650f 100644 --- a/KisaniCal/Views/SettingsView.swift +++ b/KisaniCal/Views/SettingsView.swift @@ -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)) diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index cf89f39..897183d 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -18,6 +18,7 @@ struct TodayView: View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { + Color.clear.frame(height: 0).trackScrollForTabBar() // ── Header ── HStack { diff --git a/KisaniCal/Views/WorkoutView.swift b/KisaniCal/Views/WorkoutView.swift index aeb9033..e842684 100644 --- a/KisaniCal/Views/WorkoutView.swift +++ b/KisaniCal/Views/WorkoutView.swift @@ -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)) diff --git a/project.yml b/project.yml index a6c5f09..bb37048 100644 --- a/project.yml +++ b/project.yml @@ -39,7 +39,7 @@ targets: KisaniCalWidget: type: app-extension platform: iOS - deploymentTarget: "16.0" + deploymentTarget: "17.0" sources: - path: KisaniCalWidget - path: Shared