Analytics is where most SwiftUI apps quietly rot.
You start with:
analytics.log("button_tapped")
Six months later you have:
- duplicate events
- inconsistent naming
- missing context
- analytics in views
- analytics in ViewModels
- analytics in services
- no way to test anything
This post shows how to design clean, scalable analytics architecture in SwiftUI:
- structured events
- clear ownership
- zero UI pollution
- testable pipelines
- future-proof design
This is how real products do it.
🧠 The Core Principle
Analytics is a domain concern, not a UI concern.
Views should never decide:
- what an event is called
- what properties it has
- how it’s sent
They only express intent.
🧱 1. Define Events as Types (Not Strings)
Never do:
analytics.log("profile_viewed")
Instead:
enum AnalyticsEvent {
case profileViewed(userID: String)
case searchPerformed(query: String)
case purchaseCompleted(orderID: String, value: Double)
}
This gives:
- compile-time safety
- discoverability
- refactorability
📦 2. Analytics Protocol
protocol AnalyticsTracking {
func track(_ event: AnalyticsEvent)
}
Concrete implementation:
final class AnalyticsService: AnalyticsTracking {
func track(_ event: AnalyticsEvent) {
switch event {
case .profileViewed(let id):
send(name: "profile_viewed", props: ["id": id])
case .searchPerformed(let query):
send(name: "search_performed", props: ["query": query])
case .purchaseCompleted(let orderID, let value):
send(name: "purchase_completed", props: ["order_id": orderID, "value": value])
}
}
private func send(name: String, props: [String: Any]) {
// Firebase, Segment, custom backend, etc.
}
}
Mapping happens once, centrally.
🧩 3. Inject Analytics via Dependency Graph
From your AppContainer:
final class AppContainer {
let analytics: AnalyticsTracking
init() {
self.analytics = AnalyticsService()
}
}
Injected into features:
ProfileViewModel(analytics: app.analytics)
Never use globals.
🧠 4. ViewModels Emit Events, Views Don’t
Bad:
Button("Buy") {
analytics.track(.purchaseCompleted(...))
}
Good:
Button("Buy") {
viewModel.buy()
}
Inside ViewModel:
func buy() {
analytics.track(.purchaseCompleted(orderID: id, value: price))
// business logic
}
This keeps:
- UI pure
- analytics consistent
- logic centralized
🔄 5. Lifecycle Events
Track in lifecycle hooks:
.onAppear {
viewModel.onAppear()
}
func onAppear() {
analytics.track(.profileViewed(userID: userID))
}
Never embed analytics directly in the view.
🧭 6. Screen Tracking
Model screens as types:
enum Screen {
case home
case profile
case settings
}
func trackScreen(_ screen: Screen) {
analytics.track(.screenViewed(screen))
}
Call from:
- navigation transitions
- feature entry points
- containers
Not from every view.
🧬 7. Context Injection
Your analytics service should enrich automatically:
func send(name: String, props: [String: Any]) {
var enriched = props
enriched["app_version"] = appVersion
enriched["user_id"] = session.userID
enriched["platform"] = platform
// send enriched
}
Views never care about context.
🧪 8. Testing Analytics
final class MockAnalytics: AnalyticsTracking {
var events: [AnalyticsEvent] = []
func track(_ event: AnalyticsEvent) {
events.append(event)
}
}
func testProfileViewTracksEvent() {
let analytics = MockAnalytics()
let vm = ProfileViewModel(analytics: analytics)
vm.onAppear()
XCTAssertEqual(analytics.events.count, 1)
}
No SDKs. No mocks. No hacks.
🔁 9. Avoiding Double-Fires
Never track in:
body- computed properties
- initializers
Only track in:
- user actions
- lifecycle events
- explicit intent points
SwiftUI re-renders constantly — analytics must not.
⚠️ 10. GDPR, Privacy & PII
Never send:
- emails
- names
- phone numbers
- raw IDs
Always:
- hash identifiers
- anonymize data
- respect opt-out
- centralize filtering in analytics layer
Architecture should enforce compliance.
❌ 11. Common Anti-Patterns
Avoid:
- analytics in views
- string-based events
- direct SDK calls in UI
- copy-paste event names
- tracking in
body - mixing analytics with business logic
- “just log everything”
This creates noise, not insight.
🧠 Mental Model
Think in layers:
View (intent)
→ ViewModel (meaning)
→ Analytics Service (translation)
→ SDK / Backend
Each layer has one responsibility.
🚀 Final Thoughts
A real analytics architecture gives you:
- clean data
- consistent events
- easy testing
- safe refactors
- reliable dashboards
- trust in your metrics
Once analytics is clean, product decisions become easy.
Top comments (0)