iOS

iOS/Swift Integration

Native iOS app integration with Swift

Native iOSSwift 5.0+SwiftUI & UIKit
Quick Start

1. Project Setup

swift
Click to copy
// Add to your iOS project
// Minimum iOS version: 13.0
// Swift version: 5.0+

// In your Package.swift or Xcode Package Manager, add:
// No external dependencies required - using URLSession and Foundation

// Info.plist - Add internet permission
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

// Or for production, configure specific domains:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.calligraphymaker.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <false/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>

2. API Service (Swift)

swift
Click to copy
// CalligraphyAPIService.swift
import Foundation
import UIKit

class CalligraphyAPIService {
    static let shared = CalligraphyAPIService()
    private let baseURL = "https://api.calligraphymaker.com/api/v2"
    private let apiKey = "YOUR_API_KEY_HERE"
    
    private init() {}
    
    // MARK: - Data Models
    
    struct GenerateRequest: Codable {
        let text: String
        let language: String
        let fontStyle: String
        let count: Int
        let imageFormat: String
        
        init(text: String, language: String = "hindi", fontStyle: String = "calligraphy", count: Int = 1, imageFormat: String = "png") {
            self.text = text
            self.language = language
            self.fontStyle = fontStyle
            self.count = count
            self.imageFormat = imageFormat
        }
    }
    
    struct CalligraphyResponse: Codable {
        let success: Bool
        let data: CalligraphyData?
    }
    
    struct CalligraphyData: Codable {
        let results: [CalligraphyResult]
        let metadata: CalligraphyMetadata
        let usage: CalligraphyUsage
    }
    
    struct CalligraphyResult: Codable {
        let id: String
        let resultText: String
        let fontFamily: String
        let cdnUrl: String?
        let appliedVariants: [AppliedVariant]
    }
    
    struct AppliedVariant: Codable {
        let type: String
        let position: Int
        let character: String
    }
    
    struct CalligraphyMetadata: Codable {
        let totalVariations: Int
        let averageVariantsApplied: Double
        let fontFamily: String
    }
    
    struct CalligraphyUsage: Codable {
        let creditsUsed: Int
        let remainingCredits: Int
        
        enum CodingKeys: String, CodingKey {
            case creditsUsed = "credits_used"
            case remainingCredits = "remaining_credits"
        }
    }
    
    struct SvgDownloadRequest: Codable {
        let resultText: String
        let fontName: String
    }
    
    struct SvgDownloadResponse: Codable {
        let success: Bool
        let data: SvgData?
    }
    
    struct SvgData: Codable {
        let resultText: String
        let fontFamily: String
        let svgBase64: String
        let svgString: String
        let dimensions: Dimensions
    }
    
    struct ImageDownloadRequest: Codable {
        let resultText: String
        let fontName: String
        let imageWidth: Int
        let imageHeight: Int
        let imageQuality: Int
        let imageFormat: String
        let backgroundColor: String
        let textColor: String
    }
    
    struct ImageDownloadResponse: Codable {
        let success: Bool
        let data: ImageData?
    }
    
    struct ImageData: Codable {
        let resultText: String
        let fontName: String
        let format: String
        let imageBase64: String
        let dimensions: Dimensions
        let quality: Int
        let backgroundColor: String
        let textColor: String
    }
    
    struct Dimensions: Codable {
        let width: Int
        let height: Int
    }
    
    // MARK: - API Methods
    
    func generateCalligraphy(
        text: String,
        language: String = "hindi",
        fontStyle: String = "calligraphy",
        count: Int = 1
    ) async throws -> CalligraphyData {
        let request = GenerateRequest(
            text: text,
            language: language,
            fontStyle: fontStyle,
            count: count
        )
        
        let data = try await performRequest(
            endpoint: "/generate",
            method: "POST",
            body: request
        )
        
        let response = try JSONDecoder().decode(CalligraphyResponse.self, from: data)
        
        guard response.success, let calligraphyData = response.data else {
            throw APIError.invalidResponse
        }
        
        return calligraphyData
    }
    
