EveryDiary - Push Notification (2)

ulls12·2024년 3월 25일
0

Swift TIL

목록 보기
57/60

이제 View구성을 했으니, 알람이 울리도록 Controller 설정을 해줘야 한다. 우린 복잡하게, 서버에서 Push 알림을 보낼 필요가 없다. 단순하게 반복 요일을 설정해주고 정해놓은 시간대에 하루에 한 번만 울리게 설정하면 되기 때문이다. 또한, 서버와 네트워크 통신을 해야하기 때문에 데이터 소모량이 늘어나게만 된다. 그렇기 때문에 APNs를 통해 서버와 통신할 필요가 없다. 여기서 APNs는 Apple Push Notification server의 약자로 애플의 서버를 의미한다.

알림 권한 승인 요청

사용자가 알림을 사용하는 것을 허용하는 시점도 고려를 해봐야한다. 무작정, 처음 앱을 시작했을 때, 온보딩 화면에서 다짜고자 알림 권한을 달라고 하면, 다 거절하기를 누를 것이다. 그래서, 알림 앱 권한 부여는 우선 SceneDelegate.swift에는 구성하지 않았다. 그래서 어디가 좋을까 생각하던 중, 사용자가 알람기능을 필요로 하는 시점에 권한 승인 요청을 하면 될 것 같았다. 바로 알림VC에서 스위치를 킬 때, 권한 승인 요청을 한다. 문제는 거부했을 경우이다. 아이폰은 한 번 권한 승인을 거부하면, 다시 묻지 않는다. 직접 설정 앱으로 이동해서 알림 기능을 켜줘야 앱 내에서 알림기능을 이용할 수 있다. 그렇다면, 개발자가 할 수 있는 최선은 알림기능을 이용하기 위해선 설정앱으로 자동으로 이동시켜주는 Alert창을 만드는 것 뿐이다. 그 이후는 사용자가 직접 설정 앱에서 조작해줘야 한다.

    @objc func switchChanged(_ sender: UISwitch) {
        // 스위치 상태 변경을 바로 적용하지 않고, 권한을 요청
        // 권한 상태를 확인하고, 필요한 경우 권한을 요청
        let center = UNUserNotificationCenter.current()
        center.getNotificationSettings { settings in
            DispatchQueue.main.async {
                if settings.authorizationStatus == .authorized {
                    // 권한이 이미 승인된 경우, 스위치 상태를 변경
                    self.switchValueChanged?(sender.isOn)
                } else if settings.authorizationStatus == .denied {
                    // 권한이 거부된 경우, 사용자에게 UIAlert 표시
                    self.showPermissionDeniedAlert()
                    // 스위치 상태를 false
                    sender.setOn(false, animated: true)
                } else {
                    // 권한이 아직 요청되지 않은 경우, 권한을 요청
                    self.requestNotificationPermission { granted in
                        if granted {
                            // 권한 요청이 승인된 경우
                            self.switchValueChanged?(sender.isOn)
                        } else {
                            // 권한 요청이 거부된 경우
                            sender.setOn(false, animated: true)
                        }
                    }
                }
            }
        }
    }

    private func showPermissionDeniedAlert() {
        let authorizationAlert = UIAlertController(title: "알림", message: "알림 기능을 사용하려면 설정에서 알림 권한을 허용해주세요", preferredStyle: .alert)

        let settingsAction = UIAlertAction(title: "설정으로 이동", style: .default) { (_) -> Void in
            guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
                return
            }
            // EveryDiary 앱 설정 내의 알림 창을 호출
            if UIApplication.shared.canOpenURL(settingsUrl) {
                UIApplication.shared.open(settingsUrl, completionHandler: nil)
            }
        }
        
        let cancelAction = UIAlertAction(title: "취소", style: .default, handler: nil)

        authorizationAlert.addAction(settingsAction)
        authorizationAlert.addAction(cancelAction)

        DispatchQueue.main.async { [weak self] in
            self?.present(authorizationAlert, animated: true, completion: nil)
        }
    }

여기서, settings.authorizationStatus는 애플이 제공해주는 프레임워크로 설정에서 알림 권한이 on/off 되있는 지를 감지해준다.
if 문은 총 3가지로 구성되어있다.
1. 권한이 승인된 상태
2. 권한이 거절된 상태
3. 권한 요청을 안 한 상태

권한 요청을 안 한 상태면 requestNotificationPermission 메서드를 불러온다.

    // 알림권한을 받아오는 메서드
    private func requestNotificationPermission(completion: @escaping (Bool) -> Void) {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
            DispatchQueue.main.async {
                completion(granted)
            }
        }
    }

