dev._.note

[TIL] 240216_TIL 본문

TIL (Today I Learned)

[TIL] 240216_TIL

Laena 2024. 2. 16. 19:52

팀 프로젝트 회고 (알람)

알람 앱을 개발하면서 애플의 기본 알람 앱을 모델로 채택한 결정은 사용자에게 친숙하고 효과적인 알림 경험을 제공하기 위한 의도에서 비롯되었습니다. 특히, 애플 알람 앱에서 알람이 울릴 때 상단 푸시 알림이 최대 2분 동안 지속되는 것을 관찰하고, 이와 유사한 사용자 경험을 제공하고자 로컬 알림을 선택했습니다. 이는 사용자가 이미 익숙해진 알림 방식을 따르며, 사용자의 학습 곡선을 최소화하고 앱 사용의 편리함을 증가시키는 장점을 가집니다.

그러나 로컬 알림을 구현하는 과정에서 iOS 시스템의 제약으로 인해 로컬 알림의 지속 시간이 최대 30초로 제한된다는 문제에 직면했습니다. 이는 알람 앱에서 중요한 알림이 울릴 때 사용자에게 충분한 시간 동안 알림을 제공하기에는 부적합함을 의미합니다. 특히 깊은 잠에서 사용자를 깨우거나 중요한 일정을 알리는 데 있어서 30초의 짧은 지속 시간은 불충분할 수 있습니다. 따라서, 로컬 알림만을 사용하는 것은 알람 앱의 핵심 기능을 효과적으로 수행하기에 제한적일 수 있음을 깨달았습니다.

이러한 경험을 통해 알람 앱 개발 시 사용자에게 친숙한 알림 경험을 제공하고자 하는 초기 목표와 함께, 기술적 제약과 사용자 요구 사이의 균형을 맞추는 것의 중요성을 인식하게 되었습니다. 향후 알람 앱을 개선하거나 새로운 기능을 구현할 때, 이러한 제약사항을 고려하여 더 나은 사용자 경험을 제공할 수 있는 방안을 모색할 필요가 있음을 깨달았습니다.

 

+ 알라미 어플 탐색

앱스토어에서 인기 있는 알라미 어플을 꼼꼼히 살펴보는 과정에서, 이 어플도 30초 간격으로 반복 알림을 보내는 기능을 채택하고 있는 것을 발견했습니다. 


로컬 알람 (AlarmManager)

AlarmManager 클래스는 알람 애플리케이션에서 핵심적인 역할을 수행하는 컴포넌트입니다.

이 클래스는 알람 설정, 관리, 업데이트 등의 기능을 제공하여 애플리케이션의 로컬 알림 시스템을 관리합니다. 다음은 AlarmManager의 주요 기능과 전체적인 작동 흐름에 대한 설명입니다.

 

1. 로컬 알림 스케줄링 (scheduleLocalNotification)

  • 기능 설명: 이 메서드는 AlarmEntity 객체를 인자로 받아, 해당 객체에 저장된 알람 설정에 따라 로컬 알림을 생성하고 스케줄링합니다. 이 과정은 알림의 제목, 본문, 소리 설정 등을 포함합니다.
  • 작동 흐름:
    • 알림의 기본 정보(제목, 본문, 소리)를 UNMutableNotificationContent 객체에 설정합니다.
    • 알림이 울릴 시간과 반복될 요일을 DateComponents를 사용하여 결정합니다.
    • 요일별로 UNCalendarNotificationTrigger를 생성하여 알림이 특정 요일에 반복되도록 합니다.
    • 각 요일별로 고유한 식별자를 사용하여 UNNotificationRequest를 생성하고, 시스템의 UNUserNotificationCenter에 알림 요청을 추가합니다.

2. 로컬 알림 제거 (removeLocalNotification)

  • 기능 설명: 특정 AlarmEntity에 대한 모든 로컬 알림을 제거합니다. 이는 사용자가 알람을 삭제하거나 비활성화할 경우에 필요합니다.
  • 작동 흐름:
    • 각 요일별로 생성된 알림 식별자를 기반으로 UNUserNotificationCenter에서 해당 알림 요청들을 찾아 제거합니다.
    • 이를 통해 해당 알람과 관련된 모든 로컬 알림이 시스템에서 삭제됩니다.

3. 로컬 알림 업데이트 (updateLocalNotifications)

  • 기능 설명: 저장된 모든 알람에 대해 현재 활성화 상태를 검사하고, 활성화된 알람에 대해서는 새로운 로컬 알림을 스케줄링하며, 비활성화된 알람에 대해서는 기존 로컬 알림을 제거합니다.
  • 작동 흐름:
    • 애플리케이션에서 관리하는 모든 AlarmEntity 객체를 순회합니다.
    • 각 알람의 활성화 상태(isEnabled)를 확인합니다.
    • 활성화된 알람에 대해서는 scheduleLocalNotification 메서드를 호출하여 알림을 다시 스케줄링합니다.
    • 비활성화된 알람에 대해서는 removeLocalNotification 메서드를 호출하여 해당 알람의 로컬 알림을 제거합니다.

