CloudKit Subscription Woes - cloudkit

I am struggling with the following subscription:
let predicate = NSPredicate(format: "gc_alias != %# AND distanceToLocation:fromLocation:(%K,%#) < %f",
self.localPlayer!.alias!,
"location",
self.currentLocation!,
10)
let subscription = CKSubscription(recordType: "Player", predicate: predicate, options: .FiresOnRecordCreation)
subscription.zoneID = nil
let notification = CKNotificationInfo()
notification.alertBody = "Nearby Player within Range!"
notification.soundName = UILocalNotificationDefaultSoundName
subscription.notificationInfo = notification
let container = CKContainer.defaultContainer()
let publicDb = container.publicCloudDatabase
publicDb.saveSubscription(subscription) { (result, error) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
print("SUBSCRIBED SUCCESS")
print(result)
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "subscribed")
}
}
Basically, when a new Player record is created or updated I log the user's location.
I want user A to be notified via Push when user B creates or updates their Player record and is within 10KM.
I believe I have the push permissions setup correctly in my app (user is prompted to confirm this before their sub is created, for example).
No pushes arriveth. Any ideas? Am I suffering from some fundamental CK misconception?

You don't seem to be registering for push notifications:
iOS Developer Library: Subscribing to Record Changes
Saving subscriptions to the database doesn’t automatically configure your app to receive notifications when a subscription fires. CloudKit uses the Apple Push Notification service (APNs) to send subscription notifications to your app, so your app needs to register for push notifications to receive them.
According to Hacking with Swift: Delivering notifications with CloudKit push messages: CKSubscription and saveSubscription you should:
Go to AppDelegate.swift and put this code into the didFinishLaunchingWithOptions method:
let notificationSettings = UIUserNotificationSettings(forTypes: [.Alert, .Sound], categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)
UIApplication.sharedApplication().registerForRemoteNotifications()
And
For the sake of completion, you could optionally also catch the didReceiveRemoteNotification message sent to your app delegate, which is called if a push message arrives while the app is running. Something like this ought to do the trick:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if let pushInfo = userInfo as? [String: NSObject] {
let notification = CKNotification(fromRemoteNotificationDictionary: pushInfo)
let ac = UIAlertController(title: "What's that Whistle?", message: notification.alertBody, preferredStyle: .Alert)
ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
if let nc = window?.rootViewController as? UINavigationController {
if let vc = nc.visibleViewController {
vc.presentViewController(ac, animated: true, completion: nil)
}
}
}
}

Related

How to show an Alert dialog for push notifications when not using a ViewController

I am building a SwiftUI app using Azure NotificationHubs and struggling with how to show an alert when a notification is received if I am not using a ViewController.
I am getting the notification, but don't know how to show a dialog from an event in AppDelegate since all the examples I have found are for ViewControllers.
func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification message: MSNotificationHubMessage) {
print("notificationHub...")
let title = message.title ?? ""
let body = message.body ?? ""
print("title: \(title)")
print("body: \(body)")
let userInfo = ["message": message]
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
guard let aps = message.userInfo["aps"] as? [String: AnyObject] else {
return
}
print("aps: \(aps)")
if (UIApplication.shared.applicationState == .background) {
print("Notification received in background")
} else {
print("Notification received in foreground")
}
}
I don't get the message when in the background but when I click on the notification when the app is in the background I get the Notification received in foreground message.
I suppose the desired behavior is that if they click on message while the app is in the background it will go to the appropriate view. When it's in the foreground it will ask the user if they want to go to the view.
I have the navigation piece working, just need to know how to alert the user and respond to their choice.
In the Azure examples on GitHub they show:
class SetupViewController: MSNotificationHubDelegate // And other imports
// Set up the delegate
MSNotificationHub.setDelegate(self)
// Implement the method
func notificationHub(_ notificationHub: MSNotificationHub!, didReceivePushNotification notification: MSNotificationHubMessage!) {
let title = notification.title ?? ""
let body = notification.body ?? ""
if (UIApplication.shared.applicationState == .background) {
NSLog("Notification received in background: title:\"\(title)\" body:\"\(body)\"")
} else {
let alertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel))
self.present(alertController, animated: true)
}
}
but I am not using a ViewController. This was a great resource as well but again they show a view controller (class ViewController: UIViewController).
I am new to iOS and Swift so I might be looking in the wrong area but I am essentially trying to show an alert dialog from an event triggered in the AppDelegate.
UPDATE
While working through the response below, I did come across this post recommending that I create a view with a #State private var showAlert = false; that could be bound to something I guess I change in the AppDelegate notificationHub method.
Is this the proper approach in SwiftUI vs UIWindowScene or UIApplication?
I did get this to work:
if UIApplication.shared.windows.first != nil {
UIApplication.shared.windows.first?.rootViewController!.present(alertController, animated: true)
}
You can get the rootViewController from UIApplication.shared.windows.first, then present an UIAlertController.
Usage:
//Force
UIApplication.shared.windows.first?.rootViewController!.present(..)
//Safe
guard let viewController = UIApplication.shared.windows.first?.rootViewController else {return}
viewController.present(...)
Note:
UIApplication.shared.windows is deprecated in iOS 15, you can use UIWindowScene.windows.first in iOS 16.

