From ebaf9b3c7d3a12bf0996b17de3d93f2c764b4790 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 11 Aug 2026 18:43:16 -0700 Subject: [PATCH] Adopt iOS 26 Liquid Glass across the app - Bump minimum deployment target to iOS 26.0 - Remove the global opaque-white UINavigationBarAppearance so native glass navigation bars render everywhere; system back buttons replace the custom BackButton on most screens (app-wide resellPurple tint) - Replace the custom tab bar with a native TabView: floating glass bar, SF Symbol tabs, native unread badge, minimize-on-scroll - Tab-root toolbars (wordmark/search/bell, Messages title, settings/ calendar) are defined on the TabView switching on selection, since items inside non-initial tabs don't propagate to the nav bar - PurpleButton/NavigationPurpleButton become .glassProminent tinted CTAs - ExpandableAddButton is a tinted glass FAB whose options morph via GlassEffectContainer + glassEffectID; it drops into the freed corner when the tab bar minimizes and rises when it expands, driven by a shared scroll-breakpoint state machine in MainViewModel (including manual pill-tap expansion detected by a passive window tap observer) - Messages: floating glass input bar (photo/field/send + action chips) via safeAreaInset; native glass toolbar; native Menu replaces the custom options overlay on posts, profiles, and chat - Product details: glass overlay back/ellipsis/save controls, glass page-dot lozenge, native share/report/delete Menu - Sheets: native drag indicators and backgrounds replace hand-drawn handles, presentationBackground(.clear) hacks, and opaque overrides; FilterView's hand-rolled sort popup becomes a native Menu - Search surfaces pinned via safeAreaInset get glass capsules so content scrolls beneath them Co-Authored-By: Claude Fable 5 --- Resell.xcodeproj/project.pbxproj | 16 +- Resell/ViewModels/MainViewModel.swift | 89 +++- Resell/Views/Chats/ChatsView.swift | 20 +- Resell/Views/Chats/MessagesView.swift | 120 ++--- Resell/Views/Components/BackButton.swift | 4 - .../CustomPageControlIndicatorView.swift | 8 +- .../Components/ExpandableAddButton.swift | 61 ++- Resell/Views/Components/OptionsMenuView.swift | 2 +- .../Views/Components/PaginatedImageView.swift | 17 +- Resell/Views/Components/PurpleButton.swift | 46 +- Resell/Views/Components/SearchBar.swift | 71 ++- Resell/Views/Home/DetailedFilterView.swift | 28 +- Resell/Views/Home/FilterView.swift | 448 ++++++++---------- Resell/Views/Home/HomeView.swift | 53 +-- Resell/Views/Home/NotificationsView.swift | 5 - Resell/Views/Home/ProfileView.swift | 22 +- Resell/Views/Home/SavedView.swift | 6 - Resell/Views/Home/SearchView.swift | 50 +- Resell/Views/MainTabView.swift | 178 +++++-- Resell/Views/MainView.swift | 4 +- .../NewListing/NewListingDetailsView.swift | 17 +- .../NewListing/NewListingImagesView.swift | 10 +- Resell/Views/NewListing/NewRequestView.swift | 18 +- .../CompletedTransactionView.swift | 7 - .../ProductDetails/ExternalProfileView.swift | 79 ++- .../Views/ProductDetails/FollowListView.swift | 36 +- .../ProductDetails/ProductDetailsView.swift | 186 +++----- .../Views/Report/ReportConfirmationView.swift | 5 +- Resell/Views/Report/ReportDetailsView.swift | 4 - Resell/Views/Report/ReportOptionsView.swift | 4 - .../Settings/AvailabilitySettingsView.swift | 6 - Resell/Views/Settings/BlockedUsersView.swift | 5 - Resell/Views/Settings/EditProfileView.swift | 5 - Resell/Views/Settings/SendFeedbackView.swift | 4 - Resell/Views/Settings/SettingsView.swift | 8 - 35 files changed, 735 insertions(+), 907 deletions(-) diff --git a/Resell.xcodeproj/project.pbxproj b/Resell.xcodeproj/project.pbxproj index 923b1a6..8188828 100644 --- a/Resell.xcodeproj/project.pbxproj +++ b/Resell.xcodeproj/project.pbxproj @@ -1119,7 +1119,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -1177,7 +1177,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -1195,7 +1195,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Resell/Resell.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_ASSET_PATHS = "\"Resell/Preview Content\""; DEVELOPMENT_TEAM = ZGMCXU7X3U; ENABLE_PREVIEWS = YES; @@ -1208,7 +1208,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1230,7 +1230,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Resell/Resell.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_ASSET_PATHS = "\"Resell/Preview Content\""; DEVELOPMENT_TEAM = ZGMCXU7X3U; ENABLE_PREVIEWS = YES; @@ -1243,7 +1243,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1265,7 +1265,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ZGMCXU7X3U; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.cornellappdev.ResellTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1284,7 +1284,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ZGMCXU7X3U; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.cornellappdev.ResellTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Resell/ViewModels/MainViewModel.swift b/Resell/ViewModels/MainViewModel.swift index 3b08792..41ac8b8 100644 --- a/Resell/ViewModels/MainViewModel.swift +++ b/Resell/ViewModels/MainViewModel.swift @@ -18,6 +18,74 @@ class MainViewModel: ObservableObject { @Published var userDidLogin: Bool = false @Published var selection = 0 + /// When true, the tab bar minimize behavior is .never, which forces the bar to + /// expand. Kept false ("armed", .onScrollDown) at rest so any downward scroll + /// minimizes the bar natively at any speed; crossing back above the top + /// breakpoint fires a short .never pulse to force expansion, then re-arms. + @Published var expandsTabBar: Bool = false + + /// Mirrors the tab bar's actual expanded/minimized presentation so floating + /// controls (the add button) stay exactly in sync with it. + @Published var isTabBarMinimized = false + + /// Scroll offset below which the bar expands / above which it minimizes. + static let tabBarExpandBreakpoint: CGFloat = 16 + static let tabBarMinimizeBreakpoint: CGFloat = 24 + + private var scrollIsAtTop = true + private var rearmTask: Task? + + /// Forces the tab bar to expand, then re-arms minimize-on-scroll shortly after. + func pulseExpandTabBar() { + rearmTask?.cancel() + if isTabBarMinimized { + isTabBarMinimized = false + } + if !expandsTabBar { + expandsTabBar = true + } + rearmTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(0.45)) + guard !Task.isCancelled else { return } + self?.expandsTabBar = false + } + } + + /// Called when the user taps the minimized tab bar pill — the system expands + /// the bar itself; this keeps our mirrored state (and the add button) in sync. + func handleManualTabBarExpansion() { + if isTabBarMinimized { + isTabBarMinimized = false + } + } + + /// Drives the tab bar from a tab root's scroll offset: expand when the scroll + /// crosses above the top breakpoint; past the breakpoint, minimize only on + /// downward movement (mirroring .onScrollDown, so a manually expanded bar + /// stays expanded until the user actually scrolls down again). + func updateTabBarForScroll(offset: CGFloat, previousOffset: CGFloat) { + if offset < Self.tabBarExpandBreakpoint { + if isTabBarMinimized { + isTabBarMinimized = false + } + if !scrollIsAtTop { + scrollIsAtTop = true + pulseExpandTabBar() + } + } else if offset > Self.tabBarMinimizeBreakpoint { + if scrollIsAtTop { + scrollIsAtTop = false + } + rearmTask?.cancel() + if expandsTabBar { + expandsTabBar = false + } + if offset > previousOffset + 2, !isTabBarMinimized { + isTabBarMinimized = true + } + } + } + @Published var hidesSignInButton = true // MARK: - Persistent Storage @@ -85,27 +153,6 @@ class MainViewModel: ObservableObject { return history } - func setupNavBar() { - let backButtonImage = UIImage(named: "chevron.left")? - .resized(to: CGSize(width: 38, height: 24)) - .withRenderingMode(.alwaysOriginal) - .withTintColor(.black) - - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.backgroundColor = .white - appearance.titleTextAttributes = [.foregroundColor: UIColor.black] - appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.black] - - appearance.backButtonAppearance.normal.titlePositionAdjustment = UIOffset(horizontal: -100, vertical: 0) - appearance.backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear] - appearance.setBackIndicatorImage(backButtonImage, transitionMaskImage: backButtonImage) - - UINavigationBar.appearance().standardAppearance = appearance - UINavigationBar.appearance().compactAppearance = appearance - UINavigationBar.appearance().scrollEdgeAppearance = appearance - } - @objc func logout() { // Clear any cached data clearUserData() diff --git a/Resell/Views/Chats/ChatsView.swift b/Resell/Views/Chats/ChatsView.swift index 5c4185f..42390b3 100644 --- a/Resell/Views/Chats/ChatsView.swift +++ b/Resell/Views/Chats/ChatsView.swift @@ -14,14 +14,12 @@ struct ChatsView: View { @EnvironmentObject var router: Router @EnvironmentObject var viewModel: ChatsViewModel - @EnvironmentObject var mainViewModel: MainViewModel + @EnvironmentObject private var mainViewModel: MainViewModel // MARK: - UI var body: some View { VStack(alignment: .leading) { - headerView - filtersView chatsView @@ -29,6 +27,11 @@ struct ChatsView: View { Spacer() } .background(Constants.Colors.white) + .onScrollGeometryChange(for: CGFloat.self) { geometry in + geometry.contentOffset.y + } action: { oldValue, newValue in + mainViewModel.updateTabBarForScroll(offset: newValue, previousOffset: oldValue) + } .emptyState(isEmpty: viewModel.checkEmptyState(), title: viewModel.emptyStateTitle(), text: viewModel.emptyStateMessage()) .refreshable { viewModel.refreshChats() @@ -39,17 +42,6 @@ struct ChatsView: View { .loadingView(isLoading: viewModel.isLoading) } - private var headerView: some View { - HStack { - Text("Messages") - .font(Constants.Fonts.h1) - .foregroundStyle(Constants.Colors.black) - - Spacer() - } - .padding(.horizontal, 25) - } - private var filtersView: some View { HStack { ForEach(Constants.chats, id: \.id) { filter in diff --git a/Resell/Views/Chats/MessagesView.swift b/Resell/Views/Chats/MessagesView.swift index 4f07f52..bdbae00 100644 --- a/Resell/Views/Chats/MessagesView.swift +++ b/Resell/Views/Chats/MessagesView.swift @@ -14,7 +14,6 @@ struct MessagesView: View { // MARK: - Properties @EnvironmentObject var router: Router - @State private var didShowOptionsMenu: Bool = false @State private var didShowNegotiationView: Bool = false @State private var didShowAvailabilityView: Bool = false @State private var didShowWebView: Bool = false @@ -61,31 +60,20 @@ struct MessagesView: View { // MARK: - UI var body: some View { - ZStack { - mainContentView - - if didShowOptionsMenu { - optionsMenuOverlay - } - } + mainContentView .background(Constants.Colors.white) .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .toolbarBackground(Constants.Colors.white, for: .automatic) .toolbar { - ToolbarItem(placement: .topBarLeading) { - BackButton(style: .systemChevronResizable(width: 12, height: 20)) - } - ToolbarItem(placement: .principal) { headerButton } - + ToolbarItem(placement: .topBarTrailing) { - HStack(spacing: 8) { - calendarButton - optionsButton - } + calendarButton + } + + ToolbarItem(placement: .topBarTrailing) { + optionsButton } } .sheet(isPresented: $didShowNegotiationView, onDismiss: setNegotiationText) { @@ -138,22 +126,12 @@ struct MessagesView: View { // MARK: - Extracted Subviews private var mainContentView: some View { - VStack { - messageListView - - Spacer() - - Divider() - - messageInputView - } + messageListView + .safeAreaInset(edge: .bottom) { + messageInputView + } } - private var optionsMenuOverlay: some View { - OptionsMenuView(showMenu: $didShowOptionsMenu, options: [.report(type: "User", id: otherUser.firebaseUid)]) - .zIndex(100) - } - private var otherUser: User { guard let user = GoogleAuthManager.shared.user else { return viewModel.chatInfo.buyer @@ -169,9 +147,8 @@ struct MessagesView: View { didShowAvailabilityView.toggle() } } label: { - Image("calendar") - .resizable() - .frame(width: 24, height: 24) + Image(systemName: "calendar") + .foregroundStyle(Constants.Colors.black) .opacity(hasActiveConfirmedMeeting ? 0.45 : 1) } .accessibilityHint( @@ -202,14 +179,14 @@ struct MessagesView: View { } private var optionsButton: some View { - Button { - withAnimation { - didShowOptionsMenu.toggle() + Menu { + Button { + router.push(.reportOptions(type: "User", id: otherUser.firebaseUid)) + } label: { + Label("Report", systemImage: "flag") } } label: { Image(systemName: "ellipsis") - .resizable() - .frame(width: 24, height: 6) .foregroundStyle(Constants.Colors.black) } } @@ -243,10 +220,13 @@ struct MessagesView: View { } private var messageInputView: some View { - VStack(spacing: 12) { - filtersView - textInputView + GlassEffectContainer(spacing: 8) { + VStack(spacing: 12) { + filtersView + textInputView + } } + .padding(.bottom, 8) } private var filtersView: some View { @@ -592,7 +572,7 @@ struct MessagesView: View { } } .presentationCornerRadius(25) - .presentationDragIndicator(.hidden) + .presentationDragIndicator(.visible) .onAppear { // Initialize cells from viewModel.availability when viewing someone's availability if !isEditing { @@ -695,7 +675,7 @@ struct FilterOptionsView: View { var body: some View { ScrollView(.horizontal, showsIndicators: false) { - HStack { + HStack(spacing: 12) { ForEach(Constants.chatMessageOptions, id: \.self) { option in switch option { case .negotiate: @@ -736,10 +716,7 @@ struct FilterOptionsView: View { .lineLimit(1) } .padding(12) - .overlay { - RoundedRectangle(cornerRadius: 25) - .stroke(Constants.Colors.resellGradient, lineWidth: 2) - } + .glassEffect(.regular.interactive(), in: .capsule) } } @@ -779,6 +756,10 @@ struct NegotiationSheetView: View { .frame(width: UIScreen.width - 40, height: 125) .background(Constants.Colors.white) .clipShape(.rect(cornerRadius: 18)) + .overlay { + RoundedRectangle(cornerRadius: 18) + .stroke(Constants.Colors.stroke, lineWidth: 1) + } PriceInputView( price: $priceText, @@ -786,19 +767,9 @@ struct NegotiationSheetView: View { titleText: "What price do you want to propose?" ) .padding(.bottom, 24) - .background(Constants.Colors.white) - .clipShape(.rect(cornerRadii: .init(topLeading: 25, topTrailing: 25))) - .overlay(alignment: .top) { - Rectangle() - .foregroundStyle(Constants.Colors.stroke) - .frame(width: 66, height: 6) - .clipShape(.capsule) - .padding(.top, 12) - } } .presentationDetents([.height(UIScreen.height * 3/4)]) - .presentationBackground(.clear) - .ignoresSafeArea() + .presentationDragIndicator(.visible) } } @@ -863,18 +834,13 @@ struct MessagesAvailabilitySheet: View { var body: some View { ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { - RoundedRectangle(cornerRadius: 10) - .frame(width: 66, height: 6) - .foregroundStyle(Constants.Colors.filterGray) - .padding(.top, 12) - .padding(.bottom, 16) - MonthPickerHeader( currentMonthOffset: $currentMonthOffset, showCalendar: $showCalendar, showSettings: .constant(false), maxMonthOffset: maxMonthOffset ) + .padding(.top, 16) proposeSubheader .padding(.horizontal) @@ -1558,16 +1524,16 @@ struct TextInputView: View { } } - HStack { + HStack(spacing: 12) { Button { showingPhotoPicker = true } label: { Image(systemName: "photo") - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) + .font(.system(size: 18, weight: .medium)) .foregroundStyle(Constants.Colors.secondaryGray) + .frame(width: 44, height: 44) } + .glassEffect(.regular.interactive(), in: .circle) .sheet(isPresented: $showingPhotoPicker) { ImagePicker(selectedImages: $selectedImages) } @@ -1577,9 +1543,8 @@ struct TextInputView: View { .foregroundColor(Constants.Colors.black) .padding(12) .scrollContentBackground(.hidden) - .background(Constants.Colors.wash) - .clipShape(RoundedRectangle(cornerRadius: 10)) .frame(height: 48) + .glassEffect(.regular, in: .capsule) .onChange(of: draftMessageText) { newText in if newText.count > maxCharacters { draftMessageText = String(newText.prefix(maxCharacters)) @@ -1592,11 +1557,12 @@ struct TextInputView: View { draftMessageText = "" selectedImages = [] }) { - Image("sendButton") - .resizable() - .frame(width: 24, height: 24) + Image(systemName: "arrow.up") + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(Constants.Colors.white) + .frame(width: 44, height: 44) } - .padding(.trailing, 8) + .glassEffect(.regular.tint(Constants.Colors.resellPurple).interactive(), in: .circle) } } } diff --git a/Resell/Views/Components/BackButton.swift b/Resell/Views/Components/BackButton.swift index 6155b27..75d4d8a 100644 --- a/Resell/Views/Components/BackButton.swift +++ b/Resell/Views/Components/BackButton.swift @@ -46,10 +46,6 @@ struct BackButton: View { .frame(width: hitTargetSize.width, height: hitTargetSize.height) .contentShape(Circle()) } - // `.plain` opts out of the system's automatic button chrome (the rounded-rect - // background that toolbar items pick up on iOS 26 when built against an older - // SDK), so only our chevron is drawn — no rectangle behind it. - .buttonStyle(.plain) } @ViewBuilder diff --git a/Resell/Views/Components/CustomPageControlIndicatorView.swift b/Resell/Views/Components/CustomPageControlIndicatorView.swift index 9f0fcf6..7e3db06 100644 --- a/Resell/Views/Components/CustomPageControlIndicatorView.swift +++ b/Resell/Views/Components/CustomPageControlIndicatorView.swift @@ -14,6 +14,9 @@ struct CustomPageControlIndicatorView: View { @Binding var currentPage: Int var numberOfPages: Int + /// Draws a Liquid Glass capsule behind the dots. Enable when the indicator + /// floats over imagery; disable when it sits on a plain screen background. + var showsGlassBackground: Bool = true // MARK: - UI @@ -26,6 +29,9 @@ struct CustomPageControlIndicatorView: View { .animation(.easeInOut, value: currentPage) } } - .padding(.vertical, 10) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .glassEffect(showsGlassBackground ? .regular : .identity, in: .capsule) + .padding(.vertical, 4) } } diff --git a/Resell/Views/Components/ExpandableAddButton.swift b/Resell/Views/Components/ExpandableAddButton.swift index 1ffa7ba..4c7a0c3 100644 --- a/Resell/Views/Components/ExpandableAddButton.swift +++ b/Resell/Views/Components/ExpandableAddButton.swift @@ -7,13 +7,14 @@ import SwiftUI -/// Expandable button that animates to show options to add listing or add new request +/// Expandable Liquid Glass button that morphs to show options to add listing or add new request struct ExpandableAddButton: View { // MARK: - Properties @EnvironmentObject var router: Router @State private var isExpanded: Bool = false + @Namespace private var glassNamespace // MARK: - UI @@ -29,30 +30,32 @@ struct ExpandableAddButton: View { } } } - - VStack(alignment: .trailing, spacing: 24) { - buttonOptions - HStack { - Spacer() + GlassEffectContainer(spacing: 12) { + VStack(alignment: .trailing, spacing: 24) { + if isExpanded { + buttonOptions + } - Button { - withAnimation { - isExpanded.toggle() + HStack { + Spacer() + + Button { + withAnimation { + isExpanded.toggle() + } + } label: { + Image(systemName: "plus") + .font(.system(size: 26, weight: .semibold)) + .foregroundStyle(Constants.Colors.white) + .frame(width: 64, height: 64) } - } label: { - Image("addNewListing") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 64, height: 64) - .clipShape(.circle) - .background(.red) + .rotationEffect(.degrees(isExpanded ? -45 : 0)) + .glassEffect(.regular.tint(Constants.Colors.resellPurple).interactive(), in: .circle) + .glassEffectID("addButton", in: glassNamespace) } - .rotationEffect(.degrees(isExpanded ? -45 : 0)) - .buttonStyle(PlainButtonStyle()) - .clipShape(.circle) + .fixedSize(horizontal: true, vertical: false) } - .fixedSize(horizontal: true, vertical: false) } .padding(.trailing, Constants.Spacing.horizontalPadding) .padding(.bottom, Constants.Spacing.horizontalPadding) @@ -61,7 +64,7 @@ struct ExpandableAddButton: View { } private var buttonOptions: some View { - ZStack(alignment: .trailing) { + VStack(alignment: .trailing, spacing: 16) { Button { router.push(.newListingImages) withAnimation { @@ -70,9 +73,8 @@ struct ExpandableAddButton: View { } label: { buttonContent(name: "New Listing", image: "newListing") } - .offset(y: isExpanded ? -64 : 64) - .opacity(isExpanded ? 1 : 0) - .allowsHitTesting(isExpanded) + .glassEffect(.regular.interactive(), in: .capsule) + .glassEffectID("newListing", in: glassNamespace) Button { router.push(.newRequest) @@ -82,9 +84,8 @@ struct ExpandableAddButton: View { } label: { buttonContent(name: "New Request", image: "newRequest") } - .offset(y: isExpanded ? 0 : 64) - .opacity(isExpanded ? 1 : 0) - .allowsHitTesting(isExpanded) + .glassEffect(.regular.interactive(), in: .capsule) + .glassEffectID("newRequest", in: glassNamespace) } } @@ -100,11 +101,5 @@ struct ExpandableAddButton: View { } .padding(.horizontal, 16) .padding(.vertical, 12) - .background(Constants.Colors.white) - .clipShape(.capsule) - .overlay { - RoundedRectangle(cornerRadius: 25) - .stroke(Constants.Colors.resellGradient, lineWidth: 3) - } } } diff --git a/Resell/Views/Components/OptionsMenuView.swift b/Resell/Views/Components/OptionsMenuView.swift index 021de9b..78aa817 100644 --- a/Resell/Views/Components/OptionsMenuView.swift +++ b/Resell/Views/Components/OptionsMenuView.swift @@ -102,8 +102,8 @@ struct OptionsMenuView: View { } } .frame(width: 250) - .background(Constants.Colors.wash.opacity(0.9)) .clipShape(.rect(cornerRadius: 12)) + .glassEffect(.regular, in: .rect(cornerRadius: 12)) .padding(.trailing, Constants.Spacing.horizontalPadding) .scaleEffect(showMenu ? 1 : 0, anchor: .topTrailing) .animation(.spring, value: showMenu) diff --git a/Resell/Views/Components/PaginatedImageView.swift b/Resell/Views/Components/PaginatedImageView.swift index 21beffc..5f46533 100644 --- a/Resell/Views/Components/PaginatedImageView.swift +++ b/Resell/Views/Components/PaginatedImageView.swift @@ -35,7 +35,7 @@ struct PaginatedImageView: View { } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) - CustomPageControlIndicatorView(currentPage: $currentPage, numberOfPages: $images.count + (images.count < maxImages ? 1 : 0)) + CustomPageControlIndicatorView(currentPage: $currentPage, numberOfPages: $images.count + (images.count < maxImages ? 1 : 0), showsGlassBackground: false) .frame(height: 20) .padding() } @@ -59,9 +59,12 @@ struct PaginatedImageView: View { .resizable() .foregroundColor(.white) .frame(width: 24, height: 24) - .padding(.leading, 20) - .padding(.bottom, 20) + .frame(width: 44, height: 44) + .contentShape(Circle()) } + .glassEffect(.regular.interactive(), in: .circle) + .padding(.leading, 20) + .padding(.bottom, 20) } .scaleEffect(currentPage == index ? 1.0 : 0.9) .animation(.easeInOut, value: currentPage) @@ -73,10 +76,13 @@ struct PaginatedImageView: View { Button { isImageSourceDialogPresented = true } label: { - Image("addNewListing") - .resizable() + Image(systemName: "plus") + .font(.system(size: 26, weight: .semibold)) + .foregroundStyle(Constants.Colors.white) .frame(width: 64, height: 64) + .contentShape(Circle()) } + .glassEffect(.regular.tint(Constants.Colors.resellPurple).interactive(), in: .circle) .confirmationDialog( "Select Image Source", isPresented: $isImageSourceDialogPresented, @@ -91,7 +97,6 @@ struct PaginatedImageView: View { Button("Cancel", role: .cancel) {} } } - .shadow(radius: 5) .tag(images.count) } diff --git a/Resell/Views/Components/PurpleButton.swift b/Resell/Views/Components/PurpleButton.swift index 1a86e6f..0052723 100644 --- a/Resell/Views/Components/PurpleButton.swift +++ b/Resell/Views/Components/PurpleButton.swift @@ -7,7 +7,7 @@ import SwiftUI -/// Reusable purple button +/// Reusable primary CTA rendered as prominent Liquid Glass tinted with the Resell brand color struct PurpleButton: View { // MARK: - Properties @@ -22,12 +22,14 @@ struct PurpleButton: View { // MARK: - UI var body: some View { - VStack { - Button(action: { if isActive { action() } }, label: { - buttonContent - .opacity(isActive ? 1.0 : 0.4) - }) - } + Button(action: { if isActive { action() } }, label: { + buttonContent + }) + .buttonStyle(.glassProminent) + .buttonBorderShape(.capsule) + .tint(isAlert ? Constants.Colors.errorRed : Constants.Colors.resellPurple) + .opacity(isActive ? 1.0 : 0.4) + .disabled(!isActive) } private var buttonContent: some View { @@ -38,18 +40,15 @@ struct PurpleButton: View { Text(text) .font(Constants.Fonts.title1) - .foregroundStyle(Constants.Colors.white) } - .padding(.horizontal, horizontalPadding) - .padding(.vertical, 14) - .background(isAlert ? Constants.Colors.errorRed : Constants.Colors.resellPurple) - .clipShape(.capsule) + .padding(.horizontal, max(horizontalPadding - 20, 0)) + .padding(.vertical, 6) } } -/// Reusable purple button that works as a NavigationLink +/// Reusable primary CTA that works as a NavigationLink struct NavigationPurpleButton: View { // MARK: - Properties @@ -64,19 +63,16 @@ struct NavigationPurpleButton: View { var body: some View { NavigationLink(destination: destination) { - buttonContent - .opacity(isActive ? 1.0 : 0.4) + Text(text) + .font(Constants.Fonts.title1) + .foregroundStyle(Constants.Colors.white) + .padding(.horizontal, max(horizontalPadding - 20, 0)) + .padding(.vertical, 6) } + .buttonStyle(.glassProminent) + .buttonBorderShape(.capsule) + .tint(isAlert ? Constants.Colors.errorRed : Constants.Colors.resellPurple) + .opacity(isActive ? 1.0 : 0.4) .disabled(!isActive) } - - private var buttonContent: some View { - Text(text) - .font(Constants.Fonts.title1) - .padding(.horizontal, horizontalPadding) - .padding(.vertical, 14) - .background(isAlert ? Constants.Colors.errorRed : Constants.Colors.resellPurple) - .foregroundColor(Constants.Colors.white) - .clipShape(Capsule()) - } } diff --git a/Resell/Views/Components/SearchBar.swift b/Resell/Views/Components/SearchBar.swift index 0f1dabc..21d42c4 100644 --- a/Resell/Views/Components/SearchBar.swift +++ b/Resell/Views/Components/SearchBar.swift @@ -19,45 +19,42 @@ struct SearchBar: View { } var body: some View { - RoundedRectangle(cornerRadius: 40) - .frame(width: 309, height: 43) - .overlay { - HStack { - Image(systemName: "magnifyingglass") - .foregroundStyle(.black) - .padding(.leading, 16) - - if isEditable { - ZStack(alignment: .leading) { - if textBinding.wrappedValue.isEmpty { - Text(placeholder) - .font(Constants.Fonts.body1) - .foregroundColor(Constants.Colors.secondaryGray) // Use a visible gray - } - - TextField("", text: textBinding) - .font(Constants.Fonts.body1) - .foregroundColor(Constants.Colors.black) - } - - if !textBinding.wrappedValue.isEmpty { - Button(action: { - textBinding.wrappedValue = "" - }) { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(Constants.Colors.stroke) - } - .padding(.trailing, 8) - } - } else { - Text(placeholder) - .font(Constants.Fonts.body1) - .foregroundColor(Constants.Colors.black) + HStack { + Image(systemName: "magnifyingglass") + .foregroundStyle(Constants.Colors.secondaryGray) + + if isEditable { + ZStack(alignment: .leading) { + if textBinding.wrappedValue.isEmpty { + Text(placeholder) + .font(Constants.Fonts.body1) + .foregroundColor(Constants.Colors.secondaryGray) // Use a visible gray + } + + TextField("", text: textBinding) + .font(Constants.Fonts.body1) + .foregroundColor(Constants.Colors.black) + } + + if !textBinding.wrappedValue.isEmpty { + Button(action: { + textBinding.wrappedValue = "" + }) { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(Constants.Colors.stroke) } - - Spacer() } + } else { + Text(placeholder) + .font(Constants.Fonts.body1) + .foregroundColor(Constants.Colors.black) } - .foregroundColor(Constants.Colors.wash) + + Spacer(minLength: 0) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .frame(maxWidth: .infinity) + .glassEffect(.regular, in: .capsule) } } diff --git a/Resell/Views/Home/DetailedFilterView.swift b/Resell/Views/Home/DetailedFilterView.swift index 0138188..c1c3276 100644 --- a/Resell/Views/Home/DetailedFilterView.swift +++ b/Resell/Views/Home/DetailedFilterView.swift @@ -23,11 +23,11 @@ struct DetailedFilterView: View { } var body: some View { - VStack(spacing: 0) { - headerView - ScrollView(.vertical) { - ProductsGalleryView(items: displayedItems) - } + ScrollView(.vertical) { + ProductsGalleryView(items: displayedItems) + } + .safeAreaInset(edge: .top, spacing: 0) { + headerView } .background(Constants.Colors.white) .loadingView(isLoading: viewModel.isLoading) @@ -43,42 +43,40 @@ struct DetailedFilterView: View { filtersViewModel.clearFilterSearch() } } - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .topBarLeading) { - BackButton(style: .systemChevronResizable(width: 12, height: 20)) - } - ToolbarItem(placement: .principal) { Text(filter.title) .font(Constants.Fonts.h1) .foregroundStyle(Constants.Colors.black) } } - .toolbarBackground(.hidden, for: .navigationBar) .sheet(isPresented: $presentPopup) { FilterView(home: false, isPresented: $presentPopup) .environmentObject(filtersViewModel) + .presentationDragIndicator(.visible) } } private var headerView: some View { - HStack { + GlassEffectContainer(spacing: 12) { + HStack(spacing: 16) { SearchBar(text: $searchText, placeholder: "Search in \(filter.title)", isEditable: true) .onChange(of: searchText) { newValue in filtersViewModel.searchWithinFilter(query: newValue) } - + Button(action: { presentPopup = true }, label: { - Image("filters") + Image("filters") .resizable() .frame(width: 24, height: 21) + .padding(10) }) + .glassEffect(.regular.interactive(), in: .circle) } .padding(.bottom, 12) .padding(.horizontal, Constants.Spacing.horizontalPadding) - + } } } diff --git a/Resell/Views/Home/FilterView.swift b/Resell/Views/Home/FilterView.swift index c75c846..35faf79 100644 --- a/Resell/Views/Home/FilterView.swift +++ b/Resell/Views/Home/FilterView.swift @@ -8,294 +8,242 @@ import SwiftUI import Flow -// TODO: Implement Apply Filters button. struct FilterView: View { @Binding var isPresented: Bool - @State var presentPopup = false @EnvironmentObject var filtersVM: FiltersViewModel private var categories : [String] = ["Clothing", "Books", "School", "Electronics", "Handmade", "Sports & Outdoors", "Other"] private var conditions : [String] = ["Gently Used", "Worn", "Never Used"] let home : Bool - + init(home: Bool, isPresented: Binding) { self.home = home _isPresented = isPresented } - + @ObservedObject private var homeViewModel = HomeViewModel.shared - + var body: some View { - ZStack { - Color.white - .ignoresSafeArea() - - ZStack { - VStack(spacing: 0) { - // Drag handle — centered - RoundedRectangle(cornerRadius: 10) - .frame(width: 66, height: 6) - .foregroundStyle(Constants.Colors.filterGray) - .padding(.top, 12) - .padding(.bottom, 8) - - // Title — centered - Text("Filters") - .font(.custom("Rubik-Medium", size: 22)) - .foregroundStyle(.black) - .padding(.vertical, 20) - - Divider() - - // Scrollable content - ScrollView { - // Left-aligned content sections - VStack(alignment: .leading, spacing: 0) { - - // MARK: - Sort By - - HStack { - Text("Sort by") - .font(.custom("Rubik-Medium", size: 20)) - .foregroundStyle(.black) - - Spacer() - - Button { - presentPopup.toggle() - } label: { - HStack(spacing: 2) { - Text("\(filtersVM.selectedSort?.title ?? "Any")") - .font(.custom("Rubik-Regular", size: 20)) - .foregroundStyle(.gray) - - Image(systemName: "chevron.down") - .foregroundStyle(.gray) + VStack(spacing: 0) { + // Title — centered + Text("Filters") + .font(.custom("Rubik-Medium", size: 22)) + .foregroundStyle(.black) + .padding(.vertical, 20) + + Divider() + + // Scrollable content + ScrollView { + // Left-aligned content sections + VStack(alignment: .leading, spacing: 0) { + + // MARK: - Sort By + + HStack { + Text("Sort by") + .font(.custom("Rubik-Medium", size: 20)) + .foregroundStyle(.black) + + Spacer() + + Menu { + ForEach(SortOption.allCases) { option in + Button { + filtersVM.selectedSort = option + } label: { + if filtersVM.selectedSort == option { + Label(option.title, systemImage: "checkmark") + } else { + Text(option.title) + } } } + } label: { + HStack(spacing: 2) { + Text("\(filtersVM.selectedSort?.title ?? "Any")") + .font(.custom("Rubik-Regular", size: 20)) + .foregroundStyle(.gray) + + Image(systemName: "chevron.down") + .foregroundStyle(.gray) + } } - .padding(.vertical, 24) - - Divider() - .padding(.bottom, 16) - - // MARK: - Price Range - - HStack { - Text("Price Range") - .font(.custom("Rubik-Medium", size: 20)) - .foregroundStyle(.black) - - Spacer() - - Group { - if filtersVM.lowValue == 0 && filtersVM.highValue == 1000 { - Text("Any") - } else if filtersVM.lowValue == 0 { - Text("Up to $\(Int(filtersVM.highValue))") - } else if filtersVM.highValue == 1000 { - Text("$\(Int(filtersVM.lowValue)) +") - } else { - Text("$\(Int(filtersVM.lowValue)) to $\(Int(filtersVM.highValue))") - } + } + .padding(.vertical, 24) + + Divider() + .padding(.bottom, 16) + + // MARK: - Price Range + + HStack { + Text("Price Range") + .font(.custom("Rubik-Medium", size: 20)) + .foregroundStyle(.black) + + Spacer() + + Group { + if filtersVM.lowValue == 0 && filtersVM.highValue == 1000 { + Text("Any") + } else if filtersVM.lowValue == 0 { + Text("Up to $\(Int(filtersVM.highValue))") + } else if filtersVM.highValue == 1000 { + Text("$\(Int(filtersVM.lowValue)) +") + } else { + Text("$\(Int(filtersVM.lowValue)) to $\(Int(filtersVM.highValue))") } - .font(.custom("Rubik-Regular", size: 20)) - .foregroundStyle(.gray) } - .padding(.bottom, 8) - - // Slider — extend trailing to avoid clipping the 344pt track - RangeSlider(lowValue: $filtersVM.lowValue, highValue: $filtersVM.highValue, range: 0...1000) - .padding(.trailing, -28) - - - if home { - Divider() - .padding(.top, 4) - .padding(.bottom, 12) - - // MARK: - Product Category - - Text("Product Category") - .font(.custom("Rubik-Medium", size: 20)) - .foregroundStyle(.black) - .padding(.bottom, 8) - - HFlow { - ForEach(categories, id: \.self) { category in - HStack { - Button { - if filtersVM.categoryFilters.contains(category){ - filtersVM.categoryFilters.remove(category) - } else { - filtersVM.categoryFilters.insert(category) - } - } label: { - if filtersVM.categoryFilters.contains(category) { - HStack { - Text(category) - .font(.custom("Rubik-Medium", size: 14)) - .foregroundStyle(Constants.Colors.resellPurple) - - Image(systemName: "xmark") - .font(.custom("Rubik-Medium", size: 14)) - .foregroundStyle(Constants.Colors.resellPurple) - } - } else { + .font(.custom("Rubik-Regular", size: 20)) + .foregroundStyle(.gray) + } + .padding(.bottom, 8) + + // Slider — extend trailing to avoid clipping the 344pt track + RangeSlider(lowValue: $filtersVM.lowValue, highValue: $filtersVM.highValue, range: 0...1000) + .padding(.trailing, -28) + + + if home { + Divider() + .padding(.top, 4) + .padding(.bottom, 12) + + // MARK: - Product Category + + Text("Product Category") + .font(.custom("Rubik-Medium", size: 20)) + .foregroundStyle(.black) + .padding(.bottom, 8) + + HFlow { + ForEach(categories, id: \.self) { category in + HStack { + Button { + if filtersVM.categoryFilters.contains(category){ + filtersVM.categoryFilters.remove(category) + } else { + filtersVM.categoryFilters.insert(category) + } + } label: { + if filtersVM.categoryFilters.contains(category) { + HStack { Text(category) .font(.custom("Rubik-Medium", size: 14)) - .foregroundStyle(Color.black) + .foregroundStyle(Constants.Colors.resellPurple) + + Image(systemName: "xmark") + .font(.custom("Rubik-Medium", size: 14)) + .foregroundStyle(Constants.Colors.resellPurple) } + } else { + Text(category) + .font(.custom("Rubik-Medium", size: 14)) + .foregroundStyle(Color.black) } - .padding(.horizontal, 14) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 20) - .stroke(filtersVM.categoryFilters.contains(category) ? Constants.Colors.resellPurple : Constants.Colors.filterGray, lineWidth: 1) - - .background( - RoundedRectangle(cornerRadius: 20) - .fill(filtersVM.categoryFilters.contains(category) ? Constants.Colors.purpleWash : Color.white) - ) - ) } + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 20) + .stroke(filtersVM.categoryFilters.contains(category) ? Constants.Colors.resellPurple : Constants.Colors.filterGray, lineWidth: 1) + + .background( + RoundedRectangle(cornerRadius: 20) + .fill(filtersVM.categoryFilters.contains(category) ? Constants.Colors.purpleWash : Color.white) + ) + ) } } - - Divider() - .padding(.vertical, 12) } - - // MARK: - Condition - - Text("Condition") - .font(.custom("Rubik-Medium", size: 20)) - .foregroundStyle(.black) - .padding(.bottom, 8) - .padding(.top, home ? 0 : 12) - - HStack { - ForEach(conditions, id: \.self){ condition in - Button { - if filtersVM.conditionFilters.contains(condition){ - filtersVM.conditionFilters.remove(condition) - } else { - filtersVM.conditionFilters.insert(condition) - } - } label: { - if filtersVM.conditionFilters.contains(condition) { - HStack { - Text(condition) - .font(.custom("Rubik-Medium", size: 14)) - .foregroundStyle(Constants.Colors.resellPurple) - - Image(systemName: "xmark") - .font(.custom("Rubik-Medium", size: 14)) - .foregroundStyle(Constants.Colors.resellPurple) - } - } else { + + Divider() + .padding(.vertical, 12) + } + + // MARK: - Condition + + Text("Condition") + .font(.custom("Rubik-Medium", size: 20)) + .foregroundStyle(.black) + .padding(.bottom, 8) + .padding(.top, home ? 0 : 12) + + HStack { + ForEach(conditions, id: \.self){ condition in + Button { + if filtersVM.conditionFilters.contains(condition){ + filtersVM.conditionFilters.remove(condition) + } else { + filtersVM.conditionFilters.insert(condition) + } + } label: { + if filtersVM.conditionFilters.contains(condition) { + HStack { Text(condition) .font(.custom("Rubik-Medium", size: 14)) - .foregroundStyle(Color.black) + .foregroundStyle(Constants.Colors.resellPurple) + + Image(systemName: "xmark") + .font(.custom("Rubik-Medium", size: 14)) + .foregroundStyle(Constants.Colors.resellPurple) } + } else { + Text(condition) + .font(.custom("Rubik-Medium", size: 14)) + .foregroundStyle(Color.black) } - .padding(.horizontal, 14) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 20) - .stroke(filtersVM.conditionFilters.contains(condition) ? Constants.Colors.resellPurple : Constants.Colors.filterGray, lineWidth: 1) - .background( - RoundedRectangle(cornerRadius: 20) - .fill(filtersVM.conditionFilters.contains(condition) ? Constants.Colors.purpleWash : Color.white) - ) - ) } + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 20) + .stroke(filtersVM.conditionFilters.contains(condition) ? Constants.Colors.resellPurple : Constants.Colors.filterGray, lineWidth: 1) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(filtersVM.conditionFilters.contains(condition) ? Constants.Colors.purpleWash : Color.white) + ) + ) } } - .padding(.horizontal, 28) - } // End of ScrollView - - Spacer() - - // MARK: - Reset / Apply Buttons (Fixed at bottom) - - HStack{ - Button { - filtersVM.resetFilters(homeViewModel: homeViewModel) - } label: { - Text("Reset") - .font(.custom("Rubik-Medium", size: 20)) - .foregroundStyle(.black) - } - - Spacer() - - Button{ - Task { - try await filtersVM.applyFilters(homeViewModel: homeViewModel) - isPresented = false - } - } label: { - Text("Apply filters") - .font(.custom("Rubik-Medium", size: 20)) - .foregroundStyle(Color.white) - .padding(.horizontal, 14) - .padding(.vertical, 8) - .background(!filtersVM.hasActiveFilters ? Constants.Colors.resellPurple.opacity(0.4) : Constants.Colors.resellPurple) - .cornerRadius(20) - } - .disabled(!filtersVM.hasActiveFilters) - } - .padding(.horizontal, 40) - .padding(.vertical, 16) } - - if presentPopup { - SortByView(selectedSort: $filtersVM.selectedSort) - .offset(x: 88, y: -142) - .onTapGesture { - presentPopup.toggle() - } + .padding(.horizontal, 28) + } // End of ScrollView + + Spacer() + + // MARK: - Reset / Apply Buttons (Fixed at bottom) + + HStack{ + Button { + filtersVM.resetFilters(homeViewModel: homeViewModel) + } label: { + Text("Reset") + .font(.custom("Rubik-Medium", size: 20)) + .foregroundStyle(.black) } - } - } - // TODO: Add border to filter view - } - - struct SortByView: View { - @Binding var selectedSort: SortOption? - - let sortOptions = SortOption.allCases - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - ForEach(sortOptions) { option in - Button(action: { - selectedSort = option - }) { - VStack(alignment: .leading, spacing: 0) { - Text(option.title) - .font(.system(size: 17, weight: selectedSort == option ? .bold : .regular)) - .foregroundColor(.black) - .padding(.vertical, 12) - .frame(maxWidth: .infinity, alignment: .leading) - - if option != sortOptions.last { - Divider() - } - } + Spacer() + + Button{ + Task { + try await filtersVM.applyFilters(homeViewModel: homeViewModel) + isPresented = false } + } label: { + Text("Apply filters") + .font(.custom("Rubik-Medium", size: 20)) + .foregroundStyle(Color.white) } + .buttonStyle(.glassProminent) + .buttonBorderShape(.capsule) + .tint(Constants.Colors.resellPurple) + .disabled(!filtersVM.hasActiveFilters) } - .padding(.horizontal, 16) - .background(Color.white) - .frame(width: 171) - .overlay( - RoundedRectangle(cornerRadius: 13) - .stroke(Color.gray.opacity(0.3), lineWidth: 1) - ) + .padding(.horizontal, 40) + .padding(.vertical, 16) } } } @@ -306,8 +254,8 @@ enum SortOption: String, CaseIterable, Identifiable { case newlyListed = "Newly listed" case priceHighToLow = "Price: High to Low" case priceLowToHigh = "Price: Low to High" - + var id: String { rawValue } - + var title: String { rawValue } } diff --git a/Resell/Views/Home/HomeView.swift b/Resell/Views/Home/HomeView.swift index e95ec60..815a819 100644 --- a/Resell/Views/Home/HomeView.swift +++ b/Resell/Views/Home/HomeView.swift @@ -20,12 +20,13 @@ struct HomeView: View { @State var forYouPosts: [[Post]] = [] @State private var presentPopup = false + /// Bottom safe-area inset (tab bar + home indicator), captured so the + /// dropped button can align its center with the minimized tab bar pill. + @State private var bottomSafeAreaInset: CGFloat = 0 var body: some View { ScrollView(.vertical, showsIndicators: true) { VStack { - headerView - filtersView .padding(.top, 12) .padding(.bottom, 32) @@ -63,15 +64,30 @@ struct HomeView: View { viewModel.getAllPosts() //only get all posts if no filters are applied } viewModel.getBlockedUsers() - withAnimation { mainViewModel.hidesTabBar = false } } .onDisappear { // Clean up image cache when leaving home view viewModel.cleanupMemory() } .background(Constants.Colors.white) + .onScrollGeometryChange(for: CGFloat.self) { geometry in + geometry.contentOffset.y + } action: { oldValue, newValue in + mainViewModel.updateTabBarForScroll(offset: newValue, previousOffset: oldValue) + } + .background( + GeometryReader { proxy in + Color.clear + .onAppear { bottomSafeAreaInset = proxy.safeAreaInsets.bottom } + } + ) .overlay(alignment: .bottomTrailing) { - ExpandableAddButton().padding(.bottom, 40) + // Dropped position: button bottom edge 16pt above the screen edge, + // which centers the 64pt button on the minimized tab bar pill. + ExpandableAddButton() + .padding(.bottom, 8) + .offset(y: mainViewModel.isTabBarMinimized ? max(bottomSafeAreaInset - 4, 56) : 0) + .animation(.spring(duration: 0.35), value: mainViewModel.isTabBarMinimized) } .refreshable { // Force refresh when user pulls to refresh @@ -84,40 +100,13 @@ struct HomeView: View { } } .loadingView(isLoading: viewModel.isLoading) - .navigationBarBackButtonHidden() .sheet(isPresented: $presentPopup) { FilterView(home: true, isPresented: $presentPopup) .environmentObject(filtersViewModel) + .presentationDragIndicator(.visible) } } - private var headerView: some View { - HStack { - Text("resell") - .font(Constants.Fonts.resellHeader) - .foregroundStyle(Constants.Colors.resellGradient) - - Spacer() - - Button(action: { - router.push(.search(nil)) - }, label: { - Icon(image: "search") - }) - - Button(action: { - router.push(.notifications) - }, label: { - Image(systemName: "bell") // using the native sfsymbols bell is faster + prettier - .font(.system(size: 20, weight: .medium)) // Increases thickness to bold - .foregroundStyle(.black) - }) - .padding(.leading, 12) - } - .padding(.horizontal, Constants.Spacing.horizontalPadding) - - } - private var filtersView: some View { VStack(alignment: .leading) { diff --git a/Resell/Views/Home/NotificationsView.swift b/Resell/Views/Home/NotificationsView.swift index 1345d0d..f84fc5a 100644 --- a/Resell/Views/Home/NotificationsView.swift +++ b/Resell/Views/Home/NotificationsView.swift @@ -106,12 +106,7 @@ struct NotificationsView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Constants.Colors.white) - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } - ToolbarItem(placement: .principal) { Text("Notifications") .font(Constants.Fonts.h3) diff --git a/Resell/Views/Home/ProfileView.swift b/Resell/Views/Home/ProfileView.swift index 92ff396..813db1e 100644 --- a/Resell/Views/Home/ProfileView.swift +++ b/Resell/Views/Home/ProfileView.swift @@ -69,27 +69,11 @@ struct ProfileView: View { } } .background(Constants.Colors.white) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - router.push(.settings(false)) - } label: { - Icon(image: "settings") - } - } - - ToolbarItem(placement: .topBarTrailing) { - Button { - router.push(.availability) - } label: { - Icon(image: "calendar-internal") - } - } - } - .toolbarBackground(.hidden, for: .navigationBar) + // The nested gallery ScrollView keeps the system tab bar from ever + // minimizing on this screen, so the button stays in its raised position. .overlay(alignment: .bottomTrailing) { ExpandableAddButton() - .padding(.bottom, 40) + .padding(.bottom, 8) } .onAppear { viewModel.loadCurrentUser() diff --git a/Resell/Views/Home/SavedView.swift b/Resell/Views/Home/SavedView.swift index c467fbd..4489fd9 100644 --- a/Resell/Views/Home/SavedView.swift +++ b/Resell/Views/Home/SavedView.swift @@ -30,13 +30,7 @@ struct SavedView: View { await viewModel.getSavedPosts() } } - .navigationBarBackButtonHidden(true) - .toolbarBackground(.hidden, for: .navigationBar) .toolbar { - ToolbarItem(placement: .topBarLeading) { - BackButton() - } - ToolbarItem(placement: .principal) { Text("Saved By You") .font(Constants.Fonts.h1) diff --git a/Resell/Views/Home/SearchView.swift b/Resell/Views/Home/SearchView.swift index 4532eae..7aefb9f 100644 --- a/Resell/Views/Home/SearchView.swift +++ b/Resell/Views/Home/SearchView.swift @@ -23,15 +23,27 @@ struct SearchView: View { // MARK: - UI var body: some View { - VStack(spacing: 0) { + contentView + .safeAreaInset(edge: .top, spacing: 0) { + searchHeader + } + .navigationBarBackButtonHidden() + .background(Constants.Colors.white) + .loadingView(isLoading: searchViewModel.isLoading) + .onChange(of: isFocused) { newValue in + searchViewModel.isSearching = newValue + } + } + + private var searchHeader: some View { + GlassEffectContainer(spacing: 16) { HStack(alignment: .center, spacing: 16) { TextField("", text: $searchText, prompt: Text("What are you looking for?").foregroundColor(Constants.Colors.secondaryGray)) .font(Constants.Fonts.body2) .foregroundStyle(Constants.Colors.black) .submitLabel(.search) .padding(12) - .background(Constants.Colors.wash) - .clipShape(.capsule) + .glassEffect(.regular, in: .capsule) .focused($isFocused) .onSubmit { searchViewModel.searchItems(with: searchText, userID: userID, saveQuery: false, mainViewModel: mainViewModel) {} @@ -44,38 +56,30 @@ struct SearchView: View { .resizable() .frame(width: 14, height: 14) .foregroundStyle(Constants.Colors.black) + .padding(14) } + .glassEffect(.regular.interactive(), in: .circle) } .padding(Constants.Spacing.horizontalPadding) + } + } + private var contentView: some View { + Group { if searchViewModel.isSearching { searchHistoryView - - Spacer() } else if searchViewModel.isLoading { - Spacer() - ProgressView() - - Spacer() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if searchViewModel.searchedItems.isEmpty { + emptyState + .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - if searchViewModel.searchedItems.isEmpty { - Spacer() - emptyState - Spacer() - } else { - ScrollView(.vertical) { - ProductsGalleryView(items: searchViewModel.searchedItems) - } + ScrollView(.vertical) { + ProductsGalleryView(items: searchViewModel.searchedItems) } } } - .navigationBarBackButtonHidden() - .background(Constants.Colors.white) - .loadingView(isLoading: searchViewModel.isLoading) - .onChange(of: isFocused) { newValue in - searchViewModel.isSearching = newValue - } } private var emptyState: some View { diff --git a/Resell/Views/MainTabView.swift b/Resell/Views/MainTabView.swift index 6d4cfd2..0c64219 100644 --- a/Resell/Views/MainTabView.swift +++ b/Resell/Views/MainTabView.swift @@ -13,7 +13,6 @@ struct MainTabView: View { @EnvironmentObject var router: Router - @Binding var isHidden: Bool @Binding var selection: Int // MARK: - ViewModels @@ -30,16 +29,8 @@ struct MainTabView: View { NavigationStack(path: $router.path) { Group { if mainViewModel.userDidLogin { - VStack(spacing: 0) { - mainView - - if !isHidden { - tabBarView - } - } - .ignoresSafeArea(edges: .bottom) + mainView .transition(.opacity) - .background(.white) .environmentObject(router) .onAppear { // Start listening to chat updates as soon as the @@ -124,6 +115,7 @@ struct MainTabView: View { } } } + .tint(Constants.Colors.resellPurple) .onReceive(NotificationCenter.default.publisher(for: Constants.Notifications.OpenTransactionDeepLink)) { output in guard mainViewModel.userDidLogin else { return } guard let tid = output.userInfo?["transactionId"] as? String, !tid.isEmpty else { return } @@ -152,42 +144,156 @@ struct MainTabView: View { } private var mainView: some View { - ZStack() { - if selection == 0 { + TabView(selection: $selection) { + Tab("Home", systemImage: "house", value: 0) { HomeView() - } else if selection == 1 { + } + + Tab("Messages", systemImage: "message", value: 1) { ChatsView() .environmentObject(chatsViewModel) - } else if selection == 2 { + } + .badge(chatsViewModel.totalUnread) + + Tab("Profile", systemImage: "person", value: 2) { ProfileView() } } + .tint(Constants.Colors.resellPurple) + // .never forces the bar back to full size — pulsed when a tab root's scroll + // crosses back above the top breakpoint, since the system only re-expands + // on fast flings. At rest the bar stays armed with .onScrollDown so any + // downward gesture minimizes it natively regardless of speed. + .tabBarMinimizeBehavior(mainViewModel.expandsTabBar ? .never : .onScrollDown) + .onChange(of: selection) { _, _ in + mainViewModel.pulseExpandTabBar() + } + // Detects taps on the minimized tab bar pill (bottom-leading corner) so the + // floating add button rises together with the manually re-expanded bar. + .background( + MinimizedTabBarTapObserver { + mainViewModel.handleManualTabBarExpansion() + } + .frame(width: 0, height: 0) + ) + // Tab-root toolbars are defined here, on the outer NavigationStack's root, + // because toolbar items inside non-initial TabView tabs don't reliably + // propagate to the enclosing stack's navigation bar. + .toolbar { + tabRootToolbar + } } - private var tabBarView: some View { - HStack { - ForEach(0..<3, id: \.self) { index in - TabViewIcon( - selectionIndex: $selection, - itemIndex: index, - badgeCount: index == 1 ? chatsViewModel.totalUnread : 0 - ) - .frame(width: 28, height: 28) - - if index != 2 { - Spacer() + @ToolbarContentBuilder + private var tabRootToolbar: some ToolbarContent { + if selection == 0 { + ToolbarItem(placement: .topBarLeading) { + Text("resell") + .font(Constants.Fonts.resellHeader) + .foregroundStyle(Constants.Colors.resellGradient) + .fixedSize() + } + .sharedBackgroundVisibility(.hidden) + + ToolbarItem(placement: .topBarTrailing) { + Button { + router.push(.search(nil)) + } label: { + Image(systemName: "magnifyingglass") + .foregroundStyle(Constants.Colors.black) + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button { + router.push(.notifications) + } label: { + Image(systemName: "bell") + .foregroundStyle(Constants.Colors.black) + } + } + } else if selection == 1 { + ToolbarItem(placement: .topBarLeading) { + Text("Messages") + .font(Constants.Fonts.h1) + .foregroundStyle(Constants.Colors.black) + .fixedSize() + } + .sharedBackgroundVisibility(.hidden) + } else { + ToolbarItem(placement: .topBarLeading) { + Button { + router.push(.settings(false)) + } label: { + Image(systemName: "gearshape") + .foregroundStyle(Constants.Colors.black) + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button { + router.push(.availability) + } label: { + Image(systemName: "calendar") + .foregroundStyle(Constants.Colors.black) } } } - .ignoresSafeArea(edges: .bottom) - .padding(.horizontal, 40) - .padding(.top, 16) - .padding(.bottom, 46) - .frame(width: UIScreen.width) - .background(Constants.Colors.white) - .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous)) - .shadow(radius: 4) - .transition(.move(edge: .bottom)) - .animation(.easeInOut, value: isHidden) + } +} + +// MARK: - Minimized Tab Bar Tap Detection + +/// Installs a passive, non-consuming tap recognizer on the window to notice taps +/// in the minimized tab bar pill's region (bottom-leading corner). SwiftUI exposes +/// no state for the bar's minimized/expanded presentation, so this is how floating +/// controls learn the user manually re-expanded the bar by tapping the pill. +private struct MinimizedTabBarTapObserver: UIViewRepresentable { + let onTap: () -> Void + + func makeUIView(context: Context) -> TapObserverUIView { + let view = TapObserverUIView() + view.onBottomLeadingTap = onTap + return view + } + + func updateUIView(_ uiView: TapObserverUIView, context: Context) { + uiView.onBottomLeadingTap = onTap + } +} + +final class TapObserverUIView: UIView, UIGestureRecognizerDelegate { + var onBottomLeadingTap: (() -> Void)? + + private weak var recognizer: UITapGestureRecognizer? + + override func didMoveToWindow() { + super.didMoveToWindow() + isUserInteractionEnabled = false + guard let window, recognizer == nil else { return } + + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) + tap.cancelsTouchesInView = false + tap.delegate = self + window.addGestureRecognizer(tap) + recognizer = tap + } + + @objc private func handleTap(_ gesture: UITapGestureRecognizer) { + guard let window = gesture.view as? UIWindow ?? window else { return } + let location = gesture.location(in: window) + let bounds = window.bounds + // Bottom-leading corner only: the minimized pill's zone. Excludes the + // trailing side so taps on the floating add button never trigger this. + if location.y > bounds.height - 80, location.x < bounds.width * 0.4 { + onBottomLeadingTap?() + } + } + + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + true } } diff --git a/Resell/Views/MainView.swift b/Resell/Views/MainView.swift index 08a03f8..fa96a12 100644 --- a/Resell/Views/MainView.swift +++ b/Resell/Views/MainView.swift @@ -25,7 +25,7 @@ struct MainView: View { // MARK: - UI var body: some View { - MainTabView(isHidden: $mainViewModel.hidesTabBar, selection: $mainViewModel.selection) + MainTabView(selection: $mainViewModel.selection) .environmentObject(searchViewModel) .environmentObject(router) .environmentObject(mainViewModel) @@ -43,8 +43,6 @@ struct MainView: View { // `ResellApp.init()` was unsafe (see note there). HomeViewModel.shared.configure(mainViewModel: mainViewModel) mainViewModel.restoreSignIn() - mainViewModel.setupNavBar() - mainViewModel.hidesTabBar = false } .task { await appVersionService.checkIfUpdateRequired() diff --git a/Resell/Views/NewListing/NewListingDetailsView.swift b/Resell/Views/NewListing/NewListingDetailsView.swift index 9b1d8ec..41bd793 100644 --- a/Resell/Views/NewListing/NewListingDetailsView.swift +++ b/Resell/Views/NewListing/NewListingDetailsView.swift @@ -13,7 +13,6 @@ struct NewListingDetailsView: View { @EnvironmentObject var router: Router @EnvironmentObject var viewModel: NewListingViewModel - @EnvironmentObject var mainViewModel: MainViewModel @State private var priceFieldPosition: CGFloat = 0.0 @@ -46,28 +45,16 @@ struct NewListingDetailsView: View { .scrollDismissesKeyboard(.interactively) .background(Constants.Colors.white) .safeAreaInset(edge: .bottom, spacing: 0) { - VStack(spacing: 0) { - PurpleButton(isLoading: viewModel.isLoading, isActive: viewModel.checkInputIsValid(), text: "Continue") { - viewModel.createNewListing() - - withAnimation { - mainViewModel.hidesTabBar = false - } - } + PurpleButton(isLoading: viewModel.isLoading, isActive: viewModel.checkInputIsValid(), text: "Continue") { + viewModel.createNewListing() } .padding(.horizontal, 24) .padding(.top, 12) .padding(.bottom, 8) .frame(maxWidth: .infinity) - .background(Constants.Colors.white) } .endEditingOnTap() - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .topBarLeading) { - BackButton() - } - ToolbarItem(placement: .principal) { Text("New Listing") .font(Constants.Fonts.h3) diff --git a/Resell/Views/NewListing/NewListingImagesView.swift b/Resell/Views/NewListing/NewListingImagesView.swift index e6509b6..fb5d50b 100644 --- a/Resell/Views/NewListing/NewListingImagesView.swift +++ b/Resell/Views/NewListing/NewListingImagesView.swift @@ -13,7 +13,6 @@ struct NewListingImagesView: View { @EnvironmentObject var router: Router @EnvironmentObject var viewModel: NewListingViewModel - @EnvironmentObject var mainViewModel: MainViewModel // MARK: - UI @@ -90,15 +89,10 @@ struct NewListingImagesView: View { Button { router.pop() viewModel.clear() - - withAnimation { - mainViewModel.hidesTabBar = false - } } label: { Image(systemName: "xmark") - .resizable() - .frame(width: 20, height: 20) - .tint(Constants.Colors.black) + .font(.system(size: 17, weight: .medium)) + .foregroundStyle(Constants.Colors.black) } } } diff --git a/Resell/Views/NewListing/NewRequestView.swift b/Resell/Views/NewListing/NewRequestView.swift index eca9844..05c3e0f 100644 --- a/Resell/Views/NewListing/NewRequestView.swift +++ b/Resell/Views/NewListing/NewRequestView.swift @@ -12,7 +12,6 @@ struct NewRequestView: View { // MARK: - Properties @EnvironmentObject var router: Router - @EnvironmentObject var mainViewModel: MainViewModel @StateObject private var viewModel = NewRequestViewModel() @State private var sheetHeight: CGFloat? = nil @@ -40,9 +39,6 @@ struct NewRequestView: View { PurpleButton(isLoading: viewModel.isLoading, isActive: viewModel.checkInputIsValid(), text: "Continue") { viewModel.createNewRequest() router.pop() - withAnimation { - mainViewModel.hidesTabBar = false - } } } .padding(.horizontal, 24) @@ -60,15 +56,10 @@ struct NewRequestView: View { ToolbarItem(placement: .topBarTrailing) { Button { router.pop() - withAnimation { - mainViewModel.hidesTabBar = false - } } label: { - //TODO: Place this in constants Image(systemName: "xmark") - .resizable() - .frame(width: 20, height: 20) - .tint(Constants.Colors.black) + .font(.system(size: 17, weight: .medium)) + .foregroundStyle(Constants.Colors.black) } } } @@ -85,11 +76,6 @@ struct NewRequestView: View { .presentationDragIndicator(.visible) .presentationCornerRadius(25) } - .onAppear { - withAnimation { - mainViewModel.hidesTabBar = true - } - } .onChange(of: viewModel.isLoading) { newValue in router.pop() } diff --git a/Resell/Views/ProductDetails/CompletedTransactionView.swift b/Resell/Views/ProductDetails/CompletedTransactionView.swift index 95ad9e2..70b2cf1 100644 --- a/Resell/Views/ProductDetails/CompletedTransactionView.swift +++ b/Resell/Views/ProductDetails/CompletedTransactionView.swift @@ -102,13 +102,6 @@ struct CompletedTransactionView: View { .background(Constants.Colors.white) .navigationTitle("Completed Transaction") .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .toolbarBackground(Constants.Colors.white, for: .automatic) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - BackButton(style: .systemChevronResizable(width: 12, height: 20)) - } - } .task { await loadBuyerReviewStateIfNeeded() } diff --git a/Resell/Views/ProductDetails/ExternalProfileView.swift b/Resell/Views/ProductDetails/ExternalProfileView.swift index 1312c37..a4e2def 100644 --- a/Resell/Views/ProductDetails/ExternalProfileView.swift +++ b/Resell/Views/ProductDetails/ExternalProfileView.swift @@ -24,7 +24,6 @@ struct ExternalProfileView: View { var body: some View { VStack(spacing: 0) { - customToolbar ScrollView { @@ -69,20 +68,6 @@ struct ExternalProfileView: View { .animation(.easeInOut, value: viewModel.sellerIsBlocked) } - if viewModel.didShowOptionsMenu { - OptionsMenuView(showMenu: $viewModel.didShowOptionsMenu, didShowBlockView: $viewModel.didShowBlockView, options: { - var options: [Option] = [ - .report(type: "User", id: userID), - ] - if viewModel.sellerIsBlocked { - options.append(.unblock) - } else { - options.append(.block) - } - return options - }()) - .zIndex(1) - } } .popupModal(isPresented: $viewModel.didShowBlockView) { popupModalContent @@ -95,7 +80,33 @@ struct ExternalProfileView: View { // MARK: We should not be able to click into our own posts... } } - .toolbar(.hidden, for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("@\(viewModel.externalUser?.username ?? "username")") + .font(Constants.Fonts.h3) + .foregroundStyle(viewModel.sellerIsBlocked ? Constants.Colors.white : Constants.Colors.black) + } + + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button { + router.push(.reportOptions(type: "User", id: userID)) + } label: { + Label("Report", systemImage: "flag") + } + + Button { + viewModel.didShowBlockView = true + } label: { + Label(viewModel.sellerIsBlocked ? "Unblock" : "Block", systemImage: "nosign") + } + } label: { + Image(systemName: "ellipsis") + .foregroundStyle(viewModel.sellerIsBlocked ? Constants.Colors.white : Constants.Colors.black) + } + } + } } private var profileView: some View { @@ -239,42 +250,6 @@ struct ExternalProfileView: View { } - private var customToolbar: some View { - HStack { - BackButton( - style: .systemChevronResizable(width: 12, height: 20), - hitTargetSize: CGSize(width: 24, height: 44) - ) - - Spacer() - - Text("@\(viewModel.externalUser?.username ?? "username")") - .font(Constants.Fonts.h3) - .foregroundStyle(Constants.Colors.black) - - Spacer() - - Button { - withAnimation { - viewModel.didShowOptionsMenu.toggle() - } - } label: { - Image(systemName: "ellipsis") - .resizable() - .frame(width: 24, height: 6) - .foregroundStyle(viewModel.sellerIsBlocked ? Constants.Colors.white : Constants.Colors.black) - } - .frame(width: 24, alignment: .trailing) - } - .padding(.horizontal, 24) - .padding(.bottom, 26) - .padding(.top, 10) - .overlay(alignment: .bottom) { - Divider() - } - .background(Constants.Colors.white) - } - private var profileTabBar: some View { HStack { // Listings tab diff --git a/Resell/Views/ProductDetails/FollowListView.swift b/Resell/Views/ProductDetails/FollowListView.swift index 638172b..5c70818 100644 --- a/Resell/Views/ProductDetails/FollowListView.swift +++ b/Resell/Views/ProductDetails/FollowListView.swift @@ -34,8 +34,6 @@ struct FollowListView: View { var body: some View { VStack(spacing: 0) { - customToolbar - tabBar ScrollView { @@ -54,35 +52,19 @@ struct FollowListView: View { } // .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(Constants.Colors.white) - .toolbar(.hidden, for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("@\(username)") + .font(Constants.Fonts.h3) + .foregroundStyle(Constants.Colors.black) + } + } .onAppear { loadData() } } - - private var customToolbar: some View { - HStack { - BackButton( - style: .systemChevronResizable(width: 12, height: 20), - hitTargetSize: CGSize(width: 24, height: 44) - ) - - Spacer() - - Text("@\(username)") - .font(Constants.Fonts.h3) - .foregroundStyle(Constants.Colors.black) - - Spacer() - - Color.clear - .frame(width: 24) - } - .frame(height: 44) - .padding(.horizontal, 24) - .background(Constants.Colors.white) - } - + private var tabBar: some View { HStack { Button { diff --git a/Resell/Views/ProductDetails/ProductDetailsView.swift b/Resell/Views/ProductDetails/ProductDetailsView.swift index ea5b326..729d838 100644 --- a/Resell/Views/ProductDetails/ProductDetailsView.swift +++ b/Resell/Views/ProductDetails/ProductDetailsView.swift @@ -65,68 +65,52 @@ struct ProductDetailsView: View { .zIndex(2) } - if viewModel.didShowOptionsMenu { - OptionsMenuView(showMenu: $viewModel.didShowOptionsMenu, didShowDeleteView: $viewModel.didShowDeleteView, options: { - var options: [Option] = [] - - let urlString = "resell://product/\(post.id)" - if let shareUrl = URL(string: urlString) { - options.append( - .share( - url: shareUrl, - itemName: viewModel.item?.title ?? "Check out this AWESOME item on Resell!" - )) - } - - options.append(.report(type: "Post", id: post.id)) - - if viewModel.isUserPost() { - options.append(.delete) - } - - return options - }()) - .padding(.top, topSafeArea * 2 + 30) - .zIndex(2) - } - // Custom navigation buttons overlay VStack { - HStack { - Button { - router.pop() - } label: { - Image("chevron.left.white") - .resizable() - .frame(width: 36, height: 24) - .frame(width: 44, height: 44) - .contentShape(Rectangle()) - .offset(x: -10) - .font(.system(size: 10, weight: .medium)) - .foregroundStyle(Constants.Colors.black) - } - .background(.ultraThinMaterial, in: Circle()) - .padding(.leading, 12) - - Spacer() - - Button { - withAnimation { - viewModel.didShowOptionsMenu.toggle() + GlassEffectContainer(spacing: 12) { + HStack { + Button { + router.pop() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 17, weight: .semibold)) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) } - } label: { - Image(systemName: "ellipsis") - .resizable() - .frame(width: 24, height: 6) - .foregroundStyle(Constants.Colors.white) - .frame(width: 44, height: 44) - .contentShape(Rectangle()) - + .glassEffect(.regular.interactive(), in: .circle) + .padding(.leading, 12) + + Spacer() + + Menu { + ShareLink( + item: "Check out this AWESOME \(viewModel.item?.title ?? "item") on Resell!\nresell://product/\(post.id)" + ) { + Label("Share", systemImage: "square.and.arrow.up") + } + + Button { + router.push(.reportOptions(type: "Post", id: post.id)) + } label: { + Label("Report", systemImage: "flag") + } + + if viewModel.isUserPost() { + Button(role: .destructive) { + viewModel.didShowDeleteView = true + } label: { + Label("Delete", systemImage: "trash") + } + } + } label: { + Image(systemName: "ellipsis") + .font(.system(size: 17, weight: .semibold)) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + } + .glassEffect(.regular.interactive(), in: .circle) + .padding(.trailing, 12) } - .background(.ultraThinMaterial, in: Circle()) - .padding(.trailing, 12) - - } .padding(.top, topSafeArea * 2 + 4) @@ -143,25 +127,13 @@ struct ProductDetailsView: View { .sheet(isPresented: $viewModel.didShowDeleteView) { deletePostView } - .onChange(of: viewModel.didShowDeleteView) { isPresented in - if isPresented { - viewModel.didShowOptionsMenu = false - } - } .onAppear { viewModel.setPost(post: post) - withAnimation { - mainViewModel.hidesTabBar = true - } - viewModel.maxDrag = imageHeight } .onDisappear { viewModel.didShowOptionsMenu = false - withAnimation { - mainViewModel.hidesTabBar = false - } } } @@ -192,7 +164,6 @@ struct ProductDetailsView: View { } CustomPageControlIndicatorView(currentPage: $viewModel.currentPage, numberOfPages: $viewModel.images.count) - .frame(height: 20) .padding() } } @@ -341,10 +312,6 @@ struct ProductDetailsView: View { viewModel.clear() viewModel.setPost(post: post) - withAnimation { - mainViewModel.hidesTabBar = true - } - viewModel.maxDrag = imageHeight if let existingIndex = router.path.lastIndex(where: { @@ -375,13 +342,6 @@ struct ProductDetailsView: View { } .frame(width: UIScreen.width, height: 50) .padding(.bottom, 24) - .background( - LinearGradient(stops: [ - .init(color: Color.clear, location: 0.0), - .init(color: Constants.Colors.white.opacity(0.8), location: 0.5), - .init(color: Constants.Colors.white, location: 1.0) - ], startPoint: .top, endPoint: .bottom) - ) } // TODO: FIX @@ -426,53 +386,27 @@ struct ProductDetailsView: View { @AppStorage("isNotificationAuthorized") var isNotificationAuthorized = false private var saveButton: some View { - if isNotificationAuthorized { - Button { - viewModel.isSaved.toggle() - - Task { - await viewModel.updateItemSaved() - await homeViewModel.toggleLocalSaveStatus(for: post, isSaving: viewModel.isSaved) - } - - sendNotification() - } label: { - ZStack { - Circle() - .frame(width: 72, height: 72) - .foregroundStyle(Constants.Colors.white) - .opacity(viewModel.isSaved ? 1.0 : 0.9) - .shadow(radius: 2) - - Image(viewModel.isSaved ? "saved.fill" : "saved") - .resizable() - .frame(width: 21, height: 27) - } + Button { + viewModel.isSaved.toggle() + + Task { + await viewModel.updateItemSaved() + await homeViewModel.toggleLocalSaveStatus(for: post, isSaving: viewModel.isSaved) } - } else { - Button { - viewModel.isSaved.toggle() - - Task { - await viewModel.updateItemSaved() - await homeViewModel.toggleLocalSaveStatus(for: post, isSaving: viewModel.isSaved) - } - + + if isNotificationAuthorized { + sendNotification() + } else { requestNotificationAuthorization() - } label: { - ZStack { - Circle() - .frame(width: 72, height: 72) - .foregroundStyle(Constants.Colors.white) - .opacity(viewModel.isSaved ? 1.0 : 0.9) - .shadow(radius: 2) - - Image(viewModel.isSaved ? "saved.fill" : "saved") - .resizable() - .frame(width: 21, height: 27) - } } + } label: { + Image(viewModel.isSaved ? "saved.fill" : "saved") + .resizable() + .frame(width: 21, height: 27) + .frame(width: 72, height: 72) + .contentShape(Circle()) } + .glassEffect(.regular.interactive(), in: .circle) } private var deletePostView: some View { @@ -499,11 +433,9 @@ struct ProductDetailsView: View { .foregroundStyle(Constants.Colors.black) } } - .background(Constants.Colors.white) .presentationDetents([.height(200)]) .presentationDragIndicator(.visible) .presentationCornerRadius(25) - .presentationBackground(Constants.Colors.white) } // MARK: - Functions diff --git a/Resell/Views/Report/ReportConfirmationView.swift b/Resell/Views/Report/ReportConfirmationView.swift index f55f7e2..f141836 100644 --- a/Resell/Views/Report/ReportConfirmationView.swift +++ b/Resell/Views/Report/ReportConfirmationView.swift @@ -79,9 +79,8 @@ struct ReportConfirmationView: View { viewModel.clear() } label: { Image(systemName: "xmark") - .resizable() - .frame(width: 20, height: 20) - .tint(Constants.Colors.black) + .font(.system(size: 17, weight: .medium)) + .foregroundStyle(Constants.Colors.black) } } } diff --git a/Resell/Views/Report/ReportDetailsView.swift b/Resell/Views/Report/ReportDetailsView.swift index 06c9a9b..b1403a6 100644 --- a/Resell/Views/Report/ReportDetailsView.swift +++ b/Resell/Views/Report/ReportDetailsView.swift @@ -53,11 +53,7 @@ struct ReportDetailsView: View { } .padding(.horizontal, Constants.Spacing.horizontalPadding) .background(Constants.Colors.white) - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } ToolbarItem(placement: .principal) { Text("Report \(viewModel.reportType)") .font(Constants.Fonts.h3) diff --git a/Resell/Views/Report/ReportOptionsView.swift b/Resell/Views/Report/ReportOptionsView.swift index 12ae0a6..b353103 100644 --- a/Resell/Views/Report/ReportOptionsView.swift +++ b/Resell/Views/Report/ReportOptionsView.swift @@ -39,11 +39,7 @@ struct ReportOptionsView: View { Spacer() } .background(Constants.Colors.white) - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } ToolbarItem(placement: .principal) { Text("Report \(viewModel.reportType)") .font(Constants.Fonts.h3) diff --git a/Resell/Views/Settings/AvailabilitySettingsView.swift b/Resell/Views/Settings/AvailabilitySettingsView.swift index 7370af9..80d5502 100644 --- a/Resell/Views/Settings/AvailabilitySettingsView.swift +++ b/Resell/Views/Settings/AvailabilitySettingsView.swift @@ -163,18 +163,12 @@ struct AvailabilitySettingsView: View { } } .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .toolbarBackground(Constants.Colors.white, for: .navigationBar) - .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .principal) { Text("Availability Settings") .font(Constants.Fonts.h3) .foregroundStyle(Constants.Colors.black) } - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } } .alert("Error", isPresented: .constant(errorMessage != nil)) { Button("OK") { diff --git a/Resell/Views/Settings/BlockedUsersView.swift b/Resell/Views/Settings/BlockedUsersView.swift index 6b506d4..6df5289 100644 --- a/Resell/Views/Settings/BlockedUsersView.swift +++ b/Resell/Views/Settings/BlockedUsersView.swift @@ -37,12 +37,7 @@ struct BlockedUsersView: View { } .frame(width: UIScreen.width) .background(Constants.Colors.white) - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } - ToolbarItem(placement: .principal) { Text("Blocked Users") .font(Constants.Fonts.h3) diff --git a/Resell/Views/Settings/EditProfileView.swift b/Resell/Views/Settings/EditProfileView.swift index cf3aceb..e5e933f 100644 --- a/Resell/Views/Settings/EditProfileView.swift +++ b/Resell/Views/Settings/EditProfileView.swift @@ -53,12 +53,7 @@ struct EditProfileView: View { } .padding(.top, 40) - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .topBarLeading) { - BackButton() - } - ToolbarItem(placement: .principal) { Text("Edit Profile") .font(Constants.Fonts.h3) diff --git a/Resell/Views/Settings/SendFeedbackView.swift b/Resell/Views/Settings/SendFeedbackView.swift index c9eb666..a7d467c 100644 --- a/Resell/Views/Settings/SendFeedbackView.swift +++ b/Resell/Views/Settings/SendFeedbackView.swift @@ -40,11 +40,7 @@ struct SendFeedbackView: View { .padding(.horizontal, Constants.Spacing.horizontalPadding) .padding(.top, 40) .background(Constants.Colors.white) - .navigationBarBackButtonHidden(true) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } ToolbarItem(placement: .principal) { Text("Send Feedback") .font(Constants.Fonts.h3) diff --git a/Resell/Views/Settings/SettingsView.swift b/Resell/Views/Settings/SettingsView.swift index 8fd8fdf..2666850 100644 --- a/Resell/Views/Settings/SettingsView.swift +++ b/Resell/Views/Settings/SettingsView.swift @@ -65,12 +65,6 @@ struct SettingsView: View { .background(Constants.Colors.white) .navigationTitle(isAccountSettings ? "Account Settings" : "Settings") .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - BackButton() - } - } .sheet(isPresented: $viewModel.didShowWebView) { WebView(url: URL(string: "https://www.cornellappdev.com/license/resell")!) .edgesIgnoringSafeArea(.all) @@ -135,11 +129,9 @@ struct SettingsView: View { .foregroundStyle(Constants.Colors.black) } } - .background(Constants.Colors.white) .presentationDetents([.height(200)]) .presentationDragIndicator(.visible) .presentationCornerRadius(25) - .presentationBackground(Constants.Colors.white) } private var popupModalContent: some View {