Swift・iOS

Swiftを中心に学んだことを記録に残すブログです。技術に関係ない記事もたまに書いています。

【SwiftUI】プッシュ通知を選択した時に特定の画面に遷移する

 

はじめに

Firebase Cloud Messagingで送信されたプッシュ通知を選択した時に、アプリの特定の画面に遷移する処理を実装してみたので記事に残します。

 

開発環境

  • macOS Catalina 10.15.7
  • Xcode 12.2
  • Swift 5.3.1
  • CocoaPods 1.10.0
  • FirebaseMessaging 7.3.0

 

本題

動作イメージ

  1. Firebase Cloud Messagingで送信されたプッシュ通知をユーザーが選択する
  2. アプリが開いて自動でWKWebViewが実装された画面に遷移する
  3. プッシュ通知のペイロードに含まれるURLを使ってWebページを表示する

f:id:hfoasi8fje3:20210125185431g:plain

 

全体の実装

AppDelegate.swift
import UIKit
import Firebase

@main
class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        
        UNUserNotificationCenter.current().delegate = self
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: {_, _ in })
        
        application.registerForRemoteNotifications()
        
        Messaging.messaging().delegate = self
        
        return true
    }
    
    // UIApplicationDelegateのapplication(_:configurationForConnecting:options:)メソッドと
//application(_:didDiscardSceneSessions:)メソッドは変更箇所がないため省略
} extension AppDelegate: UNUserNotificationCenterDelegate { // 通知が到着した時の挙動 func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { // バナーと音で通知が到着したことを知らせる completionHandler([[.banner, .sound]]) } // 通知を選択した時の挙動 func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { // 通知のペイロードを送る let userInfo = response.notification.request.content.userInfo NotificationCenter.default.post(name: Notification.Name("didReceiveRemoteNotification"), object: nil, userInfo: userInfo) completionHandler() } }

 

SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let rootView = RootView()
            .environmentObject(RootViewModel())
            .environmentObject(PushDetailViewModel())
       
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: rootView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

// 変更不要な実装箇所に関しては省略 }

 

RootView.swift
import SwiftUI

struct RootView: View {
    @EnvironmentObject var viewModel: RootViewModel
    
    var body: some View {
        TabView(selection: $viewModel.selection) {
            HomeView()
                .tabItem {
                    Image(systemName: "cart")
                    Text("ホーム")
                }
                .tag(1)
            
            NewsListView(viewModel: NewsListViewModel())
                .tabItem {
                    Image(systemName: "bell")
                    Text("お知らせ")
                }
                .tag(2)
        }
    }
}

 

RootViewModel.swift
import SwiftUI
import Combine

class RootViewModel: ObservableObject {
    // 選択しているタブを保持するための変数
    @Published var selection = 1
    
    var cancellable: AnyCancellable?
    
    init() {
        // プッシュ通知選択時に通知を受け取る
        cancellable = NotificationCenter.default
            .publisher(for: Notification.Name("didReceiveRemoteNotification"))
            .sink { _ in
                // 「ホーム」タブを表示する
                self.selection = 1
            }
    }
}

 

HomeView.swift
import SwiftUI

struct HomeView: View {
    @EnvironmentObject var pushDetailViewModel: PushDetailViewModel
    
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(
                    destination: PushDetailView(),
                    isActive: .constant(pushDetailViewModel.isActivePushDetailView),
                    label: {
                        EmptyView()
                    }
                )
                
                Text("Home")
            }
            .navigationTitle("HOME")
        }
    }
}

 

PushDetailView.swift
import SwiftUI

struct PushDetailView: View {
    @EnvironmentObject var viewModel: PushDetailViewModel
    
    var body: some View {
        WebView(urlString: viewModel.urlString)
            .navigationTitle(viewModel.title)
            .navigationBarTitleDisplayMode(.inline)
            .onDisappear(perform: {
                viewModel.isActivePushDetailView = false
            })
    }
}

 

PushDetailViewModel.swift
import SwiftUI
import Combine

class PushDetailViewModel: ObservableObject {
    // プッシュ通知選択時にPushDetailViewへの画面遷移を実行するためのプロパティ
    @Published var isActivePushDetailView = false
    // PushDetailViewのタイトル
    var title = ""
    // PushDetailViewに表示するページのURL
    var urlString = ""
    
    var cancellable: AnyCancellable?
    
    init() {
        // プッシュ通知選択時に通知を受け取る
        // PushDetailViewに表示するタイトルとページのURLを取得
        // 変数isActivePushDetailViewの値を変更することでPushDetailViewへの画面遷移処理を発火させる
        cancellable = NotificationCenter.default
            .publisher(for: Notification.Name("didReceiveRemoteNotification"))
            .sink { notification in
                if let userInfo = notification.userInfo, let aps = userInfo["aps"] as? [AnyHashable: Any], let alert = aps["alert"] as? [AnyHashable: Any], let title = alert["title"], let urlString = userInfo["url"] {
                    self.title = title as? String ?? ""
                    self.urlString = urlString as? String ?? ""
                    self.isActivePushDetailView = true
                }
            }
    }
}

 

WebView.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    var urlString: String
    
    class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate {
        var parent: WebView
        
        init(_ parent: WebView) {
            self.parent = parent
        }
        
        // "target="_blank""が設定されたリンクも開けるようにする
        func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
            if navigationAction.targetFrame == nil {
                webView.load(navigationAction.request)
            }
            return nil
        }
        
        // URLごとに処理を制御する
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
            if let url = navigationAction.request.url?.absoluteString {
                if (url.hasPrefix("https://apps.apple.com/")) {
                    guard let appStoreLink = URL(string: url) else {
                        return
                    }
                    UIApplication.shared.open(appStoreLink, options: [:], completionHandler: { (succes) in
                    })
                    decisionHandler(WKNavigationActionPolicy.cancel)
                } else if (url.hasPrefix("http")) {
                    decisionHandler(WKNavigationActionPolicy.allow)
                } else {
                    decisionHandler(WKNavigationActionPolicy.cancel)
                }
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        
        return webView
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        // makeCoordinatorで生成したCoordinatorクラスのインスタンスを指定
        webView.uiDelegate = context.coordinator
        webView.navigationDelegate = context.coordinator
        
        // スワイプで画面遷移できるようにする
        webView.allowsBackForwardNavigationGestures = true
    
        guard let url = URL(string: urlString) else {
            return
        }
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

 

補足
  • 【Firebase Cloud Messaging】導入の手順 - Swift・iOSに記載の設定が完了している前提です。
  • RootView.swiftに実装されているNewsListViewとNewsListViewModelに関しては、この記事の内容に関係がないため省略します。実際に動かしてみたい場合は、"NewsListView(viewModel: NewsListViewModel())"の部分を"Text("テスト")"などに置き換えてください。

 

おわりに

今回はシンプルな画面遷移のみ実装となっているため、今後、プッシュ通知の種類によって画面遷移や処理を分ける実装をする機会があれば試したいと思います。

 

参考