[Swift] UNUserNotificationCenter - 1
원래대로라면 Apollo iOS 에 대한 6번째 글을 쓰는 것이 맞지만 너무 Apollo iOS 에 대해서만 쓴 것 같기도 하고, 내가 살짝 질리기도 해서 (ㅋㅋ) 이번에는 한숨 돌릴겸 다른 주제를 가지고 와봤다 !! 한숨 돌릴겸이라고 하긴 했지만 사실 회사에서 얼마 전에 나를 제법 힘들게 했던 UNUserNotificationCenter
에 대해 공부했던 것을 정리해 놓기 위한 글이라고 할 수 있겠다 ..! 어떻게 보면 나를 힘들게 했던 아이가 UNUserNotificationCenter
가 아니라 async
/ await
라고 해야 더 맞는 말 같기는 하지만 🥹 그래도 UNUserNotificationCenter
에 대해서도 이번에 처음 알게 되었기 때문에 일단 내가 나중에 다시 보기 위해서라도 글을 써봐야겠다 ㅎ 그럼 스타트 ~!
UNUserNotificationCenter
는 앱이나 앱 확장자에서 알림과 관련된 활동을 관리하는 핵심 객체이고, 알림과 관련된 활동이라고 한다면
- 경고, 소리, 아이콘 뱃지를 통해 사용자와 상호 작용하기 위해 권한을 요청하는 경우
- 앱이 지원하는 알림의 타입과 시스템이 그 알림들을 전달할 때 사용자가 할 행동을 선언하는 경우
- 앱에서 알림이 발송되는 시점을 정하는 경우
- 시스템이 Apple Push Notification service (APNs) 를 통해 전달한 원격 알림을 처리하는 경우
- 이미 발송된 상태여서 시스템이 Notification Center 에 보여주는 알림을 관리하는 경우
- 사용자 정의 알림 타입과 관련된 사용자 행동을 다루는 경우
- 앱에서 알림과 관련된 설정을 가져오는 경우
정도가 있을 수 있다고 한다. 하지만 이렇게 텍스트로만 적으니까 이 7가지가 정확히 무엇을 말하는지 와닿지 않았던 나는 결국 이 중에 몇 가지에 대해 간단히 알아봤다.
경고, 소리, 아이콘 뱃지를 통해 사용자와 상호 작용하기 위해 권한을 요청하는 경우
는 아래에 있는 스크린샷 하나로 설명 가능할 것 같다 ㅋㅋ
iOS 앱을 쓰다보면 이렇게 생긴 경고창을 마주한 적이 한번쯤은 있을 것이다. 이 경고창은 알림이 들어왔을 때 경고를 띄우거나 소리를 재생하거나 앱 아이콘에 뱃지를 표시하는 동작을 사용자에게 허락 받기 위한 장치라고 보면 된다. 참고로 이러한 동작은 앱이 실행 중인 경우에는 발생하지 않고, 앱이 실행 중이지 않거나 백그라운드에 있을 때 발생한다. 이 알림은 사용자가 앱을 사용하고 있지 않을 때 사용자에게 어떠한 정보를 알려주는 역할을 하기 때문에 매우 유용하지만, 사람마다 취향 (?) 이 다 다르기 때문에 이 알림을 받고 싶어 하지 않는 사용자가 분명 있을 수 있을 것이다. 이 유용한 친구가 누구에게는 귀찮음의 대상이 될 수 있는 .. 그런 슬픈 .. 이야기 .. ㅎ 아무튼 알림이 방해의 요소가 될 수 있다고 생각했기 때문에 공식 문서는 알림을 표시하기 전에 사용자에게 위의 스크린샷과 같은 경고창을 띄워서 사용자의 의사를 반드시 물어 보아야 한다고 강조하고 있다.
앱이 지원하는 알림의 타입과 시스템이 그 알림들을 전달할 때 사용자가 할 행동을 선언하는 경우
도 아래 스크린샷으로 설명할 수 있을 것 같다.
iOS 앱을 쓰다보면 휴대폰 상단에 뜨는 알림을 꾹 눌렀을 때 위의 스크린샷처럼 해당 알림에 대해 어떠한 특정 행동을 할 수 있도록 버튼이 나타나는 경우가 있는데, 이때 버튼이 1개일 수도 있고 위의 스크린샷처럼 버튼이 2개일 수도 있다. (1개 이상의 버튼으로 구성되어 있다.) 이 기능은 알림을 전송한 앱을 직접 실행하지 않고도 그 알림에 응답할 수 있도록 해주는 역할을 하고, 행동할 수 있는 알림이라는 뜻에서 Actionable notification 이라는 이름을 가지고 있다. Actionable notification 에 나타나는 버튼 중 하나를 누르면 그 알림을 전송한 앱으로 해당 버튼을 눌렀다는 사실을 알리고, 백그라운드에서 그 행동에 대한 처리가 이루어진다. 참고로 Actionable notification 이 아닌 알림인 경우에는 아무리 꾹 눌러도 아무런 반응이 없고, 그저 그 알림을 보낸 앱으로 접속할 수만 있다. 이 내용은 위의 7가지 중에서 앱이 지원하는 알림의 타입과 시스템이 그 알림들을 전달할 때 사용자가 할 행동을 선언하는 경우 말고도 시스템이 Apple Push Notification service (APNs) 를 통해 전달한 원격 알림을 처리하는 경우와 사용자 정의 알림 타입과 관련된 사용자 행동을 다루는 경우에도 해당하기 때문에 따로 이 2가지에 대해서 다루지는 않겠다 ..!
앱에서 알림이 발송되는 시점을 정하는 경우
는 사용자에게 전송할 알림을 생성하고 예약해 두는 것이다. 예를 들면 백그라운드에 있는 앱에서 특정 동작이 완료되면 알림을 띄우고 싶은 경우 정도가 있을 수 있겠다. 이렇듯이 시스템에게 특정 시간이나 장소에서 알림을 전송하라는 요청을 할 수 있는데 만약 앱이 실행 중이지 않거나 백그라운드에 있을 때 알림이 전송되면 시스템은 사용자와 상호 작용하고, 포그라운드에 있을 때 알림이 전송되면 시스템은 앱으로 알림을 전송해서 그 알림을 다룰 수 있도록 한다.
나머지 경우에 대해서는 따로 내용이 있지 않아서 일단 여기까지만 알아보도록 하고 ㅎ 알림 관련 활동이 이렇다는 것은 알겠고, 그럼 이 알림을 다루려면 어떻게 해야 되는데 ? 🤔
들어오는 알림과 알림 관련 행동을 다루기 위해서는 UNUserNotificationCenterDelegate
프로토콜을 채택하는 객체를 만들고, delegate
프로퍼티에 그 객체를 할당하면 된다. 너무 당연한 소리지만 UNUserNotificationCenterDelegate
와 상호 작용을 하는 어떤 일을 수행하기 위해서는 UNUserNotificationCenterDelegate
프로토콜을 채택하는 객체를 UNUserNotificationCenter
에 있는 delegate
프로퍼티에 할당해야 하고, 앱이 시작되기 전에 할당되어 있어야 하기 때문에 AppDelegate
라는 class
안에 있는 application(_:willFinishLaunchingWithOptions:)
나 application(_:didFinishLaunchingWithOptions:)
에 delegate
할당 로직을 써주면 된다. 여기서 주의할 점은 앱의 어느 Thread 에서든 이 객체를 공유해서 써야 한다는 것이다. delegate
프로퍼티 얘기가 나오니까 UNUserNotificationCenter
안에 있는 프로퍼티들과 메소드들을 알아보는 것이 좋겠다는 생각이 들어서 코드를 까보았더니
@available(iOS 10.0, *)
open class UNUserNotificationCenter : NSObject {
weak open var delegate: UNUserNotificationCenterDelegate?
open var supportsContentExtensions: Bool { get }
open class func current() -> UNUserNotificationCenter
open func requestAuthorization(options: UNAuthorizationOptions = [], completionHandler: @escaping (Bool, Error?) -> Void)
open func requestAuthorization(options: UNAuthorizationOptions = []) async throws -> Bool
open func setNotificationCategories(_ categories: Set<UNNotificationCategory>)
open func getNotificationCategories(completionHandler: @escaping (Set<UNNotificationCategory>) -> Void)
open func notificationCategories() async -> Set<UNNotificationCategory>
open func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Void)
open func notificationSettings() async -> UNNotificationSettings
open func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil)
open func add(_ request: UNNotificationRequest) async throws
open func getPendingNotificationRequests(completionHandler: @escaping ([UNNotificationRequest]) -> Void)
open func pendingNotificationRequests() async -> [UNNotificationRequest]
open func removePendingNotificationRequests(withIdentifiers identifiers: [String])
open func removeAllPendingNotificationRequests()
open func getDeliveredNotifications(completionHandler: @escaping ([UNNotification]) -> Void)
open func deliveredNotifications() async -> [UNNotification]
open func removeDeliveredNotifications(withIdentifiers identifiers: [String])
open func removeAllDeliveredNotifications()
@available(iOS 16.0, *)
open func setBadgeCount(_ newBadgeCount: Int, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil)
@available(iOS 16.0, *)
open func setBadgeCount(_ newBadgeCount: Int) async throws
}
이렇게 구성되어 있는 것을 볼 수 있었고, 가장 위에
weak open var delegate: UNUserNotificationCenterDelegate?
이렇게 delegate
프로퍼티가 떡하니 자리 잡고 있었다 ㅋㅋ 이 아이는 Notification Center 의 위임자라고 할 수 있고, 사용자 정의 행동에 응답하기 위해서 또는 앱이 포그라운드에 있을 때 들어온 알림을 처리하기 위해서 사용한다.
그 다음에 있는
open var supportsContentExtensions: Bool { get }
이 아이는 기기가 알림 내용 확장자를 지원하는지 여부를 Bool
타입으로 나타내는데 기기가 알림 내용 확장자를 지원하는 경우에는 true
를 반환하고, 지원하지 않는 경우에는 false
를 반환한다. 알림 내용 확장자 .. 가 뭐지 ? 싶어서 찾아보니까 알림의 생김새를 커스텀할 수 있도록 해주는 아이라고 한다. 이 내용에 대해 더 깊게 다루면 이 글이 너 ~ 무 길어질 것 같아서 이 내용은 나중에 따로 다뤄 보는 것으로 하고, 혹시 모르니까 공식 문서 링크만 두고 일단 도망 ..!
그 다음에 있는
open class func current() -> UNUserNotificationCenter
이 아이는 앱의 Notification Center 를 반환하는 역할을 하고, 그렇기 때문에 UNUserNotificationCenter
를 반환한다. 아까 위에서 앱의 어느 Thread 에서든 UNUserNotificationCenter
객체를 공유해서 써야 한다는 점을 주의해야 한다고 했었는데, 이 내용에 해당한다고 할 수 있겠다. 객체를 공유한다는 것은 결국 Singleton 으로 구성해야 한다는 뜻이고, 이 current()
를 사용해서 Singleton 을 구현하라는 뜻이다. 일반적으로는 shared()
형태로 Singleton 을 구성하는데, 정확한 이유는 모르겠지만 UNUserNotificationCenter
의 경우에는 shared()
대신 current()
를 사용했다. 다 필요없고 여기서 제일 중요한 것은 Notification Center 객체를 사용할 때 무조건 current()
를 사용해야 하고, UNUserNotificationCenter
라는 class
를 직접 사용해서 객체를 만드는 일은 절대 있어서는 안 된다는 것이다 !! Notification Center 객체를 사용할 때는 무조건
UNUserNotificationCenter.current()
형태로 써야 한다는 뜻 !!
그 다음에 있는
open func requestAuthorization(options: UNAuthorizationOptions = [], completionHandler: @escaping (Bool, Error?) -> Void)
이 메소드는 알림을 허용하기 위해 사용자에게 권한을 요청하는 역할을 한다. 인자들 중에서 options
에는 사용자에게 요청할 권한을 나열하면 되는데, 이때 나열 가능한 옵션으로는 badge
, sound
, alert
, carPlay
, criticalAlert
, providesAppNotificationSettings
, provisional
이 있다. 이 옵션들은 모두 UNAuthorizationOptions
타입의 변수이고, 이 내용은 UNUserNotificationCenter.h 에 있는
@available(iOS 10.0, *)
public struct UNAuthorizationOptions : OptionSet, @unchecked Sendable {
public init(rawValue: UInt)
public static var badge: UNAuthorizationOptions { get }
public static var sound: UNAuthorizationOptions { get }
public static var alert: UNAuthorizationOptions { get }
public static var carPlay: UNAuthorizationOptions { get }
@available(iOS 12.0, *)
public static var criticalAlert: UNAuthorizationOptions { get }
@available(iOS 12.0, *)
public static var providesAppNotificationSettings: UNAuthorizationOptions { get }
@available(iOS 12.0, *)
public static var provisional: UNAuthorizationOptions { get }
@available(iOS, introduced: 13.0, deprecated: 15.0, message: "Announcement authorization is always included")
public static var announcement: UNAuthorizationOptions { get }
@available(iOS, introduced: 15.0, deprecated: 15.0, message: "Use time-sensitive entitlement")
public static var timeSensitive: UNAuthorizationOptions { get }
}
UNAuthorizationOptions
블록에서 확인할 수 있다. 참고로 위에 나열한 7가지 옵션 이외에도 announcement
와 timeSensitive
라는 옵션이 더 있는데 이 옵션들은 iOS 15.0 을 기점으로 지원이 종료되었기 때문에 제외했지만 announcement
는 AirPods 를 통해 Siri 가 메세지를 자동으로 들을 수 있도록 할 경우에 사용한다는 것만 알아두면 될 것 같다. 위의 7가지 옵션들에 대해 간략히 보고 넘어가자면
badge
는 알림이 전송되었을 때 앱 아이콘에 뱃지를 적용할 경우에 사용한다.
sound
는 알림이 전송되었을 때 소리를 재생할 경우에 사용한다.
alert
는 알림이 전송되었을 때 경고를 띄울 경우에 사용한다.
carPlay
는 알림이 전송되었을 때 CarPlay 환경에서 알림을 나타낼 경우에 사용한다.
criticalAlert
는 긴급 알림이 전송되었을 때 소리를 재생할 경우에 사용한다.
providesAppNotificationSettings
는 앱 안에서 발생하는 알림 설정을 위해 버튼을 나타낼 경우에 사용한다.
provisional
은 Notification Center 에게 잠시동안 차단 불가능한 알림을 등록할 경우에 사용한다.
이 정도 ?! 그 다음에 completionHandler
라는 인자값을 넣어야 하는데, 이 인자에는 사용자에게 권한을 요청하고 나서 그 요청에 대한 응답값이 들어온 후에 비동기적으로 실행되는 동작을 넣으면 된다. 이 블록은 백그라운드 Thread 에서 실행되고, 반환값이 없는 대신 granted
와 error
라는 2가지 인자가 있다. granted
는 사용자에게 권한 요청했을 때 사용자가 하나 이상의 권한 옵션에 대해 수락했는지 Bool
타입으로 나타낸 것이고 하나 이상의 권한 옵션에 대해 수락한 경우에는 true
를, 아무 옵션도 수락하지 않은 경우에는 false
를 반환한다. (참고로, 조금 후에 제대로 설명하겠지만 사용자가 수락한 옵션이 무엇인지 알기 위해서는 UNUserNotificationCenter
에 있는 getNotificationSettings(completionHandler:)
를 사용하면 된다.) 만약 권한 요청했을 때 오류가 발생했다면 그 오류가 error
인자에 포함된다. (오류가 발생하지 않았다면 nil
로 반환된다.) 이 메소드를 쓴 예시 코드는
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if granted {
// 사용자가 하나 이상의 권한 옵션에 대해 수락한 경우 실행될 동작
} else if let error = error {
print("Error : \(error.localizedDescription)")
}
}
이런 식으로 구성할 수 있다. 원래대로라면 그 다음으로
open func requestAuthorization(options: UNAuthorizationOptions = []) async throws -> Bool
이 친구에 대해 설명해야 하는 것이 맞지만, 이 친구는 Concurrency 라는 개념에 대해 먼저 공부한 후에 설명해야 하기 때문에 Concurrency 에 대한 글을 먼저 쓰고 나서 설명할 예정이다. 이 메소드뿐만 아니라 UNUserNotificationCenter
에 있는 async
류의 메소드들 모두 ㅎ 맛보기로 얘기하자면 Concurrency 는 Swift 5.5 부터 쓸 수 있게 된 애플의 야심작 (?) 이고, 기존의 completionHandler
에서 발견된 여러 단점을 보완한 개념이라고 보면 된다. 이것도 혹시 모르니까 일단 공식 문서 링크 놔두고 도망 - ㅋㅋ
그 다음으로 볼 메소드는
open func setNotificationCategories(_ categories: Set<UNNotificationCategory>)
이고, 앱이 시작할 때 이 메소드를 호출해서 앱이 지원하는 알림 카테고리를 등록하면 된다. 등록할 알림 카테고리는 categories
인자에 넣으면 되고, 이 인자에 카테고리들을 넣으면 그 카테고리들이 모두 한번에 등록된다. 여기서 주의할 점은 기존에 등록되어 있던 카테고리가 유지된 상태로 categories
인자에 넣은 카테고리가 추가로 등록되는 것이 아니라 대체된다는 것이다. 사실 일반적으로 이 메소드는 한번만 호출되기 때문에 이 내용은 거의 무시해도 될 것 같기도 ..? categories
인자는 UNNotificationCategory
의 집합 타입인데, 알림 인터페이스에 나타나는 행동을 포함한다.
근데 알림 카테고리가 뭐지 ..? 알림 카테고리가 무엇인지 알아보려면UNNotificationCategory
를 들춰 보아야 한다고 해서 UNNotificationCategory
를 들춰 보았더니
@available(iOS 10.0, *)
open class UNNotificationCategory : NSObject, NSCopying, NSSecureCoding {
open var identifier: String { get }
open var actions: [UNNotificationAction] { get }
open var intentIdentifiers: [String] { get }
open var options: UNNotificationCategoryOptions { get }
@available(iOS 11.0, *)
open var hiddenPreviewsBodyPlaceholder: String { get }
@available(iOS 12.0, *)
open var categorySummaryFormat: String { get }
public convenience init(identifier: String, actions: [UNNotificationAction], intentIdentifiers: [String], options: UNNotificationCategoryOptions = [])
@available(iOS 11.0, *)
public convenience init(identifier: String, actions: [UNNotificationAction], intentIdentifiers: [String], hiddenPreviewsBodyPlaceholder: String, options: UNNotificationCategoryOptions = [])
@available(iOS 12.0, *)
public convenience init(identifier: String, actions: [UNNotificationAction], intentIdentifiers: [String], hiddenPreviewsBodyPlaceholder: String?, categorySummaryFormat: String?, options: UNNotificationCategoryOptions = [])
}
이런 식으로 구성되어 있었다.
public convenience init(identifier: String, actions: [UNNotificationAction], intentIdentifiers: [String], options: UNNotificationCategoryOptions = [])
@available(iOS 11.0, *)
public convenience init(identifier: String, actions: [UNNotificationAction], intentIdentifiers: [String], hiddenPreviewsBodyPlaceholder: String, options: UNNotificationCategoryOptions = [])
@available(iOS 12.0, *)
public convenience init(identifier: String, actions: [UNNotificationAction], intentIdentifiers: [String], hiddenPreviewsBodyPlaceholder: String?, categorySummaryFormat: String?, options: UNNotificationCategoryOptions = [])
를 보면 UNNotificationCategory
를 초기화하는 방법에는 총 3가지가 있고, 공통적으로 identifier
, actions
, intentIdentifiers
, options
라는 인자를 가진다.
identifier
에는 알림 형태를 식별하기 위한 문자열을 넣으면 된다.
actions
에는 identifier
에 해당하는 형태의 알림에 응답하기 위해 사용자가 할 행동을 넣으면 된다.
intentIdentifiers
에는 이 카테고리에 있는 알림의 목적 (의도) 을 넣으면 된다.
options
에는 이 형태의 알림을 어떻게 다룰 것인지를 넣으면 된다.
hiddenPreviewsBodyPlaceholder
에는 시스템이 알림 미리보기를 띄우지 못 하는 경우에 띄울 Placeholder 텍스트를 넣으면 된다.
categorySummaryFormat
에는 시스템이 카테고리 알림들을 그룹으로 묶을 때 사용하는 짧은 설명을 넣으면 된다.
시스템은 알림을 위한 경고를 띄울 때 identifier
에 넣은 문자열들 중 하나를 알림 Payload 에서 찾고, identifier
에 넣은 문자열들 중 하나를 찾으면 시스템은 actions
에 넣은 행동을 사용자가 할 수 있는 버튼을 추가한다. 이렇게 추가한 버튼을 누르면 버튼이 눌렸다는 사실을 앱에게 알리는데, 앱을 Foreground 로 끄집어 내지 않고도 앱에게 알릴 수 있다.
그 다음으로 볼
open func getNotificationCategories(completionHandler: @escaping (Set<UNNotificationCategory>) -> Void)
이 친구는 위에서 설명했던 setNotificationCategories
를 가지고 등록했던 알림 카테고리들을 불러오는 역할을 하고, completionHandler
라는 인자를 갖는데 이 인자에는 알림 카테고리들이 반환된 후에 백그라운드 Thread 에서 비동기적으로 실행될 동작을 넣으면 된다. 이 인자에 넣은 코드 블록은 따로 값을 반환하지 않고, UNNotificationCategory
의 집합을 인자로 갖는데 앞서 말했던 setNotificationCategories
를 통해 등록했던 알림 카테고리들이 이 인자에 해당하는 것이다. 만약 아무런 카테고리도 등록하지 않은 상태로 getNotificationCategories
를 호출하면 completionHandler
에 있는 UNNotificationCategory
의 집합 형태인 인자에 공집합이 들어간다. 그 다음에 볼
open func notificationCategories() async -> Set<UNNotificationCategory>
이 친구도 마찬가지로 async
가 붙어있기 때문에 지금은 설명이 어렵지만, 위에서 설명한 getNotificationCategories
의 Concurrency 버전이라는 것만 살짝 던져두고 도망가겠다 ㅋㅋ
이 글을 처음 쓰기 시작할 때까지만 해도 UserNotificationCenter.h 파일에 있는 모든 내용을 다 쓰고 이 글을 마무리하겠다고 생각했는데 생각보다 쓸 내용이 너무 많아서 여기서 끊고 가려고 한다 😅 아마 UNUserNotificationCenter
에 대한 글을 2개 ? 3개 ? 로 나눠서 올리게 되지 않을까 싶은 ..? 그럼 쓸 내용이 아직 너무 많이 남아있기 때문에 얼른 다음 글을 쓰러 고고 🏃🏻♀️💨