본문 바로가기

학습 노트/iOS (2021)

137. Actionable Notification

Actionable Notification

사용자는 Notification이 도착했을 때 몇 가지 옵션 중 하나를 선택할 수 있다.

  • Normal Dismiss Action
    무시해서 자동으로 사라지게 하거나 미리보기 상태에서 Swipe로 직접 사라지게 할 수 있다.
  • Custom Dismiss Action
    Notification Center의 닫기 버튼을 눌러 사라지게 할 수 있다.
  • Default Action
    Banner를 터치해 앱을 실행할 수 있다.
  • Custom Action
    직접 Custom Action을 추가할 수 있고, Banner 아래쪽에 버튼 형태로 추가된다.
    이를 선택하면 연관된 코드가 Background에서 실행된다.
    Default Action과는 달리 앱이 Forground에서 실행되지는 않는다.

Notification을 받는 즉시 필요한 Action을 실행하고, 이전 화면으로 바로 돌아갈 수 있어 시간을 절약할 수 있다.

Custom Action을 추가하려면 먼저 Category를 생성하고 Category에 Action을 추가해야 한다.
앱 시작 시점에 User Notification Center에 Category를 등록해야 한다.
이때 Category와 Action은 문자열 식별자로 구분한다.
Local Notification을 예약할 때 연관된 Category 식별자를 설정하면
해당 Category에 추가되어있는 Action이 Notification Banner와 함께 표시된다.

사용할 Scene은 위와 같고

//
//  ActionableNotificationViewController.swift
//  Local Notification Practice
//
//  Created by Martin.Q on 2021/11/24.
//

import UIKit

class ActionableNotificationViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    
    @IBAction func schedule(_ sender: Any) {
        let content = UNMutableNotificationContent()
        content.title = "Hello"
        content.body = "Shared some photo"
        content.sound = UNNotificationSound.default
        
        guard let url = Bundle.main.url(forResource: "hello", withExtension: "png") else {
            return
        }
        
        guard let imageAttachment = try? UNNotificationAttachment(identifier: "logo-image", url: url, options: nil) else {
            return
        }
        
        content.attachments = [imageAttachment]
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
        let request = UNNotificationRequest(identifier: "Image Attachment", content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

연결된 클래스는 위와 같다.

Local Notification에 Like Action과 Dislike Action을 추가하고
선택에 따라 적정한 이미지를 출력해 본다.

struct ActionIdentifier {
	static let like = "ACTION_LIKE"
	static let dislike = "ACTION_DISLIKE"
	private init() {}
}

struct CategoryIdentifier {
	static let imagePosting = "CATEGORY_IMAGE_POSTING"
	private init() {}
}

먼저 식별자를 선언한다.
메서드에 문자열을 그대로 전달하는 것도 가능하지만 이렇게 구조체를 통해 선언해 두면
오타로 인한 오류도 방지할 수 있고, 코드의 가독성도 좋아진다.

class AppDelegate: UIResponder, UIApplicationDelegate {
	
	func setupCategory() {
		
	}

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
			if granted {
				UNUserNotificationCenter.current().delegate = self
			}
			
			print("granted \(granted)")
		}
		return true
	}
}

이어서 Category를 추가한다.
AppDelegate 파일에서 진행한다.

새로운 메서드를 생성하고 이 안에서 Category를 등록한다.

struct ActionIdentifier {
	static let like = "ACTION_LIKE"
	static let dislike = "ACTION_DISLIKE"
	private init() {}
}

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

	func setupCategory() {
		let likeAction = UNNotificationAction(identifier: , title: , options: )
	}

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
			if granted {
				UNUserNotificationCenter.current().delegate = self
			}
			
			print("granted \(granted)")
		}
		return true
	}
}

Action을 생성하기 위해서는 UNNotificationAction 메소드를 사용한다.

func setupCategory() {
	let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
	let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
}

첫 번째 파라미터로 식별자를 전달한다.
두 번째 파라미터로 버튼에 표시할 title을 전달한다.
세 번째 파라미터로 Action을 표시하는 방식과 동작 방식을 지정할 수 있다.
지금은 비워둔다.

func setupCategory() {
	let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
	let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
	
	let imagePostingCategory = UNNotificationCategory(identifier: CategoryIdentifier.imagePosting, actions: [likeAction, dislikeAction], intentIdentifiers: [], options: [])
}

