
Social Export
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates infrastructure for exporting app content to Instagram Stories, TikTok, and Twitter/X with platform-specific formatting, aspect ratios, and metadata.
About
Generates a social export pipeline with platform-specific formatters, aspect-ratio handling, branding overlays, and a SwiftUI export flow for Instagram Stories, TikTok, and X. A developer uses it to add share-to-platform export beyond a single share image.
- Platform-specific formatters and aspect ratios
- SwiftUI export flow with branding overlays
Social Export by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 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 social-exportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates infrastructure for exporting app content to Instagram Stories, TikTok, and Twitter/X with platform-specific formatting, aspect ratios, and metadata.
Files
Social Export Generator
Generate a production social export pipeline with platform-specific formatters, aspect ratio handling, branding overlays, and a complete SwiftUI export flow. Different from share-card (which creates the visual image) — this handles the full export pipeline to each social platform.
When This Skill Activates
Use this skill when the user:
- Asks to "export to Instagram" or "share to Instagram Stories"
- Wants "social media export" or "social sharing pipeline"
- Mentions "share to TikTok" or "export for TikTok"
- Asks about "platform-specific sharing" or "export to Twitter/X"
- Wants to "format content for social platforms"
- Mentions "story export" or "share to stories"
Pre-Generation Checks
1. Project Context Detection
- [ ] Check Swift version (requires Swift 5.9+)
- [ ] Check deployment target (iOS 16+ / macOS 13+)
- [ ] Check for @Observable support (iOS 17+ / macOS 14+)
- [ ] Identify source file locations
2. Conflict Detection
Search for existing social export or sharing code:
Glob: **/*SocialExport*.swift, **/*ShareExport*.swift, **/*StoryExport*.swift
Grep: "UIActivityViewController" or "instagram-stories" or "SocialPlatform"If existing share infrastructure found:
- Ask if user wants to extend or replace it
- If extending, integrate with existing sharing code
3. Framework Detection
Check for frameworks already in use:
Grep: "import Photos" or "import PhotosUI" or "import LinkPresentation"
Grep: "UIDocumentInteractionController" or "UIActivityViewController"If Photos framework is used, export pipeline can integrate save-to-library as a fallback.
Configuration Questions
Ask user via AskUserQuestion:
1. Target platforms? (multi-select)
- Instagram Stories (URL scheme + pasteboard)
- TikTok (URL scheme + share API)
- Twitter/X (URL scheme + activity controller)
- General share sheet (UIActivityViewController fallback)
2. Content type?
- Image only (photo, illustration, screenshot)
- Video (short-form video export)
- Text + Image (quote cards, text overlays on images)
3. Include app branding overlay?
- Yes — add app logo/name watermark to exported content
- No — export clean content
4. Watermark style? (if branding selected)
- Corner logo (small, unobtrusive)
- Bottom banner (app name + URL strip)
- None
Generation Process
Step 1: Read Templates
Read templates.md for production Swift code.
Step 2: Create Core Files
Generate these files: 1. SocialPlatform.swift — Enum for supported platforms with requirements (aspect ratio, max file size, URL scheme) 2. ExportConfiguration.swift — Configuration struct for export options (platform, quality, branding, watermark) 3. SocialExporter.swift — Protocol + platform-specific implementations for each social network
Step 3: Create Content Formatting
4. ContentFormatter.swift — Formats content per platform (resize, crop to aspect ratio, add metadata)
Step 4: Create UI Files
5. ExportPreviewView.swift — SwiftUI preview showing how content will look on each platform 6. SocialExportSheet.swift — Complete export flow with platform picker, preview, and share action
Step 5: Determine File Location
Check project structure:
- If
Sources/exists ->Sources/SocialExport/ - If
App/exists ->App/SocialExport/ - Otherwise ->
SocialExport/
Output Format
After generation, provide:
Files Created
SocialExport/
├── SocialPlatform.swift # Platform enum with requirements
├── ExportConfiguration.swift # Export options configuration
├── SocialExporter.swift # Protocol + platform exporters
├── ContentFormatter.swift # Resize, crop, metadata
├── ExportPreviewView.swift # Platform-specific preview
└── SocialExportSheet.swift # Complete export flow UIIntegration Steps
Export an image to Instagram Stories:
let config = ExportConfiguration(
platform: .instagramStories,
quality: .high,
branding: .cornerLogo(UIImage(named: "AppLogo")!)
)
let exporter = SocialExporter()
try await exporter.export(image: myImage, configuration: config)Present the export sheet:
struct ContentDetailView: View {
let content: AppContent
@State private var showExportSheet = false
var body: some View {
VStack {
ContentView(content: content)
Button("Share to Social") {
showExportSheet = true
}
}
.sheet(isPresented: $showExportSheet) {
SocialExportSheet(image: content.renderedImage)
}
}
}Quick share with fallback:
let exporter = SocialExporter()
// Tries platform-specific export first, falls back to share sheet
try await exporter.export(
image: shareImage,
configuration: .init(platform: .instagramStories),
fallbackToShareSheet: true,
presentingViewController: viewController
)Testing
@Test
func instagramStoriesExportFormatsCorrectly() async throws {
let formatter = ContentFormatter()
let testImage = UIImage.testSolidColor(.red, size: CGSize(width: 1000, height: 1000))
let formatted = try formatter.format(testImage, for: .instagramStories)
// Instagram Stories expects 9:16 aspect ratio (1080x1920)
#expect(formatted.size.width == 1080)
#expect(formatted.size.height == 1920)
}
@Test
func fallsBackToShareSheetWhenAppNotInstalled() async throws {
let exporter = MockSocialExporter(installedApps: [])
let config = ExportConfiguration(platform: .instagramStories)
let result = try await exporter.export(
image: UIImage.testSolidColor(.blue, size: CGSize(width: 500, height: 500)),
configuration: config,
fallbackToShareSheet: true
)
#expect(result == .fallbackUsed)
}
@Test
func brandingOverlayApplied() async throws {
let formatter = ContentFormatter()
let logo = UIImage.testSolidColor(.white, size: CGSize(width: 50, height: 50))
let config = ExportConfiguration(
platform: .general,
branding: .cornerLogo(logo)
)
let image = UIImage.testSolidColor(.red, size: CGSize(width: 1080, height: 1080))
let result = try formatter.format(image, for: config.platform, branding: config.branding)
// Image should still be the correct size after overlay
#expect(result.size.width == 1080)
#expect(result.size.height == 1080)
}Common Patterns
Instagram Stories Export
Instagram Stories uses URL scheme + pasteboard for background images:
// Key details:
// - URL scheme: instagram-stories://share?source_application=YOUR_APP_ID
// - Pasteboard: set image data with key "com.instagram.sharedSticker.backgroundImage"
// - Aspect ratio: 9:16 (1080x1920)
// - Max file size: ~12 MB for images
// - Supports background image, sticker image, and background colorGeneral Share Sheet Fallback
Always provide UIActivityViewController as a fallback when the target app is not installed:
// Check canOpenURL before attempting URL scheme
// If unavailable, present UIActivityViewController with formatted content
// Include UTType metadata for proper previews in share sheetVideo Export Pipeline
For video content, use AVAssetExportSession to transcode to platform-required formats:
// Instagram/TikTok: H.264, 9:16, max 60s (Stories) or 3min (Reels)
// Twitter/X: H.264, max 2:20, max 512 MB
// General: H.264, original aspect ratioGotchas
Instagram URL Scheme Requirements
- Must register
instagram-storiesin LSApplicationQueriesSchemes (Info.plist) - Facebook App ID is required as the
source_applicationparameter - Pasteboard items must be set BEFORE opening the URL scheme
- Data must be set as
Dataon the pasteboard, notUIImage
Aspect Ratios Per Platform
| Platform | Stories | Feed Post | Reels/Short |
|---|---|---|---|
| 9:16 (1080x1920) | 1:1 (1080x1080) or 4:5 (1080x1350) | 9:16 (1080x1920) | |
| TikTok | 9:16 (1080x1920) | 9:16 (1080x1920) | 9:16 (1080x1920) |
| Twitter/X | 16:9 (1200x675) | 16:9 or 1:1 | N/A |
File Size Limits
| Platform | Image Max | Video Max |
|---|---|---|
| Instagram Stories | ~12 MB | ~100 MB (H.264) |
| TikTok | ~10 MB | ~287 MB |
| Twitter/X | 5 MB (JPEG/PNG) | 512 MB (H.264) |
App Detection
UIApplication.shared.canOpenURL()requires LSApplicationQueriesSchemes in Info.plist- iOS limits you to 50 URL schemes in LSApplicationQueriesSchemes
- Always test on physical device — simulator does not have social apps installed
Thread Safety
UIPasteboard.generalmust be accessed on the main threadUIApplication.shared.open()must be called on the main thread- Image formatting/resizing should happen on a background thread to avoid UI lag
References
- templates.md — All production Swift templates for social export
- Related:
generators/share-card— Generate the visual share image/card - Related:
generators/watermark-engine— Advanced watermark and branding overlays
Social Export Code Templates
Production-ready Swift templates for social export pipeline. All code targets iOS 16+ / macOS 13+ and uses modern Swift concurrency with Sendable conformance.
SocialPlatform.swift
import Foundation
import CoreGraphics
/// Supported social platforms with their export requirements.
///
/// Each platform defines its own aspect ratio, file size limits,
/// URL scheme, and content format requirements.
enum SocialPlatform: String, CaseIterable, Identifiable, Sendable {
case instagramStories
case instagramFeed
case tiktok
case twitterX
case general
var id: String { rawValue }
/// Human-readable display name for the platform.
var displayName: String {
switch self {
case .instagramStories: return "Instagram Stories"
case .instagramFeed: return "Instagram Feed"
case .tiktok: return "TikTok"
case .twitterX: return "X (Twitter)"
case .general: return "Share"
}
}
/// SF Symbol name for the platform icon.
var iconName: String {
switch self {
case .instagramStories: return "camera.circle"
case .instagramFeed: return "photo.circle"
case .tiktok: return "play.circle"
case .twitterX: return "at.circle"
case .general: return "square.and.arrow.up.circle"
}
}
/// Target aspect ratio for exported content (width:height).
var aspectRatio: CGSize {
switch self {
case .instagramStories: return CGSize(width: 9, height: 16)
case .instagramFeed: return CGSize(width: 1, height: 1)
case .tiktok: return CGSize(width: 9, height: 16)
case .twitterX: return CGSize(width: 16, height: 9)
case .general: return CGSize(width: 1, height: 1)
}
}
/// Target export resolution in pixels.
var targetResolution: CGSize {
switch self {
case .instagramStories: return CGSize(width: 1080, height: 1920)
case .instagramFeed: return CGSize(width: 1080, height: 1080)
case .tiktok: return CGSize(width: 1080, height: 1920)
case .twitterX: return CGSize(width: 1200, height: 675)
case .general: return CGSize(width: 1080, height: 1080)
}
}
/// Maximum image file size in bytes.
var maxImageFileSize: Int {
switch self {
case .instagramStories: return 12 * 1024 * 1024 // 12 MB
case .instagramFeed: return 8 * 1024 * 1024 // 8 MB
case .tiktok: return 10 * 1024 * 1024 // 10 MB
case .twitterX: return 5 * 1024 * 1024 // 5 MB
case .general: return 20 * 1024 * 1024 // 20 MB
}
}
/// Maximum video file size in bytes.
var maxVideoFileSize: Int {
switch self {
case .instagramStories: return 100 * 1024 * 1024 // 100 MB
case .instagramFeed: return 100 * 1024 * 1024 // 100 MB
case .tiktok: return 287 * 1024 * 1024 // 287 MB
case .twitterX: return 512 * 1024 * 1024 // 512 MB
case .general: return 500 * 1024 * 1024 // 500 MB
}
}
/// URL scheme used to open the platform app.
/// Returns nil for general share sheet.
var urlScheme: String? {
switch self {
case .instagramStories: return "instagram-stories://share"
case .instagramFeed: return "instagram://library"
case .tiktok: return "tiktok://"
case .twitterX: return "twitter://post"
case .general: return nil
}
}
/// URL scheme used for canOpenURL check.
/// Must be registered in LSApplicationQueriesSchemes.
var queryScheme: String? {
switch self {
case .instagramStories, .instagramFeed: return "instagram-stories"
case .tiktok: return "tiktok"
case .twitterX: return "twitter"
case .general: return nil
}
}
/// Whether the platform app is installed on this device.
@MainActor
var isAvailable: Bool {
guard let scheme = queryScheme,
let url = URL(string: "\(scheme)://") else {
return true // General share sheet is always available
}
return UIApplication.shared.canOpenURL(url)
}
/// Info.plist LSApplicationQueriesSchemes entries required for this platform.
var requiredQuerySchemes: [String] {
switch self {
case .instagramStories, .instagramFeed:
return ["instagram-stories", "instagram"]
case .tiktok:
return ["tiktok"]
case .twitterX:
return ["twitter", "x"]
case .general:
return []
}
}
}ExportConfiguration.swift
import UIKit
/// Branding overlay style for exported content.
enum BrandingStyle: Sendable {
/// Small logo in the specified corner.
case cornerLogo(UIImage, corner: Corner = .bottomRight)
/// Banner strip at the bottom with app name and optional URL.
case bottomBanner(appName: String, urlString: String? = nil, backgroundColor: UIColor = .black)
/// No branding applied.
case none
enum Corner: Sendable {
case topLeft, topRight, bottomLeft, bottomRight
}
}
/// Export quality preset affecting JPEG compression and resolution scaling.
enum ExportQuality: String, CaseIterable, Sendable {
case low // 0.5x resolution, 60% JPEG quality
case medium // 0.75x resolution, 75% JPEG quality
case high // 1x resolution, 85% JPEG quality
case maximum // 1x resolution, 95% JPEG quality
/// JPEG compression quality (0.0 to 1.0).
var compressionQuality: CGFloat {
switch self {
case .low: return 0.6
case .medium: return 0.75
case .high: return 0.85
case .maximum: return 0.95
}
}
/// Resolution scale factor applied to the platform's target resolution.
var resolutionScale: CGFloat {
switch self {
case .low: return 0.5
case .medium: return 0.75
case .high: return 1.0
case .maximum: return 1.0
}
}
}
/// Configuration for a social export operation.
///
/// Combines platform target, quality settings, and branding options
/// into a single configuration passed to the exporter.
///
/// Usage:
/// ```swift
/// let config = ExportConfiguration(
/// platform: .instagramStories,
/// quality: .high,
/// branding: .cornerLogo(myLogo)
/// )
/// ```
struct ExportConfiguration: Sendable {
/// Target social platform.
let platform: SocialPlatform
/// Export quality preset.
let quality: ExportQuality
/// Branding overlay applied to exported content.
let branding: BrandingStyle
/// Optional caption or text to include (used by Twitter/X and share sheet).
let caption: String?
/// Optional hashtags to append to the caption.
let hashtags: [String]
/// Optional URL to attach (used by Twitter/X and share sheet).
let attachmentURL: URL?
init(
platform: SocialPlatform,
quality: ExportQuality = .high,
branding: BrandingStyle = .none,
caption: String? = nil,
hashtags: [String] = [],
attachmentURL: URL? = nil
) {
self.platform = platform
self.quality = quality
self.branding = branding
self.caption = caption
self.hashtags = hashtags
self.attachmentURL = attachmentURL
}
/// Effective resolution after applying quality scaling.
var effectiveResolution: CGSize {
let target = platform.targetResolution
let scale = quality.resolutionScale
return CGSize(
width: target.width * scale,
height: target.height * scale
)
}
/// Full caption with hashtags appended.
var fullCaption: String? {
guard let caption else {
return hashtags.isEmpty ? nil : hashtags.map { "#\($0)" }.joined(separator: " ")
}
if hashtags.isEmpty {
return caption
}
let tags = hashtags.map { "#\($0)" }.joined(separator: " ")
return "\(caption)\n\n\(tags)"
}
}SocialExporter.swift
import UIKit
/// Result of a social export operation.
enum ExportResult: Sendable {
/// Successfully exported to the target platform app.
case success(platform: SocialPlatform)
/// Target app not installed; fell back to share sheet.
case fallbackUsed
/// User cancelled the export.
case cancelled
/// Export failed with an error.
case failed(SocialExportError)
}
/// Errors specific to social export operations.
enum SocialExportError: Error, LocalizedError {
case platformNotAvailable(SocialPlatform)
case imageTooLarge(currentSize: Int, maxSize: Int)
case formattingFailed
case exportFailed(underlying: Error)
case missingFacebookAppID
case pasteboardWriteFailed
var errorDescription: String? {
switch self {
case .platformNotAvailable(let platform):
return "\(platform.displayName) is not installed on this device"
case .imageTooLarge(let current, let max):
let currentMB = current / (1024 * 1024)
let maxMB = max / (1024 * 1024)
return "Image is \(currentMB) MB, exceeds \(maxMB) MB limit"
case .formattingFailed:
return "Failed to format content for export"
case .exportFailed(let error):
return "Export failed: \(error.localizedDescription)"
case .missingFacebookAppID:
return "Facebook App ID is required for Instagram Stories export"
case .pasteboardWriteFailed:
return "Failed to write content to pasteboard"
}
}
}
/// Protocol for platform-specific social export implementations.
///
/// Conforming types handle the details of exporting to a specific
/// social platform, including URL scheme invocation, pasteboard
/// setup, and fallback behavior.
protocol SocialExporting: Sendable {
/// Export an image to the target platform.
@MainActor
func export(
image: UIImage,
configuration: ExportConfiguration,
presentingViewController: UIViewController?
) async throws -> ExportResult
}
/// Central social exporter that delegates to platform-specific implementations.
///
/// Usage:
/// ```swift
/// let exporter = SocialExporter(facebookAppID: "YOUR_FB_APP_ID")
/// let result = try await exporter.export(
/// image: shareImage,
/// configuration: .init(platform: .instagramStories)
/// )
/// ```
final class SocialExporter: Sendable {
private let facebookAppID: String?
private let formatter: ContentFormatter
init(facebookAppID: String? = nil, formatter: ContentFormatter = ContentFormatter()) {
self.facebookAppID = facebookAppID
self.formatter = formatter
}
/// Export an image to the configured platform.
///
/// Formats the image for the target platform, applies branding,
/// and attempts to open the platform app. Falls back to the
/// system share sheet if `fallbackToShareSheet` is true.
@MainActor
func export(
image: UIImage,
configuration: ExportConfiguration,
fallbackToShareSheet: Bool = true,
presentingViewController: UIViewController? = nil
) async throws -> ExportResult {
// Format the image for the target platform
let formattedImage = try formatter.format(
image,
for: configuration.platform,
branding: configuration.branding,
quality: configuration.quality
)
// Validate file size
guard let imageData = formattedImage.jpegData(
compressionQuality: configuration.quality.compressionQuality
) else {
throw SocialExportError.formattingFailed
}
let maxSize = configuration.platform.maxImageFileSize
if imageData.count > maxSize {
// Try reducing quality to fit
let reducedData = try compressToFit(
image: formattedImage,
maxSize: maxSize
)
return try await exportData(
reducedData,
originalImage: formattedImage,
configuration: configuration,
fallbackToShareSheet: fallbackToShareSheet,
presentingViewController: presentingViewController
)
}
return try await exportData(
imageData,
originalImage: formattedImage,
configuration: configuration,
fallbackToShareSheet: fallbackToShareSheet,
presentingViewController: presentingViewController
)
}
// MARK: - Private
@MainActor
private func exportData(
_ data: Data,
originalImage: UIImage,
configuration: ExportConfiguration,
fallbackToShareSheet: Bool,
presentingViewController: UIViewController?
) async throws -> ExportResult {
let platform = configuration.platform
// Check if platform app is available
guard platform.isAvailable else {
if fallbackToShareSheet {
return try await exportViaShareSheet(
image: originalImage,
configuration: configuration,
presentingViewController: presentingViewController
)
}
throw SocialExportError.platformNotAvailable(platform)
}
switch platform {
case .instagramStories:
return try await exportToInstagramStories(
data: data,
configuration: configuration
)
case .instagramFeed:
return try await exportToInstagramFeed(
image: originalImage,
configuration: configuration,
presentingViewController: presentingViewController
)
case .tiktok:
return try await exportToTikTok(
image: originalImage,
configuration: configuration,
presentingViewController: presentingViewController
)
case .twitterX:
return try await exportToTwitterX(
image: originalImage,
configuration: configuration,
presentingViewController: presentingViewController
)
case .general:
return try await exportViaShareSheet(
image: originalImage,
configuration: configuration,
presentingViewController: presentingViewController
)
}
}
// MARK: - Instagram Stories
@MainActor
private func exportToInstagramStories(
data: Data,
configuration: ExportConfiguration
) async throws -> ExportResult {
guard let appID = facebookAppID else {
throw SocialExportError.missingFacebookAppID
}
guard let url = URL(
string: "instagram-stories://share?source_application=\(appID)"
) else {
throw SocialExportError.formattingFailed
}
// Set pasteboard items BEFORE opening URL scheme
var pasteboardItems: [String: Any] = [
"com.instagram.sharedSticker.backgroundImage": data
]
// Optional: set background gradient colors
// pasteboardItems["com.instagram.sharedSticker.backgroundTopColor"] = "#FF0000"
// pasteboardItems["com.instagram.sharedSticker.backgroundBottomColor"] = "#0000FF"
if let caption = configuration.fullCaption {
pasteboardItems["com.instagram.sharedSticker.contentURL"] = caption
}
UIPasteboard.general.setItems(
[pasteboardItems],
options: [.expirationDate: Date().addingTimeInterval(300)]
)
let opened = await UIApplication.shared.open(url)
return opened ? .success(platform: .instagramStories) : .fallbackUsed
}
// MARK: - Instagram Feed
@MainActor
private func exportToInstagramFeed(
image: UIImage,
configuration: ExportConfiguration,
presentingViewController: UIViewController?
) async throws -> ExportResult {
// Instagram Feed sharing requires saving to Photos and opening
// the Instagram app. Use UIDocumentInteractionController for
// the most reliable behavior.
return try await exportViaShareSheet(
image: image,
configuration: configuration,
presentingViewController: presentingViewController
)
}
// MARK: - TikTok
@MainActor
private func exportToTikTok(
image: UIImage,
configuration: ExportConfiguration,
presentingViewController: UIViewController?
) async throws -> ExportResult {
// TikTok SDK or share sheet integration
// For basic sharing, use the activity view controller
// For deeper integration, use TikTok's OpenSDK
return try await exportViaShareSheet(
image: image,
configuration: configuration,
presentingViewController: presentingViewController
)
}
// MARK: - Twitter/X
@MainActor
private func exportToTwitterX(
image: UIImage,
configuration: ExportConfiguration,
presentingViewController: UIViewController?
) async throws -> ExportResult {
// Build tweet URL with text
if let caption = configuration.fullCaption,
let encodedCaption = caption.addingPercentEncoding(
withAllowedCharacters: .urlQueryAllowed
),
let tweetURL = URL(
string: "twitter://post?message=\(encodedCaption)"
) {
let opened = await UIApplication.shared.open(tweetURL)
if opened {
return .success(platform: .twitterX)
}
}
// Fall back to share sheet if URL scheme fails
return try await exportViaShareSheet(
image: image,
configuration: configuration,
presentingViewController: presentingViewController
)
}
// MARK: - General Share Sheet
@MainActor
private func exportViaShareSheet(
image: UIImage,
configuration: ExportConfiguration,
presentingViewController: UIViewController?
) async throws -> ExportResult {
guard let viewController = presentingViewController
?? UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.flatMap(\.windows)
.first(where: \.isKeyWindow)?
.rootViewController else {
throw SocialExportError.exportFailed(
underlying: NSError(
domain: "SocialExporter",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "No presenting view controller"]
)
)
}
var activityItems: [Any] = [image]
if let caption = configuration.fullCaption {
activityItems.append(caption)
}
if let url = configuration.attachmentURL {
activityItems.append(url)
}
let activityVC = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil
)
// Exclude irrelevant activities
activityVC.excludedActivityTypes = [
.addToReadingList,
.assignToContact,
.openInIBooks
]
// iPad requires popover presentation
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 = []
}
return await withCheckedContinuation { continuation in
activityVC.completionWithItemsHandler = { activityType, completed, _, error in
if let error {
continuation.resume(returning: .failed(.exportFailed(underlying: error)))
} else if completed {
continuation.resume(returning: .success(platform: .general))
} else {
continuation.resume(returning: .cancelled)
}
}
viewController.present(activityVC, animated: true)
}
}
// MARK: - Compression
private func compressToFit(image: UIImage, maxSize: Int) throws -> Data {
var quality: CGFloat = 0.9
let minimumQuality: CGFloat = 0.1
while quality >= minimumQuality {
guard let data = image.jpegData(compressionQuality: quality) else {
throw SocialExportError.formattingFailed
}
if data.count <= maxSize {
return data
}
quality -= 0.1
}
// Last resort: try with minimum quality
guard let data = image.jpegData(compressionQuality: minimumQuality) else {
throw SocialExportError.formattingFailed
}
if data.count <= maxSize {
return data
}
throw SocialExportError.imageTooLarge(
currentSize: data.count,
maxSize: maxSize
)
}
}ContentFormatter.swift
import UIKit
import CoreGraphics
/// Formats content for each social platform by resizing, cropping
/// to the target aspect ratio, and applying branding overlays.
///
/// Usage:
/// ```swift
/// let formatter = ContentFormatter()
/// let formatted = try formatter.format(
/// originalImage,
/// for: .instagramStories,
/// branding: .cornerLogo(logo)
/// )
/// ```
struct ContentFormatter: Sendable {
/// Format an image for the specified platform.
///
/// - Parameters:
/// - image: Source image to format.
/// - platform: Target social platform.
/// - branding: Optional branding overlay style.
/// - quality: Export quality affecting resolution scaling.
/// - Returns: Formatted image ready for export.
func format(
_ image: UIImage,
for platform: SocialPlatform,
branding: BrandingStyle = .none,
quality: ExportQuality = .high
) throws -> UIImage {
let targetSize = CGSize(
width: platform.targetResolution.width * quality.resolutionScale,
height: platform.targetResolution.height * quality.resolutionScale
)
// Step 1: Crop to target aspect ratio
let cropped = try cropToAspectRatio(
image: image,
targetAspect: platform.aspectRatio
)
// Step 2: Resize to target resolution
let resized = resize(image: cropped, to: targetSize)
// Step 3: Apply branding overlay
let branded = try applyBranding(image: resized, branding: branding)
return branded
}
// MARK: - Crop to Aspect Ratio
/// Crops the image to match the target aspect ratio using center crop.
private func cropToAspectRatio(
image: UIImage,
targetAspect: CGSize
) throws -> UIImage {
let imageSize = image.size
let targetRatio = targetAspect.width / targetAspect.height
let imageRatio = imageSize.width / imageSize.height
var cropRect: CGRect
if imageRatio > targetRatio {
// Image is wider than target — crop sides
let newWidth = imageSize.height * targetRatio
let xOffset = (imageSize.width - newWidth) / 2
cropRect = CGRect(x: xOffset, y: 0, width: newWidth, height: imageSize.height)
} else {
// Image is taller than target — crop top and bottom
let newHeight = imageSize.width / targetRatio
let yOffset = (imageSize.height - newHeight) / 2
cropRect = CGRect(x: 0, y: yOffset, width: imageSize.width, height: newHeight)
}
// Scale crop rect for retina
let scale = image.scale
let scaledRect = CGRect(
x: cropRect.origin.x * scale,
y: cropRect.origin.y * scale,
width: cropRect.size.width * scale,
height: cropRect.size.height * scale
)
guard let cgImage = image.cgImage?.cropping(to: scaledRect) else {
throw SocialExportError.formattingFailed
}
return UIImage(cgImage: cgImage, scale: scale, orientation: image.imageOrientation)
}
// MARK: - Resize
/// Resizes the image to the exact target size.
private func resize(image: UIImage, to targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
}
// MARK: - Branding Overlay
/// Applies branding overlay (logo or banner) to the image.
private func applyBranding(
image: UIImage,
branding: BrandingStyle
) throws -> UIImage {
switch branding {
case .none:
return image
case .cornerLogo(let logo, let corner):
return try applyCornerLogo(
image: image,
logo: logo,
corner: corner
)
case .bottomBanner(let appName, let urlString, let backgroundColor):
return try applyBottomBanner(
image: image,
appName: appName,
urlString: urlString,
backgroundColor: backgroundColor
)
}
}
/// Draws a small logo in the specified corner with padding and optional background.
private func applyCornerLogo(
image: UIImage,
logo: UIImage,
corner: BrandingStyle.Corner
) throws -> UIImage {
let imageSize = image.size
let padding: CGFloat = imageSize.width * 0.03 // 3% padding
let logoMaxSize = imageSize.width * 0.12 // 12% of image width
// Scale logo maintaining aspect ratio
let logoAspect = logo.size.width / logo.size.height
let logoSize: CGSize
if logoAspect >= 1 {
logoSize = CGSize(width: logoMaxSize, height: logoMaxSize / logoAspect)
} else {
logoSize = CGSize(width: logoMaxSize * logoAspect, height: logoMaxSize)
}
// Calculate position
let logoOrigin: CGPoint
switch corner {
case .topLeft:
logoOrigin = CGPoint(x: padding, y: padding)
case .topRight:
logoOrigin = CGPoint(x: imageSize.width - logoSize.width - padding, y: padding)
case .bottomLeft:
logoOrigin = CGPoint(x: padding, y: imageSize.height - logoSize.height - padding)
case .bottomRight:
logoOrigin = CGPoint(
x: imageSize.width - logoSize.width - padding,
y: imageSize.height - logoSize.height - padding
)
}
let renderer = UIGraphicsImageRenderer(size: imageSize)
return renderer.image { context in
// Draw original image
image.draw(at: .zero)
// Draw semi-transparent background behind logo
let bgRect = CGRect(
x: logoOrigin.x - padding / 2,
y: logoOrigin.y - padding / 2,
width: logoSize.width + padding,
height: logoSize.height + padding
)
UIColor.black.withAlphaComponent(0.3).setFill()
UIBezierPath(roundedRect: bgRect, cornerRadius: padding / 2).fill()
// Draw logo
let logoRect = CGRect(origin: logoOrigin, size: logoSize)
logo.draw(in: logoRect)
}
}
/// Draws a banner at the bottom of the image with app name and optional URL.
private func applyBottomBanner(
image: UIImage,
appName: String,
urlString: String?,
backgroundColor: UIColor
) throws -> UIImage {
let imageSize = image.size
let bannerHeight = imageSize.height * 0.06 // 6% of image height
let fontSize = bannerHeight * 0.5
let padding = bannerHeight * 0.2
let renderer = UIGraphicsImageRenderer(size: imageSize)
return renderer.image { context in
// Draw original image
image.draw(at: .zero)
// Draw banner background
let bannerRect = CGRect(
x: 0,
y: imageSize.height - bannerHeight,
width: imageSize.width,
height: bannerHeight
)
backgroundColor.withAlphaComponent(0.7).setFill()
context.fill(bannerRect)
// Draw app name
let nameAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize, weight: .semibold),
.foregroundColor: UIColor.white
]
let nameString = appName as NSString
let nameSize = nameString.size(withAttributes: nameAttributes)
let namePoint = CGPoint(
x: padding,
y: imageSize.height - bannerHeight + (bannerHeight - nameSize.height) / 2
)
nameString.draw(at: namePoint, withAttributes: nameAttributes)
// Draw URL if provided
if let urlString {
let urlAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize * 0.8, weight: .regular),
.foregroundColor: UIColor.white.withAlphaComponent(0.8)
]
let urlNSString = urlString as NSString
let urlSize = urlNSString.size(withAttributes: urlAttributes)
let urlPoint = CGPoint(
x: imageSize.width - urlSize.width - padding,
y: imageSize.height - bannerHeight + (bannerHeight - urlSize.height) / 2
)
urlNSString.draw(at: urlPoint, withAttributes: urlAttributes)
}
}
}
}ExportPreviewView.swift
import SwiftUI
/// Shows a preview of how content will appear on each social platform.
///
/// Displays the image cropped and scaled to each platform's aspect ratio
/// so the user can see the result before exporting.
///
/// Usage:
/// ```swift
/// ExportPreviewView(
/// image: myImage,
/// platform: .instagramStories,
/// branding: .cornerLogo(logo)
/// )
/// ```
struct ExportPreviewView: View {
let image: UIImage
let platform: SocialPlatform
let branding: BrandingStyle
let quality: ExportQuality
@State private var previewImage: UIImage?
@State private var isProcessing = false
@State private var error: String?
init(
image: UIImage,
platform: SocialPlatform,
branding: BrandingStyle = .none,
quality: ExportQuality = .high
) {
self.image = image
self.platform = platform
self.branding = branding
self.quality = quality
}
var body: some View {
VStack(spacing: 12) {
// Platform header
HStack {
Image(systemName: platform.iconName)
.font(.title3)
Text(platform.displayName)
.font(.headline)
Spacer()
Text(resolutionLabel)
.font(.caption)
.foregroundStyle(.secondary)
}
// Preview image
previewContent
.frame(maxWidth: previewWidth, maxHeight: previewHeight)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
// Aspect ratio info
Text(aspectRatioLabel)
.font(.caption2)
.foregroundStyle(.secondary)
}
.task(id: platform) {
await generatePreview()
}
}
@ViewBuilder
private var previewContent: some View {
if isProcessing {
ProgressView("Formatting...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.secondary.opacity(0.1))
} else if let previewImage {
Image(uiImage: previewImage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if let error {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.font(.title2)
.foregroundStyle(.secondary)
Text(error)
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.secondary.opacity(0.1))
} else {
Color.secondary.opacity(0.1)
}
}
// MARK: - Layout Calculations
/// Maximum preview width within the available space.
private var previewWidth: CGFloat {
300
}
/// Preview height based on platform aspect ratio.
private var previewHeight: CGFloat {
let ratio = platform.aspectRatio
return previewWidth * (ratio.height / ratio.width)
}
private var resolutionLabel: String {
let res = platform.targetResolution
return "\(Int(res.width))x\(Int(res.height))"
}
private var aspectRatioLabel: String {
let ratio = platform.aspectRatio
return "Aspect ratio: \(Int(ratio.width)):\(Int(ratio.height))"
}
// MARK: - Preview Generation
private func generatePreview() async {
isProcessing = true
error = nil
do {
let formatter = ContentFormatter()
let formatted = try formatter.format(
image,
for: platform,
branding: branding,
quality: quality
)
previewImage = formatted
} catch {
self.error = error.localizedDescription
}
isProcessing = false
}
}SocialExportSheet.swift
import SwiftUI
/// Complete export flow with platform picker, preview, and share action.
///
/// Presents a bottom sheet showing available social platforms,
/// a preview of how the content will appear, and an export button.
///
/// Usage:
/// ```swift
/// .sheet(isPresented: $showExportSheet) {
/// SocialExportSheet(
/// image: contentImage,
/// facebookAppID: "YOUR_FB_APP_ID",
/// branding: .cornerLogo(appLogo)
/// )
/// }
/// ```
struct SocialExportSheet: View {
let image: UIImage
let facebookAppID: String?
let defaultBranding: BrandingStyle
@State private var selectedPlatform: SocialPlatform = .general
@State private var quality: ExportQuality = .high
@State private var caption: String = ""
@State private var isExporting = false
@State private var exportResult: ExportResult?
@State private var showResultAlert = false
@Environment(\.dismiss) private var dismiss
init(
image: UIImage,
facebookAppID: String? = nil,
branding: BrandingStyle = .none
) {
self.image = image
self.facebookAppID = facebookAppID
self.defaultBranding = branding
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
// Platform Picker
platformPicker
// Preview
ExportPreviewView(
image: image,
platform: selectedPlatform,
branding: defaultBranding,
quality: quality
)
.padding(.horizontal)
// Caption input (for platforms that support it)
if selectedPlatform == .twitterX || selectedPlatform == .general {
captionSection
}
// Quality picker
qualityPicker
// Export button
exportButton
}
.padding(.vertical)
}
.navigationTitle("Export")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
.alert("Export Result", isPresented: $showResultAlert) {
Button("OK") {
if case .success = exportResult {
dismiss()
}
}
} message: {
Text(resultMessage)
}
}
}
// MARK: - Platform Picker
private var platformPicker: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Platform")
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(SocialPlatform.allCases) { platform in
PlatformButton(
platform: platform,
isSelected: selectedPlatform == platform
) {
withAnimation(.easeInOut(duration: 0.2)) {
selectedPlatform = platform
}
}
}
}
.padding(.horizontal)
}
}
}
// MARK: - Caption
private var captionSection: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Caption")
.font(.subheadline)
.foregroundStyle(.secondary)
TextField("Add a caption...", text: $caption, axis: .vertical)
.lineLimit(3...6)
.textFieldStyle(.roundedBorder)
}
.padding(.horizontal)
}
// MARK: - Quality Picker
private var qualityPicker: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Quality")
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.horizontal)
Picker("Quality", selection: $quality) {
ForEach(ExportQuality.allCases, id: \.self) { q in
Text(q.rawValue.capitalized).tag(q)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
}
}
// MARK: - Export Button
private var exportButton: some View {
Button {
Task { await performExport() }
} label: {
HStack {
if isExporting {
ProgressView()
.tint(.white)
} else {
Image(systemName: selectedPlatform.iconName)
}
Text(isExporting ? "Exporting..." : "Export to \(selectedPlatform.displayName)")
}
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.buttonStyle(.borderedProminent)
.disabled(isExporting)
.padding(.horizontal)
}
// MARK: - Export Logic
@MainActor
private func performExport() async {
isExporting = true
let configuration = ExportConfiguration(
platform: selectedPlatform,
quality: quality,
branding: defaultBranding,
caption: caption.isEmpty ? nil : caption
)
let exporter = SocialExporter(facebookAppID: facebookAppID)
do {
exportResult = try await exporter.export(
image: image,
configuration: configuration,
fallbackToShareSheet: true
)
} catch {
exportResult = .failed(
.exportFailed(underlying: error)
)
}
isExporting = false
showResultAlert = true
}
private var resultMessage: String {
switch exportResult {
case .success(let platform):
return "Successfully shared to \(platform.displayName)!"
case .fallbackUsed:
return "The app is not installed. Content was shared via the system share sheet."
case .cancelled:
return "Export was cancelled."
case .failed(let error):
return error.localizedDescription
case nil:
return ""
}
}
}
// MARK: - Platform Button
/// Individual platform selector button with icon and label.
private struct PlatformButton: View {
let platform: SocialPlatform
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 6) {
Image(systemName: platform.iconName)
.font(.title2)
.frame(width: 48, height: 48)
.background(
Circle()
.fill(isSelected ? Color.accentColor : Color.secondary.opacity(0.1))
)
.foregroundStyle(isSelected ? .white : .primary)
Text(platform.displayName)
.font(.caption2)
.foregroundStyle(isSelected ? .primary : .secondary)
.lineLimit(1)
}
.frame(width: 80)
}
.buttonStyle(.plain)
.accessibilityLabel("Export to \(platform.displayName)")
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}
// MARK: - Test Helpers
extension UIImage {
/// Creates a solid-color test image for use in previews and tests.
static func testSolidColor(
_ color: UIColor,
size: CGSize = CGSize(width: 100, height: 100)
) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
color.setFill()
context.fill(CGRect(origin: .zero, size: size))
}
}
}
// MARK: - Preview
#Preview {
SocialExportSheet(
image: .testSolidColor(.systemBlue, size: CGSize(width: 800, height: 600))
)
}