
Share Card
- 2 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates a SwiftUI share-card system that renders app content like achievements, stats, and quotes as shareable images with ShareLink integration.
About
Generates a share-card system that renders app content (achievements, stats, quotes, milestones) as shareable images using SwiftUI ImageRenderer with optional QR overlays and ShareLink. A developer uses it to let users export app content as social share images.
- Renders content as images via SwiftUI ImageRenderer
- Customizable styles, QR overlays, and native ShareLink
Share Card by the numbers
- 2 all-time installs (skills.sh)
- Ranked #888 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill share-cardAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates a SwiftUI share-card system that renders app content like achievements, stats, and quotes as shareable images with ShareLink integration.
Files
Share Card Generator
Generate a production share card system that renders app content (achievements, statistics, quotes, milestones) as shareable images with SwiftUI ImageRenderer, customizable styles, optional QR code overlays, and native ShareLink integration.
When This Skill Activates
Use this skill when the user:
- Asks to "generate share cards" or "create shareable images"
- Wants to "share achievements as images" or "share stats to social media"
- Mentions "social media cards" or "shareable content cards"
- Asks about "exporting content as images" or "screenshot sharing"
- Wants "branded share images" or "share card templates"
- Asks to "add ShareLink with image" or "share rendered SwiftUI view"
- Wants "QR code on share image" or "deep link in share card"
Pre-Generation Checks
1. Project Context Detection
- [ ] Check Swift version (requires Swift 5.9+)
- [ ] Check deployment target (iOS 16+ / macOS 13+ minimum for
ImageRenderer) - [ ] Check for @Observable support (iOS 17+ / macOS 14+)
- [ ] Identify source file locations
2. Conflict Detection
Search for existing share/export code:
Glob: **/*ShareCard*.swift, **/*ShareImage*.swift, **/*SocialCard*.swift
Grep: "ImageRenderer" or "ShareLink" or "UIGraphicsImageRenderer" or "share.*image"If existing share infrastructure found:
- Ask if user wants to replace or extend it
- If extending, integrate with existing models and styles
3. Platform Detection
Determine if generating for iOS (UIImage) or macOS (NSImage) or both (cross-platform PlatformImage typealias).
Configuration Questions
Ask user via AskUserQuestion:
1. Card style?
- Minimal (clean, text-focused, light/dark adaptive)
- Branded (app logo, brand colors, custom typography)
- Statistics (data-heavy with charts/numbers emphasis)
2. Share destinations?
- Social media (Instagram, Twitter/X — optimized aspect ratios)
- Messages / General sharing (flexible sizing)
- Both — recommended
3. Card size preset?
- Square (1080x1080 — Instagram, general purpose)
- Story (1080x1920 — Instagram Stories, TikTok)
- Wide (1200x630 — Twitter/X, Open Graph)
- Custom (user-specified dimensions)
4. Include QR code?
- Yes (deep link or App Store URL embedded in card corner)
- No (cleaner card, no link)
5. Content types? (multi-select)
- Achievements / Milestones
- Statistics / Progress
- Quotes / Text content
- Custom (user defines their own content model)
Generation Process
Step 1: Read Templates
Read templates.md for production Swift code.
Step 2: Create Core Files
Generate these files: 1. ShareCardContent.swift — Protocol and concrete content types (achievement, statistics, quote) 2. ShareCardStyle.swift — Enum with predefined styles, colors, fonts, layout configuration 3. ShareCardRenderer.swift — @MainActor renderer using ImageRenderer to convert SwiftUI views to images 4. ShareCardView.swift — The SwiftUI view that composes the card layout (background, content, branding)
Step 3: Create UI Files
5. ShareCardSheet.swift — Complete sharing sheet with style picker, live preview, and ShareLink
Step 4: Create Optional Files
Based on configuration:
QRCodeOverlay.swift— If QR code selected (CoreImage-based QR generator)ShareCardPreview.swift— SwiftUI Preview helpers for design iteration
Step 5: Determine File Location
Check project structure:
- If
Sources/exists ->Sources/ShareCard/ - If
App/exists ->App/ShareCard/ - Otherwise ->
ShareCard/
Output Format
After generation, provide:
Files Created
ShareCard/
├── ShareCardContent.swift # Protocol + concrete content types
├── ShareCardStyle.swift # Style enum with colors, fonts, layout
├── ShareCardRenderer.swift # ImageRenderer-based rendering
├── ShareCardView.swift # Card layout SwiftUI view
├── ShareCardSheet.swift # Share sheet with preview + ShareLink
├── QRCodeOverlay.swift # CoreImage QR code (optional)
└── ShareCardPreview.swift # Preview helpers (optional)Integration Steps
Share an achievement:
struct AchievementDetailView: View {
let achievement: Achievement
@State private var showShareCard = false
var body: some View {
VStack {
// ... achievement detail content ...
Button("Share Achievement") {
showShareCard = true
}
}
.sheet(isPresented: $showShareCard) {
ShareCardSheet(
content: AchievementCardContent(
title: achievement.title,
subtitle: achievement.description,
metric: "\(achievement.points) pts",
iconName: achievement.iconName,
brandName: "MyApp"
)
)
}
}
}Share statistics:
ShareCardSheet(
content: StatisticsCardContent(
title: "Weekly Progress",
stats: [
StatItem(label: "Steps", value: "52,340"),
StatItem(label: "Calories", value: "3,200"),
StatItem(label: "Distance", value: "28.5 km"),
],
brandName: "FitTracker"
)
)Share a quote:
ShareCardSheet(
content: QuoteCardContent(
quote: "The only way to do great work is to love what you do.",
attribution: "Steve Jobs",
brandName: "DailyQuotes"
)
)Inline ShareLink (without sheet):
struct QuickShareButton: View {
let content: any ShareCardContent
@State private var renderedImage: PlatformImage?
var body: some View {
Group {
if let image = renderedImage {
ShareLink(
item: Image(platformImage: image),
preview: SharePreview(content.title, image: Image(platformImage: image))
)
} else {
ProgressView()
}
}
.task {
let renderer = ShareCardRenderer()
renderedImage = await renderer.render(content: content, style: .branded)
}
}
}Testing
@Test
func rendersAchievementCard() async throws {
let content = AchievementCardContent(
title: "First Run",
subtitle: "Completed your first 5K run",
metric: "500 pts",
iconName: "figure.run",
brandName: "FitApp"
)
let renderer = ShareCardRenderer()
let image = await renderer.render(content: content, style: .branded)
#expect(image != nil)
#expect(image!.size.width > 0)
#expect(image!.size.height > 0)
}
@Test
func allStylesRenderSuccessfully() async throws {
let content = QuoteCardContent(
quote: "Test quote",
attribution: "Author",
brandName: "App"
)
let renderer = ShareCardRenderer()
for style in ShareCardStyle.allCases {
let image = await renderer.render(content: content, style: style)
#expect(image != nil, "Style \(style) failed to render")
}
}
@Test
func qrCodeGeneratesValidImage() throws {
let image = QRCodeGenerator.generate(
from: "https://example.com/share/123",
size: CGSize(width: 100, height: 100)
)
#expect(image != nil)
}Common Patterns
Achievement Card
Best for: gamification, milestones, unlockables.
- Large icon or badge at top
- Achievement title prominently displayed
- Metric or point value highlighted
- App branding at bottom
Statistics Card
Best for: fitness, finance, productivity apps.
- Grid or list of stat items with labels and values
- Optional trend indicators (up/down arrows)
- Date range context
- App branding at bottom
Quote Card
Best for: reading, journaling, social apps.
- Large quotation marks or decorative element
- Quote text centered with elegant typography
- Attribution below quote
- Minimal branding
Gotchas
Image Quality for Different Platforms
- Instagram: JPEG works, but use high quality (0.9+) to avoid artifacts on text
- Twitter/X: PNG preserves text sharpness but larger file size
- Use
@2xscale factor inImageRendererfor Retina-quality output - Always render at the display scale:
renderer.scale = UIScreen.main.scale(iOS) orNSScreen.main?.backingScaleFactor(macOS)
Memory for Large Renders
- A 1080x1920 card at
@3xscale = 3240x5760 pixels = ~75 MB in memory - Render at
@2xmaximum for share cards — social platforms compress anyway - Release the rendered image after sharing completes
- Use
autoreleasepoolif generating multiple cards in sequence
ImageRenderer Limitations
ImageRendereris@MainActor— must be called from the main thread- Minimum iOS 16 / macOS 13
- Does not render
Map,Camera, orWebViewcontent - Animations are not captured — renders a single frame
ScrollViewcontent beyond visible bounds is not rendered
Cross-Platform Rendering
- On macOS,
ImageRendererproducesNSImageviansImageproperty - On iOS, use
uiImageproperty - Use the
PlatformImagetypealias pattern for shared code UIGraphicsImageRendereris UIKit-only — preferImageRendererfor SwiftUI cross-platform code
ShareLink Gotchas
ShareLinkrequires the shared item to conform toTransferableImageconforms toTransferableon iOS 16+ / macOS 13+- For custom share items, implement
TransferablewithDataRepresentation ShareLinkrenders as a button — style it to match your UI
References
- templates.md — All production Swift templates
- Related:
generators/image-loading— Cache rendered share card images - Related:
ios/deep-linking— Generate deep links for QR codes
Share Card Code Templates
Production-ready Swift templates for shareable card image generation. All code targets iOS 16+ / macOS 13+ (iOS 17+ / macOS 14+ for @Observable) and uses modern Swift concurrency.
Platform Compatibility
#if canImport(UIKit)
import UIKit
typealias PlatformImage = UIImage
#elseif canImport(AppKit)
import AppKit
typealias PlatformImage = NSImage
#endif
extension Image {
init(platformImage: PlatformImage) {
#if canImport(UIKit)
self.init(uiImage: platformImage)
#elseif canImport(AppKit)
self.init(nsImage: platformImage)
#endif
}
}ShareCardContent.swift
import Foundation
/// Protocol defining content that can be rendered as a share card.
///
/// Conform to this protocol to create custom card content types.
/// The renderer uses these properties to lay out the card.
protocol ShareCardContent: Sendable {
/// Primary title displayed prominently on the card.
var title: String { get }
/// Optional subtitle or description below the title.
var subtitle: String? { get }
/// App or brand name shown in the card footer.
var brandName: String { get }
/// Optional SF Symbol name for a decorative icon.
var iconName: String? { get }
}
// MARK: - Achievement Card Content
/// Content for achievement or milestone share cards.
///
/// Usage:
/// ```swift
/// let content = AchievementCardContent(
/// title: "First 5K Run",
/// subtitle: "Completed on March 15, 2025",
/// metric: "500 pts",
/// iconName: "figure.run",
/// brandName: "FitTracker"
/// )
/// ```
struct AchievementCardContent: ShareCardContent, Sendable {
let title: String
let subtitle: String?
let metric: String
let iconName: String?
let brandName: String
init(
title: String,
subtitle: String? = nil,
metric: String,
iconName: String? = nil,
brandName: String
) {
self.title = title
self.subtitle = subtitle
self.metric = metric
self.iconName = iconName
self.brandName = brandName
}
}
// MARK: - Statistics Card Content
/// A single statistic item displayed in a statistics card.
struct StatItem: Sendable, Identifiable {
let id = UUID()
let label: String
let value: String
let trend: Trend?
enum Trend: Sendable {
case up, down, neutral
}
init(label: String, value: String, trend: Trend? = nil) {
self.label = label
self.value = value
self.trend = trend
}
}
/// Content for statistics or progress share cards.
///
/// Usage:
/// ```swift
/// let content = StatisticsCardContent(
/// title: "Weekly Progress",
/// stats: [
/// StatItem(label: "Steps", value: "52,340", trend: .up),
/// StatItem(label: "Calories", value: "3,200", trend: .neutral),
/// ],
/// dateRange: "Mar 10 - Mar 16",
/// brandName: "FitTracker"
/// )
/// ```
struct StatisticsCardContent: ShareCardContent, Sendable {
let title: String
let subtitle: String?
let stats: [StatItem]
let dateRange: String?
let brandName: String
var iconName: String? { nil }
init(
title: String,
subtitle: String? = nil,
stats: [StatItem],
dateRange: String? = nil,
brandName: String
) {
self.title = title
self.subtitle = subtitle
self.stats = stats
self.dateRange = dateRange
self.brandName = brandName
}
}
// MARK: - Quote Card Content
/// Content for quote or text share cards.
///
/// Usage:
/// ```swift
/// let content = QuoteCardContent(
/// quote: "The only way to do great work is to love what you do.",
/// attribution: "Steve Jobs",
/// brandName: "DailyQuotes"
/// )
/// ```
struct QuoteCardContent: ShareCardContent, Sendable {
let quote: String
let attribution: String?
let brandName: String
var title: String { quote }
var subtitle: String? { attribution }
var iconName: String? { nil }
init(
quote: String,
attribution: String? = nil,
brandName: String
) {
self.quote = quote
self.attribution = attribution
self.brandName = brandName
}
}ShareCardStyle.swift
import SwiftUI
/// Predefined visual styles for share cards.
///
/// Each style defines colors, fonts, spacing, and layout
/// parameters used by the card renderer.
enum ShareCardStyle: String, CaseIterable, Identifiable, Sendable {
case minimal
case branded
case statistics
var id: String { rawValue }
var displayName: String {
switch self {
case .minimal: return "Minimal"
case .branded: return "Branded"
case .statistics: return "Statistics"
}
}
// MARK: - Colors
var backgroundColor: Color {
switch self {
case .minimal: return .white
case .branded: return Color(red: 0.1, green: 0.1, blue: 0.15)
case .statistics: return Color(red: 0.05, green: 0.05, blue: 0.1)
}
}
var primaryTextColor: Color {
switch self {
case .minimal: return .black
case .branded: return .white
case .statistics: return .white
}
}
var secondaryTextColor: Color {
switch self {
case .minimal: return Color(white: 0.4)
case .branded: return Color(white: 0.7)
case .statistics: return Color(white: 0.6)
}
}
var accentColor: Color {
switch self {
case .minimal: return .blue
case .branded: return Color(red: 0.4, green: 0.7, blue: 1.0)
case .statistics: return Color(red: 0.3, green: 0.85, blue: 0.6)
}
}
// MARK: - Typography
var titleFont: Font {
switch self {
case .minimal: return .system(size: 28, weight: .semibold, design: .default)
case .branded: return .system(size: 32, weight: .bold, design: .rounded)
case .statistics: return .system(size: 24, weight: .bold, design: .monospaced)
}
}
var subtitleFont: Font {
switch self {
case .minimal: return .system(size: 16, weight: .regular, design: .default)
case .branded: return .system(size: 18, weight: .medium, design: .rounded)
case .statistics: return .system(size: 14, weight: .regular, design: .monospaced)
}
}
var metricFont: Font {
switch self {
case .minimal: return .system(size: 48, weight: .bold, design: .rounded)
case .branded: return .system(size: 56, weight: .heavy, design: .rounded)
case .statistics: return .system(size: 44, weight: .bold, design: .monospaced)
}
}
var brandFont: Font {
.system(size: 14, weight: .medium, design: .default)
}
// MARK: - Layout
var cornerRadius: CGFloat {
switch self {
case .minimal: return 16
case .branded: return 24
case .statistics: return 20
}
}
var contentPadding: EdgeInsets {
switch self {
case .minimal: return EdgeInsets(top: 40, leading: 32, bottom: 40, trailing: 32)
case .branded: return EdgeInsets(top: 48, leading: 36, bottom: 48, trailing: 36)
case .statistics: return EdgeInsets(top: 36, leading: 28, bottom: 36, trailing: 28)
}
}
}
// MARK: - Card Size Presets
/// Predefined card sizes optimized for different sharing platforms.
enum ShareCardSize: String, CaseIterable, Identifiable, Sendable {
case square // 1080x1080 — Instagram, general
case story // 1080x1920 — Instagram Stories, TikTok
case wide // 1200x630 — Twitter/X, Open Graph
case compact // 800x600 — Messages, lightweight
var id: String { rawValue }
var size: CGSize {
switch self {
case .square: return CGSize(width: 1080, height: 1080)
case .story: return CGSize(width: 1080, height: 1920)
case .wide: return CGSize(width: 1200, height: 630)
case .compact: return CGSize(width: 800, height: 600)
}
}
var displayName: String {
switch self {
case .square: return "Square (1080x1080)"
case .story: return "Story (1080x1920)"
case .wide: return "Wide (1200x630)"
case .compact: return "Compact (800x600)"
}
}
/// Aspect ratio (width / height) for preview scaling.
var aspectRatio: CGFloat {
size.width / size.height
}
}ShareCardRenderer.swift
import SwiftUI
/// Renders share card SwiftUI views into raster images using `ImageRenderer`.
///
/// Must be called from the main actor since `ImageRenderer` requires it.
///
/// Usage:
/// ```swift
/// let renderer = ShareCardRenderer()
/// let image = await renderer.render(
/// content: achievementContent,
/// style: .branded,
/// size: .square
/// )
/// ```
@MainActor
struct ShareCardRenderer {
/// Render a share card to a platform image.
///
/// - Parameters:
/// - content: The card content to render.
/// - style: The visual style to apply.
/// - size: The card size preset (default: square).
/// - qrCodeURL: Optional URL to encode as a QR code overlay.
/// - scale: The render scale factor (default: 2.0 for Retina).
/// - Returns: A rendered platform image, or nil if rendering fails.
func render(
content: any ShareCardContent,
style: ShareCardStyle,
size: ShareCardSize = .square,
qrCodeURL: URL? = nil,
scale: CGFloat = 2.0
) -> PlatformImage? {
let cardView = ShareCardView(
content: content,
style: style,
cardSize: size,
qrCodeURL: qrCodeURL
)
.frame(width: size.size.width, height: size.size.height)
let renderer = ImageRenderer(content: cardView)
renderer.scale = scale
// Propose the exact card size so the layout doesn't compress
renderer.proposedSize = ProposedViewSize(size.size)
#if canImport(UIKit)
return renderer.uiImage
#elseif canImport(AppKit)
return renderer.nsImage
#endif
}
/// Render a share card and return PNG data suitable for sharing.
///
/// PNG preserves text sharpness, which is ideal for cards with typography.
func renderPNGData(
content: any ShareCardContent,
style: ShareCardStyle,
size: ShareCardSize = .square,
qrCodeURL: URL? = nil,
scale: CGFloat = 2.0
) -> Data? {
guard let image = render(
content: content,
style: style,
size: size,
qrCodeURL: qrCodeURL,
scale: scale
) else { return nil }
#if canImport(UIKit)
return image.pngData()
#elseif canImport(AppKit)
guard let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData) else { return nil }
return bitmap.representation(using: .png, properties: [:])
#endif
}
/// Render a share card and return JPEG data with configurable quality.
///
/// JPEG is smaller but may show compression artifacts on text.
/// Use quality >= 0.9 for text-heavy cards.
func renderJPEGData(
content: any ShareCardContent,
style: ShareCardStyle,
size: ShareCardSize = .square,
qrCodeURL: URL? = nil,
scale: CGFloat = 2.0,
quality: CGFloat = 0.92
) -> Data? {
guard let image = render(
content: content,
style: style,
size: size,
qrCodeURL: qrCodeURL,
scale: scale
) else { return nil }
#if canImport(UIKit)
return image.jpegData(compressionQuality: quality)
#elseif canImport(AppKit)
guard let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData) else { return nil }
return bitmap.representation(
using: .jpeg,
properties: [.compressionFactor: quality]
)
#endif
}
}ShareCardView.swift
import SwiftUI
/// The SwiftUI view that composes the card layout.
///
/// This view is rendered to an image by `ShareCardRenderer`.
/// It should NOT be used directly in the app UI — use `ShareCardSheet` instead.
struct ShareCardView: View {
let content: any ShareCardContent
let style: ShareCardStyle
let cardSize: ShareCardSize
let qrCodeURL: URL?
init(
content: any ShareCardContent,
style: ShareCardStyle,
cardSize: ShareCardSize = .square,
qrCodeURL: URL? = nil
) {
self.content = content
self.style = style
self.cardSize = cardSize
self.qrCodeURL = qrCodeURL
}
var body: some View {
ZStack {
// Background
RoundedRectangle(cornerRadius: style.cornerRadius)
.fill(style.backgroundColor)
// Content
VStack(spacing: 0) {
Spacer(minLength: style.contentPadding.top)
contentBody
Spacer(minLength: 16)
footerView
.padding(.bottom, style.contentPadding.bottom)
}
.padding(.horizontal, style.contentPadding.leading)
// QR Code overlay (bottom-right corner)
if let qrCodeURL {
qrOverlay(url: qrCodeURL)
}
}
.clipShape(RoundedRectangle(cornerRadius: style.cornerRadius))
}
// MARK: - Content Body
@ViewBuilder
private var contentBody: some View {
if let achievement = content as? AchievementCardContent {
AchievementCardBody(content: achievement, style: style)
} else if let statistics = content as? StatisticsCardContent {
StatisticsCardBody(content: statistics, style: style)
} else if let quote = content as? QuoteCardContent {
QuoteCardBody(content: quote, style: style)
} else {
GenericCardBody(content: content, style: style)
}
}
// MARK: - Footer
private var footerView: some View {
HStack {
Text(content.brandName)
.font(style.brandFont)
.foregroundStyle(style.secondaryTextColor)
Spacer()
if qrCodeURL == nil {
// Show a subtle divider dot when there's no QR code
Circle()
.fill(style.accentColor.opacity(0.5))
.frame(width: 6, height: 6)
}
}
}
// MARK: - QR Code Overlay
private func qrOverlay(url: URL) -> some View {
VStack {
Spacer()
HStack {
Spacer()
QRCodeOverlay(url: url, style: style)
.frame(width: cardSize.size.width * 0.12,
height: cardSize.size.width * 0.12)
.padding(style.contentPadding.trailing)
.padding(.bottom, style.contentPadding.bottom)
}
}
}
}
// MARK: - Achievement Card Body
/// Renders achievement/milestone content within a share card.
private struct AchievementCardBody: View {
let content: AchievementCardContent
let style: ShareCardStyle
var body: some View {
VStack(spacing: 20) {
// Icon
if let iconName = content.iconName {
Image(systemName: iconName)
.font(.system(size: 56))
.foregroundStyle(style.accentColor)
.padding(.bottom, 8)
}
// Metric
Text(content.metric)
.font(style.metricFont)
.foregroundStyle(style.accentColor)
// Title
Text(content.title)
.font(style.titleFont)
.foregroundStyle(style.primaryTextColor)
.multilineTextAlignment(.center)
// Subtitle
if let subtitle = content.subtitle {
Text(subtitle)
.font(style.subtitleFont)
.foregroundStyle(style.secondaryTextColor)
.multilineTextAlignment(.center)
}
}
}
}
// MARK: - Statistics Card Body
/// Renders statistics/progress content within a share card.
private struct StatisticsCardBody: View {
let content: StatisticsCardContent
let style: ShareCardStyle
var body: some View {
VStack(spacing: 24) {
// Title
Text(content.title)
.font(style.titleFont)
.foregroundStyle(style.primaryTextColor)
// Date range
if let dateRange = content.dateRange {
Text(dateRange)
.font(style.subtitleFont)
.foregroundStyle(style.secondaryTextColor)
}
// Stats grid
let columns = Array(
repeating: GridItem(.flexible(), spacing: 16),
count: min(content.stats.count, 3)
)
LazyVGrid(columns: columns, spacing: 20) {
ForEach(content.stats) { stat in
StatItemView(stat: stat, style: style)
}
}
}
}
}
/// Renders a single statistic item with label, value, and optional trend.
private struct StatItemView: View {
let stat: StatItem
let style: ShareCardStyle
var body: some View {
VStack(spacing: 8) {
HStack(spacing: 4) {
Text(stat.value)
.font(style.metricFont)
.foregroundStyle(style.primaryTextColor)
if let trend = stat.trend {
trendIndicator(trend)
}
}
Text(stat.label)
.font(style.subtitleFont)
.foregroundStyle(style.secondaryTextColor)
}
}
@ViewBuilder
private func trendIndicator(_ trend: StatItem.Trend) -> some View {
switch trend {
case .up:
Image(systemName: "arrow.up.right")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(.green)
case .down:
Image(systemName: "arrow.down.right")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(.red)
case .neutral:
Image(systemName: "arrow.right")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(style.secondaryTextColor)
}
}
}
// MARK: - Quote Card Body
/// Renders quote/text content within a share card.
private struct QuoteCardBody: View {
let content: QuoteCardContent
let style: ShareCardStyle
var body: some View {
VStack(spacing: 24) {
// Decorative quotation mark
Text("\u{201C}")
.font(.system(size: 72, weight: .bold))
.foregroundStyle(style.accentColor.opacity(0.3))
.frame(height: 50)
// Quote text
Text(content.quote)
.font(style.titleFont)
.foregroundStyle(style.primaryTextColor)
.multilineTextAlignment(.center)
.lineSpacing(6)
// Attribution
if let attribution = content.attribution {
HStack(spacing: 8) {
Rectangle()
.fill(style.accentColor)
.frame(width: 24, height: 2)
Text(attribution)
.font(style.subtitleFont)
.foregroundStyle(style.secondaryTextColor)
}
}
}
}
}
// MARK: - Generic Card Body
/// Fallback renderer for custom `ShareCardContent` conformances.
private struct GenericCardBody: View {
let content: any ShareCardContent
let style: ShareCardStyle
var body: some View {
VStack(spacing: 20) {
if let iconName = content.iconName {
Image(systemName: iconName)
.font(.system(size: 48))
.foregroundStyle(style.accentColor)
}
Text(content.title)
.font(style.titleFont)
.foregroundStyle(style.primaryTextColor)
.multilineTextAlignment(.center)
if let subtitle = content.subtitle {
Text(subtitle)
.font(style.subtitleFont)
.foregroundStyle(style.secondaryTextColor)
.multilineTextAlignment(.center)
}
}
}
}ShareCardSheet.swift
import SwiftUI
/// A complete sharing sheet with live card preview, style picker, and ShareLink.
///
/// Presents a preview of the share card with options to change style and size,
/// then share via the system share sheet.
///
/// Usage:
/// ```swift
/// .sheet(isPresented: $showShareCard) {
/// ShareCardSheet(
/// content: AchievementCardContent(
/// title: "First Run",
/// metric: "500 pts",
/// brandName: "FitApp"
/// )
/// )
/// }
/// ```
struct ShareCardSheet: View {
let content: any ShareCardContent
let qrCodeURL: URL?
@State private var selectedStyle: ShareCardStyle = .branded
@State private var selectedSize: ShareCardSize = .square
@State private var renderedImage: PlatformImage?
@State private var isRendering = false
@Environment(\.dismiss) private var dismiss
init(content: any ShareCardContent, qrCodeURL: URL? = nil) {
self.content = content
self.qrCodeURL = qrCodeURL
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
cardPreview
stylePickerSection
sizePickerSection
shareSection
}
.padding()
}
.navigationTitle("Share Card")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
.task(id: StyleSizeKey(style: selectedStyle, size: selectedSize)) {
await renderCard()
}
}
}
// MARK: - Card Preview
private var cardPreview: some View {
Group {
if let renderedImage {
Image(platformImage: renderedImage)
.resizable()
.aspectRatio(selectedSize.aspectRatio, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.15), radius: 12, y: 4)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.1))
.aspectRatio(selectedSize.aspectRatio, contentMode: .fit)
.overlay(ProgressView())
}
}
.padding(.horizontal, 8)
}
// MARK: - Style Picker
private var stylePickerSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Style")
.font(.headline)
Picker("Style", selection: $selectedStyle) {
ForEach(ShareCardStyle.allCases) { style in
Text(style.displayName).tag(style)
}
}
.pickerStyle(.segmented)
}
}
// MARK: - Size Picker
private var sizePickerSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Size")
.font(.headline)
Picker("Size", selection: $selectedSize) {
ForEach(ShareCardSize.allCases) { size in
Text(size.displayName).tag(size)
}
}
#if os(iOS)
.pickerStyle(.menu)
#else
.pickerStyle(.automatic)
#endif
}
}
// MARK: - Share Section
private var shareSection: some View {
VStack(spacing: 12) {
if let renderedImage {
ShareLink(
item: Image(platformImage: renderedImage),
preview: SharePreview(
content.title,
image: Image(platformImage: renderedImage)
)
) {
Label("Share", systemImage: "square.and.arrow.up")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
} else {
Button(action: {}) {
Label("Rendering...", systemImage: "hourglass")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.disabled(true)
}
// Save to Photos button (iOS only)
#if canImport(UIKit)
if let renderedImage {
Button {
UIImageWriteToSavedPhotosAlbum(renderedImage, nil, nil, nil)
} label: {
Label("Save to Photos", systemImage: "photo.on.rectangle.angled")
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(.bordered)
.controlSize(.large)
}
#endif
}
}
// MARK: - Rendering
@MainActor
private func renderCard() async {
isRendering = true
let renderer = ShareCardRenderer()
renderedImage = renderer.render(
content: content,
style: selectedStyle,
size: selectedSize,
qrCodeURL: qrCodeURL
)
isRendering = false
}
}
/// Key for tracking style + size changes to trigger re-render.
private struct StyleSizeKey: Equatable {
let style: ShareCardStyle
let size: ShareCardSize
}QRCodeOverlay.swift
import SwiftUI
import CoreImage
import CoreImage.CIFilterBuiltins
/// Generates and displays a QR code overlay for embedding deep links in share cards.
///
/// Uses CoreImage's built-in QR code generator for reliable encoding.
///
/// Usage:
/// ```swift
/// QRCodeOverlay(
/// url: URL(string: "https://myapp.com/share/123")!,
/// style: .branded
/// )
/// .frame(width: 120, height: 120)
/// ```
struct QRCodeOverlay: View {
let url: URL
let style: ShareCardStyle
var body: some View {
ZStack {
// Background for contrast
RoundedRectangle(cornerRadius: 8)
.fill(.white)
.padding(-4)
if let qrImage = QRCodeGenerator.generate(
from: url.absoluteString,
size: CGSize(width: 200, height: 200)
) {
Image(platformImage: qrImage)
.interpolation(.none)
.resizable()
.aspectRatio(1, contentMode: .fit)
}
}
}
}
// MARK: - QR Code Generator
/// CoreImage-based QR code generator.
///
/// Generates high-resolution QR code images suitable for embedding in share cards.
/// The output is a crisp, black-on-transparent QR code.
enum QRCodeGenerator {
private static let context = CIContext()
/// Generate a QR code image from a string.
///
/// - Parameters:
/// - string: The string to encode (typically a URL).
/// - size: The desired output size in points.
/// - Returns: A platform image containing the QR code, or nil if generation fails.
static func generate(from string: String, size: CGSize) -> PlatformImage? {
guard let data = string.data(using: .utf8) else { return nil }
let filter = CIFilter.qrCodeGenerator()
filter.message = data
filter.correctionLevel = "M" // Medium error correction
guard let ciImage = filter.outputImage else { return nil }
// Scale the QR code to the requested size
let scaleX = size.width / ciImage.extent.width
let scaleY = size.height / ciImage.extent.height
let scaledImage = ciImage.transformed(
by: CGAffineTransform(scaleX: scaleX, y: scaleY)
)
guard let cgImage = context.createCGImage(
scaledImage,
from: scaledImage.extent
) else { return nil }
#if canImport(UIKit)
return UIImage(cgImage: cgImage)
#elseif canImport(AppKit)
return NSImage(
cgImage: cgImage,
size: NSSize(width: size.width, height: size.height)
)
#endif
}
}