diff --git a/DependencyGraph/mohanyang_dev_graph.png b/DependencyGraph/mohanyang_dev_graph.png index 43bc5d7..f81291e 100644 Binary files a/DependencyGraph/mohanyang_dev_graph.png and b/DependencyGraph/mohanyang_dev_graph.png differ diff --git a/DependencyGraph/mohanyang_prod_graph.png b/DependencyGraph/mohanyang_prod_graph.png index 5b13340..86b7571 100644 Binary files a/DependencyGraph/mohanyang_prod_graph.png and b/DependencyGraph/mohanyang_prod_graph.png differ diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift index 4bea898..76aaff8 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift @@ -19,4 +19,5 @@ public enum Core: String, Modulable { case StreamListener case LiveActivityClient case AudioClient + case BackgroundTaskClient } diff --git a/Projects/Core/BackgroundTaskClient/Interface/BackgroundTaskClientInterface.swift b/Projects/Core/BackgroundTaskClient/Interface/BackgroundTaskClientInterface.swift new file mode 100644 index 0000000..448aa76 --- /dev/null +++ b/Projects/Core/BackgroundTaskClient/Interface/BackgroundTaskClientInterface.swift @@ -0,0 +1,29 @@ +// +// BackgroundTaskClientInterface.swift +// BackgroundTaskClient +// +// Created by MinseokKang on 12/5/24. +// + +import Foundation +import BackgroundTasks + +import Dependencies +import DependenciesMacros + +@DependencyClient +public struct BackgroundTaskClient { + public var registerTask: @Sendable ( + _ identifier: String, + _ queue: DispatchQueue?, + _ launchHandler: @escaping (BGTask) -> Void + ) -> Bool = { _, _, _ in false } + + public var submit: @Sendable (_ taskRequest: BGTaskRequest) throws -> Void + + public var cancel: @Sendable (_ identifier: String) -> Void + + public var cancelAllTaskRequests: @Sendable () -> Void + + public var pendingTaskRequests: () async -> [BGTaskRequest] = { [] } +} diff --git a/Projects/Core/BackgroundTaskClient/Interface/TestKey.swift b/Projects/Core/BackgroundTaskClient/Interface/TestKey.swift new file mode 100644 index 0000000..e462a5e --- /dev/null +++ b/Projects/Core/BackgroundTaskClient/Interface/TestKey.swift @@ -0,0 +1,14 @@ +// +// TestKey.swift +// BackgroundTaskClientInterface +// +// Created by devMinseok on 12/5/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Dependencies + +extension BackgroundTaskClient: TestDependencyKey { + public static let previewValue = Self() + public static let testValue = Self() +} diff --git a/Projects/Core/BackgroundTaskClient/Project.swift b/Projects/Core/BackgroundTaskClient/Project.swift new file mode 100644 index 0000000..909cce9 --- /dev/null +++ b/Projects/Core/BackgroundTaskClient/Project.swift @@ -0,0 +1,27 @@ +// +// Project.swift +// BackgroundTaskClientManifests +// +// Created by MinseokKang on 12/5/24. +// + +import ProjectDescription +import ProjectDescriptionHelpers + +@_spi(Core) +@_spi(Shared) +import DependencyPlugin + +let project: Project = .makeTMABasedProject( + module: Core.BackgroundTaskClient, + scripts: [], + targets: [ + .sources, + .interface + ], + dependencies: [ + .interface: [ + .dependency(rootModule: Shared.self) + ] + ] +) diff --git a/Projects/Core/BackgroundTaskClient/Sources/BackgroundTaskClient.swift b/Projects/Core/BackgroundTaskClient/Sources/BackgroundTaskClient.swift new file mode 100644 index 0000000..6de84cc --- /dev/null +++ b/Projects/Core/BackgroundTaskClient/Sources/BackgroundTaskClient.swift @@ -0,0 +1,39 @@ +// +// BackgroundTaskClient.swift +// BackgroundTaskClient +// +// Created by MinseokKang on 12/5/24. +// + +import Foundation +import BackgroundTasks + +import BackgroundTaskClientInterface + +import Dependencies + +extension BackgroundTaskClient: DependencyKey { + public static let liveValue: BackgroundTaskClient = .live() + + public static func live() -> BackgroundTaskClient { + let backgroundTaskScheduler = BGTaskScheduler.shared + + return .init( + registerTask: { identifier, queue, handler in + return backgroundTaskScheduler.register(forTaskWithIdentifier: identifier, using: queue, launchHandler: handler) + }, + submit: { request in + try backgroundTaskScheduler.submit(request) + }, + cancel: { identifier in + backgroundTaskScheduler.cancel(taskRequestWithIdentifier: identifier) + }, + cancelAllTaskRequests: { + backgroundTaskScheduler.cancelAllTaskRequests() + }, + pendingTaskRequests: { + await backgroundTaskScheduler.pendingTaskRequests() + } + ) + } +} diff --git a/Projects/Core/LiveActivityClient/Interface/LiveActivityClientInterface.swift b/Projects/Core/LiveActivityClient/Interface/LiveActivityClientInterface.swift index 99db1f5..ad37a91 100644 --- a/Projects/Core/LiveActivityClient/Interface/LiveActivityClientInterface.swift +++ b/Projects/Core/LiveActivityClient/Interface/LiveActivityClientInterface.swift @@ -46,4 +46,6 @@ public protocol LiveActivityClientProtocol { func endAllActivityImmediately( type: T.Type ) async + + func getActivities(type: T.Type) -> [Activity] } diff --git a/Projects/Core/LiveActivityClient/Interface/TestKey.swift b/Projects/Core/LiveActivityClient/Interface/TestKey.swift index 712d72d..d0e0cbf 100644 --- a/Projects/Core/LiveActivityClient/Interface/TestKey.swift +++ b/Projects/Core/LiveActivityClient/Interface/TestKey.swift @@ -44,4 +44,8 @@ class LiveActivityClientImplTest: LiveActivityClientProtocol { func endAllActivityImmediately( type: T.Type ) async {} + + func getActivities(type: T.Type) -> [Activity] { + return [] + } } diff --git a/Projects/Core/LiveActivityClient/Sources/LiveActivityClient.swift b/Projects/Core/LiveActivityClient/Sources/LiveActivityClient.swift index eba094e..48f83c8 100644 --- a/Projects/Core/LiveActivityClient/Sources/LiveActivityClient.swift +++ b/Projects/Core/LiveActivityClient/Sources/LiveActivityClient.swift @@ -66,4 +66,8 @@ final class LiveActivityClientImpl: LiveActivityClientProtocol { await activity.end(nil, dismissalPolicy: .immediate) } } + + func getActivities(type: T.Type) -> [Activity] { + return Activity.activities + } } diff --git a/Projects/Core/UserNotificationClient/Interface/Interface.swift b/Projects/Core/UserNotificationClient/Interface/Interface.swift index 5a50930..b2d55d5 100644 --- a/Projects/Core/UserNotificationClient/Interface/Interface.swift +++ b/Projects/Core/UserNotificationClient/Interface/Interface.swift @@ -20,7 +20,9 @@ public struct UserNotificationClient { } public var removeDeliveredNotificationsWithIdentifiers: @Sendable ([String]) async -> Void public var removePendingNotificationRequestsWithIdentifiers: @Sendable ([String]) async -> Void + public var removeAllPendingNotificationRequests: @Sendable () async -> Void public var requestAuthorization: @Sendable (UNAuthorizationOptions) async throws -> Bool + public var setBadgeCount: @Sendable (Int) async throws -> Void @CasePathable public enum DelegateEvent { diff --git a/Projects/Core/UserNotificationClient/Sources/Implementation.swift b/Projects/Core/UserNotificationClient/Sources/Implementation.swift index 43382d6..0362f28 100644 --- a/Projects/Core/UserNotificationClient/Sources/Implementation.swift +++ b/Projects/Core/UserNotificationClient/Sources/Implementation.swift @@ -38,8 +38,14 @@ extension UserNotificationClient: DependencyKey { removePendingNotificationRequestsWithIdentifiers: { UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: $0) }, + removeAllPendingNotificationRequests: { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + }, requestAuthorization: { try await UNUserNotificationCenter.current().requestAuthorization(options: $0) + }, + setBadgeCount: { badgeCount in + try await UNUserNotificationCenter.current().setBadgeCount(badgeCount) } ) } diff --git a/Projects/Domain/PomodoroService/Interface/Model/PomodoroActivityAttributes.swift b/Projects/Domain/PomodoroService/Interface/Model/PomodoroActivityAttributes.swift index b7e7b1d..6cef3fa 100644 --- a/Projects/Domain/PomodoroService/Interface/Model/PomodoroActivityAttributes.swift +++ b/Projects/Domain/PomodoroService/Interface/Model/PomodoroActivityAttributes.swift @@ -15,8 +15,8 @@ public struct PomodoroActivityAttributes: Equatable, ActivityAttributes { public var goalDatetime: Date public var isRest: Bool - public var dateRange: ClosedRange { - return Date.now...goalDatetime + public func isTimerOver() -> Bool { + return Date() >= goalDatetime } public init( diff --git a/Projects/Domain/PomodoroService/Interface/PomodoroServiceInterface.swift b/Projects/Domain/PomodoroService/Interface/PomodoroServiceInterface.swift index f4d9537..27a4164 100644 --- a/Projects/Domain/PomodoroService/Interface/PomodoroServiceInterface.swift +++ b/Projects/Domain/PomodoroService/Interface/PomodoroServiceInterface.swift @@ -8,6 +8,8 @@ import UserDefaultsClientInterface import DatabaseClientInterface import APIClientInterface +import BackgroundTaskClientInterface +import LiveActivityClientInterface import Dependencies import DependenciesMacros @@ -21,6 +23,8 @@ public struct PomodoroService { public var changeCategoryTime: @Sendable (_ apiClient: APIClient, _ categoryID: Int, _ request: EditCategoryRequest) async throws -> Void public var saveFocusTimeHistory: @Sendable (_ apiClient: APIClient, _ databaseClient: DatabaseClient, _ request: [FocusTimeHistory]) async throws -> Void public var getFocusTimeSummaries: @Sendable (_ apiClient: APIClient) async throws -> FocusTimeSummary + + public var registerBGTaskToUpdateTimer: @Sendable (_ bgTaskClient: BackgroundTaskClient, _ liveActivityClient: LiveActivityClient) -> Bool = { _, _ in false } } extension PomodoroService: TestDependencyKey { diff --git a/Projects/Domain/PomodoroService/Sources/PomodoroService.swift b/Projects/Domain/PomodoroService/Sources/PomodoroService.swift index 7f66759..179b44f 100644 --- a/Projects/Domain/PomodoroService/Sources/PomodoroService.swift +++ b/Projects/Domain/PomodoroService/Sources/PomodoroService.swift @@ -8,6 +8,7 @@ @_spi(Internal) import PomodoroServiceInterface import APIClientInterface +import Foundation import Dependencies @@ -18,7 +19,8 @@ extension PomodoroService: DependencyKey { private static func live() -> PomodoroService { return .init( - syncCategoryList: { apiClient, databaseClient in + syncCategoryList: { + apiClient, databaseClient in let api = CategoryAPI.getCategoryList let categoryList = try await apiClient.apiRequest(request: api, as: [PomodoroCategory].self) for category in categoryList { @@ -50,6 +52,23 @@ extension PomodoroService: DependencyKey { getFocusTimeSummaries: { apiClient in let api = FocusTimeAPI.getSummaries return try await apiClient.apiRequest(request: api, as: FocusTimeSummary.self) + }, + registerBGTaskToUpdateTimer: { bgTaskClient, liveActivityClient in + bgTaskClient.registerTask( + identifier: "com.pomonyang.mohanyang.update_LiveActivity", + queue: nil + ) { task in + task.expirationHandler = { + task.setTaskCompleted(success: false) + } + let pomodoroActivities = liveActivityClient.protocolAdapter.getActivities(type: PomodoroActivityAttributes.self) + Task { + if let firstActivity = pomodoroActivities.first { + await firstActivity.update(firstActivity.content) + } + task.setTaskCompleted(success: true) + } + } } ) } diff --git a/Projects/Feature/Feature/Sources/AppCore.swift b/Projects/Feature/Feature/Sources/AppCore.swift index 863d546..a6c204d 100644 --- a/Projects/Feature/Feature/Sources/AppCore.swift +++ b/Projects/Feature/Feature/Sources/AppCore.swift @@ -19,11 +19,16 @@ import UserDefaultsClientInterface import UserNotificationClientInterface import CatServiceInterface import UserServiceInterface +import PomodoroServiceInterface import DatabaseClientInterface import StreamListenerInterface +import BackgroundTaskClientInterface +import LiveActivityClientInterface import ComposableArchitecture +import BackgroundTasks + @Reducer public struct AppCore { @ObservableState @@ -34,9 +39,9 @@ public struct AppCore { var onboarding: OnboardingCore.State? @Presents var networkError: NetworkErrorCore.State? @Presents var requestError: RequestErrorCore.State? - + var isLoading: Bool = false - + public init() {} } @@ -58,7 +63,9 @@ public struct AppCore { @Dependency(UserService.self) var userService @Dependency(DatabaseClient.self) var databaseClient @Dependency(StreamListener.self) var streamListener - + @Dependency(BackgroundTaskClient.self) var backgroundTaskClient + @Dependency(LiveActivityClient.self) var liveActivityClient + public init() {} public var body: some ReducerOf { @@ -97,40 +104,62 @@ public struct AppCore { await send(.serverState(serverState)) } } - + case .appDelegate: return .none + case .didChangeScenePhase(.background): + return .run { send in + let pomodoroActivity = liveActivityClient.protocolAdapter.getActivities(type: PomodoroActivityAttributes.self).first + let pendingBGTaskRequest = await backgroundTaskClient.pendingTaskRequests().first + + if let pomodoroActivity { + if let pendingBGTaskRequest { + if pendingBGTaskRequest.earliestBeginDate != pomodoroActivity.content.state.goalDatetime { + backgroundTaskClient.cancel(identifier: pendingBGTaskRequest.identifier) + await pomodoroActivity.update(pomodoroActivity.content) + try submitUpdateLiveActivityBGTask(earliestBeginDate: pomodoroActivity.content.state.goalDatetime) + } + } else { + try submitUpdateLiveActivityBGTask(earliestBeginDate: pomodoroActivity.content.state.goalDatetime) + } + } else { + if let pendingBGTaskRequest { + backgroundTaskClient.cancel(identifier: pendingBGTaskRequest.identifier) + } + } + } + case .didChangeScenePhase: return .none - + case .splash(.moveToHome): state.splash = nil state.home = HomeCore.State() return .none - + case .splash(.moveToOnboarding): state.splash = nil state.onboarding = OnboardingCore.State() return .none - + case .splash: return .none - + case .home: return .none - + case .onboarding(.selectCat(.presented(.namingCat(.presented(.moveToHome))))): state.onboarding = nil state.home = HomeCore.State() return .none - + case .onboarding: return .none - + case .networkError: return .none - + case .requestError(.presented(.moveToHome)): if state.onboarding != nil { state.onboarding = OnboardingCore.State() @@ -138,10 +167,10 @@ public struct AppCore { state.home = HomeCore.State() } return .none - + case .requestError: return .none - + case .serverState(let serverState): switch serverState { case .requestStarted: @@ -158,4 +187,12 @@ public struct AppCore { return .none } } + + func submitUpdateLiveActivityBGTask(earliestBeginDate: Date) throws { + let request = BGProcessingTaskRequest(identifier: "com.pomonyang.mohanyang.update_LiveActivity") + request.requiresExternalPower = false + request.requiresNetworkConnectivity = false + request.earliestBeginDate = earliestBeginDate + try backgroundTaskClient.submit(taskRequest: request) + } } diff --git a/Projects/Feature/Feature/Sources/AppDelegateCore.swift b/Projects/Feature/Feature/Sources/AppDelegateCore.swift index b9dd419..8b3a488 100644 --- a/Projects/Feature/Feature/Sources/AppDelegateCore.swift +++ b/Projects/Feature/Feature/Sources/AppDelegateCore.swift @@ -14,8 +14,9 @@ import UserNotificationClientInterface import KeychainClientInterface import DatabaseClientInterface import LiveActivityClientInterface -import AppService +import BackgroundTaskClientInterface import APIClientInterface +import AppService import PomodoroServiceInterface import ComposableArchitecture @@ -41,6 +42,8 @@ public struct AppDelegateCore { @Dependency(KeychainClient.self) var keychainClient @Dependency(UserNotificationClient.self) var userNotificationClient @Dependency(LiveActivityClient.self) var liveActivityClient + @Dependency(PomodoroService.self) var pomodoroService + @Dependency(BackgroundTaskClient.self) var backgroundTaskClient public init() {} @@ -54,7 +57,6 @@ public struct AppDelegateCore { ) -> EffectOf { switch action { case .didFinishLaunching: - UIApplication.shared.applicationIconBadgeNumber = 0 firebaseInitilize() datadogInitilize() @@ -62,7 +64,13 @@ public struct AppDelegateCore { let userNotificationEventStream = userNotificationClient.delegate() + _ = pomodoroService.registerBGTaskToUpdateTimer( + bgTaskClient: backgroundTaskClient, + liveActivityClient: liveActivityClient + ) + return .run { send in + try await userNotificationClient.setBadgeCount(0) await withThrowingTaskGroup(of: Void.self) { group in group.addTask { for await event in userNotificationEventStream { @@ -102,12 +110,15 @@ public struct AppDelegateCore { return .none case .willTerminate: - return .run { send in + return .run { _ in + await userNotificationClient.removeAllPendingNotificationRequests() await liveActivityClient.protocolAdapter.endAllActivityImmediately(type: PomodoroActivityAttributes.self) } } } - +} + +extension AppDelegateCore { private func firebaseInitilize() { FirebaseApp.configure() } diff --git a/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift b/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift index d1dbca7..53425af 100644 --- a/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift +++ b/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift @@ -125,7 +125,7 @@ public struct HomeView: View { } } } - .tooltipDestination(tooltip: .constant(store.homeCatTooltip), allowsHitTesting: false) + .tooltipDestination(tooltip: .constant(store.homeCatTooltip)) .tooltipDestination(tooltip: $store.homeCategoryGuideTooltip.sending(\.setHomeCategoryGuideTooltip)) .tooltipDestination(tooltip: $store.homeTimeGuideTooltip.sending(\.setHomeTimeGuideTooltip)) .toastDestination(toast: $store.toast) diff --git a/Projects/Feature/LAPomodoroFeature/Sources/LockScreen/TimerLockScreenCore.swift b/Projects/Feature/LAPomodoroFeature/Sources/LockScreen/TimerLockScreenCore.swift deleted file mode 100644 index 871b700..0000000 --- a/Projects/Feature/LAPomodoroFeature/Sources/LockScreen/TimerLockScreenCore.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TimerLockScreenCore.swift -// LAPomodoroFeature -// -// Created by devMinseok on 11/24/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import Foundation - -import PomodoroServiceInterface - -import ComposableArchitecture - -@Reducer -struct TimerLockScreenCore { - @ObservableState - struct State: Equatable { - var contentState: PomodoroActivityAttributes.ContentState - - init(contentState: PomodoroActivityAttributes.ContentState) { - self.contentState = contentState - } - } - - enum Action {} - - @Dependency(\.suspendingClock) var continuousClock - - init() {} - - var body: some ReducerOf { - Reduce(self.core) - } - - private func core(state: inout State, action: Action) -> EffectOf { - return .none - } -} diff --git a/Projects/Feature/LAPomodoroFeature/Sources/LockScreen/TimerLockScreenView.swift b/Projects/Feature/LAPomodoroFeature/Sources/LockScreen/TimerLockScreenView.swift deleted file mode 100644 index 4597e2e..0000000 --- a/Projects/Feature/LAPomodoroFeature/Sources/LockScreen/TimerLockScreenView.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// TimerLockScreenView.swift -// LAPomodoroFeature -// -// Created by devMinseok on 11/24/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import SwiftUI - -import DesignSystem - -import ComposableArchitecture - -struct TimerLockScreenView: View { - @Bindable var store: StoreOf - - init(store: StoreOf) { - self.store = store - } - - var body: some View { - HStack(spacing: .zero) { - VStack(alignment: .leading, spacing: 4) { - HStack(alignment: .center, spacing: Alias.Spacing.xSmall) { - if store.contentState.isRest { - DesignSystemAsset.Image._20Rest.swiftUIImage - Text("휴식중") - .foregroundStyle(Alias.Color.Text.tertiary) - .font(Typography.bodySB) - } else { - store.contentState.category.image - .resizable() - .frame(width: 20, height: 20) - Text(store.contentState.category.title) - .foregroundStyle(Alias.Color.Text.tertiary) - .font(Typography.bodySB) - } - Spacer() - } - - Text( - timerInterval: Date()...store.contentState.goalDatetime, - pauseTime: Date(), - countsDown: true, - showsHours: false - ) - .monospacedDigit() - .foregroundStyle(Alias.Color.Text.primary) - .font(Typography.header2) - - Spacer() - } - - Spacer() - - DesignSystemAsset.Image.hairBall.swiftUIImage - } - .padding(Alias.Spacing.xxLarge) - .activityBackgroundTint(Alias.Color.Background.accent2) - .activitySystemActionForegroundColor(Alias.Color.Background.inverse) - } -} diff --git a/Projects/Feature/LAPomodoroFeature/Sources/PomodoroLiveActivityWidget.swift b/Projects/Feature/LAPomodoroFeature/Sources/PomodoroLiveActivityWidget.swift index e022cf7..32db9f0 100644 --- a/Projects/Feature/LAPomodoroFeature/Sources/PomodoroLiveActivityWidget.swift +++ b/Projects/Feature/LAPomodoroFeature/Sources/PomodoroLiveActivityWidget.swift @@ -19,33 +19,9 @@ public struct PomodoroLiveActivityWidget: Widget { public var body: some WidgetConfiguration { ActivityConfiguration(for: PomodoroActivityAttributes.self) { context in - TimerLockScreenView( - store: .init( - initialState: .init(contentState: context.state), - reducer: { TimerLockScreenCore() }) - ) + TimerLockScreenView(context: context) } dynamicIsland: { context in - DynamicIsland { - DynamicIslandExpandedRegion(.leading) { - Text("Leading") - } - DynamicIslandExpandedRegion(.trailing) { - Text("Trailing") - } - DynamicIslandExpandedRegion(.center) { - Text("Center") - } - DynamicIslandExpandedRegion(.bottom) { - Text("Bottom") - } - } compactLeading: { - Text("Leading") - } compactTrailing: { - Text("Trailing") - } minimal: { - Text("Minimal") - } - .keylineTint(Alias.Color.Background.accent2) + TimerDynamicIsland(context: context).body } } } diff --git a/Projects/Feature/LAPomodoroFeature/Sources/TimerDynamicIsland.swift b/Projects/Feature/LAPomodoroFeature/Sources/TimerDynamicIsland.swift new file mode 100644 index 0000000..623fbf5 --- /dev/null +++ b/Projects/Feature/LAPomodoroFeature/Sources/TimerDynamicIsland.swift @@ -0,0 +1,118 @@ +// +// TimerDynamicIsland.swift +// LAPomodoroFeature +// +// Created by devMinseok on 12/18/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import WidgetKit +import SwiftUI + +import DesignSystem +import PomodoroServiceInterface + +struct TimerDynamicIsland { + let context: ActivityViewContext + + var body: DynamicIsland { + DynamicIsland { + expandedView + } compactLeading: { + compactLeadingView + } compactTrailing: { + compactTrailingView + } minimal: { + minimalView + } + .keylineTint(Alias.Color.Background.accent2) + } + + @DynamicIslandExpandedContentBuilder + var expandedView: DynamicIslandExpandedContent { + DynamicIslandExpandedRegion(.leading, priority: 1.0) { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center, spacing: Alias.Spacing.xSmall) { + if context.state.isRest { + DesignSystemAsset.Image._20Rest.swiftUIImage + Text("휴식중") + .foregroundStyle(Alias.Color.Text.disabled) + .font(Typography.bodySB) + } else { + context.state.category.image + .resizable() + .frame(width: 20, height: 20) + Text(context.state.category.title) + .foregroundStyle(Alias.Color.Text.disabled) + .font(Typography.bodySB) + } + Spacer() + } + + Group { + if context.state.isTimerOver() { + Text("0:00") + .monospacedDigit() + .foregroundStyle(Alias.Color.Text.inverse) + .font(Typography.header2) + SingleLineText { + Text( + context.state.goalDatetime, + style: .timer + ) + .monospacedDigit() + Text(" 초과") + } + .foregroundStyle(Alias.Color.Accent.red) + .font(Typography.header5) + } else { + Text( + context.state.goalDatetime, + style: .timer + ) + .monospacedDigit() + .foregroundStyle(Alias.Color.Text.inverse) + .font(Typography.header1) + } + } + .frame(width: 200, alignment: .leading) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + DynamicIslandExpandedRegion(.trailing) { + VStack(alignment: .center) { + DesignSystemAsset.Image.hairBall.swiftUIImage + } + .frame(maxHeight: .infinity) + } + } + + @ViewBuilder + var compactLeadingView: some View { + context.state.category.image + .resizable() + .frame(width: 20, height: 20) + } + + @ViewBuilder + var compactTrailingView: some View { + if context.state.isTimerOver() { + DesignSystemAsset.Image._20CheckCircle.swiftUIImage + } else { + Text( + context.state.goalDatetime, + style: .timer + ) + .font(Typography.subBodySB) + .foregroundStyle(Alias.Color.Text.inverse) + .monospacedDigit() + .frame(maxWidth: 40, alignment: .leading) + } + } + + @ViewBuilder + var minimalView: some View { + DesignSystemAsset.Image._24Timer.swiftUIImage + } +} diff --git a/Projects/Feature/LAPomodoroFeature/Sources/TimerLockScreenView.swift b/Projects/Feature/LAPomodoroFeature/Sources/TimerLockScreenView.swift new file mode 100644 index 0000000..b38e40c --- /dev/null +++ b/Projects/Feature/LAPomodoroFeature/Sources/TimerLockScreenView.swift @@ -0,0 +1,75 @@ +// +// TimerLockScreenView.swift +// LAPomodoroFeature +// +// Created by devMinseok on 11/24/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI +import WidgetKit + +import DesignSystem +import PomodoroServiceInterface + +/// 아래 Text사용하면 update시 LiveActivity가 멈추는 버그 있음 +/// Text(timerInterval:,pauseTime:,countsDown:,showsHours:) +struct TimerLockScreenView: View { + let context: ActivityViewContext + + var body: some View { + HStack(spacing: .zero) { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: Alias.Spacing.xSmall) { + if context.state.isRest { + DesignSystemAsset.Image._20Rest.swiftUIImage + Text("휴식중") + .foregroundStyle(Alias.Color.Text.tertiary) + .font(Typography.bodySB) + } else { + context.state.category.image + .resizable() + .frame(width: 20, height: 20) + Text(context.state.category.title) + .foregroundStyle(Alias.Color.Text.tertiary) + .font(Typography.bodySB) + } + Spacer() + } + + if context.state.isTimerOver() { + Text("0:00") + .monospacedDigit() + .foregroundStyle(Alias.Color.Text.primary) + .font(Typography.header2) + SingleLineText { + Text( + context.state.goalDatetime, + style: .timer + ) + .monospacedDigit() + Text(" 초과") + } + .foregroundStyle(Alias.Color.Accent.red) + .font(Typography.header5) + } else { + Text( + context.state.goalDatetime, + style: .timer + ) + .monospacedDigit() + .foregroundStyle(Alias.Color.Text.primary) + .font(Typography.header1) + } + } + + Spacer() + + DesignSystemAsset.Image.hairBall.swiftUIImage + } + .padding(Alias.Spacing.xxLarge) + .frame(height: 126) + .activityBackgroundTint(Alias.Color.Background.accent2) + .activitySystemActionForegroundColor(Alias.Color.Background.inverse) + } +} diff --git a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift index 1200e72..c7ea6be 100644 --- a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift +++ b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift @@ -268,7 +268,7 @@ public struct FocusPomodoroCore { await send(.set(\.restWaiting, restWaitingState)) } } else { - state.overTimeBySeconds = timeDifference + state.overTimeBySeconds = -(timeDifference) } } else { state.focusTimeBySeconds = timeDifference diff --git a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift index 4603476..ab88e25 100644 --- a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift +++ b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift @@ -57,10 +57,12 @@ public struct FocusPomodoroView: View { Text(formatTime(from: store.focusTimeBySeconds)) .foregroundStyle(Alias.Color.Text.primary) .font(Typography.header1) + .monospacedDigit() if store.overTimeBySeconds > 0 { Text("\(formatTime(from: store.overTimeBySeconds)) 초과") .foregroundStyle(Alias.Color.Accent.red) .font(Typography.header4) + .monospacedDigit() } else { Spacer() .frame(height: 25) diff --git a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift index 925d8f3..bcd7f31 100644 --- a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift +++ b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift @@ -164,7 +164,7 @@ public struct RestPomodoroCore { case .setupRestTime: guard let selectedCategory = state.selectedCategory else { return .none } - state.goalDatetime = Date().addingTimeInterval(Double(selectedCategory.focusTimeSeconds)) + state.goalDatetime = Date().addingTimeInterval(Double(selectedCategory.restTimeSeconds)) state.restTimeBySeconds = selectedCategory.restTimeSeconds return .none @@ -259,7 +259,7 @@ public struct RestPomodoroCore { await send(.goToHome) } } else { - state.overTimeBySeconds = timeDifference + state.overTimeBySeconds = -(timeDifference) } } else { state.restTimeBySeconds = timeDifference diff --git a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift index 20a8f8a..eac3af7 100644 --- a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift +++ b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift @@ -55,10 +55,12 @@ public struct RestPomodoroView: View { Text(formatTime(from: store.restTimeBySeconds)) .foregroundStyle(Alias.Color.Text.primary) .font(Typography.header1) + .monospacedDigit() if store.overTimeBySeconds > 0 { Text("\(formatTime(from: store.overTimeBySeconds)) 초과") .foregroundStyle(Alias.Color.Accent.red) .font(Typography.header4) + .monospacedDigit() } else { Spacer() .frame(height: 25) diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/20/20_check_circle.imageset/20_check_circle.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/20/20_check_circle.imageset/20_check_circle.svg new file mode 100644 index 0000000..4fa591c --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/20/20_check_circle.imageset/20_check_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/20/20_check_circle.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/20/20_check_circle.imageset/Contents.json new file mode 100644 index 0000000..e99329e --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/20/20_check_circle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "20_check_circle.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_timer.imageset/24_timer.svg b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_timer.imageset/24_timer.svg new file mode 100644 index 0000000..d3bf0e9 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_timer.imageset/24_timer.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_timer.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_timer.imageset/Contents.json new file mode 100644 index 0000000..4ecdc45 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Image.xcassets/24/24_timer.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "24_timer.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Sources/Component/SingleLineText.swift b/Projects/Shared/DesignSystem/Sources/Component/SingleLineText.swift new file mode 100644 index 0000000..8763021 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Component/SingleLineText.swift @@ -0,0 +1,52 @@ +// +// SingleLineText.swift +// DesignSystem +// +// Created by devMinseok on 12/17/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI + +public struct SingleLineText: View { + let content: Text + + public init(@SingleLineTextBuilder content: () -> Text) { + self.content = content() + } + + public var body: some View { + return content + } +} + +@resultBuilder +public enum SingleLineTextBuilder { + public static func buildBlock(_ components: Text...) -> Text { + return components.reduce(Text(""), { $0 + $1 }) + } + + public static func buildArray(_ components: [Text]) -> Text { + return components.reduce(Text(""), { $0 + $1 }) + } + + public static func buildLimitedAvailability(_ component: Text) -> Text { + return component + } + + public static func buildEither(first component: Text) -> Text { + return component + } + + public static func buildEither(second component: Text) -> Text { + return component + } + + public static func buildExpression(_ expression: Text) -> Text { + return expression + } + + public static func buildOptional(_ component: Text?) -> Text { + component ?? Text("") + } +} diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift index bc213d1..713dd41 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift @@ -51,6 +51,17 @@ extension InfoPlist { "NSSupportsLiveActivities": true, + // MARK: - BGTask + + "BGTaskSchedulerPermittedIdentifiers": [ + "\(AppEnv.bundleId).update_LiveActivity" + ], + "UIBackgroundModes": [ + "fetch", + "processing" + ], + + // MARK: - Cocoa "NSAppTransportSecurity": [ diff --git a/XCConfig/Project/Mohanyang.xcconfig b/XCConfig/Project/Mohanyang.xcconfig index 8cb4dd9..a9d1c1b 100644 --- a/XCConfig/Project/Mohanyang.xcconfig +++ b/XCConfig/Project/Mohanyang.xcconfig @@ -3,6 +3,6 @@ APP_NAME = 모하냥 -MARKETING_VERSION = 0.1.2 -CURRENT_PROJECT_VERSION = 12 +MARKETING_VERSION = 0.1.3 +CURRENT_PROJECT_VERSION = 15 DEVELOPMENT_TEAM = 9KL4XS83LC