Files
InvestmentTrackerApp/PortfolioJournal/Services/ChartShareService.swift
T
alexandrev-tibco a8a9ad64f3 Xcode 27: compila sin warnings y migra API deprecadas de SwiftUI
- Imports de CoreData que faltaban (MemberImportVisibility) en AccountsView,
  ImportDataView, ChartsContainerView y AddSourceView.
- Aislamiento MainActor: IsolatedDefaultValues en el target de la app,
  LinearGradient @MainActor, WatchSyncMessage nonisolated, closures de
  ReviewPromptService/MonthlyCheckInStore/AppIntents.
- Warnings de valores sin usar (SnapshotGapDetector, DashboardLayoutStore,
  OnboardingView, GoalEditorView, CoreDataStack, SettingsViewModel).
- foregroundColor→foregroundStyle, cornerRadius→clipShape,
  navigationBarLeading/Trailing→topBarLeading/Trailing,
  Task.sleep(nanoseconds:)→sleep(for:), NavigationLink(isActive:)→
  navigationDestination(isPresented:).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 11:44:49 +02:00

187 lines
6.5 KiB
Swift

import Foundation
import SwiftUI
import UIKit
// MARK: - Environment flag for image export
//
// ImageRenderer can't rasterize Metal-backed `.drawingGroup()` layers (they come
// out blank). Chart views check this flag and skip drawingGroup during export.
private struct ChartImageExportKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var chartImageExport: Bool {
get { self[ChartImageExportKey.self] }
set { self[ChartImageExportKey.self] = newValue }
}
}
extension View {
/// GPU-rasterize for scroll performance, except while exporting a share
/// image (ImageRenderer can't rasterize Metal layers they come out blank).
@ViewBuilder
func chartDrawingGroup(disabledForExport isExporting: Bool) -> some View {
if isExporting {
self
} else {
drawingGroup()
}
}
}
// MARK: - Chart share service
//
// Renders the current chart into a branded card (app mark, KPIs, QR to the App
// Store) and presents the system share sheet. Reuses the QR/App Store plumbing
// from GoalShareService.
@MainActor
final class ChartShareService {
static let shared = ChartShareService()
private init() {}
static let appStoreShareURL = URL(string: "https://apps.apple.com/app/portfolio-journal-tracker/id6757678318?ct=chart_share")!
func share<Content: View>(title: String, subtitle: String, stats: [ChartStat], @ViewBuilder chart: () -> Content) {
FirebaseService.shared.logShare(type: "chart")
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let viewController = windowScene.windows.first?.rootViewController else {
return
}
let card = ChartShareCardView(
title: title,
subtitle: subtitle,
stats: stats,
qrCodeImage: GoalShareService.generateQRCode(for: Self.appStoreShareURL, size: 200),
chart: chart()
)
let renderer = ImageRenderer(content: card)
renderer.scale = 3
renderer.proposedSize = ProposedViewSize(width: 720, height: nil)
let shareText = String(format: String(localized: "chart_share_text"), title)
+ "\n" + Self.appStoreShareURL.absoluteString
var items: [Any] = []
if let image = renderer.uiImage {
items.append(image)
}
items.append(shareText)
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
activityVC.excludedActivityTypes = [.addToReadingList, .assignToContact, .openInIBooks]
// iPad: activity sheet requires a popover anchor.
if let popover = activityVC.popoverPresentationController {
popover.sourceView = viewController.view
popover.sourceRect = CGRect(
x: viewController.view.bounds.midX,
y: viewController.view.bounds.midY,
width: 0,
height: 0
)
popover.permittedArrowDirections = []
}
var presenter = viewController
while let presented = presenter.presentedViewController {
presenter = presented
}
presenter.present(activityVC, animated: true)
}
}
// MARK: - Branded card
struct ChartShareCardView<Content: View>: View {
let title: String
let subtitle: String
let stats: [ChartStat]
let qrCodeImage: UIImage?
let chart: Content
private static var dateLabel: String {
Date().formatted(date: .abbreviated, time: .omitted)
}
var body: some View {
VStack(alignment: .leading, spacing: 18) {
// Header: brand + chart title
HStack(spacing: 12) {
if let brandMark = UIImage(named: "BrandMark") {
Image(uiImage: brandMark)
.resizable()
.scaledToFit()
.frame(width: 44, height: 44)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
VStack(alignment: .leading, spacing: 2) {
Text("Portfolio Journal")
.font(.headline)
Text("\(title) · \(Self.dateLabel)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
chart
.environment(\.chartImageExport, true)
if !stats.isEmpty {
HStack(spacing: 10) {
ForEach(stats.indices, id: \.self) { i in
VStack(alignment: .leading, spacing: 3) {
Text(stats[i].label)
.font(.caption2)
.foregroundStyle(.secondary)
Text(stats[i].value)
.font(.subheadline.weight(.semibold))
.foregroundStyle(stats[i].color)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(.systemGray6))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Spacer()
}
}
Divider()
// Footer: tagline + QR to the App Store
HStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text(String(localized: "chart_share_tagline"))
.font(.footnote.weight(.medium))
Text(verbatim: "portfoliojournal.app")
.font(.footnote)
.foregroundStyle(Color.appPrimary)
}
Spacer()
if let qrCodeImage {
VStack(spacing: 4) {
Image(uiImage: qrCodeImage)
.resizable()
.interpolation(.none)
.scaledToFit()
.frame(width: 64, height: 64)
Text(String(localized: "chart_share_scan"))
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
}
.padding(24)
.frame(width: 720)
.background(Color(.systemBackground))
.environment(\.colorScheme, .light)
}
}