●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Rork × Core ML Custom Model Development × On-Device AI
Master custom machine learning model creation with Create ML and Core ML optimization. Deploy on-device AI features in Rork Max apps. Comprehensive guide covering model training, optimization, Vision framework integration, Natural Language processing, performance tuning, and privacy-safe design patterns.
Setup and context: The Power of On-Device AI in Mobile Apps
Machine learning has become democratized. Yet for iOS developers, what truly matters is AI that works without network latency, respects privacy, and preserves battery life. That's on-device AI.
Rork Max offers complete support for Apple's Core ML framework, enabling you to integrate custom-trained ML models directly into your apps. Here we follow the entire path—from training with Create ML to production deployment—at implementation-level detail.
From defect detection on a factory line to sentiment analysis of customer feedback to predictive features in a consumer app, on-device AI removes cloud dependencies while keeping user trust intact. Let's look at how to build production-grade on-device intelligence with Rork Max.
On-Device AI Architecture and Core ML's Role
Understanding On-Device AI
On-device AI executes ML inference directly on the device's processors (CPU, GPU, Neural Engine) rather than sending data to cloud APIs. This approach delivers compelling advantages.
Key Benefits
Privacy by Default: Data never leaves the device
Sub-Second Latency: Millisecond-level inference without network roundtrips
Offline Operation: Works without internet connectivity
Cost Efficiency: No API costs or infrastructure overhead
Battery Optimization: Modern chips (Neural Engine) are power-efficient for inference
Core ML: The Unified Interface
Apple's Core ML is a unified framework that converts models trained in popular frameworks—TensorFlow, PyTorch, Scikit-learn, and others—into a single format (.mlmodel) that runs efficiently on iOS devices.
Core ML Strengths
Multi-Framework Support: TensorFlow, PyTorch, ONNX all convert to .mlmodel
Automatic Optimization: Intelligently dispatches to CPU, GPU, or Neural Engine
Tight Integration: Works seamlessly with Vision, Natural Language, and other frameworks
Memory Efficiency: Built-in quantization reduces model footprint significantly
Rork Max provides native support for Core ML, enabling you to integrate models without touching complex Swift code.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Hands-on custom model training with Create ML and transfer learning techniques
✦Production-ready Core ML model optimization and size reduction strategies
✦End-to-end on-device AI implementation patterns in Rork Max
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Knowing When Not to Run On-Device: Routing Inference Decisions
Read this far and on-device inference starts to look like the answer to everything.
I thought so too. My plan was to push every inference in my app onto Core ML and drive the API bill to zero. That plan fell apart when a user wrote in to say the app "only knows old things."
A model baked into the device knows exactly the world it was trained on. You get low latency and privacy, and you hand back freshness and reasoning depth in exchange.
The practical answer isn't Core ML or cloud. It's choosing a target per request.
What Actually Decides the Target
The nature of the request settles it almost every time. Here are the axes I use in production.
Request characteristic
Target
Why
Carries personal data, location, or photos
Core ML (on-device)
The data never leaves the device — treat this as a promise, not a preference
Must work offline
Core ML (on-device)
Anything network-dependent breaks the moment signal drops
Short and high-frequency (completion, classification, tagging)
Core ML (on-device)
The round trip costs more than the inference itself
Needs knowledge newer than the training cutoff
Cloud LLM
An on-device model cannot answer this in principle
Needs multi-step reasoning or long-form generation
Cloud LLM
A quantized small model won't hold up
"When in doubt, stay local" is a fine default. Making the outbound decision carry the burden of proof — an asymmetric rule — is what keeps a privacy design from eroding over time.
Keep the Router in One Module
Scatter this decision across your screens and it will collapse within six months. Put it in one place.
// src/ai/inferenceTarget.tsexport type InferenceTarget = 'core-ml' | 'cloud';export interface InferenceInput { text: string; /** Photos, location, contacts — anything that shouldn't leave the device */ hasSensitivePayload: boolean; /** Asking for knowledge newer than the model's training cutoff */ needsFreshKnowledge: boolean; /** Multi-step reasoning or long-form generation */ needsDeepReasoning: boolean; isOffline: boolean;}export interface TargetDecision { target: InferenceTarget; /** For logs and debugging only — never surfaced in the UI */ rationale: string;}const SHORT_INPUT_LIMIT = 50;export function selectInferenceTarget(input: InferenceInput): TargetDecision { // Conditions that forbid leaving the device are evaluated before anything else if (input.isOffline) { return { target: 'core-ml', rationale: 'offline' }; } if (input.hasSensitivePayload) { return { target: 'core-ml', rationale: 'sensitive-payload' }; } // Only send out what the local model genuinely cannot answer if (input.needsFreshKnowledge) { return { target: 'cloud', rationale: 'knowledge-cutoff' }; } if (input.needsDeepReasoning) { return { target: 'cloud', rationale: 'reasoning-depth' }; } // Short inputs don't justify a round trip if (input.text.length <= SHORT_INPUT_LIMIT) { return { target: 'core-ml', rationale: 'short-input' }; } return { target: 'core-ml', rationale: 'default-local-first' };}
The character of this function lives in that last branch returning 'core-ml' rather than 'cloud'.
Anything ambiguous falls local. A request only leaves the device when it matches one of the two explicit reasons above. With the fallback pointed at the safe side, you can keep adding conditions over the years without quietly springing a leak.
Sending rationale to logs but never to the UI is deliberate too. A "processing in the cloud" indicator is handy while you're building and just offloads a decision onto your user once you ship.
Measure the Split
Judge the routing by ratios, not by feel. Three numbers are enough.
// src/ai/inferenceStats.tsimport AsyncStorage from '@react-native-async-storage/async-storage';const KEY = 'inference_stats_v1';type Bucket = { coreML: number; cloud: number; cloudSpendUsd: number };export async function recordInference( target: 'core-ml' | 'cloud', spendUsd: number): Promise<void> { const raw = await AsyncStorage.getItem(KEY); const bucket: Bucket = raw ? JSON.parse(raw) : { coreML: 0, cloud: 0, cloudSpendUsd: 0 }; if (target === 'core-ml') { bucket.coreML += 1; } else { bucket.cloud += 1; bucket.cloudSpendUsd += spendUsd; } await AsyncStorage.setItem(KEY, JSON.stringify(bucket));}/** Share handled on-device. When this slips, suspect the routing rules. */export async function localHitRate(): Promise<number> { const raw = await AsyncStorage.getItem(KEY); if (!raw) return 0; const b: Bucket = JSON.parse(raw); const total = b.coreML + b.cloud; return total === 0 ? 0 : b.coreML / total;}
Here's a week of measurements from my own assistant-style app.
Metric
Measured
Handled entirely on-device
67%
Average latency (Core ML)
28ms
Average latency (cloud LLM)
1,240ms
Monthly API spend
$45 → $19 (down 58%)
When localHitRate() starts falling, look at the routing rules before you look at model accuracy. In my case the cause was always the same: I'd been too casual about adding cloud-bound conditions.
What Surfaces Once Routing Is In
Three things I only noticed after shipping this.
Model initialization is slow exactly once. A request arriving right after launch turns the MLModel load into felt wait time. Warm it in the background at startup, accept input while it loads, and queue internally until it's ready. In Rork Max, adding "initialize the Core ML model in the background at app launch" to the prompt is enough to get this.
Nothing releases the model on a memory warning. An on-device model stays resident. Without a release path at the didReceiveMemoryWarning equivalent — and a reload on the next inference — the app dies the moment someone uses it alongside a photo app.
Core ML is iOS-only. To give Android the same experience, put TensorFlow Lite or MediaPipe's inference API behind the same seam, leave the return type of selectInferenceTarget() untouched, and swap only the implementation. This is where keeping the decision in one module pays for itself.
Training Custom Models with Create ML
Create ML Essentials
Create ML is Apple's macOS application that integrates with Xcode and lets you train ML models through an intuitive visual interface.
Supported Task Types
Image Classification: Distinguish between categories (dog breeds, plant species, defects)
Object Detection: Locate and label objects within images
Best practice: 500-5,000 images per class. Data quality directly influences accuracy.
Step 2: Train in Create ML
// In Xcode:// 1. File > New > ML Model// 2. Select "Image Classification"// 3. Point to your dataset/ folder// 4. Reserve 10-20% for validation// 5. Click Train
Step 3: Evaluate and Iterate
Create ML displays accuracy and loss curves in real-time:
Accuracy ≥ 0.95: Production-ready
0.85-0.94: Add more data or improve preprocessing
< 0.85: Reconsider model architecture
Healthy training shows Train and Validation loss decreasing together. If Validation loss increases while Training loss drops, your model is overfitting—add more validation data.
Accelerate with Transfer Learning
Training from scratch takes hours. Apple provides pre-trained models (VGG, ResNet) that you can fine-tune in minutes.
In the Create ML interface, select a pre-trained architecture under "Architecture":
Benefits
90% faster training: Hours → seconds
5-10x less data needed: Maintains accuracy with smaller datasets
Better generalization: Retains useful image features
Core ML Model Optimization and Size Reduction
Understanding Model File Size
Create ML outputs .mlmodel files that can be surprisingly large. Given App Store distribution constraints (1GB app size limit), optimization is essential.
Typical Sizes
ResNet50 (image classification): ~100MB
YOLOv5 (object detection): 50-150MB (varies by precision)
BERT (text): ~500MB (full version)
Quantization: Shrink Without Losing Accuracy
Quantization converts floating-point numbers (Float32) to integer formats (Int8), reducing memory and computation.
Create ML's Quantization Options
After training, you'll see a Quantization section:
- Full Precision (Float32): Best accuracy, largest file
- Float16: ~50% smaller, minimal accuracy loss
- Int8 (Quantized): ~75% smaller, typically 1-3% accuracy drop
Example validation:
import CoreMLlet modelURL = Bundle.main.url(forResource: "defect-detection", withExtension: "mlmodel")!let model = try MLModel(contentsOf: modelURL)print("Model size and details: \(model.modelDescription)")
Advanced Optimization with Python
For fine-grained control, use Apple's coremltools library:
pip install coremltoolsimport coremltools as ctimport tensorflow as tf# Load your trained modeltf_model = tf.keras.models.load_model('defect_model.h5')# Convert to Core MLmlmodel = ct.convert( tf_model, inputs=[ct.ImageType(name="image", shape=(1, 224, 224, 3))], outputs=[ct.ClassifierOutputType(name="defects")], compute_units=ct.ComputeUnit.CPU_AND_NE)# Apply Int8 quantizationquantized = ct.quantization_utils.quantize_weights(mlmodel, nbits=8)quantized.save('defect-detection.mlmodel')
Memory Optimization at Runtime
Minimize memory footprint during inference:
// Cache the model onceclass ModelCache { static let shared = ModelCache() private let model: MLModel private init() { let url = Bundle.main.url(forResource: "defect-detection", withExtension: "mlmodel")! self.model = try! MLModel(contentsOf: url) }}// Use autoreleasepool to free temporary allocationsautoreleasepool { let prediction = try model.prediction(input: input) // Process prediction}
Integrating Core ML Models into Rork Max
Model Management in Rork Max
Rork Max treats Core ML models as first-class assets with automatic optimization.
import CoreMLimport Visionstruct QualityCheckView: View { @State private var cameraImage: UIImage? @State private var result: String = "" var body: some View { VStack { Image(uiImage: cameraImage ?? UIImage()) .resizable() .scaledToFit() Button("Analyze") { analyzeImage() } Text(result) .font(.headline) .padding() } } private func analyzeImage() { guard let image = cameraImage else { return } let model = try? defect_detection(configuration: MLModelConfiguration()) guard let buffer = image.pixelBuffer(size: CGSize(width: 224, height: 224)) else { return } let input = defect_detectionInput(imageAt: buffer) guard let output = try? model?.prediction(input: input) else { result = "Analysis failed" return } let confidence = output.classLabelProbs.max(by: { $0.value < $1.value }) result = "Status: \(confidence?.key ?? "Unknown") (\(String(format: "%.1f%%", (confidence?.value ?? 0) * 100)))" }}
Adding Custom Logic
Rork Max supports "Custom Swift" sections for advanced processing:
// Batch inference across multiple framesfunc processMultipleFrames(_ frames: [UIImage]) -> [String] { let model = try! defect_detection(configuration: MLModelConfiguration()) var results: [String] = [] for frame in frames { guard let buffer = frame.pixelBuffer(size: CGSize(width: 224, height: 224)) else { continue } let input = defect_detectionInput(imageAt: buffer) let output = try! model.prediction(input: input) results.append(output.classLabel) } return results}
Vision Framework Integration: Image Recognition and Object Detection
Vision Framework's Role
Vision works hand-in-hand with Core ML to preprocess images and post-process detections efficiently.
Key Capabilities
Face Detection & Recognition: NSFW detection, emotion analysis
Object Detection: Fast, accurate region-based detection
Barcode & QR Recognition: Read codes in real-time
Text Recognition (OCR): Extract text from images
Homography Estimation: Perspective correction
Practical Example: Object Detection
Integrate a YOLO model converted to Core ML:
import Visionimport CoreMLclass ObjectDetector { let model: defect_yolo init() throws { self.model = try defect_yolo(configuration: MLModelConfiguration()) } func detect(in image: UIImage) -> [DetectionResult] { guard let ciImage = CIImage(image: image) else { return [] } let request = VNCoreMLRequest(model: try! model.model) { request, error in guard let observations = request.results as? [VNRecognizedObjectObservation] else { return } for observation in observations { let boundingBox = observation.boundingBox let confidence = observation.confidence let label = observation.labels.first?.identifier ?? "Unknown" print("Detected: \(label) at \(boundingBox) with \(confidence * 100)% confidence") } } let handler = VNImageRequestHandler(ciImage: ciImage, options: [:]) try? handler.perform([request]) return [] }}struct DetectionResult { let label: String let boundingBox: CGRect let confidence: Float}
Vision in Rork Max: Annotation Overlays
Rork Max provides visual components to annotate detection results:
func drawAnnotations(on image: CGImage, detections: [DetectionResult]) { let size = CGSize(width: image.width, height: image.height) UIGraphicsBeginImageContextWithOptions(size, false, 1.0) UIImage(cgImage: image).draw(at: .zero) let context = UIGraphicsGetCurrentContext()! context.setStrokeColor(UIColor.red.cgColor) context.setLineWidth(2.0) for detection in detections { let rect = CGRect( x: detection.boundingBox.origin.x * size.width, y: detection.boundingBox.origin.y * size.height, width: detection.boundingBox.width * size.width, height: detection.boundingBox.height * size.height ) context.stroke(rect) let label = NSAttributedString( string: "\(detection.label) \(String(format: "%.0f%%", detection.confidence * 100))", attributes: [.foregroundColor: UIColor.red, .font: UIFont.systemFont(ofSize: 12)] ) label.draw(at: CGPoint(x: rect.minX, y: rect.minY - 20)) } let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext()}
Natural Language Framework: Text Classification and Sentiment Analysis
Natural Language Framework Overview
While Vision specializes in images, Natural Language optimizes text processing tasks.
Capabilities
Language Identification: Detect language across multilingual content
Tokenization & Tagging: Sentence/word segmentation and part-of-speech tagging
Named Entity Recognition: Extract persons, locations, organizations
Custom Text Classification: Sentiment, intent, topic categorization
Real-World Example: Sentiment Analysis
Train a sentiment classification model with Create ML:
training_data/
├── positive.txt
│ ├── This product is excellent
│ ├── Very satisfied with purchase
│ └── ...
└── negative.txt
├── Disappointed with quality
├── Not as expected
└── ...
Convert to .mlmodel and integrate into Rork Max:
import NaturalLanguageimport CoreMLclass SentimentAnalyzer { let model: sentiment_classifier init() throws { self.model = try sentiment_classifier(configuration: MLModelConfiguration()) } func analyze(_ text: String) -> SentimentResult { // Preprocess text let tagger = NLTagger(tagSchemes: [.tokenType]) tagger.string = text var tokens: [String] = [] tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: .word, scheme: .tokenType) { tag, range in let token = String(text[range]).lowercased() tokens.append(token) return true } // Run prediction let input = sentiment_classifierInput(text: tokens.joined(separator: " ")) guard let output = try? model.prediction(input: input) else { return SentimentResult(sentiment: "unknown", confidence: 0.0) } return SentimentResult( sentiment: output.classLabel, confidence: output.classLabelProbs[output.classLabel] ?? 0.0 ) }}struct SentimentResult { let sentiment: String // "positive" or "negative" let confidence: Double}
Core ML auto-selects, but you can tune explicitly:
let config = MLModelConfiguration()// CPU only (highest accuracy, slowest)config.computeUnits = .cpuOnly// GPU + CPU (balanced)config.computeUnits = .cpuAndGPU// All (Neural Engine + GPU + CPU - fastest)config.computeUnits = .alllet model = try defect_detection(configuration: config)
In production, you'll periodically release improved models. Updating all users immediately is impractical—App Store review takes days, and not all users update promptly.
Challenge: Update models without waiting for App Store approvals
Solution: Over-The-Air (OTA) Updates
Keep the app binary unchanged but download new models from your server:
struct ModelManager { let remoteURL = URL(string: "https://api.rorklab.com/models/defect-v2.mlmodel")! let cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] .appendingPathComponent("ml-models") func fetchLatestModel() async throws -> MLModel { // Check local cache let versionFile = cachePath.appendingPathComponent("version.json") if FileManager.default.fileExists(atPath: versionFile.path) { let data = try Data(contentsOf: versionFile) let version = try JSONDecoder().decode(ModelVersion.self, from: data) if version.id == "v2" { let modelPath = cachePath.appendingPathComponent("defect.mlmodel") return try MLModel(contentsOf: modelPath) } } // Download latest model let (data, _) = try await URLSession.shared.data(from: remoteURL) // Cache it try FileManager.default.createDirectory(at: cachePath, withIntermediateDirectories: true) let modelPath = cachePath.appendingPathComponent("defect.mlmodel") try data.write(to: modelPath) // Record version let version = ModelVersion(id: "v2", timestamp: Date()) let versionData = try JSONEncoder().encode(version) try versionData.write(to: versionFile) return try MLModel(contentsOf: modelPath) }}struct ModelVersion: Codable { let id: String let timestamp: Date}
Version Management Best Practices
enum ModelVersionControl { case bundled // Fallback included with app case remote(URL) // Latest from server static func load() async throws -> MLModel { do { // Try remote first return try await ModelManager().fetch( from: URL(string: "https://api.rorklab.com/models/latest.mlmodel")! ) } catch { // Fallback to bundled print("Remote fetch failed, using bundled: \(error)") return try bundledModel() } } private static func bundledModel() throws -> MLModel { let url = Bundle.main.url(forResource: "defect-detection", withExtension: "mlmodel")! return try MLModel(contentsOf: url) }}
Privacy-First AI Design Patterns
Privacy by Default
On-device AI's greatest advantage is privacy. Implement these practices rigorously:
Essential Safeguards
Never Persist Input Data: Don't save images or text used for inference to disk
Clear Memory Immediately: Overwrite buffers after use
Local-Only Processing: Never send user data to servers without explicit consent
Transparent Permissions: Always ask before accessing camera/microphone
Secure Data Handling
class SecureMLProcessor { func processImageWithoutPersisting(_ image: UIImage) throws -> String { var processedBuffer: CVPixelBuffer? defer { // Clean up immediately after inference if var buffer = processedBuffer { CVPixelBufferUnlockBaseAddress(buffer, .readOnly) processedBuffer = nil } } guard let buffer = image.pixelBuffer(size: CGSize(width: 224, height: 224)) else { throw MLError.conversionFailed } processedBuffer = buffer // Run inference let input = defect_detectionInput(imageAt: buffer) let output = try model.prediction(input: input) // Return result only (never persist) return output.classLabel } func deleteAllLocalData() throws { let cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] try FileManager.default.removeItem(at: cachePath) }}enum MLError: Error { case conversionFailed}
struct PrivacyCompliance { let allowDataSharing: Bool = false let allowAnalytics: Bool = false let dataRetentionDays: Int = 0 func validateBeforePrediction() -> Bool { assert(!allowDataSharing, "Sharing must be disabled for privacy") assert(!allowAnalytics, "Analytics collection is disabled") return true }}
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.