๐ User Notifications
๋ก์ปฌ ์๋ฆผ๊ณผ ์๊ฒฉ ํธ์ ์๋ฆผ ์์ ์ ๋ณต
iOS 10+Live Activities ํตํฉ
โจ User Notifications๋?
User Notifications ํ๋ ์์ํฌ๋ ๋ก์ปฌ ์๋ฆผ๊ณผ ์๊ฒฉ ํธ์ ์๋ฆผ์ ํตํฉ ๊ด๋ฆฌํฉ๋๋ค. ์๊ฐ/์์น ๊ธฐ๋ฐ ํธ๋ฆฌ๊ฑฐ, ๋ฏธ๋์ด ์ฒจ๋ถ, ์ปค์คํ ์ก์ , Notification Service Extension ๋ฑ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
๐ก ํต์ฌ ๊ธฐ๋ฅ: ๋ก์ปฌ/์๊ฒฉ ์๋ฆผ ยท ์๊ฐ/์์น ํธ๋ฆฌ๊ฑฐ ยท ๋ฏธ๋์ด ์ฒจ๋ถ ยท ์ปค์คํ
์ก์
ยท ์๋ฆผ ๊ด๋ฆฌ ยท Notification Extension ยท ์๋ฆผ ์ค์ ยท ํฌ๊ทธ๋ผ์ด๋ ์๋ฆผ
๐ฏ 1. ๊ถํ ์์ฒญ
์๋ฆผ ์ฌ์ฉ ์ ๋จผ์ ์ฌ์ฉ์ ๊ถํ์ ์์ฒญํด์ผ ํฉ๋๋ค.
NotificationManager.swift โ ๊ถํ ์์ฒญ
import UserNotifications import SwiftUI @Observable class NotificationManager: NSObject { var authorizationStatus: UNAuthorizationStatus = .notDetermined var isAuthorized = false override init() { super.init() UNUserNotificationCenter.current().delegate = self checkAuthorizationStatus() } // ๊ถํ ์ํ ํ์ธ func checkAuthorizationStatus() { UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in Task { @MainActor in self?.authorizationStatus = settings.authorizationStatus self?.isAuthorized = settings.authorizationStatus == .authorized } } } // ๊ถํ ์์ฒญ func requestAuthorization() async throws -> Bool { let center = UNUserNotificationCenter.current() // ์๋ฆผ, ์ฌ์ด๋, ๋ฐฐ์ง ๊ถํ ์์ฒญ let granted = try await center.requestAuthorization( options: [.alert, .sound, .badge, .criticalAlert] ) await MainActor.run { isAuthorized = granted } // ์๊ฒฉ ํธ์ ์๋ฆผ ๋ฑ๋ก if granted { await UIApplication.shared.registerForRemoteNotifications() } return granted } }
โฐ 2. ๋ก์ปฌ ์๋ฆผ (Local Notifications)
์ฑ์์ ์ง์ ์ค์ผ์ค๋งํ๋ ์๋ฆผ์ ๋๋ค.
LocalNotifications.swift โ ์๊ฐ ๊ธฐ๋ฐ ์๋ฆผ
import UserNotifications extension NotificationManager { // ์๊ฐ ๊ธฐ๋ฐ ์๋ฆผ func scheduleTimeNotification( title: String, body: String, timeInterval: TimeInterval, repeats: Bool = false ) async throws { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default content.badge = 1 // ํธ๋ฆฌ๊ฑฐ ์ค์ (5์ด ํ) let trigger = UNTimeIntervalNotificationTrigger( timeInterval: timeInterval, repeats: repeats ) let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: trigger ) try await UNUserNotificationCenter.current().add(request) } // ์บ๋ฆฐ๋ ๊ธฐ๋ฐ ์๋ฆผ (๋งค์ผ ์ค์ 9์) func scheduleDailyNotification( title: String, body: String, hour: Int, minute: Int ) async throws { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default var dateComponents = DateComponents() dateComponents.hour = hour dateComponents.minute = minute let trigger = UNCalendarNotificationTrigger( dateMatching: dateComponents, repeats: true ) let request = UNNotificationRequest( identifier: "daily-\(hour)-\(minute)", content: content, trigger: trigger ) try await UNUserNotificationCenter.current().add(request) } // ์์น ๊ธฐ๋ฐ ์๋ฆผ func scheduleLocationNotification( title: String, body: String, latitude: Double, longitude: Double, radius: Double ) async throws { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default let center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) let region = CLCircularRegion( center: center, radius: radius, identifier: UUID().uuidString ) region.notifyOnEntry = true region.notifyOnExit = false let trigger = UNLocationNotificationTrigger( region: region, repeats: false ) let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: trigger ) try await UNUserNotificationCenter.current().add(request) } }
๐ท 3. ๋ฏธ๋์ด ์ฒจ๋ถ (Media Attachments)
์๋ฆผ์ ์ด๋ฏธ์ง, ๋น๋์ค, ์ค๋์ค๋ฅผ ์ฒจ๋ถํฉ๋๋ค.
MediaNotifications.swift โ ๋ฏธ๋์ด ์ฒจ๋ถ
import UserNotifications extension NotificationManager { // ์ด๋ฏธ์ง ์ฒจ๋ถ func scheduleNotificationWithImage( title: String, body: String, imageURL: URL ) async throws { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default // ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ๋ฐ ์ฒจ๋ถ let (data, _) = try await URLSession.shared.data(from: imageURL) let tempDirectory = FileManager.default.temporaryDirectory let fileURL = tempDirectory.appendingPathComponent("notification-image.jpg") try data.write(to: fileURL) let attachment = try UNNotificationAttachment( identifier: "image", url: fileURL, options: nil ) content.attachments = [attachment] let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: trigger ) try await UNUserNotificationCenter.current().add(request) } // ๋น๋์ค ์ฒจ๋ถ func scheduleNotificationWithVideo(videoURL: URL) async throws { let content = UNMutableNotificationContent() content.title = "์ ๋น๋์ค" content.body = "๋น๋์ค๋ฅผ ํ์ธํ์ธ์" let attachment = try UNNotificationAttachment( identifier: "video", url: videoURL, options: [UNNotificationAttachmentOptionsTypeHintKey: "public.mpeg-4"] ) content.attachments = [attachment] let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: nil ) try await UNUserNotificationCenter.current().add(request) } }
๐ฌ 4. ์ปค์คํ ์ก์ (Actions)
์๋ฆผ์์ ์ง์ ์์ ์ ์ํํ ์ ์๋ ๋ฒํผ์ ์ถ๊ฐํฉ๋๋ค.
NotificationActions.swift โ ์ปค์คํ
์ก์
import UserNotifications extension NotificationManager { // ์ก์ ์นดํ ๊ณ ๋ฆฌ ๋ฑ๋ก func registerNotificationCategories() { // ๋ฉ์์ง ์๋ฆผ ์ก์ let replyAction = UNTextInputNotificationAction( identifier: "REPLY_ACTION", title: "๋ต์ฅ", options: [], textInputButtonTitle: "๋ณด๋ด๊ธฐ", textInputPlaceholder: "๋ฉ์์ง๋ฅผ ์ ๋ ฅํ์ธ์" ) let likeAction = UNNotificationAction( identifier: "LIKE_ACTION", title: "๐", options: [.foreground] ) let deleteAction = UNNotificationAction( identifier: "DELETE_ACTION", title: "์ญ์ ", options: [.destructive] ) let messageCategory = UNNotificationCategory( identifier: "MESSAGE_CATEGORY", actions: [replyAction, likeAction, deleteAction], intentIdentifiers: [], options: [] ) // ๋ฆฌ๋ง์ธ๋ ์๋ฆผ ์ก์ let completeAction = UNNotificationAction( identifier: "COMPLETE_ACTION", title: "์๋ฃ", options: [] ) let snoozeAction = UNNotificationAction( identifier: "SNOOZE_ACTION", title: "10๋ถ ๋ค ์๋ฆผ", options: [] ) let reminderCategory = UNNotificationCategory( identifier: "REMINDER_CATEGORY", actions: [completeAction, snoozeAction], intentIdentifiers: [], options: [] ) UNUserNotificationCenter.current().setNotificationCategories([ messageCategory, reminderCategory ]) } // ์ก์ ์ด ์๋ ์๋ฆผ ์ ์ก func scheduleActionableNotification() async throws { let content = UNMutableNotificationContent() content.title = "์ ๋ฉ์์ง" content.body = "John์ด ๋ฉ์์ง๋ฅผ ๋ณด๋์ต๋๋ค" content.categoryIdentifier = "MESSAGE_CATEGORY" content.sound = .default let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: trigger ) try await UNUserNotificationCenter.current().add(request) } }
๐ฏ 5. Delegate ์ฒ๋ฆฌ
์๋ฆผ ์๋ต๊ณผ ํฌ๊ทธ๋ผ์ด๋ ์๋ฆผ์ ์ฒ๋ฆฌํฉ๋๋ค.
NotificationDelegate.swift โ Delegate ๊ตฌํ
import UserNotifications extension NotificationManager: UNUserNotificationCenterDelegate { // ํฌ๊ทธ๋ผ์ด๋์์ ์๋ฆผ ํ์ func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification ) async -> UNNotificationPresentationOptions { // ์ฑ์ด ํ์ฑํ ์ํ์ผ ๋๋ ์๋ฆผ ํ์ return [.banner, .sound, .badge] } // ์๋ฆผ ์๋ต ์ฒ๋ฆฌ (ํญํ๊ฑฐ๋ ์ก์ ์ ํ) func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse ) async { let userInfo = response.notification.request.content.userInfo let actionIdentifier = response.actionIdentifier switch actionIdentifier { case "REPLY_ACTION": if let textResponse = response as? UNTextInputNotificationResponse { let replyText = textResponse.userText // ๋ต์ฅ ์ฒ๋ฆฌ print("๋ต์ฅ: \(replyText)") } case "LIKE_ACTION": // ์ข์์ ์ฒ๋ฆฌ print("์ข์์ ํด๋ฆญ") case "DELETE_ACTION": // ์ญ์ ์ฒ๋ฆฌ print("์ญ์ ํด๋ฆญ") case "COMPLETE_ACTION": // ์๋ฃ ์ฒ๋ฆฌ print("์๋ฃ ํด๋ฆญ") case "SNOOZE_ACTION": // 10๋ถ ๋ค ๋ค์ ์๋ฆผ try? await scheduleTimeNotification( title: "๋ฆฌ๋ง์ธ๋", body: "๋ค์ ์๋ ค๋๋ฆฝ๋๋ค", timeInterval: 600 ) case UNNotificationDefaultActionIdentifier: // ์๋ฆผ ํญ (๊ธฐ๋ณธ ์ก์ ) print("์๋ฆผ ํญ๋จ") case UNNotificationDismissActionIdentifier: // ์๋ฆผ ๋ฌด์ print("์๋ฆผ ๋ฌด์๋จ") default: break } } }
๐ฑ 6. ์๋ฆผ ๊ด๋ฆฌ
์์ฝ๋ ์๋ฆผ์ ์กฐํ, ์์ , ์ญ์ ํฉ๋๋ค.
NotificationManagement.swift โ ์๋ฆผ ๊ด๋ฆฌ
import UserNotifications extension NotificationManager { // ๋๊ธฐ ์ค์ธ ์๋ฆผ ๋ชฉ๋ก func getPendingNotifications() async -> [UNNotificationRequest] { await UNUserNotificationCenter.current().pendingNotificationRequests() } // ์ ๋ฌ๋ ์๋ฆผ ๋ชฉ๋ก func getDeliveredNotifications() async -> [UNNotification] { await UNUserNotificationCenter.current().deliveredNotifications() } // ํน์ ์๋ฆผ ์ทจ์ func cancelNotification(withIdentifier identifier: String) { UNUserNotificationCenter.current().removePendingNotificationRequests( withIdentifiers: [identifier] ) } // ๋ชจ๋ ์๋ฆผ ์ทจ์ func cancelAllNotifications() { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() } // ์ ๋ฌ๋ ์๋ฆผ ์ ๊ฑฐ func removeDeliveredNotification(withIdentifier identifier: String) { UNUserNotificationCenter.current().removeDeliveredNotifications( withIdentifiers: [identifier] ) } // ๋ฐฐ์ง ์ซ์ ์ด๊ธฐํ func clearBadge() { Task { @MainActor in UIApplication.shared.applicationIconBadgeNumber = 0 } } }
๐ฑ SwiftUI ํตํฉ
NotificationDemoView.swift โ ์ข
ํฉ ๋ฐ๋ชจ
import SwiftUI struct NotificationDemoView: View { @State private var manager = NotificationManager() @State private var pendingNotifications: [UNNotificationRequest] = [] var body: some View { NavigationStack { List { Section("๊ถํ") { if manager.isAuthorized { Label("์๋ฆผ ํ์ฉ๋จ", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) } else { Button("์๋ฆผ ๊ถํ ์์ฒญ") { Task { try? await manager.requestAuthorization() } } } } Section("๋ก์ปฌ ์๋ฆผ") { Button("5์ด ํ ์๋ฆผ") { Task { try? await manager.scheduleTimeNotification( title: "ํ ์คํธ ์๋ฆผ", body: "5์ด๊ฐ ์ง๋ฌ์ต๋๋ค", timeInterval: 5 ) } } Button("๋งค์ผ ์ค์ 9์ ์๋ฆผ") { Task { try? await manager.scheduleDailyNotification( title: "์ข์ ์์นจ!", body: "์ค๋๋ ์ข์ ํ๋ฃจ ๋์ธ์", hour: 9, minute: 0 ) } } Button("์ก์ ์ด ์๋ ์๋ฆผ") { Task { manager.registerNotificationCategories() try? await manager.scheduleActionableNotification() } } } Section("์์ฝ๋ ์๋ฆผ (\(pendingNotifications.count)๊ฐ)") { ForEach(pendingNotifications, id: \.identifier) { notification in VStack(alignment: .leading) { Text(notification.content.title) .font(.headline) Text(notification.content.body) .font(.caption) .foregroundStyle(.secondary) } .swipeActions { Button("์ญ์ ", role: .destructive) { manager.cancelNotification(withIdentifier: notification.identifier) loadPendingNotifications() } } } } Section { Button("๋ชจ๋ ์๋ฆผ ์ทจ์", role: .destructive) { manager.cancelAllNotifications() loadPendingNotifications() } Button("๋ฐฐ์ง ์ด๊ธฐํ") { manager.clearBadge() } } } .navigationTitle("User Notifications") .task { loadPendingNotifications() } .refreshable { loadPendingNotifications() } } } func loadPendingNotifications() { Task { pendingNotifications = await manager.getPendingNotifications() } } }
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
- ์์์ ์ ์ฑ: ์ฌ์ฉ์์๊ฒ ์ค์ํ๊ณ ์๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ง ์๋ฆผ์ผ๋ก ์ ์ก
- ๋ช ํ์ฑ: ์๋ฆผ ๋ด์ฉ์ ๊ฐ๊ฒฐํ๊ณ ๋ช ํํ๊ฒ ์์ฑ
- ์ก์ : ์๋ฆผ์์ ๋ฐ๋ก ์ํ ๊ฐ๋ฅํ ์๋ฏธ ์๋ ์ก์ ์ ๊ณต
- ํ์ด๋ฐ: ์ฌ์ฉ์๊ฐ ๋ฐฉํด๋ฐ์ง ์๋ ์ ์ ํ ์๊ฐ์ ์๋ฆผ ์ ์ก
- ๊ฐ์ธํ: ์ฌ์ฉ์ ํ๋๊ณผ ์ ํธ๋๋ฅผ ๋ฐ์ํ ์๋ฆผ
- ์ ์ด: ์๋ฆผ ๋น๋์ ์ข ๋ฅ๋ฅผ ์ฌ์ฉ์๊ฐ ์กฐ์ ํ ์ ์๋๋ก ์ค์ ์ ๊ณต
๐ฏ ์ค์ ํ์ฉ
- ๋ฆฌ๋ง์ธ๋ ์ฑ: ์๊ฐ/์์น ๊ธฐ๋ฐ ํ ์ผ ์๋ฆผ
- ๋ฉ์์ง ์ฑ: ์ ๋ฉ์์ง ์๋ฆผ + ๋น ๋ฅธ ๋ต์ฅ
- ํผํธ๋์ค ์ฑ: ์ด๋ ์๊ฐ ์๋ฆผ + ๋ฏธ๋์ด ์ฒจ๋ถ
- ๋ฐฐ๋ฌ ์ฑ: ์ฃผ๋ฌธ ์ํ ์ ๋ฐ์ดํธ ์๋ฆผ
- ๋ด์ค ์ฑ: ์๋ณด ์๋ฆผ with ์ด๋ฏธ์ง
๐ ๋ ์์๋ณด๊ธฐ
- User Notifications ๊ณต์ ๋ฌธ์
- WWDC: Update your app for iOS 17 notifications
- Sending Notification Requests to APNs
- Push Notification Programming Guide
โก๏ธ ํ: Critical Alerts๋ ๋ฐฉํด๊ธ์ง ๋ชจ๋๋ฅผ ๋ฌด์ํ๊ณ ์๋ฆผ์ ์ ๋ฌํฉ๋๋ค. Apple์ ํน๋ณ ๊ถํ์ด ํ์ํ๋ฉฐ, ๊ธด๊ธ ์ํฉ(์๋ฃ, ๋ณด์ ๋ฑ)์๋ง ์ฌ์ฉํด์ผ ํฉ๋๋ค.