From 693d463cb17aa9701936e6fc62f8f4d1749328ae Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Thu, 14 May 2026 09:43:57 +0200 Subject: [PATCH 1/2] refactor(ui): InsetHeaderScrollView, shared tab swipes --- .../lightbulb-figure.imageset/Contents.json | 21 ++ .../lightbulb-figure.png | Bin 0 -> 9619 bytes .../lightbulb.imageset/Contents.json | 0 .../lightbulb.imageset/lightbulb.png | Bin Bitkit/Components/Activity/ActivityList.swift | 2 - Bitkit/Components/Home/SuggestionsCard.swift | 57 ------ Bitkit/Components/InsetHeaderScrollView.swift | 64 ++++++ Bitkit/Components/SuggestionCard.swift | 89 +++++++++ .../{Home => Widgets}/Suggestions.swift | 16 +- Bitkit/Extensions/View+AllowSwipeBack.swift | 21 -- Bitkit/Extensions/View+SwipeGestures.swift | 55 +++++ .../Views/Settings/MainSettingsScreen.swift | 38 ++-- Bitkit/Views/Settings/SupportScreen.swift | 169 ++++++++-------- Bitkit/Views/Shop/ShopDiscover.swift | 188 +++++++----------- .../Wallets/Activity/AllActivityView.swift | 148 ++++++-------- changelog.d/next/550.changed.md | 1 + 16 files changed, 484 insertions(+), 385 deletions(-) create mode 100644 Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/Contents.json create mode 100644 Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/lightbulb-figure.png rename Bitkit/Assets.xcassets/{Illustrations => icons}/lightbulb.imageset/Contents.json (100%) rename Bitkit/Assets.xcassets/{Illustrations => icons}/lightbulb.imageset/lightbulb.png (100%) delete mode 100644 Bitkit/Components/Home/SuggestionsCard.swift create mode 100644 Bitkit/Components/InsetHeaderScrollView.swift create mode 100644 Bitkit/Components/SuggestionCard.swift rename Bitkit/Components/{Home => Widgets}/Suggestions.swift (94%) delete mode 100644 Bitkit/Extensions/View+AllowSwipeBack.swift create mode 100644 Bitkit/Extensions/View+SwipeGestures.swift create mode 100644 changelog.d/next/550.changed.md diff --git a/Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/Contents.json b/Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/Contents.json new file mode 100644 index 000000000..5ba053448 --- /dev/null +++ b/Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "lightbulb-figure.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/lightbulb-figure.png b/Bitkit/Assets.xcassets/Illustrations/lightbulb-figure.imageset/lightbulb-figure.png new file mode 100644 index 0000000000000000000000000000000000000000..6f5838659f0299fd1ae30826f4090f40d883effd GIT binary patch literal 9619 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 literal 0 HcmV?d00001 diff --git a/Bitkit/Assets.xcassets/Illustrations/lightbulb.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/lightbulb.imageset/Contents.json similarity index 100% rename from Bitkit/Assets.xcassets/Illustrations/lightbulb.imageset/Contents.json rename to Bitkit/Assets.xcassets/icons/lightbulb.imageset/Contents.json diff --git a/Bitkit/Assets.xcassets/Illustrations/lightbulb.imageset/lightbulb.png b/Bitkit/Assets.xcassets/icons/lightbulb.imageset/lightbulb.png similarity index 100% rename from Bitkit/Assets.xcassets/Illustrations/lightbulb.imageset/lightbulb.png rename to Bitkit/Assets.xcassets/icons/lightbulb.imageset/lightbulb.png diff --git a/Bitkit/Components/Activity/ActivityList.swift b/Bitkit/Components/Activity/ActivityList.swift index 33814d077..c3945ec15 100644 --- a/Bitkit/Components/Activity/ActivityList.swift +++ b/Bitkit/Components/Activity/ActivityList.swift @@ -5,7 +5,6 @@ struct ActivityList: View { @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var contactsManager: ContactsManager @EnvironmentObject var feeEstimatesManager: FeeEstimatesManager - @State private var isHorizontalSwipe = false let viewType: ActivityViewType @@ -36,7 +35,6 @@ struct ActivityList: View { ) } .accessibilityIdentifier("Activity-\(index)") - .disabled(isHorizontalSwipe) } } } diff --git a/Bitkit/Components/Home/SuggestionsCard.swift b/Bitkit/Components/Home/SuggestionsCard.swift deleted file mode 100644 index a44f53f74..000000000 --- a/Bitkit/Components/Home/SuggestionsCard.swift +++ /dev/null @@ -1,57 +0,0 @@ -import SwiftUI - -struct SuggestionCard: View { - let data: SuggestionCardData - var onDismiss: () -> Void = {} - - var body: some View { - ZStack(alignment: .topTrailing) { - VStack(alignment: .leading, spacing: 0) { - Spacer() - - Image(data.imageName) - .resizable() - .interpolation(.high) - .scaledToFit() - .frame(height: 80) - - Text(data.title) - .font(.custom(Fonts.black, size: 20)) - .lineLimit(1) - .kerning(-0.5) - .textCase(.uppercase) - .padding(.top, 4) - - CaptionBText(data.description) - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background( - RoundedRectangle(cornerRadius: 16) - .fill( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: data.color, location: 0.0), - .init(color: Color.black.opacity(0.1), location: 0.9), - .init(color: Color.black, location: 1.0), - ]), - startPoint: .top, - endPoint: .bottom - ) - ) - ) - - Button(action: onDismiss) { - Image("x-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.textSecondary) - .frame(width: 16, height: 16) - .padding(8) - } - .padding(8) - .accessibilityIdentifier("SuggestionDismiss") - .accessibility(label: Text("Dismiss \(data.title)")) - } - } -} diff --git a/Bitkit/Components/InsetHeaderScrollView.swift b/Bitkit/Components/InsetHeaderScrollView.swift new file mode 100644 index 000000000..d19a2dc4b --- /dev/null +++ b/Bitkit/Components/InsetHeaderScrollView.swift @@ -0,0 +1,64 @@ +import SwiftUI + +// MARK: - InsetHeaderScrollView + +// Measured top header (`safeAreaInset`) and scroll content with `minHeight` to fill the viewport below it. +// Optional `scrollModifier` for refresh, margins, etc. + +private enum HeaderHeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + let next = nextValue() + guard next > 0 else { return } + value = next + } +} + +private struct HeaderHeightMeasure: View { + var body: some View { + GeometryReader { proxy in + Color.clear.preference(key: HeaderHeightPreferenceKey.self, value: proxy.size.height) + } + } +} + +struct InsetHeaderScrollView: View { + let header: () -> Header + let content: () -> Content + let scrollModifier: ScrollModifier + + @State private var headerHeight: CGFloat = 0 + + init( + header: @escaping () -> Header, + content: @escaping () -> Content, + scrollModifier: ScrollModifier = EmptyModifier() + ) { + self.header = header + self.content = content + self.scrollModifier = scrollModifier + } + + var body: some View { + GeometryReader { geo in + ScrollView(showsIndicators: false) { + content() + .frame(minHeight: contentMinHeight(in: geo), alignment: .top) + } + .modifier(scrollModifier) + .safeAreaInset(edge: .top, spacing: 0) { + header().background(HeaderHeightMeasure()) + } + .onPreferenceChange(HeaderHeightPreferenceKey.self) { newValue in + if newValue > 0 { headerHeight = newValue } + } + } + } + + /// Before the first header measurement, use full height so `minHeight` is non-negative. + private func contentMinHeight(in geo: GeometryProxy) -> CGFloat { + let insetTop = headerHeight > 0 ? headerHeight : 0 + return max(0, geo.size.height - insetTop) + } +} diff --git a/Bitkit/Components/SuggestionCard.swift b/Bitkit/Components/SuggestionCard.swift new file mode 100644 index 000000000..49918181a --- /dev/null +++ b/Bitkit/Components/SuggestionCard.swift @@ -0,0 +1,89 @@ +import SwiftUI + +/// Shared gradient tile used by suggestions widget and shop discover +struct SuggestionCard: View { + let title: String + let description: String + let imageName: String + let accentColor: Color + let onTap: () -> Void + let onDismiss: (() -> Void)? + + init( + title: String, + description: String, + imageName: String, + accentColor: Color, + onTap: @escaping () -> Void, + onDismiss: (() -> Void)? = nil + ) { + self.title = title + self.description = description + self.imageName = imageName + self.accentColor = accentColor + self.onTap = onTap + self.onDismiss = onDismiss + } + + var body: some View { + ZStack(alignment: .topTrailing) { + Button(action: onTap) { + VStack(alignment: .leading, spacing: 0) { + Spacer() + + Image(imageName) + .resizable() + .scaledToFit() + .frame(width: 96, height: 96) + .frame(maxWidth: .infinity, alignment: .center) + + Text(title) + .font(.custom(Fonts.black, size: 20)) + .lineLimit(1) + .kerning(-0.5) + .textCase(.uppercase) + .padding(.top, 4) + + CaptionBText(description) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .background( + RoundedRectangle(cornerRadius: 16) + .fill( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: accentColor, location: 0.0), + .init(color: Color.black.opacity(0.1), location: 0.9), + .init(color: Color.black, location: 1.0), + ]), + startPoint: .top, + endPoint: .bottom + ) + ) + ) + } + .buttonStyle(.plain) + .zIndex(0) + + if let onDismiss { + Button(action: onDismiss) { + Image("x-mark") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.textSecondary) + .frame(width: 16, height: 16) + .padding(8) + .contentShape(Rectangle()) + } + .padding(8) + .contentShape(Rectangle()) + .accessibilityIdentifier("SuggestionDismiss") + .accessibility(label: Text("Dismiss \(title)")) + .buttonStyle(.plain) + .zIndex(1) + } + } + } +} diff --git a/Bitkit/Components/Home/Suggestions.swift b/Bitkit/Components/Widgets/Suggestions.swift similarity index 94% rename from Bitkit/Components/Home/Suggestions.swift rename to Bitkit/Components/Widgets/Suggestions.swift index 676bdff8c..7b38f3b2c 100644 --- a/Bitkit/Components/Home/Suggestions.swift +++ b/Bitkit/Components/Widgets/Suggestions.swift @@ -75,7 +75,7 @@ let cards: [SuggestionCardData] = [ id: "support", title: t("cards__support__title"), description: t("cards__support__description"), - imageName: "lightbulb", + imageName: "lightbulb-figure", color: .yellow24, action: .support ), @@ -245,10 +245,16 @@ struct Suggestions: View { spacing: 16 ) { ForEach(visibleCards) { card in - SuggestionCard(data: card, onDismiss: { dismissCard(card) }) - .onTapGesture { if !isPreview { onItemTap(card) } } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("Suggestion-\(card.accessibilityId)") + SuggestionCard( + title: card.title, + description: card.description, + imageName: card.imageName, + accentColor: card.color, + onTap: { if !isPreview { onItemTap(card) } }, + onDismiss: { dismissCard(card) } + ) + .accessibilityElement(children: .contain) + .accessibilityIdentifier("Suggestion-\(card.accessibilityId)") } } .allowsHitTesting(!isPreview) diff --git a/Bitkit/Extensions/View+AllowSwipeBack.swift b/Bitkit/Extensions/View+AllowSwipeBack.swift deleted file mode 100644 index 81949857a..000000000 --- a/Bitkit/Extensions/View+AllowSwipeBack.swift +++ /dev/null @@ -1,21 +0,0 @@ -import SwiftUI - -extension View { - /// Controls whether the interactive swipe-back gesture is enabled on this screen. - /// Use `.allowSwipeBack(false)` on screens that use a custom header without a back button - /// (e.g. `SheetHeader` with default `showBackButton: false`) so users can't swipe to dismiss. - /// Default is `true`; only apply this modifier when you want to disable the gesture. - func allowSwipeBack(_ allowed: Bool) -> some View { - modifier(AllowSwipeBackModifier(allowed: allowed)) - } -} - -private struct AllowSwipeBackModifier: ViewModifier { - let allowed: Bool - - func body(content: Content) -> some View { - content - .onAppear { SwipeBackState.allowSwipeBack = allowed } - .onDisappear { SwipeBackState.allowSwipeBack = true } - } -} diff --git a/Bitkit/Extensions/View+SwipeGestures.swift b/Bitkit/Extensions/View+SwipeGestures.swift new file mode 100644 index 000000000..b713e9118 --- /dev/null +++ b/Bitkit/Extensions/View+SwipeGestures.swift @@ -0,0 +1,55 @@ +import SwiftUI + +// Swipe-to-go-back (nav stack) and horizontal swipes between `SegmentedControl` tabs. + +extension View { + /// Controls whether the interactive swipe-back gesture is enabled on this screen. + /// Use `.allowSwipeBack(false)` on screens that use a custom header without a back button + /// (e.g. `SheetHeader` with default `showBackButton: false`) so users can't swipe to dismiss. + /// Default is `true`; only apply this modifier when you want to disable the gesture. + func allowSwipeBack(_ allowed: Bool) -> some View { + modifier(AllowSwipeBackModifier(allowed: allowed)) + } + + // MARK: Segmented tab swipes + + /// Swipe left/right to move between adjacent tabs (same order as `T.allCases` / `SegmentedControl`). + func swipeSegmentedTabs( + selection: Binding, + minimumDragDistance: CGFloat = 20, + swipeThreshold: CGFloat = 50, + animation: Animation = .easeInOut(duration: 0.2) + ) -> some View { + highPriorityGesture( + DragGesture(minimumDistance: minimumDragDistance, coordinateSpace: .local) + .onEnded { value in + let horizontalAmount = value.translation.width + let verticalAmount = value.translation.height + guard abs(horizontalAmount) > abs(verticalAmount) else { return } + + let tabs = Array(T.allCases) + guard let currentIndex = tabs.firstIndex(of: selection.wrappedValue) else { return } + + if horizontalAmount < -swipeThreshold, currentIndex < tabs.count - 1 { + withAnimation(animation) { + selection.wrappedValue = tabs[currentIndex + 1] + } + } else if horizontalAmount > swipeThreshold, currentIndex > 0 { + withAnimation(animation) { + selection.wrappedValue = tabs[currentIndex - 1] + } + } + } + ) + } +} + +private struct AllowSwipeBackModifier: ViewModifier { + let allowed: Bool + + func body(content: Content) -> some View { + content + .onAppear { SwipeBackState.allowSwipeBack = allowed } + .onDisappear { SwipeBackState.allowSwipeBack = true } + } +} diff --git a/Bitkit/Views/Settings/MainSettingsScreen.swift b/Bitkit/Views/Settings/MainSettingsScreen.swift index 93088c337..00506a780 100644 --- a/Bitkit/Views/Settings/MainSettingsScreen.swift +++ b/Bitkit/Views/Settings/MainSettingsScreen.swift @@ -22,22 +22,34 @@ struct MainSettingsScreen: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__settings")) - .padding(.horizontal, 16) + InsetHeaderScrollView( + header: { + VStack(spacing: 0) { + NavigationBar(title: t("settings__settings")) + .padding(.horizontal, 16) - SegmentedControl(selectedTab: $selectedTab, tabItems: settingsTabItems) - .padding(.horizontal, 16) - - Group { - switch selectedTab { - case .general: GeneralSettingsView() - case .security: SecuritySettingsView() - case .advanced: AdvancedSettingsView() + SegmentedControl(selectedTab: $selectedTab, tabItems: settingsTabItems) + .padding(.horizontal, 16) + } + .background( + BlurView() + .ignoresSafeArea(edges: .top) + ) + .compositingGroup() + .shadow(color: Color.black.opacity(0.5), radius: 8, x: 0, y: 20) + }, + content: { + Group { + switch selectedTab { + case .general: GeneralSettingsView() + case .security: SecuritySettingsView() + case .advanced: AdvancedSettingsView() + } } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .swipeSegmentedTabs(selection: $selectedTab) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } + ) .navigationBarHidden(true) } } diff --git a/Bitkit/Views/Settings/SupportScreen.swift b/Bitkit/Views/Settings/SupportScreen.swift index 2feb70bf6..7e8ce0d58 100644 --- a/Bitkit/Views/Settings/SupportScreen.swift +++ b/Bitkit/Views/Settings/SupportScreen.swift @@ -61,103 +61,102 @@ struct SupportScreen: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__support__title")) - .padding(.horizontal, 16) - .padding(.bottom, 16) - - GeometryReader { geometry in - ScrollView(showsIndicators: false) { - ZStack { - // Orange diagonal background (scrolls with content) - Color.brandAccent - .clipShape(DiagonalCut()) - .ignoresSafeArea() - - VStack(alignment: .leading, spacing: 0) { - BodyMText(t("settings__support__text")) - .padding(.bottom, 16) - - VStack(spacing: 0) { - NavigationLink(value: Route.reportIssue) { - SettingsRow(title: t("settings__support__report"), iconName: "warning") - } - - Button(action: { - openURL(URL(string: Env.helpUrl)!) - }) { - SettingsRow(title: t("settings__support__help"), iconName: "question") - } - - NavigationLink(value: Route.appStatus) { - SettingsRow(title: t("settings__support__status"), iconName: "power") - } - .accessibilityIdentifier("AppStatus") - - Button(action: { - openURL(URL(string: Env.termsOfServiceUrl)!) - }) { - SettingsRow(title: t("settings__about__legal"), iconName: "file-text") - } - - ShareLink(item: shareText, message: Text(shareText)) { - SettingsRow(title: t("settings__about__share"), iconName: "share") - } - - Button(action: { - onVersionTap() - }) { - SettingsRow( - title: t("settings__about__version"), - iconName: "stack", - rightText: appVersion, - rightIcon: nil - ) - } - .accessibilityIdentifier("DevOptions") - } + InsetHeaderScrollView( + header: { + NavigationBar(title: t("settings__support__title")) + .padding(.horizontal, 16) + .padding(.bottom, 16) + }, + content: { + ZStack { + // Orange diagonal background (scrolls with content) + Color.brandAccent + .clipShape(DiagonalCut()) + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + BodyMText(t("settings__support__text")) + .padding(.bottom, 16) - Spacer(minLength: 32) + VStack(spacing: 0) { + NavigationLink(value: Route.reportIssue) { + SettingsRow(title: t("settings__support__report"), iconName: "warning") + } - VStack(alignment: .center, spacing: 0) { - Image("logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxHeight: 100) - .accessibilityIdentifier("AboutLogo") + Button(action: { + openURL(URL(string: Env.helpUrl)!) + }) { + SettingsRow(title: t("settings__support__help"), iconName: "question") } - .frame(maxWidth: .infinity) - .padding(.bottom, 16) - Social() - .padding(.bottom, 16) + NavigationLink(value: Route.appStatus) { + SettingsRow(title: t("settings__support__status"), iconName: "power") + } + .accessibilityIdentifier("AppStatus") - BodyMText("Bitkit was crafted by Synonym Software, S.A. DE C.V. ©2025. All rights reserved.") - .padding(.bottom, 16) + Button(action: { + openURL(URL(string: Env.termsOfServiceUrl)!) + }) { + SettingsRow(title: t("settings__about__legal"), iconName: "file-text") + } - HStack(alignment: .center, spacing: 10) { - Image("synonym-logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 24) + ShareLink(item: shareText, message: Text(shareText)) { + SettingsRow(title: t("settings__about__share"), iconName: "share") + } - Image("tether-logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 16) + Button(action: { + onVersionTap() + }) { + SettingsRow( + title: t("settings__about__version"), + iconName: "stack", + rightText: appVersion, + rightIcon: nil + ) } - .frame(maxWidth: .infinity, alignment: .center) - .frame(height: 24) - .padding(.bottom, 32) + .accessibilityIdentifier("DevOptions") + } + + Spacer(minLength: 32) + + VStack(alignment: .center, spacing: 0) { + Image("logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 100) + .accessibilityIdentifier("AboutLogo") } - .frame(minHeight: geometry.size.height) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() + .frame(maxWidth: .infinity) + .padding(.bottom, 16) + + Social() + .padding(.bottom, 16) + + BodyMText("Bitkit was crafted by Synonym Software, S.A. DE C.V. ©2025. All rights reserved.") + .padding(.bottom, 16) + + HStack(alignment: .center, spacing: 10) { + Image("synonym-logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 24) + + Image("tether-logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 16) + } + .frame(maxWidth: .infinity, alignment: .center) + .frame(height: 24) + .padding(.bottom, 32) } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } + .frame(maxWidth: .infinity, maxHeight: .infinity) } - .ignoresSafeArea() - } + ) + .ignoresSafeArea(edges: .bottom) .navigationBarHidden(true) } diff --git a/Bitkit/Views/Shop/ShopDiscover.swift b/Bitkit/Views/Shop/ShopDiscover.swift index 38e849144..868ecae00 100644 --- a/Bitkit/Views/Shop/ShopDiscover.swift +++ b/Bitkit/Views/Shop/ShopDiscover.swift @@ -24,10 +24,8 @@ enum ShopTab: String, CaseIterable, CustomStringConvertible { var description: String { switch self { - case .shop: - return t("other__shop__discover__tabs__shop") - case .map: - return t("other__shop__discover__tabs__map") + case .shop: t("other__shop__discover__tabs__shop") + case .map: t("other__shop__discover__tabs__map") } } } @@ -98,130 +96,96 @@ struct ShopDiscover: View { ] var body: some View { - VStack(spacing: 0) { - NavigationBar(title: navTitle) - .padding(.horizontal, 16) - - SegmentedControl(selectedTab: $selectedTab, tabs: ShopTab.allCases) - .padding(.horizontal, 16) + InsetHeaderScrollView( + header: { + VStack(spacing: 0) { + NavigationBar(title: navTitle) + .padding(.horizontal, 16) - Group { - switch selectedTab { - case .shop: - shopContent - case .map: - ShopWebView(url: Env.btcMapUrl) - .padding(.top, 16) + SegmentedControl(selectedTab: $selectedTab, tabs: ShopTab.allCases) .padding(.horizontal, 16) } + .background( + ZStack { + BlurView() + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black, location: 0.0), + .init(color: Color.black, location: 0.4), + .init(color: Color.black.opacity(0), location: 1.0), + ]), + startPoint: .top, + endPoint: .bottom + ) + } + .ignoresSafeArea(edges: .top) + ) + .compositingGroup() + .shadow(color: Color.black.opacity(0.5), radius: 8, x: 0, y: 20) + }, + content: { + Group { + switch selectedTab { + case .shop: + shopContent + case .map: + ShopWebView(url: Env.btcMapUrl) + .padding(.top, 16) + .padding(.horizontal, 16) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .swipeSegmentedTabs(selection: $selectedTab) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } + ) .navigationBarHidden(true) .offlineOverlay(title: navTitle) } private var shopContent: some View { - GeometryReader { geometry in - let cardSize = (geometry.size.width - 32 - 16) / 2 - - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 0) { - LazyVGrid( - columns: [ - GridItem(.flexible(), spacing: 16), - GridItem(.flexible(), spacing: 16), - ], - spacing: 16 - ) { - ForEach(cards) { card in - ShopDiscoverCard( - title: card.title, - description: card.description, - imageName: card.imageName, - color: card.color, - size: cardSize - ) { - navigation.navigate(.shopMain(page: card.route)) - } + VStack(alignment: .leading, spacing: 0) { + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16), + ], + spacing: 16 + ) { + ForEach(cards) { card in + SuggestionCard( + title: card.title, + description: card.description, + imageName: card.imageName, + accentColor: card.color, + onTap: { + navigation.navigate(.shopMain(page: card.route)) } - } - .padding(.bottom, 16) + ) + } + } + .padding(.bottom, 16) - VStack { - CaptionMText(t("other__shop__discover__label")) - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(height: 50) + VStack { + CaptionMText(t("other__shop__discover__label")) .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 8) - - LazyVStack(spacing: 0) { - ForEach(categories) { category in - ShopCategoryRow( - title: category.title, - iconName: category.iconName - ) { - navigation.navigate(.shopMain(page: category.route)) - } - } + } + .frame(height: 50) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 8) + + LazyVStack(spacing: 0) { + ForEach(categories) { category in + ShopCategoryRow( + title: category.title, + iconName: category.iconName + ) { + navigation.navigate(.shopMain(page: category.route)) } } - .padding(.top, 16) - .padding(.horizontal, 16) - } - } - } -} - -// MARK: - Shop Discover Card Component - -struct ShopDiscoverCard: View { - let title: String - let description: String - let imageName: String - let color: Color - let size: CGFloat - let onTap: () -> Void - - var body: some View { - Button(action: onTap) { - VStack(alignment: .leading, spacing: 0) { - Spacer() - - Image(imageName) - .resizable() - .scaledToFit() - .frame(width: 96, height: 96) - .frame(maxWidth: .infinity, alignment: .center) - - Text(title) - .font(.custom(Fonts.black, size: 20)) - .lineLimit(1) - .kerning(-0.5) - .textCase(.uppercase) - .padding(.top, 4) - - CaptionBText(description) } - .padding() - .frame(width: size, height: size, alignment: .topLeading) - .background( - RoundedRectangle(cornerRadius: 16) - .fill( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: color, location: 0.0), - .init(color: Color.black.opacity(0.1), location: 0.9), - .init(color: Color.black, location: 1.0), - ]), - startPoint: .top, - endPoint: .bottom - ) - ) - ) } - .buttonStyle(PlainButtonStyle()) + .padding(.top, 16) + .padding(.horizontal, 16) } } diff --git a/Bitkit/Views/Wallets/Activity/AllActivityView.swift b/Bitkit/Views/Wallets/Activity/AllActivityView.swift index 64832e763..041a703a7 100644 --- a/Bitkit/Views/Wallets/Activity/AllActivityView.swift +++ b/Bitkit/Views/Wallets/Activity/AllActivityView.swift @@ -5,98 +5,51 @@ struct AllActivityView: View { @EnvironmentObject private var app: AppViewModel @EnvironmentObject private var wallet: WalletViewModel - @State private var headerContentHeight: CGFloat = 116 - - private var headerTopPadding: CGFloat { - ScreenLayout.topPaddingWithoutSafeArea + headerContentHeight - } - var body: some View { ZStack(alignment: .top) { - ScrollView(showsIndicators: false) { - ActivityList(viewType: .all) - .scrollDismissesKeyboard(.interactively) - .highPriorityGesture( - // TODO: rewrite using TabView - DragGesture(minimumDistance: 20, coordinateSpace: .local) - .onEnded { value in - let horizontalAmount = value.translation.width - let verticalAmount = value.translation.height - - if abs(horizontalAmount) > abs(verticalAmount) { - if horizontalAmount < -50 { - // Swipe left - move to next tab - if let currentIndex = ActivityTab.allCases.firstIndex(of: activity.selectedTab), - currentIndex < ActivityTab.allCases.count - 1 - { - withAnimation(.easeInOut(duration: 0.2)) { - activity.selectedTab = ActivityTab.allCases[currentIndex + 1] - } - } - } else if horizontalAmount > 50 { - // Swipe right - move to previous tab - if let currentIndex = ActivityTab.allCases.firstIndex(of: activity.selectedTab), - currentIndex > 0 - { - withAnimation(.easeInOut(duration: 0.2)) { - activity.selectedTab = ActivityTab.allCases[currentIndex - 1] - } - } - } - } - } - ) - } - .contentMargins(.top, headerTopPadding) - .contentMargins(.bottom, ScreenLayout.bottomPaddingWithSafeArea) - .padding(.horizontal, 16) - .scrollDismissesKeyboard(.interactively) - .refreshable { - do { - try await wallet.sync() - try await activity.syncLdkNodePayments() - } catch { - app.toast(error) - } - } - .transition(.move(edge: .leading).combined(with: .opacity)) - - VStack(spacing: 0) { - NavigationBar(title: t("wallet__activity")) - .padding(.bottom, 16) + InsetHeaderScrollView( + header: { + VStack(spacing: 0) { + NavigationBar(title: t("wallet__activity")) + .padding(.bottom, 16) - ActivityListFilter(viewModel: activity) - .padding(.bottom, 16) + ActivityListFilter(viewModel: activity) + .padding(.bottom, 16) - SegmentedControl(selectedTab: $activity.selectedTab, tabs: ActivityTab.allCases) - } - .background( - GeometryReader { proxy in - Color.clear.preference(key: ActivityHeaderHeightPreferenceKey.self, value: proxy.size.height) - } - ) - .onPreferenceChange(ActivityHeaderHeightPreferenceKey.self) { height in - headerContentHeight = height - } - .frame(maxWidth: .infinity, alignment: .top) - .padding(.horizontal, 16) - .background( - ZStack { - BlurView() - LinearGradient( - gradient: Gradient(stops: [ - .init(color: Color.black, location: 0.0), - .init(color: Color.black, location: 0.4), - .init(color: Color.black.opacity(0), location: 1.0), - ]), - startPoint: .top, - endPoint: .bottom + SegmentedControl(selectedTab: $activity.selectedTab, tabs: ActivityTab.allCases) + } + .frame(maxWidth: .infinity, alignment: .top) + .padding(.horizontal, 16) + .background( + ZStack { + BlurView() + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black, location: 0.0), + .init(color: Color.black, location: 0.4), + .init(color: Color.black.opacity(0), location: 1.0), + ]), + startPoint: .top, + endPoint: .bottom + ) + } + .ignoresSafeArea(edges: .top) ) - } - .ignoresSafeArea(edges: .top) + .compositingGroup() + .shadow(color: Color.black.opacity(0.5), radius: 8, x: 0, y: 20) + }, + content: { + ActivityList(viewType: .all) + .padding(.horizontal, 16) + .swipeSegmentedTabs(selection: $activity.selectedTab) + }, + scrollModifier: ActivityScrollModifier( + activity: activity, + app: app, + wallet: wallet + ) ) - .compositingGroup() - .shadow(color: Color.black.opacity(0.5), radius: 8, x: 0, y: 20) + .transition(.move(edge: .leading).combined(with: .opacity)) // Bottom gradient overlay VStack { @@ -118,11 +71,24 @@ struct AllActivityView: View { } } -private struct ActivityHeaderHeightPreferenceKey: PreferenceKey { - static let defaultValue: CGFloat = 116 +private struct ActivityScrollModifier: ViewModifier { + let activity: ActivityListViewModel + let app: AppViewModel + let wallet: WalletViewModel - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() + func body(content: Content) -> some View { + content + .contentMargins(.top, 16) + .contentMargins(.bottom, ScreenLayout.bottomPaddingWithSafeArea) + .scrollDismissesKeyboard(.interactively) + .refreshable { + do { + try await wallet.sync() + try await activity.syncLdkNodePayments() + } catch { + app.toast(error) + } + } } } @@ -130,6 +96,8 @@ private struct ActivityHeaderHeightPreferenceKey: PreferenceKey { NavigationStack { AllActivityView() .environmentObject(ActivityListViewModel()) + .environmentObject(AppViewModel()) + .environmentObject(WalletViewModel()) .preferredColorScheme(.dark) } } diff --git a/changelog.d/next/550.changed.md b/changelog.d/next/550.changed.md new file mode 100644 index 000000000..2a446847f --- /dev/null +++ b/changelog.d/next/550.changed.md @@ -0,0 +1 @@ +Added swipe gestures on the tabs in settings and shop and polished the header areas on those screens From 5a34d2a3683d685fec50f1dff480772c8db11d01 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Thu, 14 May 2026 11:40:57 +0200 Subject: [PATCH 2/2] fix(shop): disable scroll on .map tab --- Bitkit/Views/Shop/ShopDiscover.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Bitkit/Views/Shop/ShopDiscover.swift b/Bitkit/Views/Shop/ShopDiscover.swift index 868ecae00..4ecc1ec4a 100644 --- a/Bitkit/Views/Shop/ShopDiscover.swift +++ b/Bitkit/Views/Shop/ShopDiscover.swift @@ -136,7 +136,8 @@ struct ShopDiscover: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .swipeSegmentedTabs(selection: $selectedTab) - } + }, + scrollModifier: ShopDiscoverScrollModifier(disableOuterScroll: selectedTab == .map) ) .navigationBarHidden(true) .offlineOverlay(title: navTitle) @@ -189,6 +190,15 @@ struct ShopDiscover: View { } } +/// Disables the outer `InsetHeaderScrollView` vertical scroll on the map tab so `WKWebView` owns vertical panning. +private struct ShopDiscoverScrollModifier: ViewModifier { + let disableOuterScroll: Bool + + func body(content: Content) -> some View { + content.scrollDisabled(disableOuterScroll) + } +} + // MARK: - Shop Category Row Component struct ShopCategoryRow: View {