    func downloadSvg(resultText: String, fontName: String) async throws -> SvgData {
        let request = SvgDownloadRequest(resultText: resultText, fontName: fontName)
        
        let data = try await performRequest(
            endpoint: "/download-svg",
            method: "POST",
            body: request
        )
        
        let response = try JSONDecoder().decode(SvgDownloadResponse.self, from: data)
        
        guard response.success, let svgData = response.data else {
            throw APIError.invalidResponse
        }
        
        return svgData
    }
    
    func downloadImage(
        resultText: String,
        fontName: String,
        width: Int = 800,
        height: Int = 300,
        quality: Int = 90,
        format: String = "png"
    ) async throws -> ImageData {
        let request = ImageDownloadRequest(
            resultText: resultText,
            fontName: fontName,
            imageWidth: width,
            imageHeight: height,
            imageQuality: quality,
            imageFormat: format,
            backgroundColor: "#ffffff",
            textColor: "#000000"
        )
        
        let data = try await performRequest(
            endpoint: "/download-image",
            method: "POST",
            body: request
        )
        
        let response = try JSONDecoder().decode(ImageDownloadResponse.self, from: data)
        
        guard response.success, let imageData = response.data else {
            throw APIError.invalidResponse
        }
        
        return imageData
    }
    
    // MARK: - Private Methods
    
    private func performRequest<T: Codable>(
        endpoint: String,
        method: String,
        body: T? = nil
    ) async throws -> Data {
        guard let url = URL(string: baseURL + endpoint) else {
            throw APIError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue(apiKey, forHTTPHeaderField: "x-api-key")
        
        if let body = body {
            request.httpBody = try JSONEncoder().encode(body)
        }
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        
        guard 200...299 ~= httpResponse.statusCode else {
            throw APIError.httpError(httpResponse.statusCode)
        }
        
        return data
    }
}

// MARK: - Error Handling

enum APIError: Error, LocalizedError {
    case invalidURL
    case invalidResponse
    case httpError(Int)
    case decodingError
    case networkError
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .invalidResponse:
            return "Invalid response from server"
        case .httpError(let code):
            switch code {
            case 401:
                return "Invalid API key"
            case 402:
                return "Insufficient credits"
            case 429:
                return "Rate limit exceeded"
            default:
                return "HTTP Error: \(code)"
            }
        case .decodingError:
            return "Failed to decode response"
        case .networkError:
            return "Network error occurred"
        }
    }
}

3. SwiftUI Implementation

