MyCuppa

iOS Architecture

Learn how Cuppa integrates with iOS to provide a seamless cross-platform development experience while maintaining native performance and feel.

Architecture Overview

Cuppa for iOS follows a layered architecture that separates concerns and enables code sharing:

┌─────────────────────────────────────┐
│         SwiftUI Views               │  Native iOS UI
├─────────────────────────────────────┤
│         View Models                 │  Swift business logic
├─────────────────────────────────────┤
│         Cuppa Bridge                │  Swift ↔ TypeScript
├─────────────────────────────────────┤
│      Shared Business Logic          │  TypeScript/JavaScript
├─────────────────────────────────────┤
│         Cuppa Core                  │  Platform abstractions
├─────────────────────────────────────┤
│    Native iOS APIs & Frameworks     │  UIKit, Foundation, etc.
└─────────────────────────────────────┘

Key Components

1. SwiftUI Views

Your iOS UI is built entirely with SwiftUI, ensuring native look, feel, and performance:

import SwiftUI
import CuppaUI

struct ProductView: View {
    @StateObject private var viewModel: ProductViewModel

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                // Native SwiftUI components
                AsyncImage(url: viewModel.imageURL)
                Text(viewModel.title)
                    .font(.title)
                CuppaButton(title: "Add to Cart") {
                    viewModel.addToCart()
                }
            }
        }
    }
}

2. View Models

ViewModels connect your SwiftUI views to shared business logic:

import Combine
import CuppaCore

class ProductViewModel: ObservableObject {
    @Published var product: Product?
    @Published var isLoading = false

    private let productId: String
    private var cancellables = Set<AnyCancellable>()

    init(productId: String) {
        self.productId = productId
        loadProduct()
    }

    func loadProduct() {
        isLoading = true

        Task {
            // Call shared TypeScript business logic
            let data = try await CuppaBridge.call(
                function: "getProduct",
                args: ["id": productId]
            )

            await MainActor.run {
                self.product = data
                self.isLoading = false
            }
        }
    }
}

3. Cuppa Bridge

The bridge enables seamless communication between Swift and TypeScript:

import CuppaBridge

// Call TypeScript from Swift
let result = try await CuppaBridge.call(
    function: "calculateTotal",
    args: ["items": cartItems, "taxRate": 0.08]
)

// Register Swift function callable from TypeScript
CuppaBridge.register("showNativeDialog") { args in
    let title = args["title"] as? String ?? ""
    let message = args["message"] as? String ?? ""

    // Show native iOS alert
    let alert = UIAlertController(
        title: title,
        message: message,
        preferredStyle: .alert
    )
    // ... present alert
}

4. Shared Business Logic

Your core application logic is written once in TypeScript and shared across platforms:

// shared/src/services/cart.ts
export class CartService {
  calculateTotal(items: CartItem[], taxRate: number): number {
    const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    const tax = subtotal * taxRate
    return subtotal + tax
  }

  async checkout(items: CartItem[]): Promise<Order> {
    // Shared checkout logic
    const response = await fetch('/api/checkout', {
      method: 'POST',
      body: JSON.stringify({ items }),
    })
    return response.json()
  }
}

Data Flow

Unidirectional Data Flow

Cuppa follows a unidirectional data flow pattern:

User Action (SwiftUI)
    ↓
ViewModel Method
    ↓
Shared Business Logic (TypeScript)
    ↓
API Call / Data Processing
    ↓
Update Published Properties
    ↓
SwiftUI View Re-renders

Example Flow

// 1. User taps button in SwiftUI
CuppaButton(title: "Refresh") {
    viewModel.refresh()
}

// 2. ViewModel calls shared logic
func refresh() {
    Task {
        // 3. Shared TypeScript function fetches data
        let data = try await CuppaBridge.call(
            function: "fetchLatestItems",
            args: []
        )

        // 4. Update published property
        await MainActor.run {
            self.items = data
        }
    }
}

// 5. SwiftUI automatically re-renders

Plugin Architecture

Cuppa plugins extend functionality through a standardized interface:

// Plugin definition
protocol CuppaPlugin {
    var name: String { get }
    func initialize(config: [String: Any])
    func cleanup()
}

// Using plugins
import CuppaAuth

class AppDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Initialize Cuppa plugins
        CuppaPluginManager.register(CuppaAuth.plugin)
        CuppaPluginManager.register(CuppaAnalytics.plugin)

        return true
    }
}

State Management

Local State (SwiftUI)

Use SwiftUI property wrappers for view-specific state:

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        Button("Count: \\(count)") {
            count += 1
        }
    }
}

Shared State (ViewModel)

Use @Published for state shared across views:

class AppState: ObservableObject {
    @Published var user: User?
    @Published var isAuthenticated = false

    static let shared = AppState()
}

Global State (TypeScript)

Manage global state in shared TypeScript code:

// shared/src/store/auth.ts
import { create } from 'zustand'

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: null,
  setUser: (user) => set({ user }),
  setToken: (token) => set({ token }),
}))

Performance Considerations

Lazy Loading

Load views and data only when needed:

struct MainView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Products") {
                    // Lazily loaded
                    ProductListView()
                }
            }
        }
    }
}

Caching

Cache data to minimize bridge calls:

class DataCache {
    private var cache: [String: Any] = [:]

    func get<T>(_ key: String) -> T? {
        cache[key] as? T
    }

    func set<T>(_ key: String, value: T) {
        cache[key] = value
    }
}

Background Processing

Offload heavy work from the main thread:

Task {
    // Runs on background thread
    let processedData = await heavyProcessing()

    // Update UI on main thread
    await MainActor.run {
        self.data = processedData
    }
}

Next Steps