Skip to content

Commit

Permalink
feat: #109 Add logic to save notification settings to UserDefaults
Browse files Browse the repository at this point in the history
- ๋…ธํ‹ฐ ๊ด€๋ จ ๊ฐ’์„ UserDefaults์— ์ €์žฅํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
- ํ† ๊ธ€ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ฐ”์ธ๋”ฉ ํ”„๋กœํผํ‹ฐ `isNotificationToggleEnabled` ๊ตฌํ˜„
- ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์œ„ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก`NotiSettingView` ์—…๋ฐ์ดํŠธ
  • Loading branch information
zaehorang committed Nov 28, 2024
1 parent ad47bc8 commit f982743
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 96 deletions.
4 changes: 4 additions & 0 deletions FiveGuyes/FiveGuyes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
264440592CD8F6020031A290 /* CustomBackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264440582CD8F6020031A290 /* CustomBackButton.swift */; };
2644405B2CD8F9380031A290 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2644405A2CD8F9380031A290 /* UINavigationController+Extension.swift */; };
264443E82CF85277009422BA /* UserDefaultsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264443E72CF85277009422BA /* UserDefaultsKey.swift */; };
264443EA2CF861E5009422BA /* SystemSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264443E92CF861E5009422BA /* SystemSettingsManager.swift */; };
2655532F2CF6D73900288037 /* NotiSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2655532E2CF6D73900288037 /* NotiSettingView.swift */; };
26890B952CAE811A008DFF49 /* FiveGuyesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26890B942CAE811A008DFF49 /* FiveGuyesApp.swift */; };
26890B972CAE811A008DFF49 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26890B962CAE811A008DFF49 /* ContentView.swift */; };
Expand Down Expand Up @@ -110,6 +111,7 @@
264440582CD8F6020031A290 /* CustomBackButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBackButton.swift; sourceTree = "<group>"; };
2644405A2CD8F9380031A290 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = "<group>"; };
264443E72CF85277009422BA /* UserDefaultsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsKey.swift; sourceTree = "<group>"; };
264443E92CF861E5009422BA /* SystemSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSettingsManager.swift; sourceTree = "<group>"; };
2655532E2CF6D73900288037 /* NotiSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotiSettingView.swift; sourceTree = "<group>"; };
26890B912CAE811A008DFF49 /* FiveGuyes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FiveGuyes.app; sourceTree = BUILT_PRODUCTS_DIR; };
26890B942CAE811A008DFF49 /* FiveGuyesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiveGuyesApp.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -444,6 +446,7 @@
26EBDE2B2CE1341800B3A2BC /* ReadingRecord.swift */,
26EBDED02CF45AB300B3A2BC /* UserBookModelV2 */,
26EBDECE2CF4597100B3A2BC /* UserBookModelV1 */,
264443E92CF861E5009422BA /* SystemSettingsManager.swift */,
);
path = UserBook;
sourceTree = "<group>";
Expand Down Expand Up @@ -579,6 +582,7 @@
26EBDED62CF45B3400B3A2BC /* BookMetaData.swift in Sources */,
26EBDE8F2CF0684900B3A2BC /* View+Extension.swift in Sources */,
26EBDED82CF45B9300B3A2BC /* CompletionStatus.swift in Sources */,
264443EA2CF861E5009422BA /* SystemSettingsManager.swift in Sources */,
264443E82CF85277009422BA /* UserDefaultsKey.swift in Sources */,
1A010EB62CD8AF9800FBE3B3 /* BookSearchViewModel.swift in Sources */,
26EBDED22CF45ACA00B3A2BC /* ReadingProgress.swift in Sources */,
Expand Down
18 changes: 8 additions & 10 deletions FiveGuyes/FiveGuyes/Sources/Models/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@
//

import UserNotifications