swift
Click to copy
// ContentView.swift
import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = CalligraphyViewModel()
    @State private var inputText = "नमस्ते दुनिया"
    @State private var selectedLanguage = "hindi"
    @State private var selectedFontStyle = "calligraphy"
    @State private var generationCount = 3
    
    let languages = ["hindi", "marathi", "gujarati", "english"]
    let fontStyles = ["calligraphy", "decorative", "publication"]
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 20) {
                    // Header
                    VStack {
                        Text("Calligraphy Generator")
                            .font(.largeTitle)
                            .fontWeight(.bold)
                        
                        Text("Generate beautiful calligraphy with AI")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                    }
                    .padding()
                    
                    // Input Section
                    VStack(alignment: .leading, spacing: 16) {
                        GroupBox("Input Text") {
                            TextEditor(text: $inputText)
                                .frame(minHeight: 100)
                        }
                        
                        HStack {
                            GroupBox("Language") {
                                Picker("Language", selection: $selectedLanguage) {
                                    ForEach(languages, id: \.self) { language in
                                        Text(language.capitalized).tag(language)
                                    }
                                }
                                .pickerStyle(MenuPickerStyle())
                            }
                            
                            GroupBox("Font Style") {
                                Picker("Font Style", selection: $selectedFontStyle) {
                                    ForEach(fontStyles, id: \.self) { style in
                                        Text(style.capitalized).tag(style)
                                    }
                                }
                                .pickerStyle(MenuPickerStyle())
                            }
                        }
                        
                        GroupBox("Generation Count: \(generationCount)") {
                            Slider(value: .init(
                                get: { Double(generationCount) },
                                set: { generationCount = Int($0) }
                            ), in: 1...5, step: 1)
                        }
                        
                        Button(action: generateCalligraphy) {
                            HStack {
                                if viewModel.isLoading {
                                    ProgressView()
                                        .scaleEffect(0.8)
                                } else {
                                    Image(systemName: "wand.and.rays")
                                }
                                Text("Generate Calligraphy")
                            }
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.accentColor)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                        }
                        .disabled(viewModel.isLoading || inputText.isEmpty)
                    }
                    .padding()
                    
                    // Results Section
                    if !viewModel.results.isEmpty {
                        VStack(alignment: .leading) {
                            HStack {
                                Text("Generated Results (\(viewModel.results.count))")
                                    .font(.headline)
                                Spacer()
                                if let usage = viewModel.usage {
                                    Text("Credits: \(usage.remainingCredits)")
                                        .font(.caption)
                                        .foregroundColor(.secondary)
                                }
                            }
                            .padding(.horizontal)
                            
                            LazyVGrid(columns: [
                                GridItem(.flexible()),
                                GridItem(.flexible())
                            ], spacing: 16) {
                                ForEach(viewModel.results, id: \.id) { result in
                                    CalligraphyResultCard(result: result) {
                                        viewModel.selectedResult = result
                                        viewModel.showingResultDetail = true
                                    }
                                }
                            }
                            .padding(.horizontal)
                        }
                    }
                    
                    // Error Display
                    if let error = viewModel.errorMessage {
                        Text(error)
                            .foregroundColor(.red)
                            .padding()
                            .background(Color.red.opacity(0.1))
                            .cornerRadius(8)
                            .padding(.horizontal)
                    }
                }
            }
            .navigationBarHidden(true)
        }
        .sheet(isPresented: $viewModel.showingResultDetail) {
            if let result = viewModel.selectedResult {
                CalligraphyDetailView(result: result, viewModel: viewModel)
            }
        }
    }
    
    private func generateCalligraphy() {
        Task {
            await viewModel.generateCalligraphy(
                text: inputText,
                language: selectedLanguage,
                fontStyle: selectedFontStyle,
                count: generationCount
            )
        }
    }
}

// MARK: - Result Card View

struct CalligraphyResultCard: View {
    let result: CalligraphyResult
    let onTap: () -> Void
    
    var body: some View {
        VStack {
            AsyncImage(url: URL(string: result.cdnUrl ?? "")) { image in
                image
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } placeholder: {
                Rectangle()
                    .fill(Color.gray.opacity(0.3))
                    .overlay(
                        ProgressView()
                    )
            }
            .frame(height: 120)
            .background(Color.gray.opacity(0.1))
            .cornerRadius(8)
            
            VStack(alignment: .leading, spacing: 4) {
                Text(result.resultText)
                    .font(.caption)
                    .fontWeight(.medium)
                    .lineLimit(2)
                
                Text(result.fontFamily)
                    .font(.caption2)
                    .foregroundColor(.secondary)
                
                Text("\(result.appliedVariants.count) variants")
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(12)
        .shadow(radius: 2)
        .onTapGesture(perform: onTap)
    }
}

// MARK: - Detail View

struct CalligraphyDetailView: View {
    let result: CalligraphyResult
    @ObservedObject var viewModel: CalligraphyViewModel
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                AsyncImage(url: URL(string: result.cdnUrl ?? "")) { image in
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                } placeholder: {
                    ProgressView()
                }
                .frame(maxHeight: 300)
                .background(Color.gray.opacity(0.1))
                .cornerRadius(12)
                
                VStack(alignment: .leading, spacing: 12) {
                    DetailRow(title: "Text", value: result.resultText)
                    DetailRow(title: "Font Family", value: result.fontFamily)
                    DetailRow(title: "Variants Applied", value: "\(result.appliedVariants.count)")
                }
                .padding()
                .background(Color(.secondarySystemBackground))
                .cornerRadius(12)
                
                VStack(spacing: 12) {
                    Button("Download as SVG") {
                        Task {
                            await viewModel.downloadSvg(
                                resultText: result.resultText,
                                fontName: result.fontFamily
                            )
                        }
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Button("Download as Image") {
                        Task {
                            await viewModel.downloadImage(
                                resultText: result.resultText,
                                fontName: result.fontFamily
                            )
                        }
                    }
                    .buttonStyle(.bordered)
                    
                    if let imageUrl = result.cdnUrl {
                        Button("Save to Photos") {
                            saveImageToPhotos(from: imageUrl)
                        }
                        .buttonStyle(.bordered)
                    }
                }
                
                Spacer()
            }
            .padding()
            .navigationTitle("Calligraphy Details")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Done") {
                        dismiss()
                    }
                }
            }
        }
    }
    
    private func saveImageToPhotos(from urlString: String) {
        guard let url = URL(string: urlString) else { return }
        
        Task {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                if let image = UIImage(data: data) {
                    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
                }
            } catch {
                print("Failed to save image: \(error)")
            }
        }
    }
}