생성한 Action들을 Category에 추가한다.
UNNotificationCategory 메서드를 사용한다.

첫 번째 파라미터로 식별자를 전달한다.
두 번째 파라미터로 표시할 Action을 배열로 전달한다.
세 번째 파라미터는 Siri와 관련 있다. 실습에서는 비워둔다.
네 번째 파라미터도 지금은 비워둔다.

func setupCategory() {
	let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
	let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
	
	let imagePostingCategory = UNNotificationCategory(identifier: CategoryIdentifier.imagePosting, actions: [likeAction, dislikeAction], intentIdentifiers: [], options: [])
	
	UNUserNotificationCenter.current().setNotificationCategories([imagePostingCategory])
}

카테고리를 Notification Center에 등록한다.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
	UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
		if granted {
			self.setupCategory()
			UNUserNotificationCenter.current().delegate = self
		}
		
		print("granted \(granted)")
	}
	return true
}

마지막으로 해당 메서드가 초기화 시점에 호출되도록 코드를 수정한다.

@IBAction func schedule(_ sender: Any) {
	let content = UNMutableNotificationContent()
	content.title = "Hello"
	content.body = "Shared some photo"
	content.sound = UNNotificationSound.default
	
	content.categoryIdentifier = CategoryIdentifier.imagePosting
	
	guard let url = Bundle.main.url(forResource: "hello", withExtension: "png") else {
		return
	}
	
	guard let imageAttachment = try? UNNotificationAttachment(identifier: "logo-image", url: url, options: nil) else {
		return
	}
		
	content.attachments = [imageAttachment]
	
	let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
	let request = UNNotificationRequest(identifier: "Image Attachment", content: content, trigger: trigger)
	UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}

다시 원래의 클래스로 돌아가 Category Identifier를 설정한다.

이렇게 되면 Banner의 아래쪽에 Category에 포함된 Action이 표시되게 된다.
또한 Action을 선택하면 앱이 실행되는 대신 스프링보드로 돌아간다.
이때 Delegate 메서드가 호출된다.

//
//  AppDelegate.swift
//  Local Notification
//
//  Created by Martin.Q on 2021/11/23.
//

import UIKit
import UserNotifications

struct ActionIdentifier {
	static let like = "ACTION_LIKE"
	static let dislike = "ACTION_DISLIKE"
	private init() {}
}

struct CategoryIdentifier {
	static let imagePosting = "CATEGORY_IMAGE_POSTING"
	private init() {}
}

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

	func setupCategory() {
		let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
		let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
		
		let imagePostingCategory = UNNotificationCategory(identifier: CategoryIdentifier.imagePosting, actions: [likeAction, dislikeAction], intentIdentifiers: [], options: [])
		
		UNUserNotificationCenter.current().setNotificationCategories([imagePostingCategory])
	}

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
			if granted {
				self.setupCategory()
				UNUserNotificationCenter.current().delegate = self
			}
			
			print("granted \(granted)")
		}
		return true
	}
}

@available(iOS 14.0, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
	func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
		let content = notification.request.content
		let trigger = notification.request.trigger
		
		completionHandler([UNNotificationPresentationOptions.banner])
	}
	
	func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
		let content = response.notification.request.content
		let trigger = response.notification.request.trigger
		
		completionHandler()
	}
}

호출되는 메서드는 AppDelegate의 extension에 구현되어있는
userNotificationCenter(didReceive:withCompletionHandler:) 메서드이다.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
	let content = response.notification.request.content
	let trigger = response.notification.request.trigger
	
	switch response.actionIdentifier {
	case ActionIdentifier.like:
		print("like")
	case ActionIdentifier.dislike:
		print("dislike")
	default:
		print("Something else")
	}
	
	completionHandler()
}

해당 메서드의 response 파라미터를 통해 Action Identifier 속성에 접근해 어떤 Action을 선택했는지 확인할 수 있다.

선택한 Action에 맞게 Switch 문에서 분기하여 로그를 출력한다.
이제 알맞게 분기하는 것을 확인했으니 적절한 동작을 구현하면 된다.

