macOSアプリで通知バナーを表示

## 概要

- [NSUserNotification](https://developer.apple.com/documentation/foundation/nsusernotification)はmacOS 10.14からDeprecadedになりました。
    - [NSUserNotificationを使って、通知のポップアップを表示する](https://qiita.com/IKEH/items/18c3d203404e9f7bab83#nsusernotification%E3%81%AFmacos-1014%E3%81%8B%E3%82%89%E9%9D%9E%E6%8E%A8%E5%A5%A8)
- 代わりが[User Notifications](https://developer.apple.com/documentation/usernotifications)であり、今回これを実装してみます。

<img width="400" alt="image" src="https://i.imgur.com/EvXQcLE.jpg">

## 参考
- [Swift macOS Tutorial: Notifications \(UNUserNotificationCenter, adding images and actions in Big Sur\)](https://www.youtube.com/watch?v=mIztoF9CzP8)
    - 実装は主にこちらを参照しました。
    - 動画内では他にもバナーにボタンをつけてアクションできるようにしているので、必要であれば参照ください。

## 実装
### ユーザへ許可を求める
- 最初にユーザに許可を求め、通知を許可してもらう必要があります。
- 許可しない限り下記の`authorized`はfalseが返ってきます。

```swift
import UserNotifications

let userNotificationCenter = UNUserNotificationCenter.current()

userNotificationCenter.requestAuthorization(options: [.alert, .sound]) { authorized, error in
    guard authorized else {
        print("Not authorized")
        return
    }
}
```

- 下記のように通知が表示され、クリックすると設定画面が表示されます。

<img width="400" alt="image" src="https://i.imgur.com/g0hoxJo.jpg">

### 通知の作成
- まず通知が許可されているかを[getNotificationSettings\(completionHandler:\)](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649524-getnotificationsettings)で確認します。
- 具体的な通知の値は[UNMutableNotificationContent](https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent)オブジェクトを使います。
- `let image = NSImage(contentsOf: imageUrl)`の部分はメインスレッドだとUIが止まってしまうので、`DispatchQueue.global().async`を使用しています。
- （試した所通知はサブスレッドのままで動作したのですが、恐らくメインスレッドで呼ぶべきだと思うのでそうしています。）

```swift
userNotificationCenter.getNotificationSettings { settings in
    guard settings.authorizationStatus == .authorized else {
        return
    }
    
    let content = UNMutableNotificationContent()
    content.title = "Title Sample"
    content.subtitle = "Subtitle Sample"
    content.body = "Body sample"
    content.sound = UNNotificationSound.default
    
    /*
     // プロジェクトのResourceファイルを使用する場合
     guard let fileUrl = Bundle.main.url(forResource: "sampleImage1", withExtension: ".png"),
     let attachment = try? UNNotificationAttachment(identifier: fileUrl.lastPathComponent, url: fileUrl, options: nil)
     else {
     return
     }
     */
    
    // ファイルをダウンロードする場合
    DispatchQueue.global().async {
        guard let imageUrl = URL(string: "https://i.imgur.com/tanIcMA.png"),
              let image = NSImage(contentsOf: imageUrl),
              let attachment = UNNotificationAttachment.create(identifier: "attachment",
                                                               image: image,
                                                               options: .none)
        else {
            return
        }
        
        content.attachments = [attachment]
        let request = UNNotificationRequest(identifier: imageUrl.lastPathComponent, content: content, trigger: nil)
        
        DispatchQueue.main.async {
            userNotificationCenter.add(request) { error in
                if error != nil {
                    print(error?.localizedDescription as Any)
                }
            }
        }
    }
    
}
```

- 通知に画像を添付したい場合、ローカルPC上にファイルが存在する必要があります。
- 画像をNSImageで指定できるよう、下記のextensionを利用します。
    - 参考: [UNNotificationAttachment with UIImage or Remote URL](https://stackoverflow.com/questions/39103095/unnotificationattachment-with-uiimage-or-remote-url)
- Tempフォルダを作ってそこに画像を保存して、`UNNotificationAttachment`を返しています。

```swift
extension UNNotificationAttachment {
    
    static func create(identifier: String,
                       image: NSImage,
                       options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
        let fileManager = FileManager.default
        let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
        let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)
        do {
            try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil)
            let imageFileIdentifier = identifier + ".png"
            let fileURL = tmpSubFolderURL.appendingPathComponent(imageFileIdentifier)
            let imageData = image.tiffRepresentation
            try imageData?.write(to: fileURL)
            let imageAttachment = try UNNotificationAttachment.init(identifier: imageFileIdentifier,
                                                                    url: fileURL,
                                                                    options: options)
            return imageAttachment
        } catch {
            print("error " + error.localizedDescription)
        }
        return nil
    }
}
```

### フォアグラウンド時の通知表示
- iOSでも見る実装ですね。
    - [\[iOS 10\] User Notifications framework！ フォアグラウンド時も通知バナー表示](https://dev.classmethod.jp/articles/wwdc-2016-user-notifications-8/)
- macOSでは`.alert`であり、iOSのように`.banner`と`.list`へ分離してないのでこれを指定するのはちょっと違和感がありますが…。
    - [iOS 14からUNNotificationPresentationOptionsの「\.alert」が「\.banner」と「\.list」に分かれた](https://qiita.com/uhooi/items/7e7f0293a83b57eaf72f)

```swift
import Cocoa
import UserNotifications

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    


    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        UNUserNotificationCenter.current().delegate = self
    }

    // (略)

}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        return completionHandler([.list, .banner, .sound])
    }
}
```

### システム設定の通知設定を開く
- 通知を未許可とした場合の導線として、アプリからシステム設定の通知設定を開くことを考えます。
    - [Swift：画面収録の許可が必要なアプリで許可の状態を確認してダメなら許可を促す](https://qiita.com/Kyome/items/8bc0e31e81e590d519b3)
- 指定するURLは以下のページを参照。
    - [SystemPreferences](https://developer.apple.com/documentation/devicemanagement/systempreferences)
    - [macOS 10\.15 System Preference Panes](https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751)


```swift
guard let url = URL(string: "x-apple.systempreferences:com.apple.preference.notifications") else {
    return
}
NSWorkspace.shared.open(url)
```

## システム設定の通知設定の値の保存場所
- `~/Library/Preferences/com.apple.ncprefs.plist`に保存されています。

![](https://i.imgur.com/5ePxDBf.jpg)

## デバッグ時のTips
- 通知バナーにアイコンが表示されない場合
    - 設定画面から一度削除する
- 設定画面にアプリが表示されない
    - 一度設定画面を閉じて再度開く

![](https://i.imgur.com/PqcCC3p.jpg)