// TODO: ๋…ธํ‹ฐ ์‹œ๊ฐ„์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์š”์ฒญํ•œ ๋…ธํ‹ฐ๋ฅผ ์ฐพ์•„์„œ ์‹œ๊ฐ„์„ ๋ฐ”๊ฟ”์ฃผ๋Š” ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ
final class NotificationManager {
private let notificationCenter = UNUserNotificationCenter.current()
private var isGranted: Bool = false

func setupNotifications(notificationType: NotificationType) async {
await requestAuthorization()

if isGranted {
if await requestAuthorization() {
await scheduleReminderNotification(notificationType: notificationType)
}
}
Expand All @@ -25,22 +22,23 @@ final class NotificationManager {
}

/// Notification ๊ถŒํ•œ ์š”์ฒญ ํ•จ์ˆ˜
private func requestAuthorization() async {
func requestAuthorization() async -> Bool {
do {
try await notificationCenter
.requestAuthorization(options: [.sound, .badge, .alert])
return await getCurrentSettings()
} catch {
print("โŒ NotificationManager/requestAuthorization: \(error.localizedDescription)")
return false
}

await getCurrentSettings()
}

/// ํ˜„์žฌ Notification ๊ถŒํ•œ ์„ค์ •์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
private func getCurrentSettings() async {
private func getCurrentSettings() async -> Bool {
let currentSettings = await notificationCenter.notificationSettings()
let isAuthorized = (currentSettings.authorizationStatus == .authorized)

isGranted = (currentSettings.authorizationStatus == .authorized)
return isAuthorized
}

private func scheduleReminderNotification(notificationType: NotificationType) async {
Expand Down
4 changes: 3 additions & 1 deletion FiveGuyes/FiveGuyes/Sources/Models/UserDefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
//

enum UserDefaultsKeys: String {
case isNotificationDisabled, reminderHour, reminderMinute
case isNotificationDisabled // ์‹œ์Šคํ…œ ์„ค์ •์ด ์•„๋‹Œ, ์•ฑ ๋‚ด๋ถ€์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ’
case reminderHour
case reminderMinute
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,59 @@
// Created by zaehorang on 11/27/24.
//

import SwiftData
import SwiftUI

struct NotiSettingView: View {
@State private var isNotiDisabled: Bool = false // ๋ชจ๋“  ์•Œ๋žŒ ์ˆ˜์‹  ํ† ๊ธ€
@Environment(\.scenePhase) private var scenePhase // ์•ฑ ์ƒํƒœ ๊ฐ์ง€

@State private var selectedTime: Date = Date() // ๋ฐ์ดํŠธ ํ”ผ์ปค์— ์‚ฌ์šฉ๋  ์‹œ๊ฐ„

@State private var isNotificationDisabled: Bool = false // ๋ชจ๋“  ์•Œ๋žŒ ์ˆ˜์‹  ํ† ๊ธ€
@State private var isReminderTimePickerVisible: Bool = false // ๋ฐ์ดํŠธ ํ”ผ์ปค ํ‘œ์‹œ ์—ฌ๋ถ€
@State private var isSystemNotificationEnabled = true // ์‹œ์Šคํ…œ ๋…ธํ‹ฐ ๊ถŒํ•œ ์—ฌ๋ถ€

// TODO: ์‹ค์ œ ๋…ธํ‹ฐ ์„ค์ • ๊ฐ’ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
@State private var isNotificationDisabled = true
private let notificationManager = NotificationManager()

// Toggle ๋ฐ”์ธ๋”ฉ ๋ณ€์ˆ˜
private var isNotificationToggleEnabled: Binding<Bool> {
Binding(
get: { !isNotificationDisabled },
set: { isNotificationDisabled = !$0 }
)
}

// ์‹œ๊ฐ„ ๋ฒ”์œ„ ์„ค์ •: 04:00 ~ 23:55
private var timeSelectionRange: ClosedRange<Date> {
let calendar = Calendar.current
let now = Date()
let startOfDay = calendar.startOfDay(for: now)
let start = calendar.date(bySettingHour: 4, minute: 0, second: 0, of: startOfDay)!
let end = calendar.date(bySettingHour: 23, minute: 55, second: 0, of: startOfDay)!
return start...end
}

var body: some View {
ZStack {
Color.white // ๋ฐฐ๊ฒฝ์ƒ‰ ์ง€์ •
.ignoresSafeArea()

VStack(alignment: .leading, spacing: .zero) {
if isNotificationDisabled {
if !isSystemNotificationEnabled {
notificationDisabledView
}

VStack(alignment: .leading, spacing: .zero) {
Toggle("์•Œ๋ฆผ ๋„๊ธฐ", isOn: $isNotiDisabled)
.toggleStyle(.switch)
.fontStyle(.title2, weight: .semibold)
.foregroundStyle(Color.Labels.primaryBlack1)
secondaryTitle("ํ•œ์ž…๋…์„œ์™€ ๊ด€๋ จ๋œ ์•Œ๋ฆผ ์ˆ˜์‹ ์ด ์ค‘๋‹จ๋ผ์š”")
}
toggleSection

Rectangle()
.stroke(lineWidth: 2)
.frame(height: 1)
.foregroundStyle(Color.Separators.gray)
dividerLine
.padding(.top, 12)

// ํ•˜๋ฃจ ๋…์„œ ๋ฏธ์™„๋ฃŒ ์•Œ๋ฆผ
timePickerSection(
title: "๋ฆฌ๋งˆ์ธ๋“œ ์•Œ๋ฆผ",
description: "์ง€์ •๋œ ์‹œ๊ฐ„์— ์˜ค๋Š˜์˜ ๋…์„œ ๋ชฉํ‘œ๋ฅผ ์•Œ๋ฆด๊ฒŒ์š”",
selectedTime: $selectedTime,
isPickerVisible: $isReminderTimePickerVisible,
allowedRange: timeSelectionRange
)
.padding(.top, 16)
timePickerSection
.padding(.top, 16)

if isReminderTimePickerVisible {
timePicker
}

Spacer()
}
Expand All @@ -59,18 +68,31 @@ struct NotiSettingView: View {
.navigationBarBackButtonHidden(true)
.customNavigationBackButton()
.navigationTitle("์•Œ๋ฆผ ์„ค์ •")
.onChange(of: isNotiDisabled) {
UserDefaults.standard.set(isNotiDisabled, forKey: UserDefaultsKeys.isNotificationDisabled.rawValue)
.task {
isSystemNotificationEnabled = await notificationManager.requestAuthorization()
}
.onAppear {
loadInitialNotificationTime()
// ์ดˆ๊ธฐ์— ๊ฐ’์ด ์—†์œผ๋ฉด false ๋ฆฌํ„ด
isNotificationDisabled = UserDefaults.standard.bool(forKey: UserDefaultsKeys.isNotificationDisabled.rawValue)
}

.onChange(of: isNotificationDisabled) {
saveNotificationStatus() // ๋…ธํ‹ฐ ์ƒํƒœ ์ €์žฅ
}
.onChange(of: selectedTime) {
saveTime(selectedTime) // ์‹œ๊ฐ„๊ณผ ๋ถ„ ์ €์žฅ
saveNotificationTime(selectedTime) // ์‹œ๊ฐ„๊ณผ ๋ถ„ ์ €์žฅ
}
.onAppear {
loadInitialTime()
isNotiDisabled = UserDefaults.standard.bool(forKey: UserDefaultsKeys.isNotificationDisabled.rawValue)
.onChange(of: scenePhase) {
if scenePhase == .active { // ์‹œ์Šคํ…œ ์„ค์ •์— ๊ฐ”๋‹ค๊ฐ€ ๋‹ค์‹œ ์˜ค๋Š” ์ƒํ™ฉ ์ฒดํฌ
Task {
isSystemNotificationEnabled = await notificationManager.requestAuthorization()
}
}
}
}

// MARK: - View Property
private func primaryTitle(_ title: String) -> some View {
Text(title)
.fontStyle(.title2, weight: .semibold)
Expand All @@ -86,12 +108,7 @@ struct NotiSettingView: View {
}

private var notificationDisabledView: some View {
Button {
// TODO: ๋…ธํ‹ฐ ์„ค์ • ํ™”๋ฉด์œผ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ
withAnimation(.easeIn) {
isNotificationDisabled.toggle()
}
} label: {
Button(action: SystemSettingsManager.openSettings) {
HStack {
VStack(alignment: .leading, spacing: .zero) {
primaryTitle("๊ธฐ๊ธฐ์˜ ์•Œ๋ฆผ ์„ค์ •์ด ๊บผ์ ธ ์žˆ์–ด์š”!")
Expand All @@ -116,54 +133,61 @@ struct NotiSettingView: View {
}
}

// ๋ฐ์ดํ„ฐ ํ”ผ์ปค๋ฅผ ํฌํ•จํ•œ ์•Œ๋ฆผ ๋ฆฌ์ŠคํŠธ row
private func timePickerSection (
title: String,
description: String,
selectedTime: Binding<Date>,
isPickerVisible: Binding<Bool>,
allowedRange: ClosedRange<Date>
) -> some View {

private var toggleSection: some View {
VStack(alignment: .leading, spacing: .zero) {
Toggle("์•Œ๋ฆผ ๋„๊ธฐ", isOn: isNotificationToggleEnabled)
.toggleStyle(.switch)
.fontStyle(.title2, weight: .semibold)
.foregroundStyle(Color.Labels.primaryBlack1)

secondaryTitle("ํ•œ์ž…๋…์„œ์™€ ๊ด€๋ จ๋œ ์•Œ๋ฆผ ์ˆ˜์‹ ์ด ์ค‘๋‹จ๋ผ์š”")
}
}

private var dividerLine: some View {
Rectangle()
.frame(height: 1)
.foregroundStyle(Color.Separators.gray)
}

// ๋ฐ์ดํ„ฐ ํ”ผ์ปค๋ฅผ ํฌํ•จํ•œ ์„น์…˜
private var timePickerSection: some View {
VStack(alignment: .leading, spacing: .zero) {
HStack {
primaryTitle(title)

primaryTitle("๋ฆฌ๋งˆ์ธ๋“œ ์•Œ๋ฆผ")
Spacer()

Button {
withAnimation(.easeIn) {
isPickerVisible.wrappedValue.toggle()
}
} label: {
Text(selectedTime.wrappedValue, style: .time)
.fontStyle(.body)
.foregroundStyle(Color.Colors.green2)
.multilineTextAlignment(.center)
.frame(width: 80, alignment: .center)
}
.padding(4)
.background {
RoundedRectangle(cornerRadius: 6)
.foregroundStyle(Color.Fills.lightGreen)
}
timerPickerButton
}

secondaryTitle(description)

if isPickerVisible.wrappedValue {
timePicker(selectedTime: selectedTime, allowedRange: allowedRange)
secondaryTitle("์ง€์ •๋œ ์‹œ๊ฐ„์— ์˜ค๋Š˜์˜ ๋…์„œ ๋ชฉํ‘œ๋ฅผ ์•Œ๋ฆด๊ฒŒ์š”")
}
}

private var timerPickerButton: some View {
Button {
withAnimation(.easeIn) {
isReminderTimePickerVisible.toggle()
}
} label: {
Text(selectedTime, style: .time)
.fontStyle(.body)
.foregroundStyle(Color.Colors.green2)
.multilineTextAlignment(.center)
.frame(width: 80, alignment: .center)
}
.padding(4)
.background {
RoundedRectangle(cornerRadius: 6)
.foregroundStyle(Color.Fills.lightGreen)
}
}

// ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ ํ”ผ์ปค๊ฐ€ ๋ณด์ด๋Š”๊ณณ์— ์“ฐ์ด๋Š” ํ”ผ์ปค ์ปดํฌ๋„ŒํŠธ
private func timePicker(selectedTime: Binding<Date>, allowedRange: ClosedRange<Date>) -> some View {
private var timePicker: some View {
VStack {
DatePicker(
"",
selection: selectedTime,
in: allowedRange,
selection: $selectedTime,
in: timeSelectionRange,
displayedComponents: .hourAndMinute
)
.datePickerStyle(WheelDatePickerStyle())
Expand All @@ -177,29 +201,25 @@ struct NotiSettingView: View {
UIDatePicker.appearance().minuteInterval = 1
}
}

// ์‹œ๊ฐ„ ๋ฒ”์œ„ ์„ค์ •: 04:00 ~ 23:55
private var timeSelectionRange: ClosedRange<Date> {
let calendar = Calendar.current
let now = Date()
let startOfDay = calendar.startOfDay(for: now)
let start = calendar.date(bySettingHour: 4, minute: 0, second: 0, of: startOfDay)!
let end = calendar.date(bySettingHour: 23, minute: 55, second: 0, of: startOfDay)!
return start...end
}


// MARK: - Method
// ์‹œ๊ฐ„๊ณผ ๋ถ„๋งŒ ์ €์žฅ
private func saveTime(_ time: Date) {
private func saveNotificationTime(_ time: Date) {
let calendar = Calendar.current
let hour = calendar.component(.hour, from: time)
let minute = calendar.component(.minute, from: time)
print("Save: \(hour): \(minute)")

UserDefaults.standard.set(hour, forKey: UserDefaultsKeys.reminderHour.rawValue)
UserDefaults.standard.set(minute, forKey: UserDefaultsKeys.reminderMinute.rawValue)
}

private func saveNotificationStatus() {
UserDefaults.standard.set(isNotificationDisabled, forKey: UserDefaultsKeys.isNotificationDisabled.rawValue)
}

// ์ €์žฅ๋œ ์‹œ๊ฐ„๊ณผ ๋ถ„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
private func loadInitialTime() {
private func loadInitialNotificationTime() {
let calendar = Calendar.current
let hour = UserDefaults.standard.integer(forKey: UserDefaultsKeys.reminderHour.rawValue) // ์ €์žฅ๋œ ์‹œ๊ฐ„
let minute = UserDefaults.standard.integer(forKey: UserDefaultsKeys.reminderMinute.rawValue) // ์ €์žฅ๋œ ๋ถ„
Expand Down

0 comments on commit f982743

Please sign in to comment.