A Slogan Generator iOS app
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
- Open Xcode and create a new iOS App
- Choose SwiftUI for the interface
- 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:
- In Xcode, go to File → Add Package Dependencies
- Enter the repository URL:
https://github.com/Liquid4All/leap-ios.git - Select the latest version (0.6.0 or newer)
- Add both products to your target:
- ✅
LeapSDK - ✅
LeapSDKTypes
- ✅
Important: Starting with version 0.5.0, you must add both
LeapSDKandLeapSDKTypesfor 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:
- Visit the Leap Model Library
- For this tutorial, download a small model like LFM2-350M (great for mobile, ~500MB)
- Download the
.bundlefile for your chosen model - Drag the
.bundlefile into your Xcode project - ✅ 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 Foundation
import SwiftUI
import LeapSDK
import Observation
@Observable
class 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?
@Observableis 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
ModelRunnerandConversationare private—these are our LeapSDK objects
Step 4.2: Implement Model Loading
Add the model loading function:
// MARK: - Model Management
@MainActor
func 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:
- Bundle lookup: We find the model bundle in our app's resources
- Async loading:
Leap.load()is async because loading models takes time (1-5 seconds) - Conversation creation: Every generation needs a
Conversationobject that tracks history - 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
@MainActor
func 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:
-
.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
-
.reasoningChunk(reasoning): Some models show their "thinking"- Advanced feature for models that explain their reasoning
-
.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 SwiftUI
struct 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:
- Progressive disclosure: Loading screen → Main interface
- Clear visual feedback: Loading states, disabled states, animations
- Helpful instructions: Users understand what to do immediately
- Polished details: Gradient background, shadows, rounded corners
- Copy functionality: Users can easily copy the generated slogan
Troubleshooting Common Issues
Issue 1: "Model bundle not found"
Solution:
- Check that
.bundlefile is in Xcode project - Verify "Target Membership" is checked
- Ensure bundle name in code matches actual filename
Issue 2: "Failed to load model"
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
Issue 3: Slow generation speed
Solution:
- Use a physical device (10-100x faster than Simulator)
- Choose a smaller model (350M-1B)
- Lower
maxTokensin GenerationOptions - Reduce temperature for faster but less creative output
Issue 4: App crashes on launch
Solution:
- Ensure both
LeapSDKandLeapSDKTypesare 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
-
LeapChat: Build a full chat interface with history
- Check out the LeapChatExample
-
Add Structured Output: Use
@Generatablemacros- Generate JSON data structures
- Validate output format at compile-time
-
Implement Function Calling: Let AI call your functions
- Weather lookup, calculations, database queries
- See Function Calling Guide
Need help?
Join the Liquid AI Discord Community and ask.