Replace EventLogger with typed AnalyticsManager using PostHog
Complete analytics overhaul: delete EventLogger.swift, create Analytics.swift with typed event enum (~45 events), screen tracking, super properties (theme, icon pack, voting layout, etc.), session replay with kill switch, autocapture, and network telemetry. Replace all 99 call sites across 38 files with compiler-enforced typed events in object_action naming convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
625
Shared/Analytics.swift
Normal file
625
Shared/Analytics.swift
Normal file
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// Analytics.swift
|
||||
// Feels
|
||||
//
|
||||
// Singleton analytics manager wrapping PostHog SDK.
|
||||
// All analytics events flow through this single manager.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PostHog
|
||||
import UIKit
|
||||
|
||||
// MARK: - Analytics Manager
|
||||
|
||||
@MainActor
|
||||
final class AnalyticsManager {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
static let shared = AnalyticsManager()
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
private static let apiKey = "phc_3GsB3oqNft8Ykg2bJfE9MaJktzLAwr2EPMXQgwEFzAs"
|
||||
private static let host = "https://analytics.88oakapps.com"
|
||||
private static let optOutKey = "analyticsOptedOut"
|
||||
private static let sessionReplayKey = "analytics_session_replay_enabled"
|
||||
private static let iso8601Formatter = ISO8601DateFormatter()
|
||||
|
||||
// MARK: - State
|
||||
|
||||
var isOptedOut: Bool {
|
||||
UserDefaults.standard.bool(forKey: Self.optOutKey)
|
||||
}
|
||||
|
||||
var sessionReplayEnabled: Bool {
|
||||
get {
|
||||
if UserDefaults.standard.object(forKey: Self.sessionReplayKey) == nil {
|
||||
return true
|
||||
}
|
||||
return UserDefaults.standard.bool(forKey: Self.sessionReplayKey)
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: Self.sessionReplayKey)
|
||||
if newValue {
|
||||
PostHogSDK.shared.startSessionRecording()
|
||||
} else {
|
||||
PostHogSDK.shared.stopSessionRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isConfigured = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
func configure() {
|
||||
guard !isConfigured else { return }
|
||||
|
||||
let config = PostHogConfig(apiKey: Self.apiKey, host: Self.host)
|
||||
|
||||
// Auto-capture
|
||||
config.captureElementInteractions = true
|
||||
config.captureApplicationLifecycleEvents = true
|
||||
config.captureScreenViews = true
|
||||
|
||||
// Session replay
|
||||
config.sessionReplay = sessionReplayEnabled
|
||||
config.sessionReplayConfig.maskAllTextInputs = true
|
||||
config.sessionReplayConfig.maskAllImages = false
|
||||
config.sessionReplayConfig.captureNetworkTelemetry = true
|
||||
config.sessionReplayConfig.screenshotMode = true // Required for SwiftUI
|
||||
|
||||
// Respect user opt-out preference
|
||||
if isOptedOut {
|
||||
config.optOut = true
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
config.debug = true
|
||||
config.flushAt = 1
|
||||
#endif
|
||||
|
||||
PostHogSDK.shared.setup(config)
|
||||
isConfigured = true
|
||||
|
||||
// Register super properties
|
||||
updateSuperProperties()
|
||||
}
|
||||
|
||||
// MARK: - Super Properties (attached to every event)
|
||||
|
||||
func updateSuperProperties() {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
||||
let device = UIDevice.current.model
|
||||
let osVersion = UIDevice.current.systemVersion
|
||||
|
||||
let defaults = GroupUserDefaults.groupDefaults
|
||||
var props: [String: Any] = [
|
||||
"app_version": version,
|
||||
"build_number": build,
|
||||
"device_model": device,
|
||||
"os_version": osVersion,
|
||||
"is_pro": IAPManager.shared.isSubscribed,
|
||||
"animations_enabled": !UIAccessibility.isReduceMotionEnabled,
|
||||
"selected_sports": [String](),
|
||||
"theme": "n/a",
|
||||
"icon_pack": "n/a",
|
||||
"voting_layout": "n/a",
|
||||
"day_view_style": "n/a",
|
||||
"mood_shape": "n/a",
|
||||
"personality_pack": "n/a",
|
||||
"privacy_lock_enabled": false,
|
||||
"healthkit_enabled": false,
|
||||
"days_filter_count": 0,
|
||||
"days_filter_all": false,
|
||||
]
|
||||
|
||||
// Theme
|
||||
if let themeRaw = defaults.string(forKey: UserDefaultsStore.Keys.theme.rawValue) {
|
||||
props["theme"] = themeRaw
|
||||
}
|
||||
|
||||
// Icon pack
|
||||
if let iconPackRaw = defaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
|
||||
let iconPack = MoodImages(rawValue: iconPackRaw) {
|
||||
props["icon_pack"] = String(describing: iconPack)
|
||||
}
|
||||
|
||||
// Voting layout
|
||||
if let layoutRaw = defaults.object(forKey: UserDefaultsStore.Keys.votingLayoutStyle.rawValue) as? Int,
|
||||
let layout = VotingLayoutStyle(rawValue: layoutRaw) {
|
||||
props["voting_layout"] = layout.displayName
|
||||
}
|
||||
|
||||
// Day view style
|
||||
if let styleRaw = defaults.object(forKey: UserDefaultsStore.Keys.dayViewStyle.rawValue) as? Int,
|
||||
let style = DayViewStyle(rawValue: styleRaw) {
|
||||
props["day_view_style"] = style.displayName
|
||||
}
|
||||
|
||||
// Shape
|
||||
if let shapeRaw = defaults.object(forKey: UserDefaultsStore.Keys.shape.rawValue) as? Int,
|
||||
let shape = BGShape(rawValue: shapeRaw) {
|
||||
props["mood_shape"] = String(describing: shape)
|
||||
}
|
||||
|
||||
// Personality pack
|
||||
if let packRaw = defaults.object(forKey: UserDefaultsStore.Keys.personalityPack.rawValue) as? Int,
|
||||
let pack = PersonalityPack(rawValue: packRaw) {
|
||||
props["personality_pack"] = pack.title()
|
||||
}
|
||||
|
||||
// Privacy lock
|
||||
props["privacy_lock_enabled"] = defaults.bool(forKey: UserDefaultsStore.Keys.privacyLockEnabled.rawValue)
|
||||
|
||||
// HealthKit
|
||||
props["healthkit_enabled"] = defaults.bool(forKey: UserDefaultsStore.Keys.healthKitEnabled.rawValue)
|
||||
|
||||
// Days filter
|
||||
let daysFilter = UserDefaultsStore.getDaysFilter()
|
||||
props["days_filter_count"] = daysFilter.count
|
||||
props["days_filter_all"] = daysFilter.count == 7
|
||||
|
||||
PostHogSDK.shared.register(props)
|
||||
}
|
||||
|
||||
func registerSuperProperties() {
|
||||
updateSuperProperties()
|
||||
}
|
||||
|
||||
// MARK: - Event Tracking
|
||||
|
||||
func track(_ event: Event) {
|
||||
guard isConfigured else { return }
|
||||
let (name, properties) = event.payload
|
||||
#if DEBUG
|
||||
print("[Analytics] \(name)", properties ?? [:])
|
||||
#endif
|
||||
PostHogSDK.shared.capture(name, properties: properties)
|
||||
}
|
||||
|
||||
// MARK: - Screen Tracking (manual supplement to auto-capture)
|
||||
|
||||
func trackScreen(_ screen: Screen, properties: [String: Any]? = nil) {
|
||||
guard isConfigured else { return }
|
||||
var props: [String: Any] = ["screen_name": screen.rawValue]
|
||||
if let properties { props.merge(properties) { _, new in new } }
|
||||
|
||||
#if DEBUG
|
||||
print("[Analytics] screen_viewed: \(screen.rawValue)")
|
||||
#endif
|
||||
PostHogSDK.shared.capture("screen_viewed", properties: props)
|
||||
}
|
||||
|
||||
// MARK: - Subscription Funnel
|
||||
|
||||
func trackPaywallViewed(source: String) {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.capture("paywall_viewed", properties: [
|
||||
"source": source
|
||||
])
|
||||
}
|
||||
|
||||
func trackPurchaseStarted(productId: String, source: String) {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.capture("purchase_started", properties: [
|
||||
"product_id": productId,
|
||||
"source": source
|
||||
])
|
||||
}
|
||||
|
||||
func trackPurchaseCompleted(productId: String, source: String) {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.capture("purchase_completed", properties: [
|
||||
"product_id": productId,
|
||||
"source": source
|
||||
])
|
||||
}
|
||||
|
||||
func trackPurchaseFailed(productId: String?, source: String, error: String) {
|
||||
guard isConfigured else { return }
|
||||
var props: [String: Any] = [
|
||||
"source": source,
|
||||
"error": error
|
||||
]
|
||||
if let productId {
|
||||
props["product_id"] = productId
|
||||
}
|
||||
PostHogSDK.shared.capture("purchase_failed", properties: props)
|
||||
}
|
||||
|
||||
func trackPurchaseRestored(source: String) {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.capture("purchase_restored", properties: [
|
||||
"source": source
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Opt In / Opt Out
|
||||
|
||||
func optIn() {
|
||||
UserDefaults.standard.set(false, forKey: Self.optOutKey)
|
||||
if isConfigured {
|
||||
PostHogSDK.shared.optIn()
|
||||
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": true])
|
||||
}
|
||||
}
|
||||
|
||||
func optOut() {
|
||||
if isConfigured {
|
||||
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": false])
|
||||
}
|
||||
UserDefaults.standard.set(true, forKey: Self.optOutKey)
|
||||
if isConfigured {
|
||||
PostHogSDK.shared.optOut()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Person Properties (subscription segmentation)
|
||||
|
||||
func updateSubscriptionStatus(_ status: String, type: String) {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.capture("$set", properties: [
|
||||
"$set": [
|
||||
"subscription_status": status,
|
||||
"subscription_type": type
|
||||
]
|
||||
])
|
||||
}
|
||||
|
||||
func trackSubscriptionStatusObserved(
|
||||
status: String,
|
||||
type: String,
|
||||
source: String,
|
||||
isSubscribed: Bool,
|
||||
hasFullAccess: Bool,
|
||||
productId: String?,
|
||||
willAutoRenew: Bool?,
|
||||
isInGracePeriod: Bool?,
|
||||
trialDaysRemaining: Int?,
|
||||
expirationDate: Date?
|
||||
) {
|
||||
guard isConfigured else { return }
|
||||
|
||||
var props: [String: Any] = [
|
||||
"status": status,
|
||||
"type": type,
|
||||
"source": source,
|
||||
"is_subscribed": isSubscribed,
|
||||
"has_full_access": hasFullAccess
|
||||
]
|
||||
|
||||
if let productId {
|
||||
props["product_id"] = productId
|
||||
}
|
||||
if let willAutoRenew {
|
||||
props["will_auto_renew"] = willAutoRenew
|
||||
}
|
||||
if let isInGracePeriod {
|
||||
props["is_in_grace_period"] = isInGracePeriod
|
||||
}
|
||||
if let trialDaysRemaining {
|
||||
props["trial_days_remaining"] = trialDaysRemaining
|
||||
}
|
||||
if let expirationDate {
|
||||
props["expiration_date"] = Self.iso8601Formatter.string(from: expirationDate)
|
||||
}
|
||||
|
||||
PostHogSDK.shared.capture("subscription_status_changed", properties: props)
|
||||
updateSubscriptionStatus(status, type: type)
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
func flush() {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.flush()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
guard isConfigured else { return }
|
||||
PostHogSDK.shared.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Screen Names
|
||||
|
||||
extension AnalyticsManager {
|
||||
enum Screen: String {
|
||||
case day = "day"
|
||||
case month = "month"
|
||||
case monthDetail = "month_detail"
|
||||
case year = "year"
|
||||
case insights = "insights"
|
||||
case settings = "settings"
|
||||
case customize = "customize"
|
||||
case onboarding = "onboarding"
|
||||
case onboardingSubscription = "onboarding_subscription"
|
||||
case paywall = "paywall"
|
||||
case entryDetail = "entry_detail"
|
||||
case noteEditor = "note_editor"
|
||||
case lockScreen = "lock_screen"
|
||||
case sharing = "sharing"
|
||||
case themePicker = "theme_picker"
|
||||
case iconPackPicker = "icon_pack_picker"
|
||||
case votingLayoutPicker = "voting_layout_picker"
|
||||
case dayStylePicker = "day_style_picker"
|
||||
case shapePicker = "shape_picker"
|
||||
case personalityPackPicker = "personality_pack_picker"
|
||||
case appThemePicker = "app_theme_picker"
|
||||
case dayFilterPicker = "day_filter_picker"
|
||||
case iconPicker = "icon_picker"
|
||||
case widgetCreator = "widget_creator"
|
||||
case widgetPicker = "widget_picker"
|
||||
case privacy = "privacy"
|
||||
case eula = "eula"
|
||||
case specialThanks = "special_thanks"
|
||||
case reminderTimePicker = "reminder_time_picker"
|
||||
case exportView = "export_view"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Event Definitions
|
||||
|
||||
extension AnalyticsManager {
|
||||
enum Event {
|
||||
// MARK: Mood
|
||||
case moodLogged(mood: Int, entryType: String)
|
||||
case moodUpdated(mood: Int)
|
||||
case noteUpdated(characterCount: Int)
|
||||
case photoAdded
|
||||
case photoDeleted
|
||||
case missingEntriesFilled(count: Int)
|
||||
|
||||
// MARK: Customization
|
||||
case themeChanged(themeId: String)
|
||||
case themeApplied(themeName: String)
|
||||
case iconPackChanged(packId: Int)
|
||||
case votingLayoutChanged(layout: String)
|
||||
case dayViewStyleChanged(style: String)
|
||||
case moodShapeChanged(shapeId: Int)
|
||||
case personalityPackChanged(packTitle: String)
|
||||
case appIconChanged(iconTitle: String)
|
||||
|
||||
// MARK: Widget
|
||||
case widgetViewed
|
||||
case widgetCreateTapped
|
||||
case widgetCreated
|
||||
case widgetUsed
|
||||
case widgetDeleted
|
||||
case widgetShuffled
|
||||
case widgetRandomized
|
||||
case widgetEyeUpdated(style: String)
|
||||
case widgetMouthUpdated(style: String)
|
||||
case widgetBackgroundUpdated(style: String)
|
||||
case widgetColorUpdated(part: String)
|
||||
|
||||
// MARK: Notifications
|
||||
case notificationEnabled
|
||||
case notificationDisabled
|
||||
case reminderTimeUpdated
|
||||
case reminderTimeTapped
|
||||
|
||||
// MARK: Onboarding
|
||||
case onboardingCompleted(dayId: String?)
|
||||
case onboardingSubscribeTapped
|
||||
case onboardingSkipped
|
||||
|
||||
// MARK: Subscription
|
||||
case paywallSubscribeTapped(source: String)
|
||||
|
||||
// MARK: Settings
|
||||
case settingsClosed
|
||||
case deleteToggleChanged(enabled: Bool)
|
||||
case exportTapped
|
||||
case dataExported(format: String, count: Int)
|
||||
case importTapped
|
||||
case importSucceeded
|
||||
case importFailed(error: String?)
|
||||
case onboardingReshown
|
||||
|
||||
// MARK: Privacy & Security
|
||||
case privacyLockEnabled
|
||||
case privacyLockDisabled
|
||||
case privacyLockEnableFailed
|
||||
case appLocked
|
||||
case biometricUnlockSuccess
|
||||
case biometricUnlockFailed(error: String)
|
||||
|
||||
// MARK: HealthKit
|
||||
case healthKitAuthorized
|
||||
case healthKitAuthFailed(error: String)
|
||||
case healthKitEnableFailed
|
||||
case healthKitDisabled
|
||||
case healthKitNotAuthorized
|
||||
case healthKitSyncCompleted(total: Int, success: Int, failed: Int)
|
||||
|
||||
// MARK: Navigation
|
||||
case tabSwitched(tab: String)
|
||||
case viewHeaderChanged(header: String)
|
||||
|
||||
// MARK: Sharing
|
||||
case shareTemplateViewed(template: String)
|
||||
|
||||
// MARK: Legal
|
||||
case eulaViewed
|
||||
case privacyPolicyViewed
|
||||
case specialThanksViewed
|
||||
|
||||
var payload: (name: String, properties: [String: Any]?) {
|
||||
switch self {
|
||||
// Mood
|
||||
case .moodLogged(let mood, let entryType):
|
||||
return ("mood_logged", ["mood_value": mood, "entry_type": entryType])
|
||||
case .moodUpdated(let mood):
|
||||
return ("mood_updated", ["mood_value": mood])
|
||||
case .noteUpdated(let count):
|
||||
return ("note_updated", ["character_count": count])
|
||||
case .photoAdded:
|
||||
return ("photo_added", nil)
|
||||
case .photoDeleted:
|
||||
return ("photo_deleted", nil)
|
||||
case .missingEntriesFilled(let count):
|
||||
return ("missing_entries_filled", ["count": count])
|
||||
|
||||
// Customization
|
||||
case .themeChanged(let id):
|
||||
return ("theme_changed", ["theme_id": id])
|
||||
case .themeApplied(let name):
|
||||
return ("theme_applied", ["theme_name": name])
|
||||
case .iconPackChanged(let id):
|
||||
return ("icon_pack_changed", ["pack_id": id])
|
||||
case .votingLayoutChanged(let layout):
|
||||
return ("voting_layout_changed", ["layout": layout])
|
||||
case .dayViewStyleChanged(let style):
|
||||
return ("day_view_style_changed", ["style": style])
|
||||
case .moodShapeChanged(let id):
|
||||
return ("mood_shape_changed", ["shape_id": id])
|
||||
case .personalityPackChanged(let title):
|
||||
return ("personality_pack_changed", ["pack_title": title])
|
||||
case .appIconChanged(let title):
|
||||
return ("app_icon_changed", ["icon_title": title])
|
||||
|
||||
// Widget
|
||||
case .widgetViewed:
|
||||
return ("widget_viewed", nil)
|
||||
case .widgetCreateTapped:
|
||||
return ("widget_create_tapped", nil)
|
||||
case .widgetCreated:
|
||||
return ("widget_created", nil)
|
||||
case .widgetUsed:
|
||||
return ("widget_used", nil)
|
||||
case .widgetDeleted:
|
||||
return ("widget_deleted", nil)
|
||||
case .widgetShuffled:
|
||||
return ("widget_shuffled", nil)
|
||||
case .widgetRandomized:
|
||||
return ("widget_randomized", nil)
|
||||
case .widgetEyeUpdated(let style):
|
||||
return ("widget_eye_updated", ["style": style])
|
||||
case .widgetMouthUpdated(let style):
|
||||
return ("widget_mouth_updated", ["style": style])
|
||||
case .widgetBackgroundUpdated(let style):
|
||||
return ("widget_background_updated", ["style": style])
|
||||
case .widgetColorUpdated(let part):
|
||||
return ("widget_color_updated", ["part": part])
|
||||
|
||||
// Notifications
|
||||
case .notificationEnabled:
|
||||
return ("notification_enabled", nil)
|
||||
case .notificationDisabled:
|
||||
return ("notification_disabled", nil)
|
||||
case .reminderTimeUpdated:
|
||||
return ("reminder_time_updated", nil)
|
||||
case .reminderTimeTapped:
|
||||
return ("reminder_time_tapped", nil)
|
||||
|
||||
// Onboarding
|
||||
case .onboardingCompleted(let dayId):
|
||||
var props: [String: Any] = [:]
|
||||
if let dayId { props["day_id"] = dayId }
|
||||
return ("onboarding_completed", props.isEmpty ? nil : props)
|
||||
case .onboardingSubscribeTapped:
|
||||
return ("onboarding_subscribe_tapped", nil)
|
||||
case .onboardingSkipped:
|
||||
return ("onboarding_skipped", nil)
|
||||
|
||||
// Subscription
|
||||
case .paywallSubscribeTapped(let source):
|
||||
return ("paywall_subscribe_tapped", ["source": source])
|
||||
|
||||
// Settings
|
||||
case .settingsClosed:
|
||||
return ("settings_closed", nil)
|
||||
case .deleteToggleChanged(let enabled):
|
||||
return ("delete_toggle_changed", ["enabled": enabled])
|
||||
case .exportTapped:
|
||||
return ("export_tapped", nil)
|
||||
case .dataExported(let format, let count):
|
||||
return ("data_exported", ["format": format, "count": count])
|
||||
case .importTapped:
|
||||
return ("import_tapped", nil)
|
||||
case .importSucceeded:
|
||||
return ("import_succeeded", nil)
|
||||
case .importFailed(let error):
|
||||
return ("import_failed", error != nil ? ["error": error!] : nil)
|
||||
case .onboardingReshown:
|
||||
return ("onboarding_reshown", nil)
|
||||
|
||||
// Privacy & Security
|
||||
case .privacyLockEnabled:
|
||||
return ("privacy_lock_enabled", nil)
|
||||
case .privacyLockDisabled:
|
||||
return ("privacy_lock_disabled", nil)
|
||||
case .privacyLockEnableFailed:
|
||||
return ("privacy_lock_enable_failed", nil)
|
||||
case .appLocked:
|
||||
return ("app_locked", nil)
|
||||
case .biometricUnlockSuccess:
|
||||
return ("biometric_unlock_success", nil)
|
||||
case .biometricUnlockFailed(let error):
|
||||
return ("biometric_unlock_failed", ["error": error])
|
||||
|
||||
// HealthKit
|
||||
case .healthKitAuthorized:
|
||||
return ("healthkit_authorized", nil)
|
||||
case .healthKitAuthFailed(let error):
|
||||
return ("healthkit_auth_failed", ["error": error])
|
||||
case .healthKitEnableFailed:
|
||||
return ("healthkit_enable_failed", nil)
|
||||
case .healthKitDisabled:
|
||||
return ("healthkit_disabled", nil)
|
||||
case .healthKitNotAuthorized:
|
||||
return ("healthkit_not_authorized", nil)
|
||||
case .healthKitSyncCompleted(let total, let success, let failed):
|
||||
return ("healthkit_sync_completed", ["total": total, "success": success, "failed": failed])
|
||||
|
||||
// Navigation
|
||||
case .tabSwitched(let tab):
|
||||
return ("tab_switched", ["tab": tab])
|
||||
case .viewHeaderChanged(let header):
|
||||
return ("view_header_changed", ["header": header])
|
||||
|
||||
// Sharing
|
||||
case .shareTemplateViewed(let template):
|
||||
return ("share_template_viewed", ["template": template])
|
||||
|
||||
// Legal
|
||||
case .eulaViewed:
|
||||
return ("eula_viewed", nil)
|
||||
case .privacyPolicyViewed:
|
||||
return ("privacy_policy_viewed", nil)
|
||||
case .specialThanksViewed:
|
||||
return ("special_thanks_viewed", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI Screen Tracking Modifier
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ScreenTrackingModifier: ViewModifier {
|
||||
let screen: AnalyticsManager.Screen
|
||||
let properties: [String: Any]?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.onAppear {
|
||||
AnalyticsManager.shared.trackScreen(screen, properties: properties)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func trackScreen(_ screen: AnalyticsManager.Screen, properties: [String: Any]? = nil) -> some View {
|
||||
modifier(ScreenTrackingModifier(screen: screen, properties: properties))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user