I built and shipped an AI-powered iOS app with no prior iOS experience. This guide is everything I learned — the tools, the decisions, the gotchas, and the workflow that got an app from idea to the App Store.
The app is RDR2 Companion — an AI assistant for Red Dead Redemption 2 players. It uses Claude as its AI backbone, Railway as a secure API proxy, and SwiftUI for the iOS frontend. The same architecture works for any AI-powered iOS app.
Who this is for: Developers with some coding background who want to build an AI iOS app but haven't done iOS development before. I had 25 years of engineering experience but zero Swift — the stack is genuinely learnable.
The Full Stack
Before we dive in, here's everything in the stack and why each piece is there:
Open Xcode → New Project → iOS → App. Give it a name, set the interface to SwiftUI, and language to Swift. Everything else can stay default.
Your project structure out of the box looks like this:
YourApp/
YourAppApp.swift
ContentView.swift
Assets.xcassets/
I recommend creating these folders early so the project doesn't become a flat mess:
Views/
Models/
Services/
SwiftUI vs UIKit — make this decision once
If you're starting in 2025+, choose SwiftUI. Here's why:
- Declarative syntax — much closer to React/modern web frameworks than UIKit's imperative approach
- Live Previews — see your UI change as you type, no need to launch the simulator for every change
- Apple is investing here — new APIs land in SwiftUI first
- Less boilerplate — a button in SwiftUI is 2 lines; UIKit requires a subclass and delegate
The main downside: some complex UI patterns (very custom animations, certain table/collection view behaviors) are still easier in UIKit. But for an AI chat interface + informational screens, SwiftUI is perfect.
Key SwiftUI patterns to know upfront
@State private var message = ""
@StateObject private var viewModel = ChatViewModel()
@ObservedObject var viewModel: ChatViewModel
NavigationStack {
ContentView()
.navigationTitle("Chat")
}
Tip: Set up Claude Code in your terminal before writing any Swift. When you're stuck on a build error or need a complex SwiftUI component, Claude Code reads your entire codebase and writes the exact code you need. It was the single biggest accelerant in this project.
Do this before writing significant code. Git saves you constantly.
git init
git add .
git commit -m "feat: initial Xcode project setup"
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git push -u origin main
Create a .gitignore file in your project root — Xcode generates files you never want to commit:
# Xcode
*.xcuserstate
xcuserdata/
.DS_Store
DerivedData/
# Secrets — NEVER commit these
Secrets.swift
.env
Branch strategy that works for solo developers
- main — stable, releasable code only. TestFlight builds come from here.
- dev — active development. Merge into main when a feature is complete.
- feature/name — for bigger features, branch off dev.
This is the most important architecture decision in the entire project. Never put your Anthropic API key directly in your iOS app. The app binary is publicly downloadable — anyone who downloads it can extract the key and bill charges to your account.
The solution is a simple backend proxy. Your iOS app sends messages to your Railway server, the server adds the API key and forwards the request to Anthropic, then sends the response back. The key never touches the device.
iPhone → POST /api/chat → Railway server → Anthropic API → response → iPhone
Set up Railway
- Go to railway.app and create a free account
- Create a new project → Deploy from GitHub repo
- In Railway's Variables tab, add:
ANTHROPIC_API_KEY = sk-ant-...
- Railway gives you a public URL like
your-app.up.railway.app
The proxy server (Node.js / Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/chat', async (req, res) => {
if (req.headers['x-app-key'] !== process.env.APP_SECRET_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify(req.body)
});
const data = await response.json();
res.json(data);
});
app.listen(process.env.PORT || 3000);
The APP_SECRET_KEY trick: Set a second environment variable APP_SECRET_KEY in Railway, and hardcode the same value in your Swift Constants.swift. Your iOS app sends it in every request header. The server rejects requests without it. This prevents random people from hitting your Railway endpoint and running up your Anthropic bill — without requiring user accounts or complex auth.
The core of the app is a chat UI — a scrolling message list and a text input at the bottom. Here's the architecture that works well:
class ChatViewModel: ObservableObject {
@Published var messages: [ChatMessage] = []
@Published var isLoading = false
func send(text: String) async {
isLoading = true
messages.append(ChatMessage(role: .user, content: text))
let reply = await ClaudeService.shared.sendMessage(text)
messages.append(ChatMessage(role: .assistant, content: reply))
isLoading = false
}
}
func sendMessage(_ text: String) async -> String {
var request = URLRequest(url: URL(string: Constants.backendURL)!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(Constants.appSecretKey, forHTTPHeaderField: "x-app-key")
let body: [String: Any] = [
"model": "claude-haiku-4-5-20251001",
"max_tokens": 1024,
"system": systemPrompt,
"messages": [["role": "user", "content": text]]
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
let (data, _) = try! await URLSession.shared.data(for: request)
}
Model selection: Haiku vs Sonnet
For a chat interface where responses should feel fast, use claude-haiku-4-5. It's 5–10x cheaper than Sonnet and returns responses in 1–3 seconds. Use Sonnet for tasks that require deeper reasoning — not conversational Q&A.
Writing the system prompt
The system prompt is what turns Claude from a general AI into a domain expert. Be specific — the more context you give about the domain, tone, and format, the better the responses.
You are an expert guide for Red Dead Redemption 2. You have
encyclopedic knowledge of missions, characters, locations,
collectibles, honor mechanics, hunting, fishing, and secrets.
Tone: knowledgeable but friendly. Like a friend who has
completed RDR2 100% and loves helping others.
Format: short paragraphs, no markdown headers unless listing
multiple steps. Always be specific — no vague answers.
Xcode has an excellent iOS Simulator built in. Use it constantly — you don't need a physical device for most testing.
xcodebuild -scheme YourApp \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
build
xcrun simctl io booted screenshot ~/Desktop/screen.png
Things to test on a real device that the simulator can't replicate:
- Network latency (simulator is always fast; real devices vary)
- Keyboard behavior (especially avoiding keyboard overlap on chat inputs)
- Face ID / Touch ID flows
- How the app feels to actually use
TestFlight is Apple's beta testing platform — a required step before App Store submission. You upload a build, share a link, and testers install it on their device.
- In Xcode → Product → Archive. This creates a release build.
- In the Organizer that opens, click Distribute App → TestFlight & App Store
- Upload. Wait 5–15 minutes for processing.
- In App Store Connect, go to your app → TestFlight → Share the install link
You need an Apple Developer Program membership ($99/yr) to distribute via TestFlight or the App Store. There's no way around this.
The App Store submission is mostly done in App Store Connect. You'll need:
- App name and description — choose carefully; this is your App Store listing
- Screenshots — required for iPhone (6.7" is most important) and iPad if you support it. Size: 1290×2796 px for iPhone 16 Pro Max
- App icon — 1024×1024 px PNG, no transparency, no rounded corners (Apple applies them)
- Privacy label — declare what data your app collects. If you use the proxy pattern above, your app collects nothing — select "Data Not Collected"
- Age rating — answer the questionnaire honestly
Once everything is filled out, submit for review. The first review typically takes 1–3 days. Subsequent updates are often approved same-day.
The Tools Summary
Here's the complete toolkit with honest assessments:
| Tool |
Cost |
Verdict |
| Xcode + SwiftUI |
Free |
Essential. No alternative for iOS. |
| Claude API |
~$0.001/msg (Haiku) |
Excellent for expert personas. Start with Haiku. |
| Railway |
$5/mo or less |
Best DX for solo devs. Deploy from GitHub in 60s. |
| GitHub |
Free |
Non-negotiable. Use it from day one. |
| Claude Code |
Subscription |
Worth every dollar. Best AI coding assistant available. |
| Apple Developer Program |
$99/yr |
Required to ship. Pay it, no alternative. |
| Vercel (for website) |
Free tier |
Best static hosting. GitHub push = instant deploy. |
Lessons Learned
- Read the CLAUDE.md pattern. Create a CLAUDE.md file in your project root with project context — AI coding assistants load this automatically and dramatically improve their output.
- Build the proxy first. Don't touch the iOS UI until your backend is working and your Railway URL is responding. Test it with curl before writing a single line of Swift networking code.
- Screenshot everything. After any UI change, take a simulator screenshot and compare. The Xcode Live Preview is helpful but the simulator is the ground truth.
- Ship earlier than feels comfortable. The RDR2 app went from zero to App Store submission in weeks. Version 1.0 doesn't have to be perfect — it has to be good enough to learn from real users.
- The App Store review is not as scary as it sounds. Follow the guidelines, set your privacy labels honestly, and don't do anything obviously against the rules. Most apps are approved.