struct DetailRow: View {
    let title: String
    let value: String
    
    var body: some View {
        HStack {
            Text(title)
                .fontWeight(.medium)
            Spacer()
            Text(value)
                .foregroundColor(.secondary)
        }
    }
}
ViewModel & State Management

CalligraphyViewModel.swift

swift
Click to copy
// CalligraphyViewModel.swift
import Foundation
import Combine

@MainActor
class CalligraphyViewModel: ObservableObject {
    @Published var results: [CalligraphyResult] = []
    @Published var usage: CalligraphyUsage?
    @Published var isLoading = false
    @Published var errorMessage: String?
    @Published var showingResultDetail = false
    @Published var selectedResult: CalligraphyResult?
    @Published var downloadedSvg: SvgData?
    @Published var downloadedImage: ImageData?
    
    private let apiService = CalligraphyAPIService.shared
    
    func generateCalligraphy(
        text: String,
        language: String = "hindi",
        fontStyle: String = "calligraphy",
        count: Int = 1
    ) async {
        guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            errorMessage = "Please enter some text"
            return
        }
        
        isLoading = true
        errorMessage = nil
        
        do {
            let data = try await apiService.generateCalligraphy(
                text: text,
                language: language,
                fontStyle: fontStyle,
                count: count
            )
            
            results = data.results
            usage = data.usage
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
    
    func downloadSvg(resultText: String, fontName: String) async {
        isLoading = true
        errorMessage = nil
        
        do {
            let svgData = try await apiService.downloadSvg(
                resultText: resultText,
                fontName: fontName
            )
            
            downloadedSvg = svgData
            
            // Save SVG to Files app
            await saveSvgToFiles(svgData: svgData)
            
        } catch {
            errorMessage = "Failed to download SVG: \(error.localizedDescription)"
        }
        
        isLoading = false
    }
    
    func downloadImage(
        resultText: String,
        fontName: String,
        width: Int = 800,
        height: Int = 300
    ) async {
        isLoading = true
        errorMessage = nil
        
        do {
            let imageData = try await apiService.downloadImage(
                resultText: resultText,
                fontName: fontName,
                width: width,
                height: height
            )
            
            downloadedImage = imageData
            
            // Convert base64 to image and save
            await saveImageToFiles(imageData: imageData)
            
        } catch {
            errorMessage = "Failed to download image: \(error.localizedDescription)"
        }
        
        isLoading = false
    }
    
    func clearResults() {
        results = []
        usage = nil
        downloadedSvg = nil
        downloadedImage = nil
        errorMessage = nil
    }
    
    private func saveSvgToFiles(svgData: SvgData) async {
        do {
            let documentsPath = FileManager.default.urls(for: .documentDirectory, 
                                                       in: .userDomainMask)[0]
            let fileName = "calligraphy_\(Date().timeIntervalSince1970).svg"
            let fileURL = documentsPath.appendingPathComponent(fileName)
            
            try svgData.svgString.write(to: fileURL, atomically: true, encoding: .utf8)
            
            print("SVG saved to: \(fileURL)")
        } catch {
            errorMessage = "Failed to save SVG file"
        }
    }
    
    private func saveImageToFiles(imageData: ImageData) async {
        do {
            // Decode base64 image
            let base64String = imageData.imageBase64.replacingOccurrences(of: "data:image/\(imageData.format);base64,", with: "")
            
            guard let data = Data(base64Encoded: base64String) else {
                errorMessage = "Failed to decode image data"
                return
            }
            
            let documentsPath = FileManager.default.urls(for: .documentDirectory, 
                                                       in: .userDomainMask)[0]
            let fileName = "calligraphy_\(Date().timeIntervalSince1970).\(imageData.format)"
            let fileURL = documentsPath.appendingPathComponent(fileName)
            
            try data.write(to: fileURL)
            
            print("Image saved to: \(fileURL)")
        } catch {
            errorMessage = "Failed to save image file"
        }
    }
}

// MARK: - Extensions for better error handling

extension CalligraphyViewModel {
    func handleError(_ error: Error) {
        if let apiError = error as? APIError {
            switch apiError {
            case .httpError(401):
                errorMessage = "Invalid API key. Please check your credentials."
            case .httpError(402):
                errorMessage = "Insufficient credits. Please upgrade your plan."
            case .httpError(429):
                errorMessage = "Rate limit exceeded. Please try again later."
            default:
                errorMessage = apiError.localizedDescription
            }
        } else {
            errorMessage = error.localizedDescription
        }
    }
}

// MARK: - Input Validation

extension CalligraphyViewModel {
    func validateInput(text: String) -> Bool {
        let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
        
        guard !trimmedText.isEmpty else {
            errorMessage = "Please enter some text"
            return false
        }
        
        guard trimmedText.count >= 2 else {
            errorMessage = "Text must be at least 2 characters long"
            return false
        }
        
        guard trimmedText.count <= 200 else {
            errorMessage = "Text must be less than 200 characters"
            return false
        }
        
        return true
    }
}
UIKit Implementation (Alternative)

ViewController.swift

swift
Click to copy
// ViewController.swift - UIKit Implementation
import UIKit

class CalligraphyViewController: UIViewController {
    
