Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions DevLog/App/Assembler/AppLayerAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

final class AppLayerAssembler: Assembler {
func assemble(_ container: any DIContainer) {
container.register(WidgetSyncEventBus.self) {
WidgetSyncEventBusImpl()
}
container.register(WidgetSyncEventHandler.self) {
WidgetSyncEventHandler(
eventBus: container.resolve(WidgetSyncEventBus.self),
repository: container.resolve(TodoRepository.self),
snapshotUpdater: container.resolve(WidgetSnapshotUpdater.self)
)
}
container.register(FCMTokenSyncHandler.self) {
FCMTokenSyncHandler(
userService: container.resolve(UserService.self)
Expand Down
3 changes: 2 additions & 1 deletion DevLog/App/Assembler/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ final class DataAssembler: Assembler {
container.register(UserPreferencesRepository.self) {
UserPreferencesRepositoryImpl(
store: container.resolve(UserDefaultsStore.self),
themeStore: container.resolve(ThemeStore.self)
themeStore: container.resolve(ThemeStore.self),
widgetSnapshotPreferenceStore: container.resolve(WidgetSnapshotPreferenceStore.self)
)
}
}
Expand Down
21 changes: 21 additions & 0 deletions DevLog/App/Assembler/PersistenceAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,26 @@ final class PersistenceAssembler: Assembler {
container.register(WebPageImageStore.self) {
WebPageImageStore()
}

container.register(WidgetSharedDefaultsStore.self) {
WidgetSharedDefaultsStore()
}

container.register(WidgetSnapshotStore.self) {
WidgetSnapshotStore(
store: container.resolve(WidgetSharedDefaultsStore.self)
)
}

container.register(WidgetSnapshotPreferenceStore.self) {
WidgetSnapshotPreferenceStore()
}

container.register(WidgetSnapshotUpdater.self) {
WidgetSnapshotUpdater(
snapshotStore: container.resolve(WidgetSnapshotStore.self),
preferenceStore: container.resolve(WidgetSnapshotPreferenceStore.self)
)
}
}
}
1 change: 1 addition & 0 deletions DevLog/App/Delegate/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate {
FirebaseApp.configure()
_ = container.resolve(FCMTokenSyncHandler.self)
_ = container.resolve(UserTimeZoneSyncHandler.self)
_ = container.resolve(WidgetSyncEventHandler.self)

// 알림 권한 요청
UNUserNotificationCenter.current().delegate = self
Expand Down
5 changes: 5 additions & 0 deletions DevLog/App/DevLogApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SwiftUI
struct DevLogApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@Environment(\.diContainer) var container: DIContainer
@Environment(\.scenePhase) var scenePhase

init() {
AppAssembler().assemble(AppDIContainer.shared)
Expand All @@ -24,6 +25,10 @@ struct DevLogApp: App {
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self)
))
.autocorrectionDisabled()
.onChange(of: scenePhase) { _, phase in
guard phase == .background else { return }
container.resolve(WidgetSyncEventBus.self).publish(.syncRequested)
}
}
}
}
27 changes: 8 additions & 19 deletions DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {
static let pushSortOrder = "PushNotification.sortOption"
static let pushTimeFilter = "PushNotification.timeFilter"
static let pushUnreadOnly = "PushNotification.showUnreadOnly"
static let heatmapActivityTypes = "Profile.heatmap.activityTypes"
static let todayDueDateVisibility = "Today.dueDateVisibility"
static let todayFocusVisibility = "Today.focusVisibility"
}

private let store: UserDefaultsStore
private let themeStore: ThemeStore
private let widgetSnapshotPreferenceStore: WidgetSnapshotPreferenceStore

init(
store: UserDefaultsStore,
themeStore: ThemeStore
themeStore: ThemeStore,
widgetSnapshotPreferenceStore: WidgetSnapshotPreferenceStore
) {
self.store = store
self.themeStore = themeStore
self.widgetSnapshotPreferenceStore = widgetSnapshotPreferenceStore
themeStore.send(systemTheme())
}

Expand Down Expand Up @@ -85,29 +85,18 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository {
}

func heatmapActivityTypes() -> [String] {
store.stringArray(forKey: Key.heatmapActivityTypes)
widgetSnapshotPreferenceStore.heatmapActivityTypes()
}

func setHeatmapActivityTypes(_ activityTypes: [String]) {
store.setStringArray(activityTypes, forKey: Key.heatmapActivityTypes)
widgetSnapshotPreferenceStore.setHeatmapActivityTypes(activityTypes)
}

