能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

プッシュ通知のテストベッド - SwiftUI100行チャレンジ(13)

端的にいうと

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行未満でプッシュ通知のサンプルコードが書けるのは感慨深い。

通知のテストベッドとしてこちらコード利用していただければ幸いである。

参考