Skip to content

Latest commit

ย 

History

History
402 lines (327 loc) ยท 13.1 KB

File metadata and controls

402 lines (327 loc) ยท 13.1 KB

User Notifications AI Reference

ํ‘ธ์‹œ/๋กœ์ปฌ ์•Œ๋ฆผ ๊ฐ€์ด๋“œ. ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  UserNotifications ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์š”

UserNotifications๋Š” ๋กœ์ปฌ ๋ฐ ์›๊ฒฉ ์•Œ๋ฆผ์„ ๊ด€๋ฆฌํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. ์•Œ๋ฆผ ์˜ˆ์•ฝ, ์ปค์Šคํ…€ UI, ์•ก์…˜ ๋ฒ„ํŠผ ๋“ฑ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ Import

import UserNotifications

ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ

1. ๊ถŒํ•œ ์š”์ฒญ

func requestPermission() async throws -> Bool {
    let center = UNUserNotificationCenter.current()
    
    let granted = try await center.requestAuthorization(options: [
        .alert,
        .badge,
        .sound,
        .criticalAlert,  // ๊ธด๊ธ‰ ์•Œ๋ฆผ (๋ณ„๋„ ์Šน์ธ ํ•„์š”)
        .provisional     // ์กฐ์šฉํ•œ ์•Œ๋ฆผ (๊ถŒํ•œ ์—†์ด ๊ฐ€๋Šฅ)
    ])
    
    return granted
}

// ํ˜„์žฌ ๊ถŒํ•œ ์ƒํƒœ ํ™•์ธ
func checkPermission() async -> UNAuthorizationStatus {
    let settings = await UNUserNotificationCenter.current().notificationSettings()
    return settings.authorizationStatus
}

2. ๋กœ์ปฌ ์•Œ๋ฆผ ์ƒ์„ฑ

func scheduleNotification() async throws {
    let content = UNMutableNotificationContent()
    content.title = "์•Œ๋ฆผ ์ œ๋ชฉ"
    content.subtitle = "๋ถ€์ œ๋ชฉ"
    content.body = "์•Œ๋ฆผ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค."
    content.sound = .default
    content.badge = 1
    
    // ํŠธ๋ฆฌ๊ฑฐ: 5์ดˆ ํ›„
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
    
    let request = UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: trigger
    )
    
    try await UNUserNotificationCenter.current().add(request)
}

3. ํŠธ๋ฆฌ๊ฑฐ ์ข…๋ฅ˜

// ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ (์ดˆ)
let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: true)

// ํŠน์ • ๋‚ ์งœ/์‹œ๊ฐ„
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let calendarTrigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)

// ์œ„์น˜ ๊ธฐ๋ฐ˜
let center = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
let region = CLCircularRegion(center: center, radius: 100, identifier: "office")
region.notifyOnEntry = true
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)

์ „์ฒด ์ž‘๋™ ์˜ˆ์ œ

import SwiftUI
import UserNotifications

// MARK: - Notification Manager
@Observable
class NotificationManager {
    var isAuthorized = false
    var pendingNotifications: [UNNotificationRequest] = []
    
    private let center = UNUserNotificationCenter.current()
    
    func requestPermission() async {
        do {
            isAuthorized = try await center.requestAuthorization(options: [.alert, .badge, .sound])
            await setupCategories()
        } catch {
            print("๊ถŒํ•œ ์š”์ฒญ ์‹คํŒจ: \(error)")
        }
    }
    
    func checkStatus() async {
        let settings = await center.notificationSettings()
        isAuthorized = settings.authorizationStatus == .authorized
    }
    
    // ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ ์•ก์…˜ ์„ค์ •
    private func setupCategories() async {
        let completeAction = UNNotificationAction(
            identifier: "COMPLETE",
            title: "์™„๋ฃŒ",
            options: [.foreground]
        )
        
        let snoozeAction = UNNotificationAction(
            identifier: "SNOOZE",
            title: "10๋ถ„ ๋’ค ์•Œ๋ฆผ",
            options: []
        )
        
        let taskCategory = UNNotificationCategory(
            identifier: "TASK_REMINDER",
            actions: [completeAction, snoozeAction],
            intentIdentifiers: [],
            options: [.customDismissAction]
        )
        
        center.setNotificationCategories([taskCategory])
    }
    