@available(iOS 14.0, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
	func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
		let content = notification.request.content
		let trigger = notification.request.trigger
		
		completionHandler([UNNotificationPresentationOptions.banner])
	}
	
	func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
		let content = response.notification.request.content
		let trigger = response.notification.request.trigger
		
		switch response.actionIdentifier {
		case ActionIdentifier.like:
			print("like")
		case ActionIdentifier.dislike:
			print("dislike")
		default:
			print("Something else")
		}
		
		UserDefaults.standard.set(response.actionIdentifier, forKey: "userSelection")
		UserDefaults.standard.synchronize()
		
		completionHandler()
	}
}

해당 Switch문 자체에서 Scene에 접근해 Image View를 업데이트하는 것은 불가능하다.
해당 메서드가 호출되는 시점에 Scene이 존재하는지 불분명하기 때문이다.
따라서 구현에 필요한 데이터를 저장해 두고, 화면에 진입하는 시점이나 앱의 실행상태가 변할 때
업데이트하거나 기능을 수행하도록 구현해야 한다.

실습에서는 UserDefaults에 저장한다.

//
//  ActionableNotificationViewController.swift
//  Local Notification Practice
//
//  Created by Martin.Q on 2021/11/24.
//

import UIKit

class ActionableNotificationViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    
    @IBAction func schedule(_ sender: Any) {
        let content = UNMutableNotificationContent()
        content.title = "Hello"
        content.body = "Shared some photo"
        content.sound = UNNotificationSound.default
		
		content.categoryIdentifier = CategoryIdentifier.imagePosting
        
        guard let url = Bundle.main.url(forResource: "hello", withExtension: "png") else {
            return
        }
        
        guard let imageAttachment = try? UNNotificationAttachment(identifier: "logo-image", url: url, options: nil) else {
            return
        }
        
        content.attachments = [imageAttachment]
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
        let request = UNNotificationRequest(identifier: "Image Attachment", content: content, trigger: trigger)
		UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
    }
	
	func updateSelection() {
		switch UserDefaults.standard.string(forKey: "userSelection") {
		case .some(ActionIdentifier.like):
			imageView.image = UIImage(systemName: "hand.thumbsup")
		case .some(ActionIdentifier.dislike):
			imageView.image = UIImage(systemName: "hand.thumbsdown")
		default:
			imageView.image = UIImage(named: "dummyimage")
		}
	}
    

    override func viewDidLoad() {
        super.viewDidLoad()
    }
	
	override func viewWillAppear(_ animated: Bool) {
		super.viewWillAppear(animated)
		updateSelection()
	}

}

다시 클래스로 돌아와 새로운 메서드를 작성한다.
해당 메서드는 UserDefaults에 저장된 값을 사용해 적절한 이미지로 Image View를 업데이트하도록 구현했다.
이후 viewWillAppear 메서드에서 Scene이 표시되기 직전에 호출하도록 한다.

결과를 확인해 보면 Action을 선택하고 다시 앱으로 복귀했을 때는 Image View가 바뀌지 않는다.
이는 앱에 재진입했을 때 viewWillAppear 메서드가 호출되지 않기 때문으로,
이전 Scene에 진입했다가 다시 돌아오면 정상적으로 업데이트된다.
만약 다시 앱을 복귀했을 때 Actionable Notification Scene이 아닌 다른 Scene이라면 문제가 없지만,
이외의 경우에는 문제가 된다.

//
//  ActionableNotificationViewController.swift
//  Local Notification Practice
//
//  Created by Martin.Q on 2021/11/24.
//

import UIKit

class ActionableNotificationViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    
    @IBAction func schedule(_ sender: Any) {
        let content = UNMutableNotificationContent()
        content.title = "Hello"
        content.body = "Shared some photo"
        content.sound = UNNotificationSound.default
		
		content.categoryIdentifier = CategoryIdentifier.imagePosting
        
        guard let url = Bundle.main.url(forResource: "hello", withExtension: "png") else {
            return
        }
        
        guard let imageAttachment = try? UNNotificationAttachment(identifier: "logo-image", url: url, options: nil) else {
            return
        }
        
        content.attachments = [imageAttachment]
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
        let request = UNNotificationRequest(identifier: "Image Attachment", content: content, trigger: trigger)
		UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
    }
	
	@objc func updateSelection() {
		switch UserDefaults.standard.string(forKey: "userSelection") {
		case .some(ActionIdentifier.like):
			imageView.image = UIImage(systemName: "hand.thumbsup")
		case .some(ActionIdentifier.dislike):
			imageView.image = UIImage(systemName: "hand.thumbsdown")
		default:
			imageView.image = UIImage(named: "dummyimage")
		}
	}
    

    override func viewDidLoad() {
        super.viewDidLoad()
		
		NotificationCenter.default.addObserver(self, selector: #selector(updateSelection), name: UIApplication.didBecomeActiveNotification, object: nil)
    }
	
	override func viewWillAppear(_ animated: Bool) {
		super.viewWillAppear(animated)
		updateSelection()
	}

}

 

