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
- Explore SwiftUI Components provided by Cuppa
- Review the Component Reference for detailed APIs
- Learn about iOS Best Practices