4. 고유 식별자 등록

  • 같은 요일, 같은 시간에 요일만 다른 알람을 구별해야 했기 때문에 고유 식별자를 각각 등록해 주었습니다.
  •  식별자 설정 방법: UNNotificationRequest를 생성할 때, identifier 파라미터에 "\(alarm.alarmId?.uuidString ?? UUID().uuidString)-\(dayIdentifier)" 형식의 값을 사용합니다. 여기서 alarm.alarmId는 알람의 고유 ID를 나타내고, dayIdentifier는 요일을 나타내는 문자열(예: "Mon" for Monday)입니다. 이렇게 구성된 식별자는 각 알람이 고유하게 식별될 수 있게 해 주며, 알람이 설정된 요일 정보도 포함하고 있어 관리가 용이해집니다.

고유 식별자 등록

// 알림이 반복될 요일을 설정 사용자가 요일을 지정하지 않은 경우 매일 알림이 울리도록 설정
        var daysToSchedule = alarm.repeatDays as? [String] ?? []
        if daysToSchedule.isEmpty {
            daysToSchedule = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] // 사용자가 요일을 지정하지 않은 경우 일주일 내내 알람을 설정
        }
        
        // 설정된 요일마다 알림을 스케줄링
        for day in daysToSchedule {
            var matchingComponents = DateComponents()
            matchingComponents.hour = triggerDate.hour
            matchingComponents.minute = triggerDate.minute
            let dayIdentifier: String // 요일별 고유 식별자를 설정
            
            // 요일을 나타내는 DateComponents의 weekday를 설정하고, 알림 식별자에 사용될 요일별 문자열을 설정
            switch day {
            case "일요일마다", "Sun": matchingComponents.weekday = 1; dayIdentifier = "Sun"
            case "월요일마다", "Mon": matchingComponents.weekday = 2; dayIdentifier = "Mon"
            case "화요일마다", "Tue": matchingComponents.weekday = 3; dayIdentifier = "Tue"
            case "수요일마다", "Wed": matchingComponents.weekday = 4; dayIdentifier = "Wed"
            case "목요일마다", "Thu": matchingComponents.weekday = 5; dayIdentifier = "Thu"
            case "금요일마다", "Fri": matchingComponents.weekday = 6; dayIdentifier = "Fri"
            case "토요일마다", "Sat": matchingComponents.weekday = 7; dayIdentifier = "Sat"
            default: continue // 일치하는 요일이 없는 경우 다음 요일로 넘어감
            }
            
            // 알림 트리거를 설정하고 알림 요청을 추가
            let trigger = UNCalendarNotificationTrigger(dateMatching: matchingComponents, repeats: true)
            let request = UNNotificationRequest(identifier: "\(alarm.alarmId?.uuidString ?? UUID().uuidString)-\(dayIdentifier)", content: content, trigger: trigger)
            
            // 설정된 알림을 사용자 알림 센터에 추가
            UNUserNotificationCenter.current().add(request) { error in
                DispatchQueue.main.async {
                    completion(error == nil) // 알림 추가 성공 여부를 completion 핸들러를 통해 반환
                }
            }
        }

전체코드

