Skip to main content
A simple iOS app that generates creative slogans using local AI models, with no internet connection required.

This is what you will learn​

By the end of this guide, you’ll understand:
  • How to integrate the LeapSDK into your iOS project
  • How to load and run AI models locally on an iPhone or iPad
  • How to implement real-time streaming text generation

Understanding the Architecture​

Before we write code, let’s understand what we’re building. The LeapSlogan app has a clean, three-layer architecture:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚      SwiftUI View Layer         β”‚ ← User Interfaceβ”‚  (ContentView, UI Components)   β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚     ViewModel Layer             β”‚ ← Business Logicβ”‚  (SloganViewModel, @Observable) β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚      LeapSDK Layer              β”‚ ← AI Inferenceβ”‚  (ModelRunner, Conversation)    β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Let’s trace what happens when a user generates a slogan:
1. User enters "coffee shop" and taps Generate   ↓2. UI disables input and shows "Generating..."   ↓3. ViewModel creates prompt with business type   ↓4. ChatMessage is sent to Conversation   ↓5. LeapSDK starts model inference   ↓6. Tokens stream back one-by-one   β”œβ”€ "Wake" β†’ UI updates   β”œβ”€ " up" β†’ UI updates   β”œβ”€ " to" β†’ UI updates   β”œβ”€ " flavor" β†’ UI updates   └─ "!" β†’ UI updates   ↓7. .complete event fires   ↓8. UI re-enables input, shows final slogan
Let’s start building the app!

Environment setup​

You will need:
  • Xcode 15.0+ with Swift 5.9 or later
  • iOS 15.0+ deployment target
  • A physical iOS device (iPhone or iPad) for best performance
    • The iOS Simulator works but will be significantly slower
  • Basic familiarity with SwiftUI and Swift’s async/await syntax

Step 1: Create a New Xcode Project​

  1. Open Xcode and create a new iOS App
  2. Choose SwiftUI for the interface
  3. Set minimum deployment target to iOS 15.0

Step 2: Add LeapSDK via Swift Package Manager​

LeapSDK is distributed as a Swift Package, making integration straightforward:
  1. In Xcode, go to File β†’ Add Package Dependencies
  2. Enter the repository URL:
    https://github.com/Liquid4All/leap-ios.git
    
  3. Select the latest version (0.6.0 or newer)
  4. Add both products to your target:
    • βœ… LeapSDK
    • βœ… LeapSDKTypes
Important: Starting with version 0.5.0, you must add both LeapSDK and LeapSDKTypes for proper runtime linking.

Step 3: Download a Model Bundle​

Now we need an AI model. LeapSDK uses model bundles - packaged files containing the model and its configuration:
  1. Visit the Leap Model Library
  2. For this tutorial, download a small model like LFM2-350M (great for mobile, ~500MB)
  3. Download the .bundle file for your chosen model
  4. Drag the .bundle file into your Xcode project
  5. βœ… Make sure β€œAdd to target” is checked
Your project structure should now look like:
YourApp/β”œβ”€β”€ YourApp.swiftβ”œβ”€β”€ ContentView.swiftβ”œβ”€β”€ Models/β”‚   └── LFM2-350M-8da4w_output_8da8w-seq_4096.bundle  ← Your model└── Assets.xcassets

Step 4: Building the ViewModel​

The ViewModel is the heart of our app. It manages the model lifecycle and handles generation. Let’s build it step by step.

Step 4.1: Create the Basic Structure​

