diff --git a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings index 1969d187..b5d2e8bf 100644 --- a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings +++ b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings @@ -1548,7 +1548,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "알람" + "value" : "알림" } } } @@ -2053,6 +2053,23 @@ } } }, + "push_settings_custom_hint" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose a custom time in 5-minute intervals." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 설정 시간은 5분 단위로 선택할 수 있어요." + } + } + } + }, "push_settings_enable" : { "extractionState" : "manual", "localizations" : { @@ -2065,7 +2082,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "푸시 알람" + "value" : "푸시 알림" } } } @@ -2082,7 +2099,24 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "설정의 푸시 알람 권한과는 별개예요.\n기기 알림 권한을 꺼도 알림 리스트는 생성돼요." + "value" : "기기 알림 권한과는 별개예요. 권한을 꺼도 앱의 알림 목록은 생성돼요." + } + } + } + }, + "push_settings_time_section" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notification Time" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "알림 시간" } } } diff --git a/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift index 60746640..f56d6516 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift @@ -9,67 +9,30 @@ import SwiftUI import PresentationShared struct PushNotificationSettingsView: View { + @Environment(\.dismiss) private var dismiss @Environment(\.isTabContentActive) private var isTabContentActive @State var store: StoreOf var body: some View { - List { - Section(content: { - HStack { - Text(String(localized: "push_settings_enable", bundle: PresentationResources.bundle)) - Spacer() - if store.isLoading && store.activeLoadingRow == .enable { - ProgressView() - } else { - Toggle("", isOn: $store.pushNotificationEnable) - .labelsHidden() - .tint(.blue) - .disabled(store.activeLoadingRow != nil) - } - } - }, footer: { - Text(String(localized: "push_settings_footer", bundle: PresentationResources.bundle)) - .multilineTextAlignment(.leading) - }) - Section { - ForEach([9, 15, 18, 21], id: \.self) { hour in - if let date = Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: Date()) { - let loadingRow = PushNotificationSettingsFeature.activeLoadingRow(for: date) - HStack { - Text(formattedTimeString(date)) - Spacer() - if let loadingRow, - store.isLoading && store.activeLoadingRow == loadingRow { - ProgressView() - } else if store.activeLoadingRow != loadingRow - && store.pushNotificationHour == hour - && store.pushNotificationMinute == 0 { - Image(systemName: "checkmark") - .foregroundStyle(Color.blue) - } - } - .contentShape(Rectangle()) - .onTapGesture { store.send(.selectPresetTime(date)) } - } - } - HStack { - Text(String(localized: "push_settings_custom", bundle: PresentationResources.bundle)) - Spacer() - Text(formattedTimeString(store.viewPushNotificationTime)) - .foregroundStyle(.secondary) - if store.pushNotificationMinute != 0 { - Image(systemName: "checkmark") - .foregroundStyle(Color.blue) - } + ScrollView { + LazyVStack(alignment: .leading, spacing: 24) { + EnableCard(store: store) + + VStack(alignment: .leading, spacing: 12) { + Text("push_settings_time_section", bundle: PresentationResources.bundle) + .font(.title3.weight(.semibold)) + TimeCard(store: store) } - .contentShape(Rectangle()) - .onTapGesture { store.send(.tapCustomTime) } + + CustomTimeTipCard() } - .disabled(!store.pushNotificationEnable || store.activeLoadingRow != nil) - .opacity(store.pushNotificationEnable ? 1.0 : 0.2) + .padding(.horizontal, 16) + .padding(.top, 20) + .padding(.bottom, 24) } - .listStyle(.insetGrouped) - .navigationTitle(String(localized: "nav_push_settings", bundle: PresentationResources.bundle)) + .safeAreaInset(edge: .top, spacing: 0) { topBar } + .background(Color.appBackground) + .toolbarVisibility(.hidden, for: .navigationBar) .onAppear { store.send(.fetchSettings) } .prominentAlert(store, state: \.alert, action: \.alert) .sheet( @@ -83,8 +46,174 @@ struct PushNotificationSettingsView: View { } } + private var topBar: some View { + ZStack { + Text(String(localized: "nav_push_settings", bundle: PresentationResources.bundle)) + .font(.headline) + + HStack { + NavigationBackButton(action: { dismiss() }) + Spacer() + } + } + .padding(.horizontal, 16) + .padding(.bottom, 12) + .background(Color.appBackground) + .toolbarBackground(Color.appBackground) + } +} + +private struct EnableCard: View { + @Bindable var store: StoreOf + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 12) { + Image(systemName: "bell") + .font(.headline) + .frame(width: 36, height: 36) + .iconStyle(color: .accent, in: Circle()) + + Text("push_settings_enable", bundle: PresentationResources.bundle) + .font(.title3) + + Spacer(minLength: 8) + + if store.isLoading && store.activeLoadingRow == .enable { + ProgressView() + } else { + Toggle("", isOn: $store.pushNotificationEnable) + .labelsHidden() + .tint(.accent) + .disabled(store.activeLoadingRow != nil) + } + } + + Divider() + + Text("push_settings_footer", bundle: PresentationResources.bundle) + .font(.footnote) + .foregroundStyle(Color.textSecondary) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.surface, in: .rect(cornerRadius: 16)) + } +} + +private struct TimeCard: View { + @Environment(\.locale) private var locale + let store: StoreOf + + private static let presetHours = [9, 15, 18, 21] + + var body: some View { + VStack(spacing: 0) { + ForEach(Self.presetHours, id: \.self) { hour in + if let date = Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: Date()) { + presetTimeButton(date, hour: hour) + Divider() + } + } + customTimeButton + } + .background(Color.surface, in: .rect(cornerRadius: 16)) + .disabled(!store.pushNotificationEnable || store.activeLoadingRow != nil) + .opacity(store.pushNotificationEnable ? 1.0 : 0.7) + } + + private func presetTimeButton(_ date: Date, hour: Int) -> some View { + let loadingRow = PushNotificationSettingsFeature.activeLoadingRow(for: date) + let isSelected = store.activeLoadingRow != loadingRow + && store.pushNotificationHour == hour + && store.pushNotificationMinute == 0 + + return Button { + store.send(.selectPresetTime(date)) + } label: { + HStack { + Text(formattedTimeString(date)) + .foregroundStyle(Color.primary) + Spacer() + if let loadingRow, + store.isLoading && store.activeLoadingRow == loadingRow { + ProgressView() + } else { + selectionIndicator(isSelected: isSelected) + } + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + private var customTimeButton: some View { + Button { + store.send(.tapCustomTime) + } label: { + HStack(spacing: 8) { + Text("push_settings_custom", bundle: PresentationResources.bundle) + .foregroundStyle(Color.primary) + Spacer(minLength: 8) + if store.isLoading && store.activeLoadingRow == .customTime { + ProgressView() + } else { + selectionIndicator(isSelected: store.pushNotificationMinute != 0) + } + Text(formattedTimeString(store.viewPushNotificationTime)) + .foregroundStyle(Color.textSecondary) + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundStyle(Color.textSecondary) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + @ViewBuilder + private func selectionIndicator(isSelected: Bool) -> some View { + if isSelected { + ZStack { + Image(systemName: "circle.fill") + .foregroundStyle(Color.accent) + Image(systemName: "checkmark") + .font(.caption2.bold()) + .foregroundStyle(Color.white) + } + .font(.title2) + } else { + Image(systemName: "circle") + .font(.title2) + .foregroundStyle(Color.border) + } + } + private func formattedTimeString(_ date: Date) -> String { - date.formatted(.dateTime.hour().minute()) + date.formatted(.dateTime.hour().minute().locale(locale)) + } +} + +private struct CustomTimeTipCard: View { + var body: some View { + HStack(spacing: 12) { + Image(systemName: "info") + .font(.caption.weight(.semibold)) + .frame(width: 28, height: 28) + .background(Color.surface, in: Circle()) + Text("push_settings_custom_hint", bundle: PresentationResources.bundle) + .font(.footnote) + .frame(maxWidth: .infinity, alignment: .leading) + } + .foregroundStyle(Color.accent) + .padding(16) + .background(Color.accent.opacity(0.08), in: .rect(cornerRadius: 16)) + .padding(.bottom, 16) + .background(Color.appBackground) } }