How To Move To Spesific Screen iOS When User Tap Notification FCM

I made a notification, now my notification has appeared on iOS when I tap the notifications bagde the notification appears I want to switch to another View Controller page.
This my code in AppDelegate.swfit
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// TODO: Handle data of notification
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
// Print full message.
print("user data msg \(userInfo)")
guard let aps = userInfo["aps"] as? [String : AnyObject] else {
print("Error parsing aps")
return
}
print(aps)
if let alert = aps["alert"] as? String {
body = alert
} else if let alert = aps["alert"] as? [String : String] {
body = alert["body"]!
title = alert["title"]!
}
if let alert1 = aps["category"] as? String {
msgURL = alert1
}
print("Body\(body)")
print(title)
print(msgURL)
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(withIdentifier: "register")
vc.modalPresentationStyle = .overFullScreen
present(vc, animated: true)
}
}
But I got error: Cannot find 'present' in scope Where I should put my code for naviagtion when the user got a notification in Swift.
present() is a method on UIViewController, so you'll have to have a reference to a view controller that you can call that method on.
This can vary by the structure of your app -- especially if you're using a SceneDelegate, you have multiple windows, etc.
My suggestion is that you post a Notification that you can listen for in whatever has a reference to your top view controller. For example, instead of where you have present, you could do something like this:
let dataDict : [String : UIViewController] = ["vc": vc]
NotificationCenter.default.post(name: Notification.Name("ShowVC"), object: nil, userInfo: dataDict)
Then, assuming you're using a SceneDelegate, you could do this in your scene(_ scene: UIScene, willConnectTo) function:
NotificationCenter.default.publisher(for: Notification.Name("ShowVC"))
.compactMap {
$0.userInfo as? [String: UIViewController]
}
.sink {
guard let vc = $0["vc"] else {
return
}
window?.rootViewController?.present(vc, animated: true)
}.store(in: &cancelSet)
At the top of your file you'll need to import Combine and then your SceneDelegate will need a new property in order for the above code to work:
var cancelSet: Set<AnyCancellable> = []
Even if you aren't using a SceneDelegate, the basic concepts here should apply to other scenarios.
Note, however, that this is not a foolproof plan -- the root view controller has to be the visible view in order for this to work. This all may depend on your architecture. Search SO for "topmost view controller" to see plenty of discussion on this topic.

UNUserNotificationCenter. Notifications are not allowed for this application

