Modern iOS food delivery application with Salesforce Data Cloud and AI-powered personalization
Pronto is a next-generation food delivery platform that leverages Salesforce Data Cloud and real-time personalization to deliver individualized experiences across all channels. Whether a user browses on the web or mobile app, their preferences and behaviors are unified in real-time, enabling intelligent recommendations that drive engagement and conversion.
Traditional food delivery apps treat each channel (web, mobile, tablet) as isolated silos. A user who clicks "Sushi" 6 times on the website still sees generic "Pizza" recommendations when they open the mobile app. This disconnect leads to:
- β Poor user experience (irrelevant recommendations)
- β Lost revenue (lower conversion rates)
- β Wasted engineering effort (complex custom logic per channel)
Pronto uses Salesforce Data Cloud for omni-channel identity resolution and Pro Code Personalization to deliver context-aware recommendations:
- β Real-time data sync (<300ms from web click to mobile update)
- β Cross-channel identity resolution (automatic device linking)
- β AI-driven personalization (no custom ranking logic needed)
- β Developer-friendly SDK (single API call replaces 500+ lines of code)
Result: Users see "Sushi" recommendations on mobile because the system knows they prefer it from their web behaviorβautomatically, in real-time, with zero custom logic.
- Pro Code SDK Integration: One SDK call fetches personalized content based on unified user profiles
- Real-Time Decision Engine: Data Cloud evaluates rules server-side and returns optimal content
- Cross-Platform Context: Web clicks instantly influence mobile recommendations (<300ms)
- Dynamic Content Rendering: "Sushi" vs "Pizza" decided by actual user engagement, not static rules
- Automatic Identity Resolution: Links browser sessions to mobile devices via Unified Individual profiles
- Comprehensive Event Tracking: Cart, catalog, favorites, orders, searchesβall tracked automatically
- Privacy-First Consent Management: GDPR-compliant opt-in/opt-out with full transparency
- Location-Aware Personalization: GPS tracking for hyper-local restaurant recommendations
- SwiftUI + MVVM: Declarative UI with clean separation of concerns
- Async/Await: Modern concurrency for smooth, responsive UX
- Type-Safe Networking: Strongly-typed API layer with Combine support
- Modular Codebase: Feature-based organization for scalability
- Customer Data Platform (CDP): Unified customer profiles across all touchpoints
- Real-Time Event Streaming: Engagement data ingested in <300ms
- Data Graph API Access: Query user behavior and preferences on-demand
- Mobile Connector: Native iOS SDK for seamless Data Cloud communication
Traditional personalization requires developers to:
- Fetch all content options from an API
- Fetch user engagement data from another API
- Write custom scoring/ranking algorithms
- Handle identity resolution across devices
- Maintain complex rule engines
With Salesforce Personalization SDK, this entire flow collapses into a single SDK call:
// β Old Way: 500+ lines of code
let allOptions = try await fetchPersonalizationOptions()
let engagements = try await fetchUserEngagements()
let deviceLinks = try await linkDeviceIdentities()
let winner = customScoringAlgorithm(allOptions, engagements, deviceLinks)
renderContent(winner)// β
New Way: 3 lines of code
let response = try await PersonalizationModule.fetchDecisions(
personalizationPointNames: ["Pronto"]
)
renderContent(response.personalizations["Pronto"]) // Done!Problem: Users interact with brands across multiple devices. Linking these identities manually requires complex correlation logic.
Solution: The SDK handles cross-channel identity resolution seamlessly:
- Web browser session (device ID:
4be44eaae0770cdd) - iOS app session (device ID:
D46475B0-B111-4EA0-A6BF-5E1AD8FC4675) - Automatically linked via
UnifiedIndividual(9ea2aa85ce5b5a1e15498c204306aa76)
Developer Impact: Zero lines of identity stitching code. The SDK correlates devices automatically using email, cookies, device IDs, and more.
Problem: REST APIs return raw data, forcing apps to implement custom scoring, ranking, and rule evaluation.
Solution: The SDK evaluates personalization rules in Data Cloud and returns the optimal decision:
- User clicks "Sushi" 6 times on web + "Pizza" 4 times β SDK returns "Sushi"
- No client-side logic needed
- Rules managed centrally in Data Cloud (no app updates required)
Developer Impact: No conditional logic, no scoring algorithms, no rule engines. Just render the result.
1. Web Interaction
User clicks "Sushi" (6x) and "Pizza" (4x) on website
β
2. Data Cloud Ingestion (<300ms)
Events streamed β Identity resolved β Profile updated
β
3. Mobile App Launch
iOS app calls: fetchDecisions("Pronto")
β
4. Smart Response
SDK returns "Sushi" (higher engagement) automatically
β
5. Instant Render
App displays personalized "Sushi" contentβno custom logic
Key Insight: What would typically require 500+ lines of identity resolution, engagement tracking, scoring logic, and rule evaluation is reduced to a single SDK call.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SwiftUI Views β
β (Declarative UI, @StateObject, @ObservedObject) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β
β Observes @Published properties
β
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β ViewModels β
β (Business Logic, State Management, ObservableObject)β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β
β Uses Services
β
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β Service Layer β
β β’ PersonalizationService (Pro Code SDK) β
β β’ DataCloudService (Event Tracking) β
β β’ DataGraphQueryService (Real-Time Data) β
β β’ ProfileDataService (Identity Management) β
β β’ ConsentService (Privacy Controls) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β
β Communicates with
β
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β Salesforce Data Cloud β
β β’ Customer Data Platform (CDP) β
β β’ Personalization Engine β
β β’ Data Graph API β
β β’ Identity Resolution β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Service | Purpose | Key Features |
|---|---|---|
| PersonalizationService | Fetch AI-driven content decisions | β’ Single SDK call β’ Server-side rule evaluation β’ Cross-channel context |
| DataCloudService | Track user engagement events | β’ Cart, catalog, favorites β’ Automatic batching β’ Real-time streaming |
| DataGraphQueryService | Query user behavior on-demand | β’ Direct API access β’ JWT token management β’ Unified profile data |
| ProfileDataService | Manage identity lifecycle | β’ Anonymous β Known transition β’ Device info capture β’ Contact attributes |
| ConsentService | GDPR-compliant privacy | β’ Opt-in/opt-out β’ Persistent storage β’ Auto event blocking |
| LocationTrackingService | GPS-based personalization | β’ CoreLocation integration β’ Automatic permission mgmt β’ 60s expiration |
Every user action is tracked and streamed to Data Cloud in real-time:
| User Action | Event Type | Data Captured |
|---|---|---|
| Browse menu | catalog |
Product ID, category, timestamp |
| Add to favorites | addToFavorite |
Product ID, name, price |
| Add to cart | cart + cartItem |
Quantity, price, currency, session |
| Place order | order + orderItem |
Order ID, items, total, address |
| Update profile | identity |
Name, email, phone |
| Screen navigation | appEvents |
Screen name, duration |
| Consent changes | consentLog |
Status, purpose, timestamp |
Example: Add to Cart
engagementService.trackAddToCart(
productId: "pizza-123",
productName: "Margherita Pizza",
quantity: 1,
price: 11.88,
currency: "USD"
)This automatically includes:
- β Device ID
- β Session ID
- β Timestamp (ISO 8601)
- β User identity (if known)
- β Location (if enabled)
Step 1: User Browses (Web)
User clicks "Sushi" 6x on website
β
Event: { productId: "sushi", channel: "web" }
β
Data Cloud: Streamed + Ingested (<300ms)
β
Individual Profile: Updated with engagement
Step 2: User Opens Mobile App
// iOS App calls Personalization SDK
let response = try await PersonalizationModule.fetchDecisions(
personalizationPointNames: ["Pronto"]
)Step 3: Data Cloud Evaluates Rules
Data Cloud:
1. Identifies user via UnifiedIndividual
2. Queries ProductBrowseEngagement events
3. Aggregates clicks: { Sushi: 6, Pizza: 4 }
4. Evaluates personalization rules
5. Returns winning decision: "Sushi"
Step 4: App Renders Content
// SDK response contains winning decision
let winner = response.personalizations["Pronto"]
// Display: Sushi background image, CTA, etc.Key Insight: The app never writes custom logic to determine what to show. Data Cloud does the heavy lifting.
Anonymous β Known User Journey
// 1. App Launch (Anonymous)
ProfileDataService.shared.setAnonymousProfile()
// β SDK generates anonymous ID
// β All events tagged with anonymous ID
// 2. User Signs Up / Logs In
ProfileDataService.shared.setKnownProfile(
firstName: "Leander",
lastName: "Paes",
email: "leander.paes@example.com"
)
// β Profile transitions to "known"
// β ALL past anonymous events linked to known user
// β Identity attributes propagated to future events
// 3. Update Contact Info
ProfileDataService.shared.updateContactInformation(
phone: "+1-555-123-4567",
address: Address(...)
)
// β Contact attributes sent to Data Cloud
// β Future events include phone + addressWhy This Matters:
- No manual event migration needed
- Historical behavior preserved
- Single unified profile across all channels
Direct access to unified customer data:
let result = try await DataGraphQueryService.shared.queryDataGraph(
dataGraphName: "C360_Contact_RT",
dmoName: "UnifiedLinkssotIndividualI1__dlm",
fieldName: "UnifiedRecordId__c",
value: "9ea2aa85ce5b5a1e15498c204306aa76"
)Returns:
{
"data": [{
"ssot__FirstName__c": "Leander",
"ssot__LastName__c": "Paes",
"ssot__ProductBrowseEngagement__dlm": [
{ "ssot__ProductId__c": "Sushi", "ssot__CreatedDate__c": "..." },
{ "ssot__ProductId__c": "Pizza", "ssot__CreatedDate__c": "..." }
]
}]
}Use Cases:
- Display user's order history
- Show favorite products
- Calculate engagement metrics
- Personalize content in real-time
- iOS 15.0+
- Xcode 14.0+
- Swift 5.7+
- Salesforce Data Cloud Tenant (with Mobile Connector configured)
- SPM (Swift Package Manager) for dependencies
-
Clone the Repository
git clone https://github.com/yourusername/ProntoFoodDeliveryApp.git cd ProntoFoodDeliveryApp -
Open in Xcode
open ProntoFoodDeliveryApp.xcodeproj
-
Configure Salesforce Credentials
Update
Sources/Core/Services/Salesforce/DataCloud/DataCloudConfiguration.swift:static var development: DataCloudConfiguration { DataCloudConfiguration( appId: "YOUR_DEV_APP_ID", // From Mobile Connector endpoint: "YOUR_DEV_ENDPOINT", // From Mobile Connector enableLogging: true ) }
-
Install Dependencies
Xcode will automatically resolve SPM dependencies on first build:
- Salesforce Marketing Cloud SDK
- CDP Module
- Personalization Module
-
Build and Run
β + R (or click the Play button)
-
Create Mobile Connector
- Navigate to Data Cloud β Mobile & Web
- Create a new Mobile Connector
- Copy
appIdandendpoint
-
Configure Personalization Point
- Navigate to Personalization β Points
- Create "Pronto" personalization point
- Add decision records: "Pizza", "Sushi"
- Define attributes:
BackgroundImageUrl,CallToActionText,CallToActionUrl
-
Set Up Identity Resolution
- Configure reconciliation rules (email, device ID)
- Map data sources (Web, Mobile)
- Enable real-time identity resolution
-
Test Integration
- Run the app
- Check Xcode console for SDK initialization logs
- Verify events in Data Cloud β Engagement Streams
ProntoFoodDeliveryApp/
βββ README.md # This file
βββ Package.swift # SPM dependencies
βββ Sources/
β βββ App/
β β βββ ProntoFoodDeliveryAppApp.swift # App entry point & SDK init
β β
β βββ Core/
β β βββ Models/
β β β βββ Product.swift # Business entities
β β β βββ PersonalizationDecisionRecord.swift # Pro Code models
β β β
β β βββ Services/
β β β βββ Salesforce/
β β β βββ DataCloud/ # Event tracking services
β β β β βββ DataCloudService.swift
β β β β βββ EngagementTrackingService.swift
β β β β βββ ProfileDataService.swift
β β β β βββ ConsentService.swift
β β β β βββ LocationTrackingService.swift
β β β β βββ DataGraphQueryService.swift
β β β β
β β β βββ Personalization/ # Pro Code SDK
β β β βββ PersonalizationService.swift
β β β
β β βββ Networking/ # API layer
β β βββ Persistence/ # Local storage
β β βββ Utilities/ # Helpers
β β
β βββ Features/ # MVVM feature modules
β β βββ Home/
β β β βββ Views/
β β β β βββ HomeView.swift
β β β βββ ViewModels/
β β β βββ HomeViewModel.swift
β β β
β β βββ Profile/
β β β βββ Views/
β β β β βββ FavoritesView.swift # Pro Code Personalization UI
β β β βββ ViewModels/
β β β βββ PersonalizationViewModel.swift # Decision logic
β β β
β β βββ Cart/ # Shopping cart
β β βββ Order/ # Order management
β β βββ Search/ # Restaurant search
β β
β βββ Shared/
β βββ UI/
β β βββ Components/ # Reusable SwiftUI views
β β βββ Modifiers/ # Custom view modifiers
β βββ Protocols/ # Shared interfaces
β
βββ Tests/
β βββ UnitTests/ # Logic tests
β βββ IntegrationTests/ # Service tests
β βββ UITests/ # End-to-end tests
β
βββ Resources/
β βββ Images/ # App assets
β βββ Configuration/ # Environment configs
β βββ Localizations/ # i18n strings
β
βββ Documentation/
βββ API/ # Service docs
βββ Architecture/ # Design docs
import Foundation
import Combine
@MainActor
final class PersonalizationViewModel: ObservableObject {
// MARK: - Published Properties
@Published var winningDecision: PersonalizationDecisionRecord?
@Published var allDecisionRecords: [PersonalizationDecisionRecord] = []
@Published var isLoading = false
@Published var error: String?
// MARK: - Services
private let personalizationService: PersonalizationServiceProtocol
private let dataGraphService: DataGraphQueryServiceProtocol
// MARK: - Initialization
init(
personalizationService: PersonalizationServiceProtocol = PersonalizationService.shared,
dataGraphService: DataGraphQueryServiceProtocol = DataGraphQueryService.shared
) {
self.personalizationService = personalizationService
self.dataGraphService = dataGraphService
}
// MARK: - Fetch Personalization with Winner Selection
func fetchPersonalizationWithWinner() async {
isLoading = true
defer { isLoading = false }
do {
// Step 1: Fetch all decision records from SDK
let result = try await personalizationService.fetchDecisions(
personalizationPointNames: ["Pronto"]
)
// Step 2: Parse decision records
let decisions = parseAllDecisionRecords(from: result)
self.allDecisionRecords = decisions
// Step 3: Fetch click counts from Data Graph
let clickCounts = try await fetchClickCountsFromDataGraph()
// Step 4: Select winner based on clicks
let winner = selectWinner(decisions: decisions, clickCounts: clickCounts)
self.winningDecision = winner
} catch {
self.error = error.localizedDescription
}
}
// MARK: - Parse Decision Records
private func parseAllDecisionRecords(
from result: PersonalizationDecisionsResult
) -> [PersonalizationDecisionRecord] {
guard let prontoDecision = result.personalizations["Pronto"] else {
return []
}
var records: [PersonalizationDecisionRecord] = []
// Parse from attributes (single decision)
if let header = prontoDecision.attributes["Header"] as? String {
let record = PersonalizationDecisionRecord(
id: prontoDecision.personalizationId,
name: header,
backgroundImageUrl: prontoDecision.attributes["BackgroundImageUrl"] as? String,
callToActionText: prontoDecision.attributes["CallToActionText"] as? String,
callToActionUrl: prontoDecision.attributes["CallToActionUrl"] as? String,
header: prontoDecision.attributes["Header"] as? String,
subheader: prontoDecision.attributes["Subheader"] as? String
)
records.append(record)
}
// Parse from contentObjects (multiple decisions)
for contentObject in prontoDecision.contentObjects {
if let name = contentObject.data["name"] as? String ?? contentObject.data["Header"] as? String {
let record = PersonalizationDecisionRecord(
id: contentObject.personalizationContentId,
name: name,
backgroundImageUrl: contentObject.data["BackgroundImageUrl"] as? String,
callToActionText: contentObject.data["CallToActionText"] as? String,
callToActionUrl: contentObject.data["CallToActionUrl"] as? String,
header: contentObject.data["Header"] as? String,
subheader: contentObject.data["Subheader"] as? String
)
records.append(record)
}
}
return records
}
// MARK: - Fetch Click Counts from Data Graph
private func fetchClickCountsFromDataGraph() async throws -> [String: Int] {
let result = try await dataGraphService.queryDataGraph(
dataGraphName: "C360_Contact_RT",
dmoName: "UnifiedLinkssotIndividualI1__dlm",
fieldName: "UnifiedRecordId__c",
value: "9ea2aa85ce5b5a1e15498c204306aa76"
)
var counts: [String: Int] = [:]
// Parse ProductBrowseEngagement events
if let dataArray = result["data"] as? [[String: Any]],
let firstData = dataArray.first,
let jsonBlobString = firstData["json_blob__c"] as? String,
let jsonBlobData = jsonBlobString.data(using: .utf8),
let jsonBlob = try? JSONSerialization.jsonObject(with: jsonBlobData) as? [String: Any],
let unifiedLinks = jsonBlob["UnifiedLinkssotIndividualI1__dlm"] as? [[String: Any]] {
for link in unifiedLinks {
if let individuals = link["ssot__Individual__dlm"] as? [[String: Any]],
let individual = individuals.first,
let engagements = individual["ssot__ProductBrowseEngagement__dlm"] as? [[String: Any]] {
for engagement in engagements {
if let productId = engagement["ssot__ProductId__c"] as? String {
// Normalize product name
let normalized = productId.prefix(1).uppercased() + productId.dropFirst().lowercased()
counts[normalized, default: 0] += 1
}
}
}
}
}
return counts
}
// MARK: - Select Winner (Case-Insensitive)
private func selectWinner(
decisions: [PersonalizationDecisionRecord],
clickCounts: [String: Int]
) -> PersonalizationDecisionRecord? {
guard !decisions.isEmpty else { return nil }
// Merge click counts (case-insensitive)
var decisionsWithCounts = decisions.map { decision in
var d = decision
for (key, count) in clickCounts {
if key.lowercased() == decision.name.lowercased() {
d.clickCount = count
break
}
}
return d
}
// Sort by click count (descending)
decisionsWithCounts.sort { $0.clickCount > $1.clickCount }
return decisionsWithCounts.first
}
}struct FavoritesView: View {
@StateObject private var viewModel = PersonalizationViewModel()
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Pro Code Personalization Section
proCodePersonalizationCard
// Other content...
}
}
.task {
await viewModel.fetchPersonalizationWithWinner()
}
}
private var proCodePersonalizationCard: some View {
VStack(alignment: .leading, spacing: 0) {
// Header with Dynamic Background
ZStack(alignment: .topTrailing) {
if let winner = viewModel.winningDecision,
let imageUrl = winner.backgroundImageUrl {
AsyncImage(url: URL(string: imageUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: 200)
.clipped()
} placeholder: {
gradientBackground
}
} else {
gradientBackground
}
// "Pro Code" Badge
HStack(spacing: 8) {
Image(systemName: "wand.and.stars")
Text("Pro Code")
.fontWeight(.semibold)
}
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Capsule().fill(Color.black.opacity(0.6)))
.padding(12)
}
.frame(height: 200)
// Winning Decision Content
if let winner = viewModel.winningDecision {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("π Top Choice for You")
.font(.headline)
Spacer()
if winner.clickCount > 0 {
Text("\(winner.clickCount) clicks")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Text(winner.name)
.font(.title2)
.fontWeight(.bold)
if let cta = winner.callToActionText,
let url = winner.callToActionUrl {
Link(destination: URL(string: url)!) {
HStack {
Text(cta)
Image(systemName: "arrow.right.circle.fill")
}
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding()
.background(Capsule().fill(Color.blue))
}
}
}
.padding()
}
}
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 4)
.padding()
}
private var gradientBackground: some View {
LinearGradient(
colors: [Color.purple.opacity(0.6), Color.pink.opacity(0.6)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}// β
GOOD: Set known profile immediately after login
func handleSuccessfulLogin(user: User) {
ProfileDataService.shared.setKnownProfile(
firstName: user.firstName,
lastName: user.lastName,
email: user.email
)
}// β
GOOD: Services automatically check consent
engagementService.trackAddToCart(...) // Only tracks if user opted in
// β BAD: Direct SDK calls bypass consent
SFMCSdk.track(event: myEvent) // Don't do this// β
GOOD: Inject services for testability
class MyViewModel: ObservableObject {
private let personalizationService: PersonalizationServiceProtocol
init(personalizationService: PersonalizationServiceProtocol = PersonalizationService.shared) {
self.personalizationService = personalizationService
}
}// β
GOOD: Automatic screen tracking
struct HomeView: View {
var body: some View {
VStack { ... }
.trackScreen("Home")
.locationAware()
}
}// β
GOOD: Protocol-based services enable mocking
final class MockPersonalizationService: PersonalizationServiceProtocol {
var mockResponse: PersonalizationDecisionsResult?
func fetchDecisions(...) async throws -> PersonalizationDecisionsResult {
return mockResponse ?? .empty
}
}// In DEBUG builds, logging is automatic
#if DEBUG
let config = DataCloudConfiguration.development // enableLogging: true
#endif// Print comprehensive SDK status
DataCloudLoggingService.shared.printSdkStatus()
// Output:
// ============================================================
// π SFMC SDK Status Report
// ============================================================
// SDK State: operational
// CDP Module Status: operational
// Consent Status: optIn
// ============================================================| Issue | Solution |
|---|---|
| Events not appearing in Data Cloud | β’ Check appId and endpoint in configβ’ Verify consent is set to optInβ’ Check network connectivity |
| Personalization returning empty | β’ Verify personalization point "Pronto" exists in Data Cloud β’ Ensure decision records are published β’ Check identity resolution is working |
| Identity not linking devices | β’ Verify email reconciliation is configured β’ Check that setKnownProfile() was calledβ’ Ensure web and mobile use same email |
| Location not working | β’ Request location permission β’ Call startTracking()β’ Check Info.plist has location usage strings |
- Salesforce Data Cloud Developer Guide
- Mobile SDK Integration Guide
- Personalization SDK Deep Dive
- Event Tracking Reference
We welcome contributions! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Follow MVVM architecture and existing code patterns
- Write unit tests for new features
- Update documentation as needed
- Commit with meaningful messages
- Push to your branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is proprietary and confidential. All rights reserved.
- Salesforce Data Cloud Team for the powerful CDP platform
- Salesforce Mobile SDK Team for the robust iOS SDK
- SwiftUI Community for best practices and inspiration
For questions or issues:
- Email: support@pronto.com
- Slack: #pronto-dev-support
- Internal Wiki: confluence.pronto.com/ios-app
Built with β€οΈ by the Pronto Engineering Team
Last Updated: November 2025
Version: 1.0.0
iOS SDK Version: Salesforce Marketing Cloud 8.0+
Minimum iOS: 15.0+