diff --git a/Projects/Presentation/Timer/Sources/Container/TimerContainer.swift b/Projects/Presentation/Timer/Sources/Container/TimerContainer.swift new file mode 100644 index 0000000..21fc0b7 --- /dev/null +++ b/Projects/Presentation/Timer/Sources/Container/TimerContainer.swift @@ -0,0 +1,235 @@ +// +// TimerContainer.swift +// Timer +// +// Created by 홍석현 on 2025-11-10 +// Copyright © 2025 DDD , Ltd., All rights reserved. +// + +import Foundation +import Combine +import ActivityKit +import UserNotifications +import AVFoundation + +// MARK: - Timer Container (ViewModel) +public final class TimerContainer: ObservableObject { + // MARK: - Published State + @Published public private(set) var state: TimerState + + // MARK: - Private Properties + private let model: TimerModelProtocol + private let notificationService: TimerNotificationServiceProtocol + private var cancellables = Set() + private var timerCancellable: AnyCancellable? + private var currentActivity: Activity? + private var audioPlayer: AVAudioPlayer? + + // MARK: - Published Alert State + @Published public var showCompletionAlert: Bool = false + + // MARK: - Initialization + public init( + model: TimerModelProtocol = TimerModel(), + notificationService: TimerNotificationServiceProtocol = TimerNotificationService(), + initialState: TimerState = TimerState() + ) { + self.model = model + self.notificationService = notificationService + self.state = initialState + + // 알림 권한 요청 + Task { + await notificationService.requestAuthorization() + } + } + + // MARK: - Intent Processing + public func send(_ intent: TimerIntent) { + let (newState, sideEffect) = model.reduce(state: state, intent: intent) + state = newState + + if let sideEffect = sideEffect { + handleSideEffect(sideEffect) + } + } + + // MARK: - Side Effect Handling + private func handleSideEffect(_ sideEffect: TimerSideEffect) { + switch sideEffect { + case .startTimerTicking: + startTimer() + startLiveActivity() + scheduleNotification() + + case .stopTimerTicking: + stopTimer() + updateLiveActivity() + cancelNotification() + + case .playAlarm: + playAlarm() + endLiveActivity() + cancelNotification() + + case .showCompletionAlert: + displayCompletionAlert() + } + } + + // MARK: - Timer Management + private var tickCount: Int = 0 + + private func startTimer() { + stopTimer() // 기존 타이머 정리 + tickCount = 0 + + // 애니메이션과 동기화를 위해 0.3초 지연 + Task { @MainActor in + try? await Task.sleep(nanoseconds: 300_000_000) // 0.3초 + + self.timerCancellable = Timer.publish(every: 0.1, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + guard let self = self else { return } + + // endDate 기반으로 남은 시간 계산 (백그라운드에서도 정확함) + if let endDate = self.state.endDate { + let remaining = endDate.timeIntervalSinceNow + + if remaining > 0 { + self.send(.updateRemainingTime(remaining)) + + // 1초마다 Live Activity 업데이트 (10틱 = 1초) + self.tickCount += 1 + if self.tickCount >= 10 { + self.updateLiveActivity() + self.tickCount = 0 + } + } else { + self.stopTimer() + self.send(.timerCompleted) + } + } + } + } + } + + private func stopTimer() { + timerCancellable?.cancel() + timerCancellable = nil + } + + // MARK: - Alarm & Notifications + private func playAlarm() { + // 시스템 사운드 재생 (진동 포함) + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + AudioServicesPlaySystemSound(1005) // 알림 사운드 + + print("🔔 타이머 완료! 알람 울림") + + // 완료 알림도 함께 표시 + displayCompletionAlert() + } + + private func displayCompletionAlert() { + Task { @MainActor in + self.showCompletionAlert = true + } + print("✅ 타이머 완료 알림") + } + + // MARK: - Notification Management + private func scheduleNotification() { + guard let endDate = state.endDate else { return } + notificationService.scheduleNotification(endDate: endDate, totalTime: state.totalTime) + } + + private func cancelNotification() { + notificationService.cancelNotification() + } + + // MARK: - Live Activity Management + private func startLiveActivity() { + let authInfo = ActivityAuthorizationInfo() + guard authInfo.areActivitiesEnabled else { + print("❌ Live Activities가 비활성화되어 있습니다.") + return + } + + if currentActivity != nil { + updateLiveActivity() + return + } + + Task { + let contentState = TimerActivityAttributes.ContentState( + totalTime: state.totalTime, + remainingTime: state.remainingTime, + endDate: state.endDate, + isRunning: state.isRunning, + isPaused: state.isPaused + ) + + do { + let activity = try Activity.request( + attributes: TimerActivityAttributes(), + content: .init(state: contentState, staleDate: nil) + ) + currentActivity = activity + print("✅ Timer Live Activity 시작: \(activity.id)") + } catch { + print("❌ Timer Live Activity 시작 실패: \(error)") + } + } + } + + private func updateLiveActivity() { + guard let activity = currentActivity else { return } + + Task { + let contentState = TimerActivityAttributes.ContentState( + totalTime: state.totalTime, + remainingTime: state.remainingTime, + endDate: state.endDate, + isRunning: state.isRunning, + isPaused: state.isPaused + ) + + let staleDate = Date().addingTimeInterval(2) + await activity.update(.init(state: contentState, staleDate: staleDate)) + } + } + + private func endLiveActivity() { + guard let activity = currentActivity else { return } + + Task { + let finalState = TimerActivityAttributes.ContentState( + totalTime: state.totalTime, + remainingTime: 0, + endDate: nil, + isRunning: false, + isPaused: false + ) + + await activity.end( + .init(state: finalState, staleDate: nil), + dismissalPolicy: .default + ) + currentActivity = nil + print("✅ Timer Live Activity 종료") + } + } + + // MARK: - Cleanup + deinit { + stopTimer() + cancelNotification() + Task { @MainActor [weak self] in + if let activity = self?.currentActivity { + await activity.end(nil, dismissalPolicy: .immediate) + } + } + } +} diff --git a/Projects/Presentation/Timer/Sources/TimerIntent.swift b/Projects/Presentation/Timer/Sources/Intent/TimerIntent.swift similarity index 90% rename from Projects/Presentation/Timer/Sources/TimerIntent.swift rename to Projects/Presentation/Timer/Sources/Intent/TimerIntent.swift index d6a2812..efd29f2 100644 --- a/Projects/Presentation/Timer/Sources/TimerIntent.swift +++ b/Projects/Presentation/Timer/Sources/Intent/TimerIntent.swift @@ -18,5 +18,6 @@ public enum TimerIntent { case resumeTimer case cancelTimer case timerTick + case updateRemainingTime(TimeInterval) case timerCompleted } diff --git a/Projects/Presentation/Timer/Sources/TimerModel.swift b/Projects/Presentation/Timer/Sources/Model/TimerModel.swift similarity index 82% rename from Projects/Presentation/Timer/Sources/TimerModel.swift rename to Projects/Presentation/Timer/Sources/Model/TimerModel.swift index c94f82d..389197c 100644 --- a/Projects/Presentation/Timer/Sources/TimerModel.swift +++ b/Projects/Presentation/Timer/Sources/Model/TimerModel.swift @@ -40,20 +40,24 @@ public final class TimerModel: TimerModelProtocol { newState.remainingTime = TimeInterval(totalSeconds) newState.progress = 1.0 newState.timerStatus = .running + newState.endDate = Date().addingTimeInterval(TimeInterval(totalSeconds)) sideEffect = .startTimerTicking case .pauseTimer: newState.timerStatus = .paused + newState.endDate = nil sideEffect = .stopTimerTicking case .resumeTimer: newState.timerStatus = .running + newState.endDate = Date().addingTimeInterval(state.remainingTime) sideEffect = .startTimerTicking case .cancelTimer: newState.timerStatus = .idle newState.remainingTime = 0 newState.progress = 1.0 + newState.endDate = nil sideEffect = .stopTimerTicking case .timerTick: @@ -62,10 +66,15 @@ public final class TimerModel: TimerModelProtocol { newState.progress = state.totalTime > 0 ? newState.remainingTime / state.totalTime : 0 } + case .updateRemainingTime(let remaining): + newState.remainingTime = max(0, remaining) + newState.progress = state.totalTime > 0 ? newState.remainingTime / state.totalTime : 0 + case .timerCompleted: newState.timerStatus = .idle newState.remainingTime = 0 newState.progress = 1.0 + newState.endDate = nil sideEffect = .playAlarm } diff --git a/Projects/Presentation/Timer/Sources/TimerSideEffect.swift b/Projects/Presentation/Timer/Sources/Model/TimerSideEffect.swift similarity index 100% rename from Projects/Presentation/Timer/Sources/TimerSideEffect.swift rename to Projects/Presentation/Timer/Sources/Model/TimerSideEffect.swift diff --git a/Projects/Presentation/Timer/Sources/TimerState.swift b/Projects/Presentation/Timer/Sources/Model/TimerState.swift similarity index 94% rename from Projects/Presentation/Timer/Sources/TimerState.swift rename to Projects/Presentation/Timer/Sources/Model/TimerState.swift index 8de9948..d277ada 100644 --- a/Projects/Presentation/Timer/Sources/TimerState.swift +++ b/Projects/Presentation/Timer/Sources/Model/TimerState.swift @@ -20,6 +20,7 @@ public struct TimerState: Equatable { public var remainingTime: TimeInterval public var totalTime: TimeInterval public var progress: Double + public var endDate: Date? public init( selectedHours: Int = 0, @@ -28,7 +29,8 @@ public struct TimerState: Equatable { timerStatus: TimerStatus = .idle, remainingTime: TimeInterval = 0, totalTime: TimeInterval = 0, - progress: Double = 1.0 + progress: Double = 1.0, + endDate: Date? = nil ) { self.selectedHours = selectedHours self.selectedMinutes = selectedMinutes @@ -37,6 +39,7 @@ public struct TimerState: Equatable { self.remainingTime = remainingTime self.totalTime = totalTime self.progress = progress + self.endDate = endDate } } diff --git a/Projects/Presentation/Timer/Sources/Service/TimerNotificationService.swift b/Projects/Presentation/Timer/Sources/Service/TimerNotificationService.swift new file mode 100644 index 0000000..f8ab4a4 --- /dev/null +++ b/Projects/Presentation/Timer/Sources/Service/TimerNotificationService.swift @@ -0,0 +1,88 @@ +// +// TimerNotificationService.swift +// Timer +// +// Created by 홍석현 on 11/11/25. +// + +import Foundation +import UserNotifications + +public protocol TimerNotificationServiceProtocol { + func requestAuthorization() async -> Bool + func scheduleNotification(endDate: Date, totalTime: TimeInterval) + func cancelNotification() +} + +public final class TimerNotificationService: TimerNotificationServiceProtocol { + private let notificationCenter = UNUserNotificationCenter.current() + private let notificationIdentifier = "com.wakeyalarm.timer.completion" + + public init() {} + + // MARK: - Authorization + public func requestAuthorization() async -> Bool { + do { + let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) + if granted { + print("✅ 타이머 알림 권한 허용됨") + } else { + print("🚫 타이머 알림 권한 거부됨") + } + return granted + } catch { + print("❌ 타이머 알림 권한 요청 실패: \(error.localizedDescription)") + return false + } + } + + // MARK: - Schedule + public func scheduleNotification(endDate: Date, totalTime: TimeInterval) { + let content = UNMutableNotificationContent() + content.title = "⏰ 타이머 완료" + content.body = formatTimeForMessage(totalTime) + " 타이머가 완료되었습니다" + content.sound = .default + content.categoryIdentifier = "TIMER_COMPLETION" + + let timeInterval = endDate.timeIntervalSinceNow + + // 최소 1초 이상이어야 알림이 예약됨 + guard timeInterval > 1 else { + print("⚠️ 타이머 종료 시간이 너무 짧아 알림을 예약할 수 없습니다") + return + } + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false) + let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) + + notificationCenter.add(request) { error in + if let error = error { + print("❌ 타이머 알림 예약 실패: \(error.localizedDescription)") + } else { + print("✅ 타이머 알림 예약 완료: \(self.formatTimeForMessage(totalTime))") + } + } + } + + // MARK: - Cancel + public func cancelNotification() { + notificationCenter.removePendingNotificationRequests(withIdentifiers: [notificationIdentifier]) + notificationCenter.removeDeliveredNotifications(withIdentifiers: [notificationIdentifier]) + print("🗑️ 타이머 알림 취소됨") + } + + // MARK: - Helper + private func formatTimeForMessage(_ timeInterval: TimeInterval) -> String { + let hours = Int(timeInterval) / 3600 + let minutes = (Int(timeInterval) % 3600) / 60 + let seconds = Int(timeInterval) % 60 + + if hours > 0 { + return String(format: "%d시간 %d분 %d초", hours, minutes, seconds) + } else if minutes > 0 { + return String(format: "%d분 %d초", minutes, seconds) + } else { + return String(format: "%d초", seconds) + } + } +} diff --git a/Projects/Presentation/Timer/Sources/TimerContainer.swift b/Projects/Presentation/Timer/Sources/TimerContainer.swift deleted file mode 100644 index 19ed87f..0000000 --- a/Projects/Presentation/Timer/Sources/TimerContainer.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// TimerContainer.swift -// Timer -// -// Created by 홍석현 on 2025-11-10 -// Copyright © 2025 DDD , Ltd., All rights reserved. -// - -import Foundation -import Combine - -// MARK: - Timer Container (ViewModel) -public final class TimerContainer: ObservableObject { - // MARK: - Published State - @Published public private(set) var state: TimerState - - // MARK: - Private Properties - private let model: TimerModelProtocol - private var cancellables = Set() - private var timerCancellable: AnyCancellable? - - // MARK: - Initialization - public init( - model: TimerModelProtocol = TimerModel(), - initialState: TimerState = TimerState() - ) { - self.model = model - self.state = initialState - } - - // MARK: - Intent Processing - public func send(_ intent: TimerIntent) { - let (newState, sideEffect) = model.reduce(state: state, intent: intent) - state = newState - - if let sideEffect = sideEffect { - handleSideEffect(sideEffect) - } - } - - // MARK: - Side Effect Handling - private func handleSideEffect(_ sideEffect: TimerSideEffect) { - switch sideEffect { - case .startTimerTicking: - startTimer() - - case .stopTimerTicking: - stopTimer() - - case .playAlarm: - playAlarm() - - case .showCompletionAlert: - showCompletionAlert() - } - } - - // MARK: - Timer Management - private func startTimer() { - stopTimer() // 기존 타이머 정리 - - // 애니메이션과 동기화를 위해 0.3초 지연 - Task { @MainActor in - try? await Task.sleep(nanoseconds: 300_000_000) // 0.3초 - - self.timerCancellable = Timer.publish(every: 0.1, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - guard let self = self else { return } - - if self.state.remainingTime > 0 { - self.send(.timerTick) - } else { - self.send(.timerCompleted) - } - } - } - } - - private func stopTimer() { - timerCancellable?.cancel() - timerCancellable = nil - } - - // MARK: - Alarm & Notifications - private func playAlarm() { - // TODO: 알람 사운드 재생 - print("🔔 타이머 완료! 알람 울림") - } - - private func showCompletionAlert() { - // TODO: 완료 알림 표시 - print("✅ 타이머 완료 알림") - } - - // MARK: - Cleanup - deinit { - stopTimer() - } -} diff --git a/Projects/Presentation/Timer/Sources/TimerView.swift b/Projects/Presentation/Timer/Sources/TimerView.swift index fc08567..cea1969 100644 --- a/Projects/Presentation/Timer/Sources/TimerView.swift +++ b/Projects/Presentation/Timer/Sources/TimerView.swift @@ -61,6 +61,13 @@ public struct TimerView: View { .padding(.bottom, 60) } .animation(.easeInOut(duration: 0.3), value: container.state.timerStatus) + .alert("타이머 완료", isPresented: $container.showCompletionAlert) { + Button("확인", role: .cancel) { + container.showCompletionAlert = false + } + } message: { + Text("설정한 타이머가 완료되었습니다.") + } } } diff --git a/Projects/Presentation/Timer/Sources/Widget/TimerActivityAttributes.swift b/Projects/Presentation/Timer/Sources/Widget/TimerActivityAttributes.swift new file mode 100644 index 0000000..d1c4bbb --- /dev/null +++ b/Projects/Presentation/Timer/Sources/Widget/TimerActivityAttributes.swift @@ -0,0 +1,68 @@ +// +// TimerActivityAttributes.swift +// Timer +// +// Created by 홍석현 on 11/11/25. +// + +import Foundation +import ActivityKit + +public struct TimerActivityAttributes: ActivityAttributes { + + public struct ContentState: Codable, Hashable { + public var totalTime: TimeInterval + public var remainingTime: TimeInterval + public var endDate: Date? + public var isRunning: Bool + public var isPaused: Bool + + public init( + totalTime: TimeInterval, + remainingTime: TimeInterval, + endDate: Date? = nil, + isRunning: Bool, + isPaused: Bool + ) { + self.totalTime = totalTime + self.remainingTime = remainingTime + self.endDate = endDate + self.isRunning = isRunning + self.isPaused = isPaused + } + } + + public init() { + + } + +} + +// MARK: - Preview Support +extension TimerActivityAttributes { + public static var preview: TimerActivityAttributes { + TimerActivityAttributes() + } +} + +extension TimerActivityAttributes.ContentState { + public static var sample: TimerActivityAttributes.ContentState { + TimerActivityAttributes.ContentState( + totalTime: 300, + remainingTime: 185, + endDate: Date().addingTimeInterval(185), + isRunning: true, + isPaused: false + ) + } + + public static var almostDone: TimerActivityAttributes.ContentState { + TimerActivityAttributes.ContentState( + totalTime: 300, + remainingTime: 60, + endDate: Date().addingTimeInterval(60), + isRunning: true, + isPaused: false + ) + } +} diff --git a/Projects/Presentation/Widget/Project.swift b/Projects/Presentation/Widget/Project.swift index cc1a67d..98e3c57 100644 --- a/Projects/Presentation/Widget/Project.swift +++ b/Projects/Presentation/Widget/Project.swift @@ -17,7 +17,8 @@ let project = Project( sources: ["Sources/**"], resources: ["Resources/**"], dependencies: [ - .Presentation(implements: .StopWatch) + .Presentation(implements: .StopWatch), + .Presentation(implements: .Timer) // WidgetKit과 SwiftUI는 시스템 프레임워크로 자동 링크됨 ] ) diff --git a/Projects/Presentation/Widget/Sources/TimerLiveActivity.swift b/Projects/Presentation/Widget/Sources/TimerLiveActivity.swift new file mode 100644 index 0000000..77bbc64 --- /dev/null +++ b/Projects/Presentation/Widget/Sources/TimerLiveActivity.swift @@ -0,0 +1,253 @@ +// +// TimerLiveActivity.swift +// Timer +// +// Created by 홍석현 on 11/11/25. +// + +import ActivityKit +import WidgetKit +import SwiftUI +import Timer +import DesignSystem + +public struct TimerLiveActivity: Widget { + + public init() {} + + public var body: some WidgetConfiguration { + ActivityConfiguration(for: TimerActivityAttributes.self) { context in + TimerLockScreenView(context: context) + } dynamicIsland: { context in + DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + TimerExpandedLeadingView() + } + + DynamicIslandExpandedRegion(.trailing) { + TimerExpandedTrailingView(context: context) + } + + DynamicIslandExpandedRegion(.bottom) { + TimerExpandedBottomView(context: context) + } + } compactLeading: { + TimerCompactLeadingView() + } compactTrailing: { + TimerCompactTrailingView(remainingTime: context.state.remainingTime) + } minimal: { + TimerMinimalView(isActive: context.state.isRunning && !context.state.isPaused) + } + } + } +} + +// MARK: - Lock Screen Components + +private struct TimerLockScreenView: View { + let context: ActivityViewContext + + var body: some View { + VStack(spacing: 12) { + TimerHeaderView(isActive: context.state.isRunning && !context.state.isPaused) + TimerMainTimeView(remainingTime: context.state.remainingTime) + Divider() + TimerTotalTimeRow(totalTime: context.state.totalTime) + } + .padding() + .background( + RoundedRectangle(cornerRadius: 16) + .fill(.ultraThinMaterial) + ) + } +} + +private struct TimerHeaderView: View { + let isActive: Bool + + var body: some View { + HStack { + Image(systemName: "timer") + .foregroundColor(.blue) + + Text("타이머") + .font(.headline) + .fontWeight(.medium) + + Spacer() + + StatusBadge(isActive: isActive) + } + } +} + +private struct StatusBadge: View { + let isActive: Bool + + var body: some View { + Text(isActive ? "실행 중" : "정지됨") + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(isActive ? .green.opacity(0.2) : .gray.opacity(0.2)) + ) + .foregroundColor(isActive ? .green : .secondary) + } +} + +private struct TimerMainTimeView: View { + let remainingTime: TimeInterval + + var body: some View { + VStack(spacing: 4) { + Text(formatTime(remainingTime)) + .font(.system(size: 32, weight: .bold, design: .monospaced)) + .foregroundColor(.primary) + + Text("남은 시간") + .font(.caption) + .foregroundColor(.secondary) + } + } +} + +private struct TimerTotalTimeRow: View { + let totalTime: TimeInterval + + var body: some View { + HStack { + Text("전체 시간") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text(formatTime(totalTime)) + .font(.caption) + .fontWeight(.medium) + .monospacedDigit() + .foregroundColor(.primary) + } + } +} + +// MARK: - Dynamic Island Components + +private struct TimerExpandedLeadingView: View { + var body: some View { + HStack { + Image(systemName: "timer") + .foregroundColor(.blue) + + Text("타이머") + .font(.pretendardFont(family: .medium, size: 14)) + .foregroundStyle(.whiteSmoke) + } + .padding(.leading, 10) + } +} + +private struct TimerExpandedTrailingView: View { + let context: ActivityViewContext + + var body: some View { + VStack(alignment: .trailing) { + Text(formatTime(context.state.remainingTime)) + .font(.pretendardFont(family: .bold, size: 18)) + .foregroundStyle(.whiteSmoke) + .monospacedDigit() + + Text("남은 시간") + .font(.pretendardFont(family: .medium, size: 12)) + .foregroundColor(.secondary) + } + .padding(.trailing, 10) + } +} + +private struct TimerExpandedBottomView: View { + let context: ActivityViewContext + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text("전체 시간") + .font(.pretendardFont(family: .medium, size: 12)) + .foregroundColor(.secondary) + + Text(formatTime(context.state.totalTime)) + .font(.pretendardFont(family: .medium, size: 14)) + .monospacedDigit() + } + + Spacer() + + StatusIndicator(isActive: context.state.isRunning && !context.state.isPaused) + } + .padding(.horizontal, 10) + } +} + +private struct StatusIndicator: View { + let isActive: Bool + + var body: some View { + HStack(spacing: 4) { + Circle() + .fill(isActive ? .green : .gray) + .frame(width: 8, height: 8) + + Text(isActive ? "실행 중" : "정지됨") + .font(.pretendardFont(family: .medium, size: 12)) + .foregroundColor(.secondary) + } + } +} + +private struct TimerCompactLeadingView: View { + var body: some View { + Image(systemName: "timer") + .foregroundColor(.blue) + .padding(.leading, 10) + } +} + +private struct TimerCompactTrailingView: View { + let remainingTime: TimeInterval + + var body: some View { + Text(formatTime(remainingTime)) + .font(.pretendardFont(family: .medium, size: 12)) + .monospacedDigit() + .padding(.trailing, 10) + } +} + +private struct TimerMinimalView: View { + let isActive: Bool + + var body: some View { + Image(systemName: isActive ? "play.fill" : "pause.fill") + .foregroundColor(isActive ? .green : .orange) + } +} + +// MARK: - Helper Functions + +private func formatTime(_ timeInterval: TimeInterval) -> String { + let minutes = Int(timeInterval) / 60 + let seconds = Int(timeInterval) % 60 + return String(format: "%02d:%02d", minutes, seconds) +} + +@available(iOS 17.0, *) +#Preview( + "타이머 잠금 화면", + as: .content, + using: TimerActivityAttributes.preview +) { + TimerLiveActivity() +} contentStates: { + TimerActivityAttributes.ContentState.sample + TimerActivityAttributes.ContentState.almostDone +} diff --git a/Projects/Presentation/Widget/Sources/WakeyAlarmWidget.swift b/Projects/Presentation/Widget/Sources/WakeyAlarmWidget.swift index 392e7d8..d588480 100644 --- a/Projects/Presentation/Widget/Sources/WakeyAlarmWidget.swift +++ b/Projects/Presentation/Widget/Sources/WakeyAlarmWidget.swift @@ -5,11 +5,7 @@ import SwiftUI @main struct WakeyAlarmWidgetBundle: WidgetBundle { var body: some Widget { - // iOS 16.1+ Live Activity 지원 - #if canImport(ActivityKit) - if #available(iOS 16.1, *) { - StopWatchLiveActivity() - } - #endif + StopWatchLiveActivity() + TimerLiveActivity() } }