알람 생성

권한 승인을 얻었으니, 이제 알람을 생성할 차례다. 우선 TableView로 구성했던 DayPicker와 TimePicker로 반복할 요일, 지정된 시간을 가져와야 한다. cell에서 지정한 데이터를 가져오기 위해 delegate 패턴을 사용했다. 지정한 시간을 TimePicker에서 가져왔고, 반복할 요일은 두 가지 형태로 가져온다. TableView의 label에 표현하기 위해 String의 형태로 SelectedDaysString 에 할당 해준다. 나머지 하나는 실제로 사용할 데이터로 반복 한 요일의 Bool 값을 가져오며 [Bool] = Array(repeating: false, count: 7)의 형태로 저장된다. Bool 값을 배열 형태로 가져오며 배열은 7일 구성인 7개로 가져오게 된다.

    private func scheduleNotification() {
        guard let selectedTime = selectedTime else { return }
        
        let content = UNMutableNotificationContent()
        content.title = NSString.localizedUserNotificationString(forKey: "오늘의 여정을 기록해보세요", arguments: nil)
        content.body = NSString.localizedUserNotificationString(forKey: "오늘 하루를 되돌아 보며, 얻은 경험들을 기록해보는 건 어떨까요?", arguments: nil)
        content.sound = UNNotificationSound.default
        
        let calendar = Calendar.current
        var dateComponents = calendar.dateComponents([.hour, .minute], from: selectedTime)
        
        let daysofWeek = [1,2,3,4,5,6,7] // 1 = 일요일, 7 = 토요일
        for (index, isSelected) in selectedDays.enumerated() where isSelected {
            dateComponents.weekday = daysofWeek[index]
            
            let uniqueIdentifier = "\(daysofWeek[index])" // 각 요일별 고유한 identifier 생성
            let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
            let request = UNNotificationRequest(identifier: uniqueIdentifier, content: content, trigger: trigger)
            
            UNUserNotificationCenter.current().add(request) { error in
                if error != nil {
                    print("알람 스케쥴링 실패")
                } else {
                    print("알람 스케줄링 성공")
                }
            }
        }
    }

여기서 주목할 점은 3가지다. content, trigger, request

  • content
    알림이 울릴 때, 구성할 내용, 사운드 등이 포함된다. 알림 내용이 터무니 없거나, 근거가 없으면 배포 심사에 reject을 당할 수 있기 때문에 성의껏 써준다.
  • trigger
    지정한 시간에 알람이 작동하도록 만들어주는 말그대로 트리거다. 거기에 반복할 건지를 추가할 수 있다.
  • request
    최종적으로 지금까지 지정한 데이터들을 바탕으로 알람이 울리도록 해주고, identifier가 매우 중요하다. 알람은 고유의 identifier가 있는데, 이 identifier가 알람들을 구별해준다.

여기서 반복된 요일의 갯수에 따라 식별자의 갯수가 결정된다. 이 부분이 처음 구현할 때, 많이 헷갈렸다. 하나의 알람 당 하나의 식별자 인 줄 알았지만, 요일별로 식별자 값이 달라져야한다는 걸 프린트문으로 디버깅을 하면서 알게 되었다. 그래서, 식별자를 배열의 index값으로 구별했다.

알람 수정

사용자가 알람을 초기 설정해놓고, 사용 중에 시간이나 요일을 수정하게 될 경우 로직을 어떻게 구현할 지에 대한 고민이 빠졌다. 사실 생성 기능을 구현한 것만으로 기뻐서 뒤늦게야 기능이 부족하다는 것을 알아차릴 수 있었다. 가장 쉬운 방법이자, 효과적인 방법이 번뜩 떠올랐다. 사용자는 수정을 한다고 느끼지만, 실제로는 삭제와 생성이 반복되게 구성하면 된다는 것이다. 그러면, 데이터를 복잡하게 관리해 줄 필요도 없었고, 코드도 간단하게 줄여준다.

    private func updateAndRescheduleNotification() {
        // 기존의 모든 알림 취소
        UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
        
        scheduleNotification() // 새로운 설정으로 알림을 재스케줄링하는 메서드 호출
    }

View 리셋

알림 기능은 제대로 구현이 되었고, 지정된 시간, 반복된 요일에 잘 울린다. 다만, VC를 나갔다 들어오면, 켜져있던 스위치의 알람이 꺼져있게 되고 리셋이 되는 것이다. 문제는 알람은 On상태인데 스위치만 OFF가 되는 것이다. 해결 방법을 찾아야겠다.

profile
I am 개발해요

0개의 댓글