func todayDisplayOptions() -> TodayDisplayOptions {
let dueDateVisibilityRawValue = store.string(forKey: Key.todayDueDateVisibility)
let focusVisibilityRawValue = store.string(forKey: Key.todayFocusVisibility)

return TodayDisplayOptions(
dueDateVisibility: TodayDisplayOptions.DueDateVisibility(
rawValue: dueDateVisibilityRawValue ?? ""
) ?? .all,
focusVisibility: TodayDisplayOptions.FocusVisibility(
rawValue: focusVisibilityRawValue ?? ""
) ?? .all
)
widgetSnapshotPreferenceStore.todayDisplayOptions()
}

func setTodayDisplayOptions(_ options: TodayDisplayOptions) {
store.setString(options.dueDateVisibility.rawValue, forKey: Key.todayDueDateVisibility)
store.setString(options.focusVisibility.rawValue, forKey: Key.todayFocusVisibility)
widgetSnapshotPreferenceStore.setTodayDisplayOptions(options)
}
}
19 changes: 19 additions & 0 deletions DevLog/Domain/Extension/Calendar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Calendar.swift
// DevLog
//
// Created by opfic on 4/30/26.
//

import Foundation

extension Calendar {
func startOfQuarter(for date: Date) -> Date {
let month = component(.month, from: date)
let startMonth = ((month - 1) / 3) * 3 + 1
var components = dateComponents([.year], from: date)
components.month = startMonth
components.day = 1
return self.date(from: components) ?? startOfDay(for: date)
}
}
17 changes: 2 additions & 15 deletions DevLog/Presentation/ViewModel/ProfileViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ final class ProfileViewModel: Store {
case fetchActivityQuarter(Date)
case updateStatusMessage(String)
case updateHeatmapActivityKinds(Set<ActivityKind>)
case syncHeatmapWidget
}

private(set) var state = State()
Expand All @@ -71,10 +70,8 @@ final class ProfileViewModel: Store {
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
private let fetchHeatmapActivityTypesUseCase: FetchHeatmapActivityTypesUseCase
private let updateHeatmapActivityTypesUseCase: UpdateHeatmapActivityTypesUseCase
private let widgetCoordinator: HeatmapWidgetSyncCoordinator
private let calendar = Calendar.current
private let loadingState = LoadingState()
private var syncHeatmapWidgetTask: Task<Void, Never>?
private var cancellables = Set<AnyCancellable>()

init(
Expand All @@ -91,9 +88,6 @@ final class ProfileViewModel: Store {
self.networkConnectivityUseCase = networkConnectivityUseCase
self.fetchHeatmapActivityTypesUseCase = fetchHeatmapActivityTypesUseCase
self.updateHeatmapActivityTypesUseCase = updateHeatmapActivityTypesUseCase
self.widgetCoordinator = HeatmapWidgetSyncCoordinator(
fetchTodosUseCase: fetchTodosUseCase
)
setupNetworkObserving()
}

Expand All @@ -107,7 +101,7 @@ final class ProfileViewModel: Store {
guard let quarterStart = quarterStart(for: Date()) else { break }
state.selectedQuarterStart = quarterStart
}
effects = [.fetchUserData, .syncHeatmapWidget]
effects = [.fetchUserData]
let rawValues = fetchHeatmapActivityTypesUseCase.execute()
let settings = normalizeActivityKinds(rawValues)
if !settings.isEmpty {
Expand Down Expand Up @@ -180,7 +174,7 @@ final class ProfileViewModel: Store {
} else {
state.selectedActivityKinds.insert(activityKind)
}
effects = [.updateHeatmapActivityKinds(state.selectedActivityKinds), .syncHeatmapWidget]
effects = [.updateHeatmapActivityKinds(state.selectedActivityKinds)]
case .willUpdateStatusMessage:
if !state.isNetworkConnected { break }
let message = self.state.statusMessage
Expand Down Expand Up @@ -241,13 +235,6 @@ final class ProfileViewModel: Store {
return activityKinds.contains(activityKind)
}
updateHeatmapActivityTypesUseCase.execute(rawValues)
case .syncHeatmapWidget:
syncHeatmapWidgetTask?.cancel()
syncHeatmapWidgetTask = Task { [selectedActivityKinds = state.selectedActivityKinds] in
await widgetCoordinator.sync(
selectedActivityKinds: selectedActivityKinds
)
}
}
}
}
Expand Down
13 changes: 0 additions & 13 deletions DevLog/Presentation/ViewModel/TodayViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ final class TodayViewModel: Store {
case fetchTodos
case completeTodo(TodayTodoItem)
case togglePinned(TodayTodoItem)
case syncTodayWidget
}