didBecomeActiveNotification에 대한 Observer를 추가하고,
업데이트 메서드를 호출하도록 구현한다.
이때 selector에 전달할 수 있도록 메서드에 objc 키워드를 추가한다.

이제는 앱으로 복귀했을 때에도 Image View가 업데이트된다.

Action을 처리하는 코드를 구현할 때에는 앱이 Background에 있을 때와,
아예 실행 중이 아닐 때를 고려해야 한다.

Banner에는 항상 Title과 Body가 표시된다.
이는 미리 보기 설정에 영향을 받은 결과이다.

설정 앱의 Show Previews 설정을 Never로 변경하면 오른쪽 사진처럼 Title과 Body가 표시되지 않는다.
Category 설정을 활용하면 이러한 설정 값에 관계없이 표시하는 것이 가능하다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

	func setupCategory() {
		let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
		let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
		
		let imagePostingCategory = UNNotificationCategory(identifier: CategoryIdentifier.imagePosting, actions: [likeAction, dislikeAction], intentIdentifiers: [], options: [.hiddenPreviewsShowTitle])
		
		UNUserNotificationCenter.current().setNotificationCategories([imagePostingCategory])
	}

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
			if granted {
				self.setupCategory()
				UNUserNotificationCenter.current().delegate = self
			}
			
			print("granted \(granted)")
		}
		return true
	}
}

AppDelegate에서 Category의 options에 hiddenPreviewShowTitle 옵션을 추가한다.
해당 옵션은 iOS 11에서 도입된 옵션이다.
만약 hiddenPreviewShowSubtitle을 추가한다면 Subtitle을 표시할 수도 있다.

옵션에 따라 Title이 표시된다.
이러한 옵션들은 사용자에게 반드시 알려야 하는 Category에 한해 제한적으로 사용해야 한다.
그 외에는 사용자의 선택을 존중하는 것이 바람직하다.

Notification이 전달됐을 때 Swipe 해 Banner을 사라지게 하거나 빈 공간을 터치하는 것은 Normal Dismiss Action이다.
하지만 iOS Notification Center에서 Swipe해 Clear 버튼을 통해 Notification을 삭제하는 것은 Custom Dismiss Action이다.
카테고리를 전달할 때 Custom Dismiss Action을 option으로 전달하면
Clear 버튼 이벤트를 다른 Action과 동일한 방식으로 처리할 수 있다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

	func setupCategory() {
		let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
		let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
		
		var options = UNNotificationCategoryOptions.customDismissAction
		options.insert(.hiddenPreviewsShowTitle)
		let imagePostingCategory = UNNotificationCategory(identifier: CategoryIdentifier.imagePosting, actions: [likeAction, dislikeAction], intentIdentifiers: [], options: options)
		
		UNUserNotificationCenter.current().setNotificationCategories([imagePostingCategory])
	}

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
			if granted {
				self.setupCategory()
				UNUserNotificationCenter.current().delegate = self
			}
			
			print("granted \(granted)")
		}
		return true
	}
}

@available(iOS 14.0, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
	func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
		let content = notification.request.content
		let trigger = notification.request.trigger
		
		completionHandler([UNNotificationPresentationOptions.banner])
	}
	
	func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
		let content = response.notification.request.content
		let trigger = response.notification.request.trigger
		
		switch response.actionIdentifier {
		case ActionIdentifier.like:
			print("like")
		case ActionIdentifier.dislike:
			print("dislike")
		case UNNotificationDismissActionIdentifier:
			print("Custom Dismiss")
		case UNNotificationDefaultActionIdentifier:
			print("Default Action")
		default:
			print("Something else")
		}
		
		UserDefaults.standard.set(response.actionIdentifier, forKey: "userSelection")
		UserDefaults.standard.synchronize()
		
		completionHandler()
	}
}

