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(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: 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) .foregroundColor(.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) .foregroundColor(.secondary) Text(stats[i].value) .font(.subheadline.weight(.semibold)) .foregroundColor(stats[i].color) } .padding(.horizontal, 12) .padding(.vertical, 8) .background(Color(.systemGray6)) .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) .foregroundColor(.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) .foregroundColor(.secondary) } } } } .padding(24) .frame(width: 720) .background(Color(.systemBackground)) .environment(\.colorScheme, .light) } }