Complete redesign of sharing to support: - Trip summary cards with route maps - 4 achievement card types (spotlight, collection, milestone, context) - Stadium progress cards - 8 user-customizable color themes - Instagram Stories as primary target (1080×1920) - Contextual share buttons throughout app Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
17 KiB
Sharing System Overhaul
Design finalized: January 13, 2026
Overview
Complete overhaul of the sharing system to support shareable cards for trip summaries, achievements, and stadium progress. Primary target is Instagram Stories (1080×1920 vertical cards) with user-customizable color themes.
Goals
- Share trip summaries as visual cards (not just deep links)
- Share achievements (single, collection, milestone, contextual)
- Share stadium progress with map visualization
- User-customizable color themes (8 presets)
- Contextual share buttons throughout the app (low friction)
- Instagram Stories as primary destination
Architecture
Protocol-Based Design
All shareable content types conform to a common protocol:
protocol ShareableContent {
var cardType: ShareCardType { get }
func render(theme: ShareTheme) async throws -> UIImage
}
enum ShareCardType {
case tripSummary
case achievementSpotlight
case achievementCollection
case achievementMilestone
case achievementContext
case stadiumProgress
}
Component Hierarchy
ShareableContent (protocol)
├── TripShareContent → TripCardGenerator
├── AchievementShareContent → AchievementCardGenerator
├── ProgressShareContent → ProgressCardGenerator
└── CollectionShareContent → CollectionCardGenerator
Unified Components
ShareTheme- Color preset definitionsShareCardFooter- Reusable SportsTime branding footerShareCardHeader- Sport icon + title treatmentShareService- Instagram URL scheme, image export, fallback share sheet
Theme System
8 Color Presets
| Theme | Background Gradient | Accent | Text | Use Case |
|---|---|---|---|---|
| Dark | #1A1A2E → #16213E | #FF6B35 | White | Default, premium feel |
| Light | #FFFFFF → #F5F5F5 | #FF6B35 | #1A1A2E | Clean, minimal |
| Midnight | #0D1B2A → #1B263B | #00D4FF | White | Night game energy |
| Forest | #1B4332 → #2D6A4F | #95D5B2 | White | Baseball/outdoor |
| Sunset | #FF6B35 → #F7931E | #FFFFFF | White | Bold, attention-grabbing |
| Berry | #4A0E4E → #81267E | #FF85A1 | White | Unique, stands out |
| Ocean | #023E8A → #0077B6 | #90E0EF | White | Cool, professional |
| Slate | #2B2D42 → #3D405B | #F4A261 | #EDF2F4 | Sophisticated neutral |
Theme Definition
struct ShareTheme: Identifiable, Hashable {
let id: String
let name: String
let gradientColors: [Color]
let accentColor: Color
let textColor: Color
let secondaryTextColor: Color
let mapStyle: MapStyle // .light or .dark
static let all: [ShareTheme] = [.dark, .light, .midnight, .forest, .sunset, .berry, .ocean, .slate]
}
Theme preference is persisted per content type via UserDefaults.
Card Designs
Card Dimensions
All cards use Instagram Story dimensions: 1080 × 1920 pixels (9:16 aspect ratio).
Trip Summary Card
┌─────────────────────────────────┐
│ [Sport Icon] │ ← Header: 120px
│ "My Baseball Road Trip" │
├─────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ │ │
│ │ MAP SNAPSHOT │ │ ← Hero map: 600px
│ │ with route line │ │ Route drawn in accent color
│ │ + city markers │ │ Visited cities = filled dots
│ │ │ │
│ └─────────────────────┘ │
│ │
├─────────────────────────────────┤
│ "Jun 15 - Jun 22, 2026" │ ← Date range
├─────────────────────────────────┤
│ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 1,847 │ │ 5 │ │ 4 │ │ ← Stats row
│ │ miles │ │ games │ │cities │ │
│ └───────┘ └───────┘ └───────┘ │
│ │
├─────────────────────────────────┤
│ Chicago → Milwaukee → Detroit │ ← City trail
│ → Cleveland → Pittsburgh │
├─────────────────────────────────┤
│ [SportsTime logo] │ ← Footer: 100px
│ "Plan your stadium adventure" │
└─────────────────────────────────┘
Map Features:
- Route line drawn using MKPolyline in accent color
- City markers: filled circles with abbreviation labels
- Map style matches theme (muted standard for dark themes)
Achievement Cards
Type 1: Single Spotlight
┌─────────────────────────────────┐
│ │
│ ┌───────────┐ │
│ │ │ │
│ │ BADGE │ │ ← Large badge: 400×400px
│ │ GRAPHIC │ │
│ │ │ │
│ └───────────┘ │
│ │
│ "ALL NL WEST" │ ← Achievement name
│ │
│ "Visited all 5 stadiums │ ← Description
│ in the National League │
│ West division" │
│ │
│ ✓ Unlocked Jan 12, 2026 │ ← Unlock date
│ │
│ [SportsTime logo + tagline] │
└─────────────────────────────────┘
Type 2: Collection Grid
┌─────────────────────────────────┐
│ "My 2026 Achievements" │ ← Header with year
├─────────────────────────────────┤
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Badge│ │Badge│ │Badge│ │ ← 3×3 or 3×4 grid
│ │ #1 │ │ #2 │ │ #3 │ │
│ └─────┘ └─────┘ └─────┘ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Badge│ │Badge│ │Badge│ │
│ │ #4 │ │ #5 │ │ #6 │ │
│ └─────┘ └─────┘ └─────┘ │
├─────────────────────────────────┤
│ "12 achievements unlocked" │ ← Summary count
│ [SportsTime logo + tagline] │
└─────────────────────────────────┘
Type 3: Milestone Celebration
Same as Single Spotlight with:
- Animated particle/confetti burst pattern (static for image)
- Larger badge (500×500px)
- "MILESTONE" label above badge
- Gold accent elements regardless of theme
Type 4: Achievement + Context
┌─────────────────────────────────┐
│ ┌─────────┐ │
│ │ BADGE │ "Coast to Coast" │ ← Badge + name side by side
│ └─────────┘ Unlocked! │
├─────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ TRIP MAP or │ │ ← Context: trip/visit
│ │ STADIUM PHOTO │ │ that unlocked it
│ └─────────────────────┘ │
│ │
│ "Unlocked during my │
│ Summer 2026 Road Trip" │ ← Trip name or visit date
│ │
│ [SportsTime logo + tagline] │
└─────────────────────────────────┘
Stadium Progress Card
┌─────────────────────────────────┐
│ [Sport Icon] │
│ "MLB Stadium Quest" │ ← Header
├─────────────────────────────────┤
│ │
│ ┌───────────┐ │
│ ╱ ╲ │
│ │ 18 │ │ ← Progress ring: 320px
│ │ ──── │ │ Accent color fill
│ │ of 30 │ │
│ ╲ ╱ │
│ └───────────┘ │
│ │
│ 60% Complete │ ← Percentage
│ │
├─────────────────────────────────┤
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 18 │ │ 12 │ │ 4 │ │ ← Stats row
│ │visited│ │remain │ │trips │ │
│ └───────┘ └───────┘ └───────┘ │
├─────────────────────────────────┤
│ ┌─────────────────────┐ │
│ │ │ │
│ │ US MAP SNAPSHOT │ │ ← Map: 500px height
│ │ ● visited (accent)│ │
│ │ ○ remaining (gray)│ │
│ │ │ │
│ └─────────────────────┘ │
├─────────────────────────────────┤
│ @username (optional) │ ← Optional username
│ [SportsTime logo + tagline] │
└─────────────────────────────────┘
Share UI/UX
Contextual Share Buttons
ShareButton component appears on:
- Trip cards in saved trips list
- Trip detail view toolbar
- Achievement badges (on tap)
- Progress ring on Progress tab
- Achievement list header (for collection)
struct ShareButton: View {
let content: ShareableContent
@State private var showPreview = false
var body: some View {
Button {
showPreview = true
} label: {
Image(systemName: "square.and.arrow.up")
}
.sheet(isPresented: $showPreview) {
SharePreviewView(content: content)
}
}
}
SharePreviewView
┌─────────────────────────────────┐
│ ← Cancel Share → │ ← Nav bar
├─────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ │ │
│ │ LIVE PREVIEW │ │ ← 40% scale preview
│ │ of card │ │ Updates in real-time
│ │ │ │
│ └─────────────────────┘ │
│ │
├─────────────────────────────────┤
│ Theme │
│ [Dark] [Light] [Midnight] ... │ ← Horizontal scroll
├─────────────────────────────────┤
│ ☑ Include username │ ← Toggle (progress only)
│ [@yourhandle____________] │
├─────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ Share to Instagram │ │ ← Primary CTA
│ └─────────────────────────┘ │
│ │
│ [Copy Image] [More Options] │ ← Secondary actions
│ │
└─────────────────────────────────┘
Instagram Integration
func shareToInstagram(image: UIImage) {
guard let imageData = image.pngData() else { return }
// Write to shared container for Instagram
let pasteboardItems: [String: Any] = [
"com.instagram.sharedSticker.backgroundImage": imageData
]
UIPasteboard.general.setItems([pasteboardItems])
// Open Instagram Stories
if let url = URL(string: "instagram-stories://share?source_application=com.sportstime.app") {
UIApplication.shared.open(url)
}
}
Falls back to standard UIActivityViewController if Instagram not installed.
File Structure
Files to Delete
| File | Lines |
|---|---|
SportsTime/Export/Services/ProgressCardGenerator.swift |
607 |
New Files
| File | Purpose | ~Lines |
|---|---|---|
Export/Sharing/ShareableContent.swift |
Protocol + ShareTheme | 120 |
Export/Sharing/ShareCardComponents.swift |
Header, footer, stats, map | 200 |
Export/Sharing/TripCardGenerator.swift |
Trip summary cards | 180 |
Export/Sharing/AchievementCardGenerator.swift |
All 4 achievement types | 350 |
Export/Sharing/ProgressCardGenerator.swift |
Stadium progress cards | 180 |
Export/Sharing/ShareService.swift |
Instagram, export, share sheet | 150 |
Export/Views/SharePreviewView.swift |
Unified preview UI | 250 |
Export/Views/ShareButton.swift |
Contextual button | 40 |
Files to Modify
| File | Changes |
|---|---|
TripDetailView.swift |
Replace shareTrip() with ShareButton, remove old ShareSheet |
ProgressTabView.swift |
Add ShareButton to progress rings |
| Achievement views | Add ShareButton to badges |
Implementation Notes
Map Snapshot Generation
Reuse pattern from existing code but extract to ShareCardComponents:
func generateRouteMapSnapshot(
stops: [TripStop],
theme: ShareTheme
) async -> UIImage? {
// Calculate bounding region
// Configure MKMapSnapshotter with theme-appropriate style
// Draw route polyline in accent color
// Draw city markers
}
func generateProgressMapSnapshot(
visited: [Stadium],
remaining: [Stadium],
theme: ShareTheme
) async -> UIImage? {
// Calculate US-wide region
// Draw visited markers in accent color
// Draw remaining markers in gray
}
Image Rendering
Use SwiftUI ImageRenderer at 3x scale for high-res output:
func render(theme: ShareTheme) async throws -> UIImage {
let cardView = TripCardView(trip: trip, theme: theme)
let renderer = ImageRenderer(content: cardView)
renderer.scale = 3.0
guard let image = renderer.uiImage else {
throw ShareError.renderingFailed
}
return image
}
Theme Persistence
@AppStorage("shareTheme.trip") private var tripThemeId: String = "dark"
@AppStorage("shareTheme.achievement") private var achievementThemeId: String = "dark"
@AppStorage("shareTheme.progress") private var progressThemeId: String = "dark"
Scope Summary
| Category | Count |
|---|---|
| New lines | ~1,470 |
| Deleted lines | ~607 |
| Modified lines | ~50 |
| New files | 8 |
| Deleted files | 1 |
| Modified files | 3+ |
Success Criteria
- All 3 content types shareable (trips, achievements, progress)
- All 8 theme presets working
- Instagram Stories direct share working
- Fallback to share sheet when Instagram not installed
- Share buttons appear contextually throughout app
- Theme preference persisted per content type
- Cards render at 1080×1920 with proper scaling