Skip to content

Commit db2bef6

Browse files
committed
알림 설정 기능 추가
## 수정 사항 - 알림 설정 기능 추가 - 설정 화면에서 원하는 시간에 알림을 받을 수 있도록 시간 설정 기능 추가 - 테스트 코드 추가
1 parent e31167f commit db2bef6

10 files changed

+392
-7
lines changed

PlantingMind/PlantingMind.xcodeproj/project.pbxproj

+28-4
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@
3838
7B63CAA72BA9CB4700943DED /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B63CAA62BA9CB4700943DED /* Assets.xcassets */; };
3939
7B63CAAB2BA9CB4700943DED /* PlantingWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7B63CA9B2BA9CB4600943DED /* PlantingWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4040
7B661C382B6393880082F9DB /* MoodRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B661C372B6393880082F9DB /* MoodRecordView.swift */; };
41+
7B786A6F2BCD0458005D69A0 /* NotifiactionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B786A6E2BCD0458005D69A0 /* NotifiactionManager.swift */; };
42+
7B786A712BCD08D3005D69A0 /* NotificationSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B786A702BCD08D3005D69A0 /* NotificationSettingView.swift */; };
43+
7B786A772BCD0DC6005D69A0 /* NotificationSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B786A762BCD0DC6005D69A0 /* NotificationSettingViewModel.swift */; };
44+
7B786A792BCD6318005D69A0 /* NotificationSettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B786A782BCD6318005D69A0 /* NotificationSettingsViewModelTests.swift */; };
4145
7B8430A22BC7C1DE00D5939E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7B8430A12BC7C1DE00D5939E /* GoogleService-Info.plist */; };
4246
7B8430AB2BC81FC400D5939E /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8430AA2BC81FC400D5939E /* FirebaseCrashlytics */; };
4347
7B8430AF2BC8DE7100D5939E /* CrashlyticsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8430AE2BC8DE7100D5939E /* CrashlyticsLog.swift */; };
4448
7B907F732B9981E90050D05B /* DayCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B907F722B9981E90050D05B /* DayCellViewModel.swift */; };
4549
7B907F752B9997060050D05B /* DayCellViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B907F742B9997060050D05B /* DayCellViewModelTest.swift */; };
4650
7B907F782B9B4CCE0050D05B /* FetchNotificationSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B907F772B9B4CCE0050D05B /* FetchNotificationSpy.swift */; };
47-
7B907F7A2B9B4FBE0050D05B /* NoticiationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B907F792B9B4FBE0050D05B /* NoticiationExtension.swift */; };
51+
7B907F7A2B9B4FBE0050D05B /* UpdateNoticiationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B907F792B9B4FBE0050D05B /* UpdateNoticiationExtension.swift */; };
4852
7B907F7C2B9C6F1B0050D05B /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B907F7B2B9C6F1B0050D05B /* DateExtension.swift */; };
4953
7B95D17A2BA1645C00484F9D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 7B95D1792BA1645C00484F9D /* Localizable.xcstrings */; };
5054
7B95D17C2BA166AC00484F9D /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B95D17B2BA166AC00484F9D /* StringExtension.swift */; };
@@ -146,12 +150,16 @@
146150
7B63CAA82BA9CB4700943DED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
147151
7B661C372B6393880082F9DB /* MoodRecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodRecordView.swift; sourceTree = "<group>"; };
148152
7B715EDB2BAC734400B4F720 /* PlantingMind.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PlantingMind.xctestplan; sourceTree = "<group>"; };
153+
7B786A6E2BCD0458005D69A0 /* NotifiactionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifiactionManager.swift; sourceTree = "<group>"; };
154+
7B786A702BCD08D3005D69A0 /* NotificationSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingView.swift; sourceTree = "<group>"; };
155+
7B786A762BCD0DC6005D69A0 /* NotificationSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingViewModel.swift; sourceTree = "<group>"; };
156+
7B786A782BCD6318005D69A0 /* NotificationSettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModelTests.swift; sourceTree = "<group>"; };
149157
7B8430A12BC7C1DE00D5939E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
150158
7B8430AE2BC8DE7100D5939E /* CrashlyticsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashlyticsLog.swift; sourceTree = "<group>"; };
151159
7B907F722B9981E90050D05B /* DayCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayCellViewModel.swift; sourceTree = "<group>"; };
152160
7B907F742B9997060050D05B /* DayCellViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayCellViewModelTest.swift; sourceTree = "<group>"; };
153161
7B907F772B9B4CCE0050D05B /* FetchNotificationSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchNotificationSpy.swift; sourceTree = "<group>"; };
154-
7B907F792B9B4FBE0050D05B /* NoticiationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticiationExtension.swift; sourceTree = "<group>"; };
162+
7B907F792B9B4FBE0050D05B /* UpdateNoticiationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateNoticiationExtension.swift; sourceTree = "<group>"; };
155163
7B907F7B2B9C6F1B0050D05B /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
156164
7B95D1792BA1645C00484F9D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
157165
7B95D17B2BA166AC00484F9D /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
@@ -272,7 +280,7 @@
272280
7B11F8272B4549DF00346A41 /* PlantingMindApp.swift */,
273281
7B11F8292B4549DF00346A41 /* ContentView.swift */,
274282
7BABB0102B5F850200EEEEBB /* ColorExtension.swift */,
275-
7B907F792B9B4FBE0050D05B /* NoticiationExtension.swift */,
283+
7B907F792B9B4FBE0050D05B /* UpdateNoticiationExtension.swift */,
276284
7B907F7B2B9C6F1B0050D05B /* DateExtension.swift */,
277285
7B95D17B2BA166AC00484F9D /* StringExtension.swift */,
278286
7B9F19EC2BC5160F0063E613 /* UTTypeExtension.swift */,
@@ -307,6 +315,7 @@
307315
7B34415E2BA1F85600DA43F6 /* MonthPickerViewModelTests.swift */,
308316
7B4C07BB2BB6C1F200B60CCD /* AnalysisViewModelTests.swift */,
309317
7B9F19F32BC688060063E613 /* SettingsViewModelTests.swift */,
318+
7B786A782BCD6318005D69A0 /* NotificationSettingsViewModelTests.swift */,
310319
7B9F19EB2BC512510063E613 /* test.mind */,
311320
);
312321
path = PlantingMindTests;
@@ -369,6 +378,16 @@
369378
path = PlantingWidget;
370379
sourceTree = "<group>";
371380
};
381+
7B786A722BCD09FE005D69A0 /* Notification */ = {
382+
isa = PBXGroup;
383+
children = (
384+
7B786A6E2BCD0458005D69A0 /* NotifiactionManager.swift */,
385+
7B786A762BCD0DC6005D69A0 /* NotificationSettingViewModel.swift */,
386+
7B786A702BCD08D3005D69A0 /* NotificationSettingView.swift */,
387+
);
388+
path = Notification;
389+
sourceTree = "<group>";
390+
};
372391
7B8430AC2BC8DD7B00D5939E /* Firebase */ = {
373392
isa = PBXGroup;
374393
children = (
@@ -390,6 +409,7 @@
390409
7B9F19EE2BC528820063E613 /* Settings */ = {
391410
isa = PBXGroup;
392411
children = (
412+
7B786A722BCD09FE005D69A0 /* Notification */,
393413
7B9F19F52BC694BF0063E613 /* MindDocument.swift */,
394414
7BE2475E2BCA3CA900DD3B76 /* PrivacyPolicyWebview.swift */,
395415
7B9F19EF2BC528980063E613 /* SettingsView.swift */,
@@ -630,14 +650,16 @@
630650
7B0A77112B93196D00ADC039 /* MoodRecordViewModel.swift in Sources */,
631651
7B4C07B82BB6B6B200B60CCD /* AnalysisView.swift in Sources */,
632652
7B0A76CE2B904A1200ADC039 /* MoodRecords.xcdatamodeld in Sources */,
653+
7B786A772BCD0DC6005D69A0 /* NotificationSettingViewModel.swift in Sources */,
633654
7BC300F52B5E4CE100059068 /* CalendarView.swift in Sources */,
634655
7B2CFC1D2B5A6FFB00641F6D /* CalendarGridView.swift in Sources */,
635656
7B661C382B6393880082F9DB /* MoodRecordView.swift in Sources */,
636657
7BE2475F2BCA3CA900DD3B76 /* PrivacyPolicyWebview.swift in Sources */,
637-
7B907F7A2B9B4FBE0050D05B /* NoticiationExtension.swift in Sources */,
658+
7B907F7A2B9B4FBE0050D05B /* UpdateNoticiationExtension.swift in Sources */,
638659
7B34415D2BA1B87A00DA43F6 /* MonthPickerViewModel.swift in Sources */,
639660
7BAB04F42B9466BC00948CF2 /* Mood.swift in Sources */,
640661
7B2CFC1B2B5A6DE900641F6D /* DayCellView.swift in Sources */,
662+
7B786A6F2BCD0458005D69A0 /* NotifiactionManager.swift in Sources */,
641663
7B9F19F02BC528980063E613 /* SettingsView.swift in Sources */,
642664
7B9F19F62BC694BF0063E613 /* MindDocument.swift in Sources */,
643665
7BDA3CD72B58F25F006D74FB /* CalendarHeaderView.swift in Sources */,
@@ -646,6 +668,7 @@
646668
7B8430AF2BC8DE7100D5939E /* CrashlyticsLog.swift in Sources */,
647669
7B571B552B95E5DA0019DEB8 /* MoodRecord+CoreDataClass.swift in Sources */,
648670
7B11F8282B4549DF00346A41 /* PlantingMindApp.swift in Sources */,
671+
7B786A712BCD08D3005D69A0 /* NotificationSettingView.swift in Sources */,
649672
7B9F19F22BC52FED0063E613 /* SettingsViewModel.swift in Sources */,
650673
);
651674
runOnlyForDeploymentPostprocessing = 0;
@@ -656,6 +679,7 @@
656679
files = (
657680
7BAB04F62B946B8700948CF2 /* MoodRecordViewModelTests.swift in Sources */,
658681
7B34415F2BA1F85600DA43F6 /* MonthPickerViewModelTests.swift in Sources */,
682+
7B786A792BCD6318005D69A0 /* NotificationSettingsViewModelTests.swift in Sources */,
659683
7B4C07BC2BB6C1F200B60CCD /* AnalysisViewModelTests.swift in Sources */,
660684
7B9F19F42BC688060063E613 /* SettingsViewModelTests.swift in Sources */,
661685
7B907F782B9B4CCE0050D05B /* FetchNotificationSpy.swift in Sources */,

PlantingMind/PlantingMind/ContentView.swift

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ struct ContentView: View {
1515
CalendarView(calendarViewModel: CalendarViewModel(today: Date(), context: context))
1616
}
1717
.padding([.horizontal, .top])
18+
.onAppear {
19+
UNUserNotificationCenter.current().setBadgeCount(0, withCompletionHandler: nil)
20+
}
1821
}
1922
}
2023

PlantingMind/PlantingMind/Localization/Localizable.xcstrings

+66-2
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,13 @@
266266
"en" : {
267267
"stringUnit" : {
268268
"state" : "translated",
269-
"value" : "Please enter your content."
269+
"value" : "Please enter your content. (Optional)"
270270
}
271271
},
272272
"ko" : {
273273
"stringUnit" : {
274274
"state" : "translated",
275-
"value" : "내용을 입력하세요."
275+
"value" : "내용을 입력하세요. (귀찮으면 안 적어도 돼요.)"
276276
}
277277
}
278278
}
@@ -497,6 +497,38 @@
497497
}
498498
}
499499
},
500+
"notification_allow" : {
501+
"localizations" : {
502+
"en" : {
503+
"stringUnit" : {
504+
"state" : "translated",
505+
"value" : "Allow Notification"
506+
}
507+
},
508+
"ko" : {
509+
"stringUnit" : {
510+
"state" : "translated",
511+
"value" : "알림 허용"
512+
}
513+
}
514+
}
515+
},
516+
"notification_setting" : {
517+
"localizations" : {
518+
"en" : {
519+
"stringUnit" : {
520+
"state" : "translated",
521+
"value" : "Notification Setting"
522+
}
523+
},
524+
"ko" : {
525+
"stringUnit" : {
526+
"state" : "translated",
527+
"value" : "알림 설정"
528+
}
529+
}
530+
}
531+
},
500532
"ok" : {
501533
"extractionState" : "manual",
502534
"localizations" : {
@@ -580,6 +612,38 @@
580612
}
581613
}
582614
},
615+
"set_notification_time" : {
616+
"localizations" : {
617+
"en" : {
618+
"stringUnit" : {
619+
"state" : "translated",
620+
"value" : "Set notification time"
621+
}
622+
},
623+
"ko" : {
624+
"stringUnit" : {
625+
"state" : "translated",
626+
"value" : "알림 시간 설정"
627+
}
628+
}
629+
}
630+
},
631+
"set_time_picker" : {
632+
"localizations" : {
633+
"en" : {
634+
"stringUnit" : {
635+
"state" : "translated",
636+
"value" : "Set Time"
637+
}
638+
},
639+
"ko" : {
640+
"stringUnit" : {
641+
"state" : "translated",
642+
"value" : "설정 시간"
643+
}
644+
}
645+
}
646+
},
583647
"settings" : {
584648
"extractionState" : "manual",
585649
"localizations" : {

PlantingMind/PlantingMind/PlantingMindApp.swift

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ struct PlantingMindApp: App {
1414

1515
init() {
1616
FirebaseApp.configure()
17+
18+
let notificationManager = NotificationManager()
19+
notificationManager.requestPermission()
20+
notificationManager.addNotification(date: nil)
1721
}
1822

1923
var body: some Scene {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// NotifiactionManager.swift
3+
// PlantingMind
4+
//
5+
// Created by 최은주 on 4/15/24.
6+
//
7+
8+
import Foundation
9+
import UserNotifications
10+
11+
enum UserDefaultsKeys: String {
12+
case hour
13+
case minute
14+
}
15+
16+
struct LocalNotification {
17+
var id: String
18+
var title: String
19+
}
20+
21+
protocol Notificable {
22+
var hour: Int { get }
23+
var minute: Int { get }
24+
25+
func checkPermission(completionHandler: @escaping (Bool) -> Void) -> Void
26+
func addNotification(date: Date?) -> Void
27+
}
28+
29+
class NotificationManager: Notificable {
30+
var hour: Int {
31+
UserDefaults.standard.integer(forKey: UserDefaultsKeys.hour.rawValue)
32+
}
33+
34+
var minute: Int {
35+
UserDefaults.standard.integer(forKey: UserDefaultsKeys.minute.rawValue)
36+
}
37+
38+
var notifications = [LocalNotification]()
39+
40+
func checkPermission(completionHandler: @escaping (Bool) -> Void) -> Void {
41+
UNUserNotificationCenter.current()
42+
.getNotificationSettings { settings in
43+
completionHandler(settings.authorizationStatus == .authorized)
44+
}
45+
}
46+
47+
func requestPermission() -> Void {
48+
UNUserNotificationCenter.current()
49+
.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
50+
if let error = error {
51+
CrashlyticsLog.shared.record(error: error)
52+
}
53+
}
54+
}
55+
56+
func addNotification(date: Date?) -> Void {
57+
self.notifications.removeAll()
58+
self.notifications.append(LocalNotification(id: UUID().uuidString, title: ""))
59+
self.updateTime(date)
60+
self.schedule()
61+
}
62+
63+
private func updateTime(_ time: Date?) {
64+
guard let time = time else { return }
65+
let calendar = Calendar.current
66+
UserDefaults.standard.set(calendar.component(.hour, from: time), forKey: UserDefaultsKeys.hour.rawValue)
67+
UserDefaults.standard.set(calendar.component(.minute, from: time), forKey: UserDefaultsKeys.minute.rawValue)
68+
}
69+
70+
private func schedule() -> Void {
71+
UNUserNotificationCenter.current().getNotificationSettings { settings in
72+
switch settings.authorizationStatus {
73+
case .notDetermined:
74+
self.requestPermission()
75+
case .authorized, .provisional:
76+
self.scheduleNotifications()
77+
default:
78+
break
79+
}
80+
}
81+
}
82+
83+
private func scheduleNotifications() -> Void {
84+
for notification in self.notifications {
85+
var dateComponents = DateComponents()
86+
dateComponents.calendar = Calendar.current
87+
88+
let hour = self.hour
89+
dateComponents.hour = self.hour == 0 ? 22 : hour
90+
dateComponents.minute = self.minute
91+
92+
let content = UNMutableNotificationContent()
93+
content.title = notification.title
94+
content.sound = UNNotificationSound.default
95+
content.body = "mood_title".localized
96+
content.badge = 1
97+
98+
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
99+
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
100+
101+
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
102+
103+
UNUserNotificationCenter.current().add(request) { error in
104+
if let error = error {
105+
CrashlyticsLog.shared.record(error: error)
106+
}
107+
}
108+
}
109+
}
110+
111+
112+
}

0 commit comments

Comments
 (0)