Create a new Swift file called SloganViewModel.swift:
import Foundationimport SwiftUIimport LeapSDKimport Observation@Observableclass SloganViewModel {    // MARK: - Published State    var isModelLoading = true    var isGenerating = false    var generatedSlogan = ""    var errorMessage: String?        // MARK: - Private Properties    private var modelRunner: ModelRunner?    private var conversation: Conversation?        // MARK: - Initialization    init() {        // Model will be loaded when view appears    }}
What’s happening here?
  • @Observable is Swift’s new observation macro (iOS 17+, but works great on iOS 15 with backports)
  • We track four pieces of UI state: loading, generating, the slogan text, and any errors
  • ModelRunner and Conversation are privateβ€”these are our LeapSDK objects

Step 4.2: Implement Model Loading​

Add the model loading function:
// MARK: - Model Management@MainActorfunc setupModel() async {    isModelLoading = true    errorMessage = nil        do {        // 1. Get the model bundle URL from app bundle        guard let modelURL = Bundle.main.url(            forResource: "qwen-0.6b",  // Change to match your bundle name            withExtension: "bundle"        ) else {            errorMessage = "Model bundle not found in app bundle"            isModelLoading = false            return        }                // 2. Load the model using LeapSDK        print("Loading model from: \(modelURL.path)")        modelRunner = try await Leap.load(url: modelURL)                // 3. Create an initial conversation        conversation = Conversation(            modelRunner: modelRunner!,            history: []        )                isModelLoading = false        print("Model loaded successfully!")            } catch {        errorMessage = "Failed to load model: \(error.localizedDescription)"        isModelLoading = false        print("Error loading model: \(error)")    }}
Understanding the code:
  1. Bundle lookup: We find the model bundle in our app’s resources
  2. Async loading: Leap.load() is async because loading models takes time (1-5 seconds)
  3. Conversation creation: Every generation needs a Conversation object that tracks history
  4. Error handling: We catch and display any loading failures
πŸ’‘ Pro Tip: Model loading is the slowest part. In production apps, show a nice loading screen!

Step 4.3: Implement Slogan Generation​

Now for the exciting partβ€”generating slogans! Add this function:
// MARK: - Generation@MainActorfunc generateSlogan(for businessType: String) async {    // Guard against invalid states    guard let conversation = conversation,          !isGenerating else { return }        isGenerating = true    generatedSlogan = ""  // Clear previous slogan    errorMessage = nil        // 1. Create the prompt    let prompt = """    Create a catchy, memorable slogan for a \(businessType) business. \    Make it creative, concise, and impactful. \    Return only the slogan, nothing else.    """        // 2. Create a chat message    let userMessage = ChatMessage(        role: .user,        content: [.text(prompt)]    )        // 3. Generate response with streaming    let stream = conversation.generateResponse(message: userMessage)        // 4. Process the stream    do {        for await response in stream {            switch response {            case .chunk(let text):                // Append each text chunk as it arrives                generatedSlogan += text                            case .reasoningChunk(let reasoning):                // Some models output reasoning - we can log it                print("Reasoning: \(reasoning)")                            case .complete(let usage, let completeInfo):                // Generation finished!                print("βœ… Generation complete!")                print("Tokens used: \(usage.totalTokens)")                print("Speed: \(completeInfo.stats?.tokenPerSecond ?? 0) tokens/sec")                isGenerating = false            }        }    } catch {        errorMessage = "Generation failed: \(error.localizedDescription)"        isGenerating = false    }}
Breaking down the streaming API: The generateResponse() method returns an AsyncStream that emits three types of events:
  1. .chunk(text): Each piece of generated text arrives here
    • This is what makes the UI feel responsive!
    • Text appears word-by-word, just like ChatGPT
  2. .reasoningChunk(reasoning): Some models show their β€œthinking”
    • Advanced feature for models that explain their reasoning
  3. .complete(usage, info): The final event when generation finishes
    • Contains token usage statistics
    • Includes performance metrics (tokens/second)

Step 5: Building the User Interface​

Now let’s create a beautiful, interactive UI. Create or modify ContentView.swift:
import SwiftUIstruct ContentView: View {    @State private var viewModel = SloganViewModel()    @State private var businessType = ""        var body: some View {        NavigationStack {            ZStack {                // Background gradient                LinearGradient(                    colors: [.blue.opacity(0.1), .purple.opacity(0.1)],                    startPoint: .topLeading,                    endPoint: .bottomTrailing                )                .ignoresSafeArea()                                VStack(spacing: 24) {                    if viewModel.isModelLoading {                        modelLoadingView                    } else {                        mainContentView                    }                }                .padding()            }            .navigationTitle("AI Slogan Generator")            .navigationBarTitleDisplayMode(.large)        }        .task {            // Load model when view appears            await viewModel.setupModel()        }    }        // MARK: - Subviews        private var modelLoadingView: some View {        VStack(spacing: 20) {            ProgressView()                .scaleEffect(1.5)            Text("Loading AI Model...")                .font(.headline)            Text("This may take a few seconds")                .font(.caption)                .foregroundColor(.secondary)        }    }        private var mainContentView: some View {        VStack(spacing: 24) {            // Error message if any            if let error = viewModel.errorMessage {                errorBanner(error)            }                        // Instructions            instructionsCard                        // Input field            businessTypeInput                        // Generate button            generateButton                        // Generated slogan display            if !viewModel.generatedSlogan.isEmpty {                sloganResultCard            }                        Spacer()        }    }        private var instructionsCard: some View {        VStack(alignment: .leading, spacing: 12) {            Label("How it works", systemImage: "lightbulb.fill")                .font(.headline)                .foregroundColor(.blue)                        Text("Enter a business type and I'll generate a creative slogan using AIβ€”completely on your device!")                .font(.subheadline)                .foregroundColor(.secondary)        }        .padding()        .frame(maxWidth: .infinity, alignment: .leading)        .background(Color.blue.opacity(0.1))        .cornerRadius(12)    }        private var businessTypeInput: some View {        VStack(alignment: .leading, spacing: 8) {            Text("Business Type")                .font(.subheadline)                .fontWeight(.semibold)                        TextField("e.g., coffee shop, tech startup, bakery", text: $businessType)                .textFieldStyle(.roundedBorder)                .autocapitalization(.none)                .disabled(viewModel.isGenerating)        }    }        private var generateButton: some View {        Button(action: {            Task {                await viewModel.generateSlogan(for: businessType)            }        }) {            HStack {                if viewModel.isGenerating {                    ProgressView()                        .tint(.white)                } else {                    Image(systemName: "sparkles")                }                                Text(viewModel.isGenerating ? "Generating..." : "Generate Slogan")                    .fontWeight(.semibold)            }            .frame(maxWidth: .infinity)            .padding()            .background(                businessType.isEmpty || viewModel.isGenerating                     ? Color.gray                     : Color.blue            )            .foregroundColor(.white)            .cornerRadius(12)        }        .disabled(businessType.isEmpty || viewModel.isGenerating)    }        private var sloganResultCard: some View {        VStack(alignment: .leading, spacing: 12) {            HStack {                Label("Your Slogan", systemImage: "quote.bubble.fill")                    .font(.headline)                    .foregroundColor(.purple)                                Spacer()                                // Copy button                Button(action: {                    UIPasteboard.general.string = viewModel.generatedSlogan                }) {                    Image(systemName: "doc.on.doc")                        .foregroundColor(.blue)                }            }                        Text(viewModel.generatedSlogan)                .font(.title3)                .fontWeight(.medium)                .foregroundColor(.primary)                .padding()                .frame(maxWidth: .infinity, alignment: .leading)                .background(Color.purple.opacity(0.1))                .cornerRadius(8)        }        .padding()        .background(Color.white)        .cornerRadius(12)        .shadow(color: .black.opacity(0.1), radius: 5, y: 2)    }        private func errorBanner(_ message: String) -> some View {        HStack {            Image(systemName: "exclamationmark.triangle.fill")            Text(message)                .font(.caption)            Spacer()        }        .padding()        .background(Color.red.opacity(0.1))        .foregroundColor(.red)        .cornerRadius(8)    }}#Preview {    ContentView()}
UI Design Highlights:
  1. Progressive disclosure: Loading screen β†’ Main interface
  2. Clear visual feedback: Loading states, disabled states, animations
  3. Helpful instructions: Users understand what to do immediately
  4. Polished details: Gradient background, shadows, rounded corners
  5. Copy functionality: Users can easily copy the generated slogan

Troubleshooting Common Issues​

Solution:
  • Check that .bundle file is in Xcode project
  • Verify β€œTarget Membership” is checked
  • Ensure bundle name in code matches actual filename
Solution:
  • Test on a physical device (Simulator is unreliable)
  • Ensure iOS version is 15.0+
  • Check device has enough free storage (~2-3x model size)
  • Try a smaller model first
Solution:
  • Use a physical device (10-100x faster than Simulator)
  • Choose a smaller model (350M-1B)
  • Lower maxTokens in GenerationOptions
  • Reduce temperature for faster but less creative output
Solution:
  • Ensure both LeapSDK and LeapSDKTypes are added
  • Check frameworks are set to β€œEmbed & Sign”
  • Clean build folder (Cmd+Shift+K)
  • Restart Xcode

Next Steps​

Congratulations! πŸŽ‰ You’ve built a fully functional on-device AI app. Here are some ideas to expand your skills:

Immediate Next Projects​

  1. LeapChat: Build a full chat interface with history
  2. Add Structured Output: Use @Generatable macros
    • Generate JSON data structures
    • Validate output format at compile-time
  3. Implement Function Calling: Let AI call your functions

Need help?​

Join the Liquid AI Discord Community and ask. Discord Edit this page