Quitar swipe entre charts + ventana del brush deslizable (build 80) #31 #32

Feedback TF build 79: fuera el swipe entre gráficas (navegación = chip bar;
el gesto chocaba con scrub/pan/brush). Brush con gesto unificado decidido en
touch-down: cerca de un asa (18pt) redimensiona; en cualquier otro punto
(dentro de la ventana o sobre el historial atenuado) desliza la ventana entera
manteniendo el ancho — seleccionar ene–mar y arrastrar hasta jun–ago. Con
ventanas estrechas las asas solo agarran desde fuera, el interior siempre mueve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
This commit is contained in:
alexandrev-tibco
2026-08-06 21:46:07 +02:00
parent f1aeafccf6
commit cf984dbf09
3 changed files with 69 additions and 77 deletions
+10 -10
View File
@@ -541,7 +541,7 @@
CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournalDebug.entitlements;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets;
DEVELOPMENT_TEAM = 2825Q76T7H;
ENABLE_PREVIEWS = YES;
@@ -582,7 +582,7 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
PROVISIONING_PROFILE_SPECIFIER = "porfoliojournal";
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets;
DEVELOPMENT_TEAM = 2825Q76T7H;
ENABLE_PREVIEWS = YES;
@@ -739,7 +739,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO;
@@ -774,7 +774,7 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
PROVISIONING_PROFILE_SPECIFIER = "Portfolio Journalwidget";
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO;
@@ -805,7 +805,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
@@ -829,7 +829,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
@@ -852,7 +852,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
@@ -875,7 +875,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
@@ -899,7 +899,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = PortfolioJournalQuickUpdate/Info.plist;
@@ -927,7 +927,7 @@
CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 2825Q76T7H;
PROVISIONING_PROFILE_SPECIFIER = "PortfolioJournal QuickUpdate AppStore";
GENERATE_INFOPLIST_FILE = NO;
+56 -38
View File
@@ -145,9 +145,16 @@ struct ChartRangeBrush: View {
@Binding var selection: ClosedRange<Int>
@State private var dragStartIndices: (lo: Int, hi: Int)?
@State private var dragTarget: DragTarget?
@State private var lastTickedIndex: Int = -1
@State private var showingPickers = false
/// What a drag manipulates, decided ONCE at touch-down by where it starts.
/// One unified gesture instead of per-element gestures: with a narrow
/// window (e.g. 2 months) the handles' hit areas used to swallow the whole
/// window and moving it was impossible.
private enum DragTarget { case lowerHandle, upperHandle, moveWindow }
private static let labelFormatter: DateFormatter = {
let f = DateFormatter()
f.locale = .autoupdatingCurrent
@@ -219,7 +226,7 @@ struct ChartRangeBrush: View {
.frame(width: max(0, width - xHi))
.offset(x: xHi)
// Window frame draggable as a whole to slide through history
// Window frame
RoundedRectangle(cornerRadius: 6)
.stroke(Color.appPrimary.opacity(0.7), lineWidth: 1.4)
.background(
@@ -227,15 +234,13 @@ struct ChartRangeBrush: View {
)
.frame(width: max(10, xHi - xLo))
.offset(x: xLo)
.contentShape(Rectangle())
.gesture(moveGesture(width: width))
// Handles
// Handles (visual only hit-testing lives in the unified gesture)
handle(at: xLo)
.gesture(handleGesture(isLower: true, width: width))
handle(at: xHi)
.gesture(handleGesture(isLower: false, width: width))
}
.contentShape(Rectangle())
.gesture(unifiedGesture(width: width))
}
private func handle(at x: CGFloat) -> some View {
@@ -245,9 +250,9 @@ struct ChartRangeBrush: View {
.overlay(
Capsule().stroke(Color(.systemBackground), lineWidth: 1.5)
)
.frame(width: 30, height: 44) // generous hit area
.contentShape(Rectangle())
.offset(x: x - 15)
.frame(height: 44)
.offset(x: x - 2.5)
.allowsHitTesting(false)
}
// MARK: Geometry helpers
@@ -263,22 +268,41 @@ struct ChartRangeBrush: View {
return Int((frac * CGFloat(dates.count - 1)).rounded())
}
// MARK: Gestures
// MARK: Unified gesture
//
// Decided at touch-down: grabbing near a handle resizes that edge; grabbing
// anywhere else (inside the window OR on the dimmed history) slides the
// whole window keeping its width select JanMar, then swipe to land on
// JunAug. Narrow windows stay movable because the edge grip only wins in
// its 18pt zone (from outside when the window is too narrow to share).
private func handleGesture(isLower: Bool, width: CGFloat) -> some Gesture {
private func unifiedGesture(width: CGFloat) -> some Gesture {
DragGesture(minimumDistance: 1)
.onChanged { value in
let (lo, hi) = dragStartIndices ?? clampedSelection
if dragStartIndices == nil { dragStartIndices = (lo, hi) }
let i = index(atX: value.location.x, width: width)
if dragStartIndices == nil {
dragStartIndices = (lo, hi)
dragTarget = target(
forX: value.startLocation.x,
xLo: xPosition(index: lo, width: width),
xHi: xPosition(index: hi, width: width)
)
}
guard let target = dragTarget else { return }
var newLo = lo
var newHi = hi
if isLower {
newLo = min(i, hi)
} else {
newHi = max(i, lo)
switch target {
case .lowerHandle:
newLo = min(index(atX: value.location.x, width: width), hi)
case .upperHandle:
newHi = max(index(atX: value.location.x, width: width), lo)
case .moveWindow:
let span = hi - lo
let deltaIdx = Int((value.translation.width / max(width, 1) * CGFloat(dates.count - 1)).rounded())
newLo = max(0, min(lo + deltaIdx, dates.count - 1 - span))
newHi = newLo + span
}
let ticked = isLower ? newLo : newHi
let ticked = (target == .upperHandle) ? newHi : newLo
if ticked != lastTickedIndex {
lastTickedIndex = ticked
ChartHaptics.tick()
@@ -287,30 +311,24 @@ struct ChartRangeBrush: View {
}
.onEnded { _ in
dragStartIndices = nil
dragTarget = nil
lastTickedIndex = -1
}
}
/// Dragging the window itself slides it through history keeping its width
/// e.g. fix a 1-month window and replay the portfolio month by month.
private func moveGesture(width: CGFloat) -> some Gesture {
DragGesture(minimumDistance: 1)
.onChanged { value in
let (lo, hi) = dragStartIndices ?? clampedSelection
if dragStartIndices == nil { dragStartIndices = (lo, hi) }
let span = hi - lo
let deltaIdx = Int((value.translation.width / max(width, 1) * CGFloat(dates.count - 1)).rounded())
let newLo = max(0, min(lo + deltaIdx, dates.count - 1 - span))
if newLo != lastTickedIndex {
lastTickedIndex = newLo
ChartHaptics.tick()
}
selection = newLo...(newLo + span)
}
.onEnded { _ in
dragStartIndices = nil
lastTickedIndex = -1
}
private func target(forX x: CGFloat, xLo: CGFloat, xHi: CGFloat) -> DragTarget {
let grip: CGFloat = 18
let windowWidth = xHi - xLo
if windowWidth >= grip * 3 {
// Wide window: edges grab from either side of the handle.
if abs(x - xLo) <= grip { return .lowerHandle }
if abs(x - xHi) <= grip { return .upperHandle }
} else {
// Narrow window: the inside belongs to MOVE; edges only from outside.
if x < xLo && xLo - x <= grip { return .lowerHandle }
if x > xHi && x - xHi <= grip { return .upperHandle }
}
return .moveWindow
}
}
@@ -615,16 +615,11 @@ struct ChartsContainerView: View {
if hasActiveFilters {
activeFilterChip
}
// No swipe-between-charts: it collided with every horizontal
// interaction (scrub, pan, brush). The chip bar navigates.
chartContent
// Swipe left/right to move to the adjacent chart type.
// simultaneousGesture so charts with their own scrub drag
// don't swallow the swipe.
.simultaneousGesture(chartSwipeGesture)
.id(viewModel.selectedChartType)
.transition(.asymmetric(
insertion: .move(edge: swipeInsertionEdge).combined(with: .opacity),
removal: .opacity
))
.transition(.opacity)
if viewModel.hiddenHistoryMonths > 0 {
lockedHistoryTeaser
}
@@ -721,32 +716,11 @@ struct ChartsContainerView: View {
}
}
// MARK: - iPhone swipe between charts
/// Chart types in the same grouped order as the title switcher menu.
private var orderedChartTypes: [ChartsViewModel.ChartType] {
Self.chartGroups.flatMap { $0.types }.filter { showForecast || $0 != .prediction }
}
@State private var swipeInsertionEdge: Edge = .trailing
private var chartSwipeGesture: some Gesture {
DragGesture(minimumDistance: 30)
.onEnded { value in
// Horizontal-dominant swipes only, so vertical scroll still works.
guard abs(value.translation.width) > abs(value.translation.height) * 1.5 else { return }
let ordered = orderedChartTypes
guard let idx = ordered.firstIndex(of: viewModel.selectedChartType) else { return }
if value.translation.width < 0, idx < ordered.count - 1 {
swipeInsertionEdge = .trailing
withAnimation(.snappy) { viewModel.selectChart(ordered[idx + 1]) }
} else if value.translation.width > 0, idx > 0 {
swipeInsertionEdge = .leading
withAnimation(.snappy) { viewModel.selectChart(ordered[idx - 1]) }
}
}
}
/// Stocks-style segmented period control above the chart (the chart switcher
/// lives in the title menu, filters in the toolbar), plus the global range
/// brush toggle. The brush lives HERE outside chartContent so its drags