端的にいうと
SwiftUIベースPush通知のテストベッドコード。アプリ表示中での通知有効。
1)プッシュ通知のテストコードが欲しい
プッシュ通知はXcode11からシミュレーターへのプッシュ通知が可能となった。プッシュ通知経由の機能(プッシュ通経由のアプリランチ、アプリの呼び出し、アクションボタンの確認)が容易となった。他プッシュ通知表示のスクリーンショットを各シミュレーター分取ることで通知に表示される内容を確認しやすくもなった。
通知を出すための手間が減れば実際に通知を出してみたい。そのような要望に際しテストベッドコードをSwiftUIで作成した。マルチプラットフォームも意識しmacOSアプリとしての動作するサンプルコードとなっている。
2)サンプルコード解説
2-1)OSごとの切り分け
SwiftUIでプッシュ通知を取得するためにはUIKitの場合はUIAppDelegete、AppKitの場合はNSApplicationDelegateプロトコルに準拠したクラスを実装しSwiftUIに組み込む必要がある。
マルチプラットフォームで動作するコードの場合、OSに依存するクラスがどうしても出てくるがその場合はOSを判断する#if 〜 #endコードを記述する。
#if os(iOS)
// ここにiOS依存コード
#endif
#if os(macOS)
// ここにmacOS依存コード
#endifコードを記述するテクニックとしてはiOS/macOSで同名クラスを作成。extensionで共用で呼び出すコードについてはosで分けないでコードの共有を実現することができる。
#if os(iOS)
class Foo {
}
#endif
#if
class Foo {
}
#endif
extension Foo { // iOS/macOS
func test() { }
}2-2)イベントとScene
通知を表示するまでは実現できているが実際に使用する場合はAppDelegeteから得られたイベントについて考慮する必要がある。
具体的には通知を受け取った後で通知のコンテキストに合致するSceneを経由して何らかのイベントを渡す必要がある(UserNotificationがAppオブジェクトで受け取るように設計されている理由でもある)。
ドキュメントタイプのアプリであれ複数Sceneが存在するし、チャットアプリなどであればチャット部屋ごとにSceneを切り分ける必要があるだろう。通知のコンテキストに基づいたSceneが起動していない場合は新規にSceneを作成するといった実装も必要になってくる。
SwiftUJIであればイベントを伝達する実装としてはObservableObjectと@Publishedを組み合わせてが適切だろうと思う。
2-3)OSごとのAdaptor
AppDelegateをSwiftUIに組み込むための@UIApplicationDelegateAdaptor、macOSの場合は@NSApplicationDelegateAdaptorを使用する。いずれのAdaptorについてもSwiftUIの一部となっている。
については以下の記事を参考にして欲しい。
既存iOSプロジェクトをSwiftUI Appへ移行する - アプリ開発者はSwiftUIにおけるAppDelegate/SceneDelegateの扱いをよく理解していない?
import SwiftUI
import UserNotifications
#if os(iOS)
class AppDelegate : UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerForPushNotifications()
UNUserNotificationCenter.current().delegate = self
return true
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
// Enabled in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
[.badge , .banner, .sound]
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
// reset app badge
UIApplication.shared.applicationIconBadgeNumber = 0
}
}
#endif
#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
registerForPushNotifications()
UNUserNotificationCenter.current().delegate = self
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
// Enabled in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
[.badge , .banner, .sound]
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
// reset app badge
await MainActor.run{
NSApp.dockTile.badgeLabel = ""
}
}
}
#endif
// register notification(macOS/iOS)
extension AppDelegate {
func registerForPushNotifications() {
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) {
(granted, error) in
print("Permission granted: \(granted)")
}
}
}
@main
struct SimplePushNotificationApp: App {
#if os(iOS)
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
Text("Push notification testbed")
.padding()
}
}3)課題
xcode13でマルチプラットフォームのプロジェクトを作成するとエディター上でAppKit SDKに含まれるクラスやメソッドへJumpできない問題が発生している。
サンプルコードを作成する際は、マルチプラットフォーム向けのプロジェクトとは別途macOS向けのダミープロジェクトを作成した上でエディター上でAPIを確認するといった手間があった。
APIをエディター上で参照できない問題についてはxcode内で優先するAPIリファレンスの設定する方法があるかもしれない。
まとめ
テストベッドなのでシンプルになるのは当然だが100行未満でプッシュ通知のサンプルコードが書けるのは感慨深い。
通知のテストベッドとしてこちらコード利用していただければ幸いである。
参考
- [SwiftUIを使ってmacOSステータスバーアプリをつくる方法 | 株式会社ヌーラボ(Nulab inc.)]-(https://nulab.com/ja/blog/nulab/how-to-make-statusbar-app-with-swiftui/)