    // ์•Œ๋ฆผ ์˜ˆ์•ฝ
    func scheduleReminder(title: String, body: String, date: Date) async throws {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default
        content.categoryIdentifier = "TASK_REMINDER"
        content.userInfo = ["taskId": UUID().uuidString]
        
        let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: date)
        let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
        
        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: trigger
        )
        
        try await center.add(request)
        await fetchPending()
    }
    
    // ๋งค์ผ ๋ฐ˜๋ณต ์•Œ๋ฆผ
    func scheduleDailyReminder(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 center.add(request)
    }
    
    // ๋Œ€๊ธฐ ์ค‘์ธ ์•Œ๋ฆผ ์กฐํšŒ
    func fetchPending() async {
        pendingNotifications = await center.pendingNotificationRequests()
    }
    
    // ์•Œ๋ฆผ ์ทจ์†Œ
    func cancel(identifier: String) {
        center.removePendingNotificationRequests(withIdentifiers: [identifier])
    }
    
    func cancelAll() {
        center.removeAllPendingNotificationRequests()
    }
    
    // ๋ฐฐ์ง€ ์ดˆ๊ธฐํ™”
    func clearBadge() async {
        try? await center.setBadgeCount(0)
    }
}

// MARK: - View
struct NotificationDemoView: View {
    @State private var manager = NotificationManager()
    @State private var reminderTitle = ""
    @State private var reminderDate = Date().addingTimeInterval(60)
    
    var body: some View {
        NavigationStack {
            Form {
                // ๊ถŒํ•œ ์„น์…˜
                Section("๊ถŒํ•œ") {
                    HStack {
                        Text("์•Œ๋ฆผ ๊ถŒํ•œ")
                        Spacer()
                        Text(manager.isAuthorized ? "ํ—ˆ์šฉ๋จ" : "๊ฑฐ๋ถ€๋จ")
                            .foregroundStyle(manager.isAuthorized ? .green : .red)
                    }
                    
                    if !manager.isAuthorized {
                        Button("๊ถŒํ•œ ์š”์ฒญ") {
                            Task { await manager.requestPermission() }
                        }
                    }
                }
                
                // ์•Œ๋ฆผ ์˜ˆ์•ฝ
                Section("์ƒˆ ์•Œ๋ฆผ") {
                    TextField("์ œ๋ชฉ", text: $reminderTitle)
                    DatePicker("์‹œ๊ฐ„", selection: $reminderDate, displayedComponents: [.date, .hourAndMinute])
                    
                    Button("์•Œ๋ฆผ ์˜ˆ์•ฝ") {
                        Task {
                            try? await manager.scheduleReminder(
                                title: reminderTitle,
                                body: "์˜ˆ์•ฝ๋œ ์•Œ๋ฆผ์ž…๋‹ˆ๋‹ค",
                                date: reminderDate
                            )
                            reminderTitle = ""
                        }
                    }
                    .disabled(reminderTitle.isEmpty)
                }
                
                // ๋Œ€๊ธฐ ์ค‘์ธ ์•Œ๋ฆผ
                Section("์˜ˆ์•ฝ๋œ ์•Œ๋ฆผ (\(manager.pendingNotifications.count))") {
                    ForEach(manager.pendingNotifications, id: \.identifier) { request in
                        VStack(alignment: .leading) {
                            Text(request.content.title)
                                .font(.headline)
                            if let trigger = request.trigger as? UNCalendarNotificationTrigger,
                               let nextDate = trigger.nextTriggerDate() {
                                Text(nextDate, style: .relative)
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                            }
                        }
                        .swipeActions {
                            Button("์‚ญ์ œ", role: .destructive) {
                                manager.cancel(identifier: request.identifier)
                                Task { await manager.fetchPending() }
                            }
                        }
                    }
                    
                    if !manager.pendingNotifications.isEmpty {
                        Button("๋ชจ๋‘ ์ทจ์†Œ", role: .destructive) {
                            manager.cancelAll()
                            Task { await manager.fetchPending() }
                        }
                    }
                }
            }
            .navigationTitle("์•Œ๋ฆผ")
            .task {
                await manager.checkStatus()
                await manager.fetchPending()
            }
        }
    }
}