lass AlarmManager {
    // 로컬 알림을 스케줄링하는 메서드
    // AlarmEntity 객체를 받아 해당 알람 설정에 따라 로컬 알림을 생성
    func scheduleLocalNotification(for alarm: AlarmEntity, completion: @escaping (Bool) -> Void) {
        // 알림의 내용을 설정
        let content = UNMutableNotificationContent()
        content.categoryIdentifier = "ALARM_CATEGORY" // 알림의 카테고리를 설정,  알림의 유형을 구분
        content.title = "Alarm Butler" // 알림의 제목을 설정
        content.body = alarm.title ?? "Alarm" // 알림의 본문을 설정, 알람 객체에 제목이 없는 경우 "Alarm"을 기본값으로 사용
        
        // 알림의 소리를 설정, 사용자가 알람에 소리를 지정한 경우 해당 소리 파일을 사용하고, 그렇지 않은 경우 기본 소리를 사용
        if let soundFileName = alarm.sound {
            var soundFileNameWithExtension = soundFileName
            if !soundFileName.lowercased().hasSuffix(".mp3") && !soundFileName.lowercased().hasSuffix(".wav") {
                soundFileNameWithExtension += ".mp3" // 소리 파일에 확장자가 없는 경우 ".mp3"를 추가
            }
            content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundFileNameWithExtension))
        } else {
            content.sound = UNNotificationSound.default // 사용자가 소리를 지정하지 않은 경우 기본 소리를 사용
        }
        
        // 알림이 발생할 시간을 설정
        let triggerDate = Calendar.current.dateComponents([.hour, .minute], from: alarm.time ?? Date())
        
        // 알림이 반복될 요일을 설정 사용자가 요일을 지정하지 않은 경우 매일 알림이 울리도록 설정
        var daysToSchedule = alarm.repeatDays as? [String] ?? []
        if daysToSchedule.isEmpty {
            daysToSchedule = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] // 사용자가 요일을 지정하지 않은 경우 일주일 내내 알람을 설정
        }
        
        // 설정된 요일마다 알림을 스케줄링
        for day in daysToSchedule {
            var matchingComponents = DateComponents()
            matchingComponents.hour = triggerDate.hour
            matchingComponents.minute = triggerDate.minute
            let dayIdentifier: String // 요일별 고유 식별자를 설정
            
            // 요일을 나타내는 DateComponents의 weekday를 설정하고, 알림 식별자에 사용될 요일별 문자열을 설정
            switch day {
            case "일요일마다", "Sun": matchingComponents.weekday = 1; dayIdentifier = "Sun"
            case "월요일마다", "Mon": matchingComponents.weekday = 2; dayIdentifier = "Mon"
            case "화요일마다", "Tue": matchingComponents.weekday = 3; dayIdentifier = "Tue"
            case "수요일마다", "Wed": matchingComponents.weekday = 4; dayIdentifier = "Wed"
            case "목요일마다", "Thu": matchingComponents.weekday = 5; dayIdentifier = "Thu"
            case "금요일마다", "Fri": matchingComponents.weekday = 6; dayIdentifier = "Fri"
            case "토요일마다", "Sat": matchingComponents.weekday = 7; dayIdentifier = "Sat"
            default: continue // 일치하는 요일이 없는 경우 다음 요일로 넘어감
            }
            
            // 알림 트리거를 설정하고 알림 요청을 추가
            let trigger = UNCalendarNotificationTrigger(dateMatching: matchingComponents, repeats: true)
            let request = UNNotificationRequest(identifier: "\(alarm.alarmId?.uuidString ?? UUID().uuidString)-\(dayIdentifier)", content: content, trigger: trigger)
            
            // 설정된 알림을 사용자 알림 센터에 추가
            UNUserNotificationCenter.current().add(request) { error in
                DispatchQueue.main.async {
                    completion(error == nil) // 알림 추가 성공 여부를 completion 핸들러를 통해 반환
                }
            }
        }
    }
    
    // 특정 알람에 대한 모든 로컬 알림을 제거하는 메서드
    func removeLocalNotification(for alarm: AlarmEntity) {
        let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] // 요일별 식별자에 사용될 요일 문자열
        // 각 요일에 해당하는 알림 식별자를 생성
        let identifiers = days.map { "\(alarm.alarmId?.uuidString ?? UUID().uuidString)-\($0)" }
        // 생성된 식별자에 해당하는 모든 알림을 사용자 알림 센터에서 제거
        UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
        print("Removed notifications for alarm ID: \(alarm.alarmId?.uuidString ?? "Unknown ID")") // 제거 작업이 완료되면 콘솔에 메시지를 출력
    }

    // 모든 활성화된 알람에 대해 로컬 알림을 업데이트하는 메서드
    func updateLocalNotifications(for alarms: [AlarmEntity]) {
        for alarm in alarms {
            if alarm.isEnabled {
                // 활성화된 알람에 대해 알림을 다시 스케줄링
                scheduleLocalNotification(for: alarm) { _ in }
            } else {
                // 비활성화된 알람에 대한 알림을 제거
                removeLocalNotification(for: alarm)
            }
        }
    }
}

로컬알람 등록 확인

// 로컬알림 목록 테스트용
    func printPendingNotificationRequests() {
        let notificationCenter = UNUserNotificationCenter.current()
        notificationCenter.getPendingNotificationRequests { requests in
            for request in requests {
                print("Pending Notification Request:")
                print("Identifier: \(request.identifier)")
                //print("Content: \(request.content)")
                if let trigger = request.trigger as? UNCalendarNotificationTrigger {
                    let dateComponents = trigger.dateComponents
                    let hour = dateComponents.hour ?? 0
                    let minute = dateComponents.minute ?? 0
                    let weekday = dateComponents.weekday ?? 0
                    print("Date Components: \(hour):\(minute), Weekday: \(weekday)")
                }
                print("Repeats: \(request.trigger?.repeats ?? false)")
                print("-------------------------------------------------")
            }
        }
    }

'TIL (Today I Learned)' 카테고리의 다른 글

[Swift] XCTest란?  (0) 2024.03.05
[TIL] 240215_TIL  (0) 2024.02.15
[TIL] 240208_TIL  (0) 2024.02.09
[TIL] 240103_TIL  (1) 2024.01.03
[TIL] 231220_TIL  (0) 2023.12.20