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:
@@ -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;
|
||||
|
||||
@@ -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 Jan–Mar, then swipe to land on
|
||||
// Jun–Aug. 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
|
||||
|
||||
Reference in New Issue
Block a user