private(set) var state = State()
Expand All @@ -83,7 +82,6 @@ final class TodayViewModel: Store {
private let upsertTodoUseCase: UpsertTodoUseCase
private let updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase
private let loadingState = LoadingState()
private let widgetCoordinator = TodayWidgetSyncCoordinator()

init(
fetchTodosUseCase: FetchTodosUseCase,
Expand Down Expand Up @@ -258,11 +256,6 @@ final class TodayViewModel: Store {
send(.setAlert(true))
}
}
case .syncTodayWidget:
widgetCoordinator.sync(
todos: state.todos,
displayOptions: state.displayOptions
)
}
}
}
Expand Down Expand Up @@ -292,15 +285,12 @@ private extension TodayViewModel {
case .setDueDateVisibility(let visibility):
state.displayOptions.dueDateVisibility = visibility
updateTodayDisplayOptionsUseCase.execute(state.displayOptions)
return [.syncTodayWidget]
case .setFocusVisibility(let visibility):
state.displayOptions.focusVisibility = visibility
updateTodayDisplayOptionsUseCase.execute(state.displayOptions)
return [.syncTodayWidget]
case .resetDisplayOptions:
state.displayOptions = .default
updateTodayDisplayOptionsUseCase.execute(state.displayOptions)
return [.syncTodayWidget]
case .completeTodo(let item):
return [.completeTodo(item)]
case .togglePinned(let item):
Expand All @@ -325,7 +315,6 @@ private extension TodayViewModel {
switch action {
case .fetchTodos(let items):
state.todos = items
return [.syncTodayWidget]
case .setLoading(let isLoading):
state.isLoading = isLoading
case .updateTodo(let item):
Expand All @@ -334,10 +323,8 @@ private extension TodayViewModel {
} else {
state.todos.append(item)
}
return [.syncTodayWidget]
case .removeTodo(let todoId):
state.todos.removeAll { $0.id == todoId }
return [.syncTodayWidget]
default:
break
}
Expand Down
61 changes: 61 additions & 0 deletions DevLog/Storage/Persistence/WidgetSnapshotPreferenceStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// WidgetSnapshotPreferenceStore.swift
// DevLog
//
// Created by opfic on 4/30/26.
//

import Foundation

final class WidgetSnapshotPreferenceStore {
private enum Key {
static let heatmapActivityTypes = "Profile.heatmap.activityTypes"
static let todayDueDateVisibility = "Today.dueDateVisibility"
static let todayFocusVisibility = "Today.focusVisibility"
}

private let userDefaults: UserDefaults

init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}

func heatmapActivityTypes() -> [String] {
userDefaults.stringArray(forKey: Key.heatmapActivityTypes) ?? []
}

func setHeatmapActivityTypes(_ activityTypes: [String]) {
userDefaults.set(activityTypes, forKey: Key.heatmapActivityTypes)
}

func selectedActivityKinds() -> Set<ActivityKind> {
let selectedActivityKinds = Set(
heatmapActivityTypes().compactMap(ActivityKind.init(rawValue:))
)
let selectableActivityKinds: [ActivityKind] = [.created, .completed, .deleted]
let normalizedActivityKinds = Set(
selectableActivityKinds.filter { selectedActivityKinds.contains($0) }
)

return normalizedActivityKinds.isEmpty ? Set(selectableActivityKinds) : normalizedActivityKinds
}

func todayDisplayOptions() -> TodayDisplayOptions {
let dueDateVisibilityRawValue = userDefaults.string(forKey: Key.todayDueDateVisibility)
let focusVisibilityRawValue = userDefaults.string(forKey: Key.todayFocusVisibility)

return TodayDisplayOptions(
dueDateVisibility: TodayDisplayOptions.DueDateVisibility(
rawValue: dueDateVisibilityRawValue ?? ""
) ?? .all,
focusVisibility: TodayDisplayOptions.FocusVisibility(
rawValue: focusVisibilityRawValue ?? ""
) ?? .all
)
}

func setTodayDisplayOptions(_ options: TodayDisplayOptions) {
userDefaults.set(options.dueDateVisibility.rawValue, forKey: Key.todayDueDateVisibility)
userDefaults.set(options.focusVisibility.rawValue, forKey: Key.todayFocusVisibility)
}
}
Loading