    // MARK: - IBOutlets
    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var languageSegmentedControl: UISegmentedControl!
    @IBOutlet weak var fontStyleSegmentedControl: UISegmentedControl!
    @IBOutlet weak var countSlider: UISlider!
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var generateButton: UIButton!
    @IBOutlet weak var resultsCollectionView: UICollectionView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var creditsLabel: UILabel!
    
    // MARK: - Properties
    private var results: [CalligraphyResult] = []
    private let apiService = CalligraphyAPIService.shared
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupCollectionView()
    }
    
    // MARK: - Setup Methods
    
    private func setupUI() {
        title = "Calligraphy Generator"
        
        textView.text = "नमस्ते दुनिया"
        textView.layer.borderColor = UIColor.systemGray4.cgColor
        textView.layer.borderWidth = 1
        textView.layer.cornerRadius = 8
        
        languageSegmentedControl.setTitle("Hindi", forSegmentAt: 0)
        languageSegmentedControl.setTitle("Marathi", forSegmentAt: 1)
        languageSegmentedControl.setTitle("Gujarati", forSegmentAt: 2)
        languageSegmentedControl.setTitle("English", forSegmentAt: 3)
        languageSegmentedControl.selectedSegmentIndex = 0
        
        fontStyleSegmentedControl.setTitle("Calligraphy", forSegmentAt: 0)
        fontStyleSegmentedControl.setTitle("Decorative", forSegmentAt: 1)
        fontStyleSegmentedControl.setTitle("Publication", forSegmentAt: 2)
        fontStyleSegmentedControl.selectedSegmentIndex = 0
        
        countSlider.minimumValue = 1
        countSlider.maximumValue = 5
        countSlider.value = 3
        updateCountLabel()
        
        generateButton.layer.cornerRadius = 8
        generateButton.backgroundColor = .systemBlue
        
        activityIndicator.hidesWhenStopped = true
        creditsLabel.isHidden = true
    }
    
