端的にいうと
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/)