I noob in development application on OSX. I want to create app with Share extension. After content loading I want to show Notification, but I getting error "Notifications are not allowed for this application". I don't understand why requestAuthorization method not show dialog windows with permission, and how I can allow application to send notifications.
This is my code:
import Cocoa
import UserNotifications
class ShareViewController: NSViewController {
override func loadView() {
self.view = NSView()
// Insert code here to customize the view
let item = self.extensionContext!.inputItems[0] as! NSExtensionItem
NSLog("Attachment = %#", item.attachments! as NSArray)
showNotification()
let outputItem = NSExtensionItem()
let outputItems = [outputItem]
self.extensionContext!.completeRequest(returningItems: outputItems, completionHandler: nil)
}
func showNotification() -> Void {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.requestAuthorization(options: [.alert, .badge]) {
(granted, error) in
if granted {
print("Yay!")
} else {
print("D'oh") // Print this, not authorized
}
}
let content = UNMutableNotificationContent()
content.title = "Hello"
content.body = "This is example"
content.sound = UNNotificationSound.default
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
notificationCenter.add(request) { (error : Error?) in
if let theError = error {
print(theError) // Print Domain=UNErrorDomain Code=1 "Notifications are not allowed for this application"
}
}
}
}
I don't see anywhere in the documentation that says you can't schedule new Local Notifications from an extension. I do however see an apple support ticket where someone had to deal with this issue, as do I.
Basically this failure is a race-condition. The key is to not call the contentHandler of this extension method:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
until after the completion handler of
notificationCenter.add(request: UNNotificationRequest>, withCompletionHandler: ((Error?) -> Void)
is called. It worked for me. Make sense?
Apple does't allow send Notifications from Share extensions.
Documentation

Using openTok after accept a Call (CallKit)

I am working on a project which requires opentok and callkit for notifying users. However, the application keeps crashing when openTok tries to connect to a session. IDK what is going on right now. Here is my work flow and codes:
Push notification about available opentok session -> start incoming call -> user accept the call -> start the opentok storyboard -> do some backend stuff -> connect to a session !!!! THIS IS WHEN IT CRASHES.
ERROR:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x1968a0ad8)
Besides, I would like to ask for advice about receive notification. Instead of showing the notification on the screen. I would like to start the call, using callkit, like whatsapp or fb messenger. Please give me a hint about this also. Right now, I can only start the call when the app is in foreground when push notification is sent.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// TODO: Handle data of notification
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
// Print full message.
print(userInfo)
let aps = userInfo["aps"] as! [String: AnyObject]
// 1
if aps["content-available"] as? Int == 1 {
let uuid = UUID(uuidString: MyVariables.uuid)
AppDelegate.shared.displayIncomingCall(uuid: uuid!, handle: "Sanoste", hasVideo: false) { _ in
}
}else {
return
}
completionHandler(UIBackgroundFetchResult.newData)
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
action.fulfill()
AppDelegate.shared.openTok()
}
func openTok() {
let mainStoryboard = UIStoryboard(name: "callView", bundle: nil)
let vc = mainStoryboard.instantiateViewController(withIdentifier: "callViewController") as! callViewController
vc.view.frame = UIScreen.main.bounds
UIView.transition(with: self.window!, duration: 0.5, options: .transitionCrossDissolve, animations: {
self.window!.rootViewController = vc
}, completion: nil)
}
// Join a session from MBE
func joinSession() {
var error: OTError?
pSession = OTSession(apiKey: openTokSessionKey.pApiKey, sessionId: openTokSessionKey.pSessionId, delegate: self as OTSessionDelegate)
pSession?.connect(withToken: openTokSessionKey.pToken, error: &error)
if error != nil {
print(error!)
}
sSession = OTSession(apiKey: openTokSessionKey.sApiKey, sessionId: openTokSessionKey.sSessionId, delegate: self as OTSessionDelegate)
sSession?.connect(withToken: openTokSessionKey.sToken, error: &error)
if error != nil {
print(error!)
}
}
Anyone helps please ?
This crash is a kind of warning. If you disable "Main Thread Sanitizer" in your project settings in Xcode 9 and rebuild your project, can fix the issue.

How to display a specific ViewController in OneSignal Swift

I do app with OneSignal and Swift 3. I got push. How to display a specific ViewController with WebView in OneSignal when notification is clicked. In push in additional data with field "link" I got link, but can't display this link in my WebView.
I try use global variable tempURL to put url from additional data. But it not work.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
OneSignal.initWithLaunchOptions(launchOptions, appId: "MYID", handleNotificationAction: { (result) in
let payload = result?.notification.payload
print("This is Payload \(payload)")
var fullMessage = payload?.title
let messageTitle = "OneSignal Example"
if (result?.action.actionID) != nil {
let additionalData = payload?.additionalData
let url = additionalData?["link"] as! String?
tempURL = url!
fullMessage = fullMessage! + "\nPressed ButtonId:\(url)"
}
let alertController = UIAlertController(title: messageTitle, message: fullMessage, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default)
alertController.addAction(okAction)
alertController.show(alertController, sender: nil)
})
return true
}
Try to show:
func application(_ application: UIApplication, didReceiveRemoteNotification data: [AnyHashable : Any]) {
let aViewController = ViewController()
aViewController.loadAddressURL(url: tempURL)
UIApplication.shared.keyWindow?.rootViewController?.present(aViewController, animated: true, completion:nil)
}
I have error:
fatal error: unexpectedly found nil while unwrapping an Optional value
first of all you can't pass data in your way. You should instantiate ViewController with StoryboardID. I explained how to use it with this link.
If you are pretty sure, your additionalData is not nil, you can pass your data with StoryboardID.