    private func setupCollectionView() {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: (view.frame.width - 48) / 2, height: 200)
        layout.minimumInteritemSpacing = 16
        layout.minimumLineSpacing = 16
        layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
        
        resultsCollectionView.collectionViewLayout = layout
        resultsCollectionView.delegate = self
        resultsCollectionView.dataSource = self
        resultsCollectionView.register(CalligraphyResultCell.self, 
                                     forCellWithReuseIdentifier: "CalligraphyResultCell")
    }
    
    private func updateCountLabel() {
        countLabel.text = "Count: \(Int(countSlider.value))"
    }
    
    // MARK: - IBActions
    
    @IBAction func sliderValueChanged(_ sender: UISlider) {
        updateCountLabel()
    }
    
    @IBAction func generateButtonTapped(_ sender: UIButton) {
        generateCalligraphy()
    }
    
    @IBAction func clearButtonTapped(_ sender: UIButton) {
        results.removeAll()
        resultsCollectionView.reloadData()
        creditsLabel.isHidden = true
    }
    
    // MARK: - API Methods
    
    private func generateCalligraphy() {
        guard !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            showAlert(title: "Error", message: "Please enter some text")
            return
        }
        
        setLoading(true)
        
        let language = getSelectedLanguage()
        let fontStyle = getSelectedFontStyle()
        let count = Int(countSlider.value)
        
        Task {
            do {
                let data = try await apiService.generateCalligraphy(
                    text: textView.text,
                    language: language,
                    fontStyle: fontStyle,
                    count: count
                )
                
                await MainActor.run {
                    self.results = data.results
                    self.resultsCollectionView.reloadData()
                    self.updateCreditsLabel(usage: data.usage)
                    self.setLoading(false)
                }
                
            } catch {
                await MainActor.run {
                    self.setLoading(false)
                    self.showAlert(title: "Error", message: error.localizedDescription)
                }
            }
        }
    }
    
    private func getSelectedLanguage() -> String {
        switch languageSegmentedControl.selectedSegmentIndex {
        case 0: return "hindi"
        case 1: return "marathi"
        case 2: return "gujarati"
        case 3: return "english"
        default: return "hindi"
        }
    }
    
    private func getSelectedFontStyle() -> String {
        switch fontStyleSegmentedControl.selectedSegmentIndex {
        case 0: return "calligraphy"
        case 1: return "decorative"
        case 2: return "publication"
        default: return "calligraphy"
        }
    }
    
    // MARK: - Helper Methods
    
    private func setLoading(_ loading: Bool) {
        if loading {
            activityIndicator.startAnimating()
            generateButton.isEnabled = false
        } else {
            activityIndicator.stopAnimating()
            generateButton.isEnabled = true
        }
    }
    
    private func updateCreditsLabel(usage: CalligraphyUsage) {
        creditsLabel.text = "Credits remaining: \(usage.remainingCredits)"
        creditsLabel.isHidden = false
    }
    
    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    private func showResultDetail(_ result: CalligraphyResult) {
        let alert = UIAlertController(
            title: "Calligraphy Result",
            message: "Text: \(result.resultText)\nFont: \(result.fontFamily)\nVariants: \(result.appliedVariants.count)",
            preferredStyle: .alert
        )
        
        alert.addAction(UIAlertAction(title: "Download SVG", style: .default) { _ in
            self.downloadSvg(result: result)
        })
        
        alert.addAction(UIAlertAction(title: "Save to Photos", style: .default) { _ in
            self.saveToPhotos(result: result)
        })
        
        alert.addAction(UIAlertAction(title: "Close", style: .cancel))
        
        present(alert, animated: true)
    }
    
    private func downloadSvg(result: CalligraphyResult) {
        setLoading(true)
        
        Task {
            do {
                let svgData = try await apiService.downloadSvg(
                    resultText: result.resultText,
                    fontName: result.fontFamily
                )
                
                await MainActor.run {
                    self.setLoading(false)
                    self.saveSvgToFiles(svgData: svgData)
                }
                
            } catch {
                await MainActor.run {
                    self.setLoading(false)
                    self.showAlert(title: "Error", message: "Failed to download SVG")
                }
            }
        }
    }
    
    private func saveToPhotos(result: CalligraphyResult) {
        guard let urlString = result.cdnUrl, let url = URL(string: urlString) else {
            showAlert(title: "Error", message: "No image URL available")
            return
        }
        
        Task {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                if let image = UIImage(data: data) {
                    await MainActor.run {
                        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
                        self.showAlert(title: "Success", message: "Image saved to Photos")
                    }
                }
            } catch {
                await MainActor.run {
                    self.showAlert(title: "Error", message: "Failed to save image")
                }
            }
        }
    }
    
    private func saveSvgToFiles(svgData: SvgData) {
        do {
            let documentsPath = FileManager.default.urls(for: .documentDirectory, 
                                                       in: .userDomainMask)[0]
            let fileName = "calligraphy_\(Date().timeIntervalSince1970).svg"
            let fileURL = documentsPath.appendingPathComponent(fileName)
            
            try svgData.svgString.write(to: fileURL, atomically: true, encoding: .utf8)
            
            showAlert(title: "Success", message: "SVG saved to Files app")
        } catch {
            showAlert(title: "Error", message: "Failed to save SVG file")
        }
    }
}

