[Swift] UNUserNotificationCenter - 1

r1verfuture
24 min readMar 26, 2024

--

원래대로라면 Apollo iOS 에 대한 6번째 글을 쓰는 것이 맞지만 너무 Apollo iOS 에 대해서만 쓴 것 같기도 하고, 내가 살짝 질리기도 해서 (ㅋㅋ) 이번에는 한숨 돌릴겸 다른 주제를 가지고 와봤다 !! 한숨 돌릴겸이라고 하긴 했지만 사실 회사에서 얼마 전에 나를 제법 힘들게 했던 UNUserNotificationCenter 에 대해 공부했던 것을 정리해 놓기 위한 글이라고 할 수 있겠다 ..! 어떻게 보면 나를 힘들게 했던 아이가 UNUserNotificationCenter 가 아니라 async / await 라고 해야 더 맞는 말 같기는 하지만 🥹 그래도 UNUserNotificationCenter 에 대해서도 이번에 처음 알게 되었기 때문에 일단 내가 나중에 다시 보기 위해서라도 글을 써봐야겠다 ㅎ 그럼 스타트 ~!

UNUserNotificationCenter 는 앱이나 앱 확장자에서 알림과 관련된 활동을 관리하는 핵심 객체이고, 알림과 관련된 활동이라고 한다면

  • 경고, 소리, 아이콘 뱃지를 통해 사용자와 상호 작용하기 위해 권한을 요청하는 경우
  • 앱이 지원하는 알림의 타입과 시스템이 그 알림들을 전달할 때 사용자가 할 행동을 선언하는 경우
  • 앱에서 알림이 발송되는 시점을 정하는 경우
  • 시스템이 Apple Push Notification service (APNs) 를 통해 전달한 원격 알림을 처리하는 경우
  • 이미 발송된 상태여서 시스템이 Notification Center 에 보여주는 알림을 관리하는 경우
  • 사용자 정의 알림 타입과 관련된 사용자 행동을 다루는 경우
  • 앱에서 알림과 관련된 설정을 가져오는 경우

정도가 있을 수 있다고 한다. 하지만 이렇게 텍스트로만 적으니까 이 7가지가 정확히 무엇을 말하는지 와닿지 않았던 나는 결국 이 중에 몇 가지에 대해 간단히 알아봤다.

경고, 소리, 아이콘 뱃지를 통해 사용자와 상호 작용하기 위해 권한을 요청하는 경우

는 아래에 있는 스크린샷 하나로 설명 가능할 것 같다 ㅋㅋ

출처 : Apple 공식 문서

iOS 앱을 쓰다보면 이렇게 생긴 경고창을 마주한 적이 한번쯤은 있을 것이다. 이 경고창은 알림이 들어왔을 때 경고를 띄우거나 소리를 재생하거나 앱 아이콘에 뱃지를 표시하는 동작을 사용자에게 허락 받기 위한 장치라고 보면 된다. 참고로 이러한 동작은 앱이 실행 중인 경우에는 발생하지 않고, 앱이 실행 중이지 않거나 백그라운드에 있을 때 발생한다. 이 알림은 사용자가 앱을 사용하고 있지 않을 때 사용자에게 어떠한 정보를 알려주는 역할을 하기 때문에 매우 유용하지만, 사람마다 취향 (?) 이 다 다르기 때문에 이 알림을 받고 싶어 하지 않는 사용자가 분명 있을 수 있을 것이다. 이 유용한 친구가 누구에게는 귀찮음의 대상이 될 수 있는 .. 그런 슬픈 .. 이야기 .. ㅎ 아무튼 알림이 방해의 요소가 될 수 있다고 생각했기 때문에 공식 문서는 알림을 표시하기 전에 사용자에게 위의 스크린샷과 같은 경고창을 띄워서 사용자의 의사를 반드시 물어 보아야 한다고 강조하고 있다.

앱이 지원하는 알림의 타입과 시스템이 그 알림들을 전달할 때 사용자가 할 행동을 선언하는 경우

도 아래 스크린샷으로 설명할 수 있을 것 같다.

출처 : Apple 공식 문서

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가지 옵션 이외에도 announcementtimeSensitive 라는 옵션이 더 있는데 이 옵션들은 iOS 15.0 을 기점으로 지원이 종료되었기 때문에 제외했지만 announcementAirPods 를 통해 Siri 가 메세지를 자동으로 들을 수 있도록 할 경우에 사용한다는 것만 알아두면 될 것 같다. 위의 7가지 옵션들에 대해 간략히 보고 넘어가자면

badge

는 알림이 전송되었을 때 앱 아이콘에 뱃지를 적용할 경우에 사용한다.

sound

는 알림이 전송되었을 때 소리를 재생할 경우에 사용한다.

alert

는 알림이 전송되었을 때 경고를 띄울 경우에 사용한다.

carPlay

는 알림이 전송되었을 때 CarPlay 환경에서 알림을 나타낼 경우에 사용한다.

criticalAlert

는 긴급 알림이 전송되었을 때 소리를 재생할 경우에 사용한다.

providesAppNotificationSettings

는 앱 안에서 발생하는 알림 설정을 위해 버튼을 나타낼 경우에 사용한다.

provisional

Notification Center 에게 잠시동안 차단 불가능한 알림을 등록할 경우에 사용한다.

이 정도 ?! 그 다음에 completionHandler 라는 인자값을 넣어야 하는데, 이 인자에는 사용자에게 권한을 요청하고 나서 그 요청에 대한 응답값이 들어온 후에 비동기적으로 실행되는 동작을 넣으면 된다. 이 블록은 백그라운드 Thread 에서 실행되고, 반환값이 없는 대신 grantederror 라는 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 류의 메소드들 모두 ㅎ 맛보기로 얘기하자면 ConcurrencySwift 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 가 붙어있기 때문에 지금은 설명이 어렵지만, 위에서 설명한 getNotificationCategoriesConcurrency 버전이라는 것만 살짝 던져두고 도망가겠다 ㅋㅋ

이 글을 처음 쓰기 시작할 때까지만 해도 UserNotificationCenter.h 파일에 있는 모든 내용을 다 쓰고 이 글을 마무리하겠다고 생각했는데 생각보다 쓸 내용이 너무 많아서 여기서 끊고 가려고 한다 😅 아마 UNUserNotificationCenter 에 대한 글을 2개 ? 3개 ? 로 나눠서 올리게 되지 않을까 싶은 ..? 그럼 쓸 내용이 아직 너무 많이 남아있기 때문에 얼른 다음 글을 쓰러 고고 🏃🏻‍♀️💨

--

--