I am trying to get a notification action to open a custom view. My notifications are basically news and I want the user to go to a page displaying a simple text (for purposes of this question) when he taps the "read notification" action. I have tried tons of tutorials but they all use some existing views like "imagePicker" that already have a ton of stuff enabled by default and I don't know what are all the things I need to add to my custom view to make this work. Like UIViewControllerRepresentable, coordinator or whatever else I may need.
This is my main swift file which handles notifications.(notifications are working fine) At the end of the file is the extension AppDelegate: UNUserNotificationCenterDelegate {} taken from the tutorial I was following which creates a NewsItem which I will also include here. But as I'm working in SwiftUI and not UIKit this is the point where I cannot follow any of the tutorials I've managed to find anymore to get it to work the way I want to.
I have included the complete app delegate extension and newsItem just to make the code here compilable but I put the part where I need the changes to start in a comment block.
import SwiftUI
import UserNotifications
enum Identifiers {
static let viewAction = "VIEW_IDENTIFIER"
static let readableCategory = "READABLE"
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
TabView{
NavigationView{
ContentView()
}
.tabItem {
Label("Home", systemImage : "house")
}
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self// set the delegate
registerForPushNotifications()
return true
}
func application( // registers for notifications and gets token
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("device token : \(token)")
}//handles sucessful register for notifications
func application( //handles unsucessful register for notifications
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register: \(error)")
}//handles unsucessful register for notifications
func application( //handles notifications when app in foreground
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler:
@escaping (UIBackgroundFetchResult) -> Void
) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
print("new notification received")
}//handles notifications when app in foreground
func registerForPushNotifications() {
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
print("permission granted: \(granted)")
guard granted else { return }
let viewAction = UNNotificationAction(
identifier: Identifiers.viewAction,
title: "Mark as read",
options: [.foreground])
let readableNotification = UNNotificationCategory(
identifier: Identifiers.readable,
actions: [viewAction2],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([readableNotification])
self?.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
print("notification settings: \(settings)")
}
}
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if let aps = userInfo["aps"] as? [String: AnyObject],
let newsItem = NewsItem.makeNewsItem(aps) {
(window?.rootViewController as? UITabBarController)?.selectedIndex = 1
if response.actionIdentifier == Identifiers.viewAction,
let url = URL(string: newsItem.link) {
let safari = SFSafariViewController(url: url)
window?.rootViewController?.present(safari, animated: true, completion: nil)
}
}
completionHandler()
}
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if let aps = userInfo["aps"] as? [String: AnyObject],
/*
let newsItem = NewsItem.makeNewsItem(aps) {
(window?.rootViewController as? UITabBarController)?.selectedIndex = 1
if response.actionIdentifier == Identifiers.viewAction,
let url = URL(string: newsItem.link) {
let safari = SFSafariViewController(url: url)
window?.rootViewController?.present(safari, animated: true, completion: nil)
}
}
*/
completionHandler()
}
}
Here is NewsItem.swift from the tutorial just in case but this is a file I don't need or want to use.
import Foundation
struct NewsItem: Codable {
let title: String
let date: Date
let link: String
@discardableResult
static func makeNewsItem(_ notification: [String: AnyObject]) -> NewsItem? {
guard
let news = notification["alert"] as? String,
let url = notification["link_url"] as? String
else {
return nil
}
let newsItem = NewsItem(title: news, date: Date(), link: url)
let newsStore = NewsStore.shared
newsStore.add(item: newsItem)
NotificationCenter.default.post(
name: NewsFeedTableViewController.refreshNewsFeedNotification,
object: self)
return newsItem
}
}
the simplified ContentView
import SwiftUI
struct ContentView: View {
var body: some View {
VStack{
Text(DataForApp.welcomeText)
.font(.title)
.bold()
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.shadow(radius: 8 )
} .navigationTitle("My Mobile App")
}
}
now my goal is to use this MyView and once a user taps the "mark as read" action I want this view to show.
import SwiftUI
struct MyView: View {
var body: some View {
Text("Notification text here")
}
}
Obviously MyView does not contain anything it needs but I don't want to post the code I tried here as I tried 200 different things and since none of them work I realise I'm not even close to the right track.
I solved this by using an @ObservableObject I created called NotificationManager
-- this stores the text of the most recent notification (you could expand it to store an array if you like) and provides a binding to tell the app whether or not to show a new view in the stack based on whether or not there's a notification to be shown.
This NotificationManager
has to be an @ObservedObject on ContentView
for this to work, since ContentView needs to watch for changes in the state of currentNotificationText
, which is a @Published property.
ContentView
has an invisible NavigationLink
(via .overlay
with an EmptyView
) that gets activated only in the event that there's notification.
In the App Delegate methods, I just hand off the notification to a simple function handleNotification
that parse the aps
and puts the resulting String
in the NotificationManager
. You could also easily augment this with more robust features, including parsing other fields from the aps
import SwiftUI
import UserNotifications
//new class to store notification text and to tell the NavigationView to go to a new page
class NotificationManager : ObservableObject {
@Published var currentNotificationText : String?
var navigationBindingActive : Binding<Bool> {
.init { () -> Bool in
self.currentNotificationText != nil
} set: { (newValue) in
if !newValue { self.currentNotificationText = nil }
}
}
}
enum Identifiers {
static let viewAction = "VIEW_IDENTIFIER"
static let readableCategory = "READABLE"
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
TabView{
NavigationView{
ContentView(notificationManager: appDelegate.notificationManager) //pass the notificationManager as a dependency
}
.tabItem {
Label("Home", systemImage : "house")
}
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
var notificationManager = NotificationManager() //here's where notificationManager is stored
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self// set the delegate
registerForPushNotifications()
return true
}
func application( // registers for notifications and gets token
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("device token : \(token)")
}//handles sucessful register for notifications
func application( //handles unsucessful register for notifications
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register: \(error)")
}//handles unsucessful register for notifications
func application( //handles notifications when app in foreground
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler:
@escaping (UIBackgroundFetchResult) -> Void
) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
print("new notification received")
handleNotification(aps: aps)
completionHandler(.noData)
}//handles notifications when app in foreground
func registerForPushNotifications() {
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
print("permission granted: \(granted)")
guard granted else { return }
let viewAction = UNNotificationAction(
identifier: Identifiers.viewAction,
title: "Mark as read",
options: [.foreground])
let readableNotification = UNNotificationCategory(
identifier: Identifiers.readableCategory,
actions: [viewAction],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([readableNotification])
self?.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
print("notification settings: \(settings)")
}
}
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if let aps = userInfo["aps"] as? [String: AnyObject] {
handleNotification(aps: aps)
}
}
}
extension AppDelegate {
@discardableResult func handleNotification(aps: [String:Any]) -> Bool {
guard let alert = aps["alert"] as? String else { //get the "alert" field
return false
}
self.notificationManager.currentNotificationText = alert
return true
}
}
struct ContentView: View {
@ObservedObject var notificationManager : NotificationManager
var body: some View {
VStack{
Text("Welcome")
.font(.title)
.bold()
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.shadow(radius: 8 )
}
.navigationTitle("My Mobile App")
.overlay(NavigationLink(destination: MyView(text: notificationManager.currentNotificationText ?? ""), isActive: notificationManager.navigationBindingActive, label: {
EmptyView()
}))
}
}
struct MyView: View {
var text : String
var body: some View {
Text(text)
}
}
(I had to fix a number of typos/compilation errors with the original code in your question, so make sure that if you use this, you're copying and pasting directly to get the right method signatures, etc.)