diff --git a/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift b/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift
index 12fc5e13..9e2589db 100644
--- a/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift
+++ b/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift
@@ -31,5 +31,10 @@ final class AppLayerAssembler: Assembler {
userService: container.resolve(UserService.self)
)
}
+ container.register(PushNotificationOpenHandler.self) {
+ PushNotificationOpenHandler(
+ trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self)
+ )
+ }
}
}
diff --git a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift
index c856f936..294bb6af 100644
--- a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift
+++ b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift
@@ -52,8 +52,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// 앱이 완전 종료되어도, 알림을 통해 앱이 시작된 경우 처리
if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] {
+ let handler = container.resolve(PushNotificationOpenHandler.self)
Task { @MainActor in
- PushNotificationRoute.shared.handlePushTap(userInfo: remoteNotification)
+ handler.handlePushOpen(userInfo: remoteNotification)
}
}
@@ -110,8 +111,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
) {
logger.info("Tapped notification: \(response.notification.request.content.userInfo)")
let userInfo = response.notification.request.content.userInfo
+ let handler = container.resolve(PushNotificationOpenHandler.self)
Task { @MainActor in
- PushNotificationRoute.shared.handlePushTap(userInfo: userInfo)
+ handler.handlePushOpen(userInfo: userInfo)
}
completionHandler()
}
diff --git a/Application/DevLogApp/Sources/App/DevLogApp.swift b/Application/DevLogApp/Sources/App/DevLogApp.swift
index 645d183a..3737e02a 100644
--- a/Application/DevLogApp/Sources/App/DevLogApp.swift
+++ b/Application/DevLogApp/Sources/App/DevLogApp.swift
@@ -27,6 +27,7 @@ struct DevLogApp: App {
sessionUseCase: container.resolve(ObserveAuthSessionUseCase.self),
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self),
+ trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self),
widgetURLTab: { MainTab(widgetURL: $0) },
pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(),
clearPushNotificationRoute: { PushNotificationRoute.shared.clear() }
diff --git a/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift
new file mode 100644
index 00000000..c54d5790
--- /dev/null
+++ b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift
@@ -0,0 +1,22 @@
+//
+// PushNotificationOpenHandler.swift
+// DevLog
+//
+// Created by opfic on 5/28/26.
+//
+
+import Foundation
+import DevLogDomain
+
+final class PushNotificationOpenHandler {
+ private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
+
+ init(trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase) {
+ self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
+ }
+
+ func handlePushOpen(userInfo: [AnyHashable: Any]) {
+ trackAnalyticsEventUseCase.execute(.pushOpen)
+ PushNotificationRoute.shared.handlePushTap(userInfo: userInfo)
+ }
+}
diff --git a/Application/DevLogApp/Sources/Resource/Info.plist b/Application/DevLogApp/Sources/Resource/Info.plist
index 13e3de1b..f3bd7703 100644
--- a/Application/DevLogApp/Sources/Resource/Info.plist
+++ b/Application/DevLogApp/Sources/Resource/Info.plist
@@ -20,6 +20,8 @@
FirebaseAppDelegateProxyEnabled
+ FirebaseAutomaticScreenReportingEnabled
+
GIDClientID
$(CLIENT_ID)
GITHUB_CLIENT_ID
diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift
index f79bdee0..902ee0cb 100644
--- a/Application/DevLogData/Sources/DataAssembler.swift
+++ b/Application/DevLogData/Sources/DataAssembler.swift
@@ -79,6 +79,12 @@ public final class DataAssembler: Assembler {
UserDataRepositoryImpl(userService: container.resolve(UserService.self))
}
+ container.register(AnalyticsRepository.self) {
+ AnalyticsRepositoryImpl(
+ analyticsService: container.resolve(AnalyticsService.self)
+ )
+ }
+
container.register(PushNotificationRepository.self) {
PushNotificationRepositoryImpl(
pushNotificationService: container.resolve(PushNotificationService.self),
diff --git a/Application/DevLogData/Sources/Protocol/AnalyticsService.swift b/Application/DevLogData/Sources/Protocol/AnalyticsService.swift
new file mode 100644
index 00000000..710d9244
--- /dev/null
+++ b/Application/DevLogData/Sources/Protocol/AnalyticsService.swift
@@ -0,0 +1,14 @@
+//
+// AnalyticsService.swift
+// DevLogData
+//
+// Created by opfic on 5/27/26.
+//
+
+public protocol AnalyticsService {
+ func trackScreenView(_ name: String)
+ func trackTodoCreate()
+ func trackTodoComplete()
+ func trackWebPageCreate()
+ func trackPushOpen()
+}
diff --git a/Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift
new file mode 100644
index 00000000..af36972a
--- /dev/null
+++ b/Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift
@@ -0,0 +1,31 @@
+//
+// AnalyticsRepositoryImpl.swift
+// DevLogData
+//
+// Created by opfic on 5/27/26.
+//
+
+import DevLogDomain
+
+final class AnalyticsRepositoryImpl: AnalyticsRepository {
+ private let analyticsService: AnalyticsService
+
+ init(analyticsService: AnalyticsService) {
+ self.analyticsService = analyticsService
+ }
+
+ func track(_ event: AnalyticsEvent) {
+ switch event {
+ case .screenView(let name):
+ analyticsService.trackScreenView(name)
+ case .todoCreate:
+ analyticsService.trackTodoCreate()
+ case .todoComplete:
+ analyticsService.trackTodoComplete()
+ case .webPageCreate:
+ analyticsService.trackWebPageCreate()
+ case .pushOpen:
+ analyticsService.trackPushOpen()
+ }
+ }
+}
diff --git a/Application/DevLogDomain/Sources/DomainAssembler.swift b/Application/DevLogDomain/Sources/DomainAssembler.swift
index 07b8fb26..4a78965c 100644
--- a/Application/DevLogDomain/Sources/DomainAssembler.swift
+++ b/Application/DevLogDomain/Sources/DomainAssembler.swift
@@ -11,6 +11,7 @@ public final class DomainAssembler: Assembler {
public init() { }
public func assemble(_ container: any DIContainer) {
+ registerAnalyticsUseCases(container)
registerAuthUseCases(container)
registerConnectivityUseCases(container)
registerAuthProviderUseCases(container)
@@ -24,6 +25,12 @@ public final class DomainAssembler: Assembler {
}
private extension DomainAssembler {
+ func registerAnalyticsUseCases(_ container: any DIContainer) {
+ container.register(TrackAnalyticsEventUseCase.self) {
+ TrackAnalyticsEventUseCaseImpl(container.resolve(AnalyticsRepository.self))
+ }
+ }
+
func registerAuthUseCases(_ container: any DIContainer) {
container.register(SignInUseCase.self) {
SignInUseCaseImpl(container.resolve(AuthenticationRepository.self))
diff --git a/Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift b/Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift
new file mode 100644
index 00000000..08da0aad
--- /dev/null
+++ b/Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift
@@ -0,0 +1,14 @@
+//
+// AnalyticsEvent.swift
+// DevLogDomain
+//
+// Created by opfic on 5/28/26.
+//
+
+public enum AnalyticsEvent {
+ case screenView(String)
+ case todoCreate
+ case todoComplete
+ case webPageCreate
+ case pushOpen
+}
diff --git a/Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift b/Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift
new file mode 100644
index 00000000..f3887506
--- /dev/null
+++ b/Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift
@@ -0,0 +1,10 @@
+//
+// AnalyticsRepository.swift
+// DevLogDomain
+//
+// Created by opfic on 5/27/26.
+//
+
+public protocol AnalyticsRepository {
+ func track(_ event: AnalyticsEvent)
+}
diff --git a/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift
new file mode 100644
index 00000000..8fb2ca52
--- /dev/null
+++ b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift
@@ -0,0 +1,10 @@
+//
+// TrackAnalyticsEventUseCase.swift
+// DevLogDomain
+//
+// Created by opfic on 5/27/26.
+//
+
+public protocol TrackAnalyticsEventUseCase {
+ func execute(_ event: AnalyticsEvent)
+}
diff --git a/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift
new file mode 100644
index 00000000..2a7d900a
--- /dev/null
+++ b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift
@@ -0,0 +1,18 @@
+//
+// TrackAnalyticsEventUseCaseImpl.swift
+// DevLogDomain
+//
+// Created by opfic on 5/27/26.
+//
+
+public final class TrackAnalyticsEventUseCaseImpl: TrackAnalyticsEventUseCase {
+ private let repository: AnalyticsRepository
+
+ init(_ repository: AnalyticsRepository) {
+ self.repository = repository
+ }
+
+ public func execute(_ event: AnalyticsEvent) {
+ repository.track(event)
+ }
+}
diff --git a/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj b/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj
index 957da7a4..1bd609b2 100644
--- a/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj
+++ b/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
3CE81EA6CD7444739AC73633 /* DevLogInfra.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 592C8B7B099933759AB316A5 /* DevLogInfra.framework */; };
5F7A14F2C5294547884212E1 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 06ADD17826F44543B09286DA /* FirebaseCore */; };
+ 62DB1494C0A5B13B49950914 /* FirebaseAnalyticsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 612F6D27B38A4D058F85C8AC /* FirebaseAnalyticsCore */; };
65623F19F34A9A75BC72EE36 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = CE8F32B22E13743B3FF3B66B /* FirebaseFirestore */; };
B11111111111111111111111 /* DevLogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B55555555555555555555555 /* DevLogCore.framework */; };
E747320CAD03A579976E87F8 /* FirebaseFunctions in Frameworks */ = {isa = PBXBuildFile; productRef = 172AF9DE3BC54E9E0B4EFD19 /* FirebaseFunctions */; };
@@ -76,6 +77,7 @@
files = (
F75AA9259C6636CF733C4D82 /* Foundation.framework in Frameworks */,
5F7A14F2C5294547884212E1 /* FirebaseCore in Frameworks */,
+ 62DB1494C0A5B13B49950914 /* FirebaseAnalyticsCore in Frameworks */,
FB5186BC5A89B7DADAB8A82A /* FirebaseAuth in Frameworks */,
65623F19F34A9A75BC72EE36 /* FirebaseFirestore in Frameworks */,
E747320CAD03A579976E87F8 /* FirebaseFunctions in Frameworks */,
@@ -181,6 +183,7 @@
);
name = DevLogInfra;
packageProductDependencies = (
+ 612F6D27B38A4D058F85C8AC /* FirebaseAnalyticsCore */,
06ADD17826F44543B09286DA /* FirebaseCore */,
2AC59F98E5A6BFA339C3E5BD /* FirebaseAuth */,
CE8F32B22E13743B3FF3B66B /* FirebaseFirestore */,
@@ -564,6 +567,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ 612F6D27B38A4D058F85C8AC /* FirebaseAnalyticsCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6A88F5113FA6A29A059E7035 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseAnalyticsCore;
+ };
06ADD17826F44543B09286DA /* FirebaseCore */ = {
isa = XCSwiftPackageProductDependency;
package = 6A88F5113FA6A29A059E7035 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
diff --git a/Application/DevLogInfra/Sources/InfraAssembler.swift b/Application/DevLogInfra/Sources/InfraAssembler.swift
index b8d0ec04..bfa50b9d 100644
--- a/Application/DevLogInfra/Sources/InfraAssembler.swift
+++ b/Application/DevLogInfra/Sources/InfraAssembler.swift
@@ -16,6 +16,10 @@ public final class InfraAssembler: Assembler {
FirebaseAppServiceImpl()
}
+ container.register(AnalyticsService.self) {
+ FirebaseAnalyticsServiceImpl()
+ }
+
container.register(PushMessagingService.self) {
PushMessagingServiceImpl()
}
diff --git a/Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift b/Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift
new file mode 100644
index 00000000..6f8eff72
--- /dev/null
+++ b/Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift
@@ -0,0 +1,43 @@
+//
+// FirebaseAnalyticsServiceImpl.swift
+// DevLogInfra
+//
+// Created by opfic on 5/27/26.
+//
+
+import DevLogData
+import FirebaseAnalytics
+
+final class FirebaseAnalyticsServiceImpl: AnalyticsService {
+ private enum EventName {
+ static let todoCreate = "todo_create"
+ static let todoComplete = "todo_complete"
+ static let webPageCreate = "webpage_create"
+ static let pushOpen = "push_open"
+ }
+
+ func trackScreenView(_ name: String) {
+ Analytics.logEvent(
+ AnalyticsEventScreenView,
+ parameters: [
+ AnalyticsParameterScreenName: name,
+ ]
+ )
+ }
+
+ func trackTodoCreate() {
+ Analytics.logEvent(EventName.todoCreate, parameters: nil)
+ }
+
+ func trackTodoComplete() {
+ Analytics.logEvent(EventName.todoComplete, parameters: nil)
+ }
+
+ func trackWebPageCreate() {
+ Analytics.logEvent(EventName.webPageCreate, parameters: nil)
+ }
+
+ func trackPushOpen() {
+ Analytics.logEvent(EventName.pushOpen, parameters: nil)
+ }
+}
diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift
index 0eafabc7..cfb78512 100644
--- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift
+++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift
@@ -41,7 +41,8 @@ final class HomeViewCoordinator {
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
fetchTodosUseCase: fetchTodosUseCase,
fetchWebPagesUseCase: fetchWebPagesUseCase,
- networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self)
+ networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
+ trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self)
)
}
diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift
index dbda9812..c492ae5d 100644
--- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift
+++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift
@@ -110,6 +110,7 @@ final class HomeViewModel: Store {
private let fetchTodosUseCase: FetchTodosUseCase
private let fetchWebPagesUseCase: FetchWebPagesUseCase
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
+ private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
private let loadingState = LoadingState()
private var deletedWebPageURLString: String?
private var cancellables = Set()
@@ -123,7 +124,8 @@ final class HomeViewModel: Store {
upsertTodoUseCase: UpsertTodoUseCase,
fetchTodosUseCase: FetchTodosUseCase,
fetchWebPagesUseCase: FetchWebPagesUseCase,
- networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
+ networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
+ trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
) {
self.fetchPreferencesUseCase = fetchPreferencesUseCase
self.updatePreferencesUseCase = updatePreferencesUseCase
@@ -134,6 +136,7 @@ final class HomeViewModel: Store {
self.fetchTodosUseCase = fetchTodosUseCase
self.fetchWebPagesUseCase = fetchWebPagesUseCase
self.networkConnectivityUseCase = networkConnectivityUseCase
+ self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
setupNetworkObserving()
}
@@ -186,6 +189,7 @@ final class HomeViewModel: Store {
do {
defer { endLoading(for: .overlay, mode: .delayed) }
try await upsertTodoUseCase.execute(todo)
+ trackAnalyticsEventUseCase.execute(.todoCreate)
let page = try await fetchRecentTodos()
let items = page.items
.filter { $0.createdAt != $0.updatedAt }
@@ -217,6 +221,7 @@ final class HomeViewModel: Store {
do {
defer { endLoading(for: .overlay, mode: .delayed) }
try await addWebPageUseCase.execute(urlString)
+ trackAnalyticsEventUseCase.execute(.webPageCreate)
let pages = try await fetchWebPagesUseCase.execute("")
send(.updateWebPages(pages.map { WebPageItem(from: $0) }))
} catch {
diff --git a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift
index 37b9552f..ebab4b66 100644
--- a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift
+++ b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift
@@ -89,6 +89,7 @@ final class TodoListViewModel: Store {
private let upsertTodoUseCase: UpsertTodoUseCase
private let deleteTodoUseCase: DeleteTodoUseCase
private let undoDeleteTodoUseCase: UndoDeleteTodoUseCase
+ private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
private let loadingState = LoadingState()
private var undoTodoId: String?
private var nextCursor: TodoCursor?
@@ -101,6 +102,7 @@ final class TodoListViewModel: Store {
upsertTodoUseCase: UpsertTodoUseCase,
deleteTodoUseCase: DeleteTodoUseCase,
undoDeleteTodoUseCase: UndoDeleteTodoUseCase,
+ trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase,
category: TodoCategory
) {
self.fetchTodosUseCase = fetchTodosUseCase
@@ -108,6 +110,7 @@ final class TodoListViewModel: Store {
self.upsertTodoUseCase = upsertTodoUseCase
self.deleteTodoUseCase = deleteTodoUseCase
self.undoDeleteTodoUseCase = undoDeleteTodoUseCase
+ self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
self.category = category
self.state = State(
query: TodoQuery(categoryId: category.storageValue)
@@ -209,6 +212,7 @@ final class TodoListViewModel: Store {
do {
defer { endLoading(.delayed) }
try await upsertTodoUseCase.execute(item)
+ trackAnalyticsEventUseCase.execute(.todoCreate)
send(.refresh)
} catch {
send(.setAlert(true))
@@ -225,6 +229,9 @@ final class TodoListViewModel: Store {
todo.completedAt = todo.isCompleted ? now : nil
todo.updatedAt = now
try await upsertTodoUseCase.execute(todo)
+ if todo.isCompleted {
+ trackAnalyticsEventUseCase.execute(.todoComplete)
+ }
guard let todoListItem = TodoListItem(from: todo) else {
send(.setAlert(true))
return
diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift
index 604b063f..59b2442b 100644
--- a/Application/DevLogPresentation/Sources/Main/MainView.swift
+++ b/Application/DevLogPresentation/Sources/Main/MainView.swift
@@ -42,6 +42,8 @@ struct MainView: View {
coordinator.mainViewModel.send(.onAppear)
}
.onChange(of: selectedTab, initial: true) { _, newValue in
+ guard let newValue else { return }
+ coordinator.mainViewModel.send(.selectedTabChanged(newValue))
if newValue == .home {
homeViewCoordinator.fetchData()
} else if newValue == .today {
diff --git a/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift b/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift
index ce0565d9..493b069e 100644
--- a/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift
+++ b/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift
@@ -24,6 +24,7 @@ final class MainViewCoordinator {
init(container: DIContainer) {
self.diContainer = container
self.mainViewModel = MainViewModel(
+ trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self),
unreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self)
)
self.pushNotificationListViewModel = PushNotificationListViewModel(
@@ -48,6 +49,7 @@ final class MainViewCoordinator {
upsertTodoUseCase: diContainer.resolve(UpsertTodoUseCase.self),
deleteTodoUseCase: diContainer.resolve(DeleteTodoUseCase.self),
undoDeleteTodoUseCase: diContainer.resolve(UndoDeleteTodoUseCase.self),
+ trackAnalyticsEventUseCase: diContainer.resolve(TrackAnalyticsEventUseCase.self),
category: category
)
self.todoListViewModel = todoListViewModel
diff --git a/Application/DevLogPresentation/Sources/Main/MainViewModel.swift b/Application/DevLogPresentation/Sources/Main/MainViewModel.swift
index 707f948f..4679faaf 100644
--- a/Application/DevLogPresentation/Sources/Main/MainViewModel.swift
+++ b/Application/DevLogPresentation/Sources/Main/MainViewModel.swift
@@ -22,12 +22,14 @@ final class MainViewModel: Store {
enum Action {
case onAppear
+ case selectedTabChanged(MainTab)
case setUnreadPushCount(Int)
case setAlert(Bool)
}
enum SideEffect {
case observeUnreadPushCount
+ case trackScreenView(MainTab)
case updateBadgeCount(Int)
}
@@ -35,11 +37,14 @@ final class MainViewModel: Store {
private let logger = Logger(category: "MainViewModel")
private var cancellables = Set()
private var isObservingUnreadPushCount = false
+ private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
private let unreadPushCountUseCase: ObserveUnreadPushCountUseCase
init(
+ trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase,
unreadPushCountUseCase: ObserveUnreadPushCountUseCase
) {
+ self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
self.unreadPushCountUseCase = unreadPushCountUseCase
}
@@ -53,6 +58,10 @@ final class MainViewModel: Store {
isObservingUnreadPushCount = true
sideEffects = [.observeUnreadPushCount]
}
+ case .selectedTabChanged(let tab):
+ if tab.analyticsScreenName != nil {
+ sideEffects = [.trackScreenView(tab)]
+ }
case .setUnreadPushCount(let count):
state.unreadPushCount = count
sideEffects = [.updateBadgeCount(count)]
@@ -68,6 +77,8 @@ final class MainViewModel: Store {
switch effect {
case .observeUnreadPushCount:
observeUnreadPushCount()
+ case .trackScreenView(let tab):
+ trackScreenView(tab)
case .updateBadgeCount(let count):
updateBadgeCount(count)
}
@@ -116,4 +127,24 @@ private extension MainViewModel {
}
}
}
+
+ func trackScreenView(_ tab: MainTab) {
+ guard let screenName = tab.analyticsScreenName else { return }
+ trackAnalyticsEventUseCase.execute(.screenView(screenName))
+ }
+}
+
+private extension MainTab {
+ var analyticsScreenName: String? {
+ switch self {
+ case .home:
+ return "home"
+ case .today:
+ return "today"
+ case .notification:
+ return nil
+ case .profile:
+ return "profile"
+ }
+ }
}
diff --git a/Application/DevLogPresentation/Sources/Root/RootView.swift b/Application/DevLogPresentation/Sources/Root/RootView.swift
index 81c34307..0ba4bec5 100644
--- a/Application/DevLogPresentation/Sources/Root/RootView.swift
+++ b/Application/DevLogPresentation/Sources/Root/RootView.swift
@@ -23,6 +23,7 @@ public struct RootView: View {
sessionUseCase: ObserveAuthSessionUseCase,
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
systemThemeUseCase: ObserveSystemThemeUseCase,
+ trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase,
widgetURLTab: @escaping (URL) -> MainTab?,
pushNotificationTodoIdPublisher: AnyPublisher,
clearPushNotificationRoute: @escaping () -> Void
@@ -30,7 +31,8 @@ public struct RootView: View {
self._viewModel = State(initialValue: RootViewModel(
sessionUseCase: sessionUseCase,
networkConnectivityUseCase: networkConnectivityUseCase,
- systemThemeUseCase: systemThemeUseCase
+ systemThemeUseCase: systemThemeUseCase,
+ trackAnalyticsEventUseCase: trackAnalyticsEventUseCase
))
self.widgetURLTab = widgetURLTab
self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher
diff --git a/Application/DevLogPresentation/Sources/Root/RootViewModel.swift b/Application/DevLogPresentation/Sources/Root/RootViewModel.swift
index 73c336f5..3c63f0da 100644
--- a/Application/DevLogPresentation/Sources/Root/RootViewModel.swift
+++ b/Application/DevLogPresentation/Sources/Root/RootViewModel.swift
@@ -32,6 +32,7 @@ final class RootViewModel: Store {
enum SideEffect {
case clearApplicationBadgeCount
+ case trackLoginScreen
}
private(set) var state: State
@@ -39,15 +40,18 @@ final class RootViewModel: Store {
private let sessionUseCase: ObserveAuthSessionUseCase
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
private let systemThemeUseCase: ObserveSystemThemeUseCase
+ private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
init(
sessionUseCase: ObserveAuthSessionUseCase,
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
- systemThemeUseCase: ObserveSystemThemeUseCase
+ systemThemeUseCase: ObserveSystemThemeUseCase,
+ trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
) {
self.sessionUseCase = sessionUseCase
self.networkConnectivityUseCase = networkConnectivityUseCase
self.systemThemeUseCase = systemThemeUseCase
+ self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
self.state = State()
setupNetworkObserving()
@@ -74,6 +78,9 @@ final class RootViewModel: Store {
state.theme = theme
case .didLogined(let result):
state.signIn = result
+ if !result {
+ effects = [.trackLoginScreen]
+ }
}
if self.state != state { self.state = state }
@@ -84,6 +91,8 @@ final class RootViewModel: Store {
switch effect {
case .clearApplicationBadgeCount:
UNUserNotificationCenter.current().setBadgeCount(0) { _ in }
+ case .trackLoginScreen:
+ trackAnalyticsEventUseCase.execute(.screenView("login"))
}
}
}
diff --git a/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift b/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift
index 290bb0d3..22b052bf 100644
--- a/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift
+++ b/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift
@@ -21,7 +21,8 @@ final class TodayViewCoordinator {
fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self),
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
fetchTodayDisplayOptionsUseCase: container.resolve(FetchTodayDisplayOptionsUseCase.self),
- updateTodayDisplayOptionsUseCase: container.resolve(UpdateTodayDisplayOptionsUseCase.self)
+ updateTodayDisplayOptionsUseCase: container.resolve(UpdateTodayDisplayOptionsUseCase.self),
+ trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self)
)
}
diff --git a/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift b/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift
index 0281bfbb..ccaef572 100644
--- a/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift
+++ b/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift
@@ -83,6 +83,7 @@ final class TodayViewModel: Store {
private let fetchTodoByIdUseCase: FetchTodoByIdUseCase
private let upsertTodoUseCase: UpsertTodoUseCase
private let updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase
+ private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
private let loadingState = LoadingState()
init(
@@ -90,12 +91,14 @@ final class TodayViewModel: Store {
fetchTodoByIdUseCase: FetchTodoByIdUseCase,
upsertTodoUseCase: UpsertTodoUseCase,
fetchTodayDisplayOptionsUseCase: FetchTodayDisplayOptionsUseCase,
- updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase
+ updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase,
+ trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase
) {
self.fetchTodosUseCase = fetchTodosUseCase
self.fetchTodoByIdUseCase = fetchTodoByIdUseCase
self.upsertTodoUseCase = upsertTodoUseCase
self.updateTodayDisplayOptionsUseCase = updateTodayDisplayOptionsUseCase
+ self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
self.state.displayOptions = fetchTodayDisplayOptionsUseCase.execute()
}
@@ -235,6 +238,7 @@ final class TodayViewModel: Store {
todo.completedAt = now
todo.updatedAt = now
try await upsertTodoUseCase.execute(todo)
+ trackAnalyticsEventUseCase.execute(.todoComplete)
send(.removeTodo(todo.id))
} catch {
send(.setAlert(true))
diff --git a/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift b/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift
index 78726cf5..2ee7ea76 100644
--- a/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift
+++ b/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift
@@ -32,6 +32,7 @@ struct DeleteWebPageTests {
]
)
let observeNetworkConnectivityUseCaseSpy = ObserveNetworkConnectivityUseCaseSpy()
+ let trackAnalyticsEventUseCaseSpy = TrackAnalyticsEventUseCaseSpy()
let homeViewModel = HomeViewModel(
fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCaseSpy,
@@ -42,7 +43,8 @@ struct DeleteWebPageTests {
upsertTodoUseCase: upsertTodoUseCaseSpy,
fetchTodosUseCase: fetchTodosUseCaseSpy,
fetchWebPagesUseCase: fetchWebPagesUseCaseSpy,
- networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy
+ networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy,
+ trackAnalyticsEventUseCase: trackAnalyticsEventUseCaseSpy
)
homeViewModel.send(.fetchData)
@@ -84,6 +86,7 @@ struct DeleteWebPageTests {
]
)
let observeNetworkConnectivityUseCaseSpy = ObserveNetworkConnectivityUseCaseSpy()
+ let trackAnalyticsEventUseCaseSpy = TrackAnalyticsEventUseCaseSpy()
let homeViewModel = HomeViewModel(
fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCaseSpy,
@@ -94,7 +97,8 @@ struct DeleteWebPageTests {
upsertTodoUseCase: upsertTodoUseCaseSpy,
fetchTodosUseCase: fetchTodosUseCaseSpy,
fetchWebPagesUseCase: fetchWebPagesUseCaseSpy,
- networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy
+ networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy,
+ trackAnalyticsEventUseCase: trackAnalyticsEventUseCaseSpy
)
homeViewModel.send(.fetchData)
@@ -119,3 +123,7 @@ struct DeleteWebPageTests {
#expect(!restoredWebPageItem.isHidden)
}
}
+
+private struct TrackAnalyticsEventUseCaseSpy: TrackAnalyticsEventUseCase {
+ func execute(_ event: AnalyticsEvent) { }
+}