// MARK: - Collection View Data Source & Delegate

extension CalligraphyViewController: UICollectionViewDataSource, UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return results.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalligraphyResultCell", for: indexPath) as! CalligraphyResultCell
        cell.configure(with: results[indexPath.item])
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        showResultDetail(results[indexPath.item])
    }
}

// MARK: - Custom Collection View Cell

class CalligraphyResultCell: UICollectionViewCell {
    
    private let imageView = UIImageView()
    private let textLabel = UILabel()
    private let fontLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupUI() {
        backgroundColor = .systemBackground
        layer.cornerRadius = 8
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOffset = CGSize(width: 0, height: 2)
        layer.shadowRadius = 4
        layer.shadowOpacity = 0.1
        
        imageView.contentMode = .scaleAspectFit
        imageView.backgroundColor = .systemGray6
        imageView.layer.cornerRadius = 4
        
        textLabel.font = .systemFont(ofSize: 12, weight: .medium)
        textLabel.numberOfLines = 2
        
        fontLabel.font = .systemFont(ofSize: 10)
        fontLabel.textColor = .systemGray
        
        [imageView, textLabel, fontLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            addSubview($0)
        }
        
        NSLayoutConstraint.activate([
            imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
            imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
            imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
            imageView.heightAnchor.constraint(equalToConstant: 120),
            
            textLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
            textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
            textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
            
            fontLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 4),
            fontLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
            fontLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
            fontLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
        ])
    }
    
    func configure(with result: CalligraphyResult) {
        textLabel.text = result.resultText
        fontLabel.text = result.fontFamily
        
        if let urlString = result.cdnUrl, let url = URL(string: urlString) {
            Task {
                do {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    let image = UIImage(data: data)
                    
                    await MainActor.run {
                        self.imageView.image = image
                    }
                } catch {
                    await MainActor.run {
                        self.imageView.image = nil
                    }
                }
            }
        }
    }
}
What You'll Build
Native iOS App
SwiftUI and UIKit implementations
MVVM Pattern
Clean architecture with ObservableObject
Async/Await Support
Modern Swift concurrency
File Management
Save to Photos and Files app
Requirements
iOS 13.0+ (SwiftUI) / iOS 11.0+ (UIKit)
Swift 5.0+
Xcode 12+
API Key from Calligraphy API