Files
InvestmentTrackerApp/PortfolioJournal/Views/Components/HiddenBalance.swift
T
alexandrev-tibco 326be6db04 1.6.0 #10 ocultar saldos + Face ID · #11 widget interactivo + Siri
#10 Ocultar saldos: BalancePrivacyManager (balancesHidden + requireBiometricToReveal),
modifier .hiddenBalance() (blur + máscara de bullets, ancho estable), botón ojo en Dashboard,
revelar con Face ID vía AppLockService, sección Privacidad en Settings. Aplicado a total
portfolio, forecast, sources, goals, monthly summary.

#11 Siri/App Intents: NetWorthIntent ('¿cuál es mi patrimonio?' → 'Tu patrimonio es X'),
StreakIntent, 5 AppShortcuts con frases. Widget interactivo iOS 17: botón Refresh
(Button(intent:) → reloadAllTimelines, in-process) en medium/large, gated #available(iOS 17).
Reader read-only del AppGroup. Dialogs Siri refactorizados a String(localized:).

Localización: strings de #10 y de Siri en los 7 idiomas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-25 17:52:09 +02:00

79 lines
2.9 KiB
Swift

import SwiftUI
/// A view modifier that redacts monetary amounts when the "Hide balances"
/// privacy feature is active.
///
/// When `hidden` is true the underlying content is blurred and its value masked
/// with a bullet string, while keeping the original layout footprint so the UI
/// doesn't jump. When false, the real content shows through untouched.
///
/// Usage:
/// ```swift
/// Text(viewModel.formattedTotalValue)
/// .hiddenBalance() // observes BalancePrivacyManager.shared
/// // or explicitly:
/// Text(amount).hiddenBalance(isHidden)
/// ```
struct HiddenBalanceModifier: ViewModifier {
let hidden: Bool
func body(content: Content) -> some View {
content
.opacity(hidden ? 0 : 1)
.overlay {
if hidden {
// Bullet mask sized to the underlying content so layout width
// stays roughly stable and the amount is fully obscured.
GeometryReader { proxy in
Text("••••••")
.redacted(reason: .placeholder)
.blur(radius: 6)
.frame(width: proxy.size.width, height: proxy.size.height)
.allowsHitTesting(false)
}
}
}
.accessibilityLabel(hidden ? Text("Balance hidden") : Text(""))
}
}
/// A self-observing variant of `HiddenBalanceModifier` that subscribes to the
/// shared `BalancePrivacyManager` so any monetary label updates live when the
/// user toggles visibility without threading the flag through view params.
struct ObservingHiddenBalanceModifier: ViewModifier {
@ObservedObject private var privacy = BalancePrivacyManager.shared
func body(content: Content) -> some View {
content.modifier(HiddenBalanceModifier(hidden: privacy.balancesHidden))
}
}
extension View {
/// Redact this monetary label with an explicit hidden flag. Use inside views
/// that already observe `BalancePrivacyManager`.
func hiddenBalance(_ hidden: Bool) -> some View {
modifier(HiddenBalanceModifier(hidden: hidden))
}
/// Redact this monetary label, self-observing the shared privacy manager.
/// Use anywhere without threading the flag through view parameters.
func hiddenBalance() -> some View {
modifier(ObservingHiddenBalanceModifier())
}
}
/// A tap-to-reveal wrapper for the quick eye toggle. Renders an eye / eye.slash
/// button bound to the shared `BalancePrivacyManager`.
struct BalanceVisibilityToggle: View {
@ObservedObject var privacy = BalancePrivacyManager.shared
var body: some View {
Button {
privacy.toggleHidden()
} label: {
Image(systemName: privacy.balancesHidden ? "eye.slash" : "eye")
}
.accessibilityLabel(privacy.balancesHidden ? Text("Show balances") : Text("Hide balances"))
}
}