option을 저장할 인스턴스를 생성한 다음, UNNotificationCategoryOptions의
customDismissAction을 저장해 options 파라미터에 전달한다.

이후 actionIdentifier을 통해 Switch 문에서 분기하는 것이 가능하다.
Custom Dismiss Action의 식별자는 UNNotificationDismissActionIdentifier이고,
Banner를 터치해 앱으로 이동하는 Default Action의 식별자는 UNNotificationDefaultActionIdentifier이다.
식별자에 따라 분기하여 로그를 출력하도록 구현한다.

각각의 동작에 맞게 로그가 출력됐다.

이 둘은 모두 기본 Action이지만 차이가 존재한다.
Custom Dismiss는 Category에 CustomDismissActionOption을 적용한 경우에만 처리할 수 있지만,
Default Action은 별도의 과정 없이 바로 처리할 수 있다.

//
//  AppDelegate.swift
//  Local Notification
//
//  Created by Martin.Q on 2021/11/23.
//

import UIKit
import UserNotifications

struct ActionIdentifier {
	static let like = "ACTION_LIKE"
	static let dislike = "ACTION_DISLIKE"
	static let unfollow = "ACTION_UNFOLLOW"
	static let setting = "ACTION_SETTING"
	private init() {}
}

struct CategoryIdentifier {
	static let imagePosting = "CATEGORY_IMAGE_POSTING"
	private init() {}
}

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

	func setupCategory() {
		let likeAction = UNNotificationAction(identifier: ActionIdentifier.like, title: "Like", options: [])
		let dislikeAction = UNNotificationAction(identifier: ActionIdentifier.dislike, title: "Dislike", options: [])
		let unfollowAction = UNNotificationAction(identifier: ActionIdentifier.unfollow, title: "Unfollow", options: [.authenticationRequired, .destructive])
		let settingAction = UNNotificationAction(identifier: ActionIdentifier.setting, title: "Setting", options: [.foreground])
		
		var options = UNNotificationCategoryOptions.customDismissAction
		options.insert(.hiddenPreviewsShowTitle)
		let imagePostingCategory = UNNotificationCategory(identifier: CategoryIdentifier.imagePosting, actions: [likeAction, dislikeAction, unfollowAction, settingAction], intentIdentifiers: [], options: options)
		
		UNUserNotificationCenter.current().setNotificationCategories([imagePostingCategory])
	}

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
			if granted {
				self.setupCategory()
				UNUserNotificationCenter.current().delegate = self
			}
			
			print("granted \(granted)")
		}
		return true
	}
}

@available(iOS 14.0, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
	func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
		let content = notification.request.content
		let trigger = notification.request.trigger
		
		completionHandler([UNNotificationPresentationOptions.banner])
	}
	
	func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
		let content = response.notification.request.content
		let trigger = response.notification.request.trigger
		
		switch response.actionIdentifier {
		case ActionIdentifier.like:
			print("like")
		case ActionIdentifier.dislike:
			print("dislike")
		case UNNotificationDismissActionIdentifier:
			print("Custom Dismiss")
		case UNNotificationDefaultActionIdentifier:
			print("Default Action")
		default:
			print("Something else")
		}
		
		UserDefaults.standard.set(response.actionIdentifier, forKey: "userSelection")
		UserDefaults.standard.synchronize()
		
		completionHandler()
	}
}

이번에는 두 개의 Action을 추가했다.

SNS의 Unfollow 기능은 누구든지 사용할 수 있으면 곤란한 기능이다.
따라서 option에 authenicationRequired를 추가한다.
해당 옵션은 기기가 잠금 상태인 Action을 표시하지 않는다.
또한 desruvtice 옵션을 추가해 다른 Action들과 시작적으로 차별화했다.

setting은 사용자가 알림 설정을 다시 할 수 있도록 설정 창으로 이동시켜야 한다.
Action은 기본적으로 Background에서 코드를 실행하고 종료하지만 이번엔 앱을 실제로 실행해야 한다.
foreground 옵션을 추가해 앱을 실행하도록 구현했다.

의도한 대로 동작하는 것을 확인할 수 있다.