// MARK: - AppDelegate์—์„œ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }
    
    // ์•ฑ์ด foreground์ผ ๋•Œ ์•Œ๋ฆผ ํ‘œ์‹œ
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        return [.banner, .badge, .sound]
    }
    
    // ์•Œ๋ฆผ ํƒญ ๋˜๋Š” ์•ก์…˜ ๋ฒ„ํŠผ ์ฒ˜๋ฆฌ
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
        let userInfo = response.notification.request.content.userInfo
        let actionId = response.actionIdentifier
        
        switch actionId {
        case "COMPLETE":
            // ์™„๋ฃŒ ์ฒ˜๋ฆฌ
            if let taskId = userInfo["taskId"] as? String {
                print("Task completed: \(taskId)")
            }
        case "SNOOZE":
            // 10๋ถ„ ๋’ค ๋‹ค์‹œ ์•Œ๋ฆผ
            let content = response.notification.request.content.mutableCopy() as! UNMutableNotificationContent
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 600, repeats: false)
            let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
            try? await center.add(request)
        default:
            break
        }
    }
}

๊ณ ๊ธ‰ ํŒจํ„ด

1. ์ด๋ฏธ์ง€ ์ฒจ๋ถ€

func scheduleWithImage(imageURL: URL) async throws {
    let content = UNMutableNotificationContent()
    content.title = "์‚ฌ์ง„ ์•Œ๋ฆผ"
    content.body = "์ƒˆ ์‚ฌ์ง„์ด ๋„์ฐฉํ–ˆ์Šต๋‹ˆ๋‹ค"
    
    let attachment = try UNNotificationAttachment(identifier: "image", url: imageURL, 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)
}

2. ์ปค์Šคํ…€ ์•Œ๋ฆผ UI (Notification Content Extension)

// NotificationViewController.swift (Extension Target)
import UIKit
import UserNotifications
import UserNotificationsUI

class NotificationViewController: UIViewController, UNNotificationContentExtension {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var imageView: UIImageView!
    
    func didReceive(_ notification: UNNotification) {
        let content = notification.request.content
        titleLabel.text = content.title
        
        if let attachment = content.attachments.first,
           attachment.url.startAccessingSecurityScopedResource() {
            imageView.image = UIImage(contentsOfFile: attachment.url.path)
            attachment.url.stopAccessingSecurityScopedResource()
        }
    }
}

3. ์›๊ฒฉ ํ‘ธ์‹œ ์•Œ๋ฆผ (APNs)

// AppDelegate
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
    print("Device Token: \(token)")
    // ์„œ๋ฒ„๋กœ ํ† ํฐ ์ „์†ก
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("APNs ๋“ฑ๋ก ์‹คํŒจ: \(error)")
}

// ๋“ฑ๋ก ์š”์ฒญ
UIApplication.shared.registerForRemoteNotifications()

์ฃผ์˜์‚ฌํ•ญ

  1. ๊ถŒํ•œ ์š”์ฒญ ํƒ€์ด๋ฐ

    • ์•ฑ ์ฒซ ์‹คํ–‰ ์‹œ ๋ฐ”๋กœ ์š”์ฒญ โŒ
    • ์•Œ๋ฆผ์ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์ง์ „ ์š”์ฒญ โœ…
  2. ์•Œ๋ฆผ ์‹๋ณ„์ž

    • ๊ฐ™์€ ์‹๋ณ„์ž๋กœ ๋“ฑ๋กํ•˜๋ฉด ๊ธฐ์กด ์•Œ๋ฆผ ๋ฎ์–ด์”€
    • ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅํ•œ ์•Œ๋ฆผ์— ํ™œ์šฉ
  3. ๋ฐฐ์ง€ ๊ด€๋ฆฌ

    // ๋ฐฐ์ง€ ์„ค์ •
    try await center.setBadgeCount(5)
    
    // ๋ฐฐ์ง€ ์ดˆ๊ธฐํ™” (์•ฑ ์—ด ๋•Œ)
    try await center.setBadgeCount(0)
  4. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ œํ•œ

    • ์›๊ฒฉ ํ‘ธ์‹œ ์•Œ๋ฆผ์€ ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ๋งŒ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
    • ๋กœ์ปฌ ์•Œ๋ฆผ์€ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ๊ฐ€๋Šฅ