iOS 11 Core NFC - any sample code? - ios

I just installed the first iOS 11 beta to an iPhone 7 and am interested in trying the NFC. There's nothing about it in settings. I am wondering if there's any sample code out there showing how to read a tag. Can anyone show how to use the Core NFC SDK, in a code snippet?

In the Apple Developer site, create a new App ID and make sure that NFC Tag Reading is enabled.
Add the following lines to your .plist file:
<key>NFCReaderUsageDescription</key>
<string>NFC Tag!</string>
and these to the entitlements file:
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
</array>
It should look something like this in the corresponding files:
Also Core NFC can be enabled via the Capabilities tab in Xcode.
Objective-c
Import CoreNFC
#import <CoreNFC/CoreNFC.h>
and set the delegate:
#interface YourViewController : UIViewController <NFCNDEFReaderSessionDelegate>
In viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NFCNDEFReaderSession *session = [[NFCNDEFReaderSession alloc] initWithDelegate:self queue:dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT) invalidateAfterFirstRead:NO];
[session beginSession];
}
In the delegate callback:
- (void) readerSession:(nonnull NFCNDEFReaderSession *)session didDetectNDEFs:(nonnull NSArray<NFCNDEFMessage *> *)messages {
for (NFCNDEFMessage *message in messages) {
for (NFCNDEFPayload *payload in message.records) {
NSLog(#"Payload data:%#",payload.payload);
}
}
}
You must also add the didInvalidateWithError delegate callback or you'll not conform with protocol:
- (void)readerSession:(nonnull NFCNDEFReaderSession *)session didInvalidateWithError:(nonnull NSError *)error {
}
You can stop the reader with:
[session invalidateSession];
Swift 3/4
Import CoreNFC
import CoreNFC
and set the delegate:
class YourViewController: UIViewController, NFCNDEFReaderSessionDelegate
In viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
let session = NFCNDEFReaderSession(delegate: self,
queue: DispatchQueue(label: "queueName", attributes: .concurrent), invalidateAfterFirstRead: false)
session?.begin()
}
In the delegate callback:
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
for message in messages {
for record in message.records {
print(record.payload)
}
}
}
You can stop the reader with:
session.invalidateSession
Usage
After launching the view you should immediately see the iOS NFC reader dialog like so:
Once this dialog appears you have about a second to place the iPhone near the NFC tag you want to read. Otherwise, the field is deactivated (this seems to be a bug on Apple's end). I often needed to cancel and retry to get consistent readings. More details here.

To fix this issue you can add com.apple.developer.nfc.readersession.formats key into your entitlements file. The key should be associated with the array of enabled NFS types.
For instance, you can try the following:
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
</array>
It worked for me.

Updated for second Xcode beta.
Add the NFC capability to your app from the Developer Center. Identifiers -> App IDs -> enable "NFC Tag Reading".
If your project does not have an entitlement file, let Xcode create one for you by just activating and then subsequently deactivating any capability from within Xcode -> Project Targets -> Capabilities. You will find a new [AppName].entitlements file in your project navigator. Right-click on that file and select "Open as -> Source Code". Enter the following entry manually between <dict></dict>:
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
</array>
As soon as Xcode 9 allows enabling NFC Tag Reading from the Capabilities selection this step becomes obsolete because all you have to do is to enable it there. The current (first) Beta version does not support this.
You also need to enter a usage description for the privacy warning iOS will show the user. (At the moment (beta 1) this warning will be displayed when the device is ready to scan and shows a native system dialogue which will include this message. However, this seems to be unploished.) Open your target's Info.plist and start typing "Privacy" and you can scroll down to "Privacy - NFC Usage Description" to select it by hitting return. Enter a meaningful explanation to your user in the right column.
Now you should be able to import CoreNFC, in Swift:
import CoreNFC
Then head over to Apple's documentation.
Important: If the compiler returns an error with No such module 'CoreNFC' check if you have selected an actual iOS 11 device to build for, not a simulator. It also has to be the iPhone 7 or 7 plus. This might change in a future version, but testing NFC will only ever be fully working with actual hardware. (cp. Core Bluetooth where you can run on Simulator but not test the actual capabilities.) Beta 2 doesn't have this issue anymore. However, actual hardware in form of iPhone 7/p is still required for actual testing.

Just to enrich previous answers, it's important to bear in mind these considerations specifically of the NFCNDEFReaderSession class:
Reader session for processing NFC Data Exchange Format (NDEF) tags. This session requires the "com.apple.developer.nfc.readersession.formats" entitlement in your process. In addition your application's Info.plist must contain a non-empty usage description string.
A NDEF reader session will automatically scan and detect NFC Forum
tags that contain a valid NDEF message. NFC Forum Tag type 1 to 5
that is NDEF formatted are supported. A modal system UI will present
once -beginSession is called to inform the start of the session; the
UI sheet is automatically dismissed when the session is invalidated
either by the user or by calling -invalidateSession.
An opened session has a 60 seconds time limit restriction after -beginSession is called; -readerSession:didInvalidateWithError: will return NFCReaderSessionInvalidationErrorSessionTimeout error when the time limit is reached.
Only 1 active reader session is allowed in the system; -readerSession:didInvalidateWithError: will return NFCReaderSessionInvalidationErrorSystemIsBusy when a new reader session is initiated by -beginSession when there is an active reader session.
-readerSession:didInvalidateWithError: will return NFCReaderSessionInvalidationErrorUserCanceled when user clicks on the done button on the UI.
-readerSession:didInvalidateWithError: will return NFCReaderSessionInvalidationErrorSessionTerminatedUnexpectedly when the client application enters the background state.
-readerSession:didInvalidateWithError: will return NFCReaderErrorUnsupportedFeature when
reader mode feature is not available on the hardware
client application does not have the required entitlement.

You need to make sure the usage description is in place and also add the capability to the app inside of the Apple Developer Center.
I have a tutorial based on my experience (Swift 4-based). It's available here: Core NFC Tutorial

I added Core NFC to a project using the resources in these answers. One additional thing that wasn't noted though was that even if you add the capability manually via the entitlements, Xcode doesn't seem to look at the file unless you have a capability turned on. This is probably due to Xcode 9 Beta 1 not having Core NFC as a capability switch for lots of people's sample projects. So just be sure to turn at least one other capability on if you're still seeing issues! I was seeing an unexpected termination error immediately return until I did this.
I would leave this as a comment as it belongs, but don't have enough reputation yet to do so. Figured this was important enough to note.

my two cents:
1) under xcode 9.0 (beta 4 9M189t ) if You have already added capabilities,
no need to manually add:
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
</array>
it is done automatically
2) no crash if not using iPhone 7 OR you are in simulator:
You wil be called in:
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
print(error.localizedDescription)
}
it will show:
"Feature not supported"
3) dont miss:
self.nfcSession?.begin() // will trigger callback
so:
final private func setup(){
self.nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
self.nfcSession?.alertMessage = "Put your NFC TAG over iPhone.."
self.nfcSession?.begin() // will trigger callback
}
4) if user cancels, you will get:
"Session is invalidated by user cancellation"
in didInvalidateWithError callback.

Related

WatchKit App Cannot Connect to the Internet. Works on Simulator but not on Physical Device

This is my first WatchKit App.
I am working on an app that gets an API call to an iCal file and displays the time remaining in two events.
I am working on Xcode 13.4.1 and created an iOS app with watchOS app using swift and storyboard. I set the target deployment for the iOS app to 15.5, which is the highest version of iOS that my current version of Xcode will let me set. The target deployment for the watchOS app is set to version 6.2 because I have an Apple Watch series 2, and the latest version of watchOS is 6.3. I want the app to at least work for my watch. The code for the iOS app and watch app are pretty much the same. The app works as expected on the watch and phone simulators and on my physical iPhone 8 plus. The problem is that the app does not work on my physical watch.
I created a custom class called API_Manager that handles the API call. I handle my API call as a giant string because I am getting an iCal file and parse it later on in the class. url is defined above. Here is my API call for both the phone and watch app:
public func getData() throws{
//api call
do{
// real url is defined above
let url:String = ""
savedData = try String(contentsOf: URL(string: url)!)
} catch{
print("there was an error with the api call")
throw API_Error.apiCallError
}
// parsing data
}
getData() is called in the init function of the API_Manager class.
init() throws {
// checking internet connection
if (!NetworkMonitor.shared.isConnected){
throw API_Error.noInternet
}
do{
try getData()
} catch{
throw API_Error.apiCallError
}
}
I believe the root of the problem is an Internet connection. I check for an Internet connection before the API call, and if there is no Internet connection, then I throw a custom error. I used this video to create a Network_Monitor class. This is the startMonitoring() function which is called in the applicationDidFinishLaunching() in extensionDelegate of the watch app and application didFinishLaunchingWithOptions in the appDelegate.
public func startMonitoring(){
monitor.start(queue: queue)
monitor.pathUpdateHandler = { [weak self] path in
self?.isConnected = path.status == .satisfied
// getting connection type
self?.getConnectionType(path)
}
}
I created a custom error that is thrown throughout the API_Manager if things don't work as expected. Here is how I created the custom errors:
enum API_Error: Error{
case noInternet
case apiCallError
}
extension API_Error: CustomStringConvertible{
public var description:String {
switch self{
case .noInternet:
return "Error: No internet connection"
case .apiCallError:
return "There was an error with the api call"
}
}
}
I created another enum that is used to display text on a label in the Interface Controller if a specific error is thrown.
enum API_Manager_Status{
case active
case noInternet
case apiCallError
case null // if the api manager is nill
}
Here is how I used the two enums in InterfaceController:
main() is called in the awake() function.
func main(){
do{
apiManager = try API_Manager()
apiManagerStatus = .active
} catch API_Error.noInternet{
apiManagerStatus = .noInternet
} catch API_Error.apiCallError{
apiManagerStatus = .apiCallError
} catch{
apiManagerStatus = .null
}
if (apiManagerStatus == .noInternet){
periodLabel.setText("Error")
timeRemaining.setText("No Internet")
print(API_Error.noInternet)
} else if (apiManagerStatus == .apiCallError){
periodLabel.setText("Error")
timeRemaining.setText("API Call")
print(API_Error.apiCallError)
} else if (apiManagerStatus == .null){
periodLabel.setText("Error")
timeRemaining.setText("Other Error")
print("Cought all errors")
} else{
// main code to display time remaining
}
So I guess my questions are:
Why is my Apple Watch not getting an Internet connection?
How can I fix my app if it worked on the simulators and iPhone, but not watch?
Can the Apple Watch do an API call?
Do I need to configure my app to connect with the iPhone, given that my watchOS verison is 6.2?
Sorry if this is a long question. This is my first time posting a question on Stack Overflow. I would appreciate any help. Let me know if I need to add any more information to help clear things up.
I have tried many potential solutions to fix the problem, but none have worked:
I distributed the app on Test Flight and had my dad download it. He has an Apple Watch Series 4 running on watchOS 9.2, and the app still does not work on his watch.
I have restarted my phone and watch multiple times and even tried to run the app with my phone off, so the watch could connect to wifi.
I tried to install a watchOS 9 beta profile on my watch, knowing it would not work, but I still wanted to see what would happen.
I tried setting the build target to watchOS 8.5, which is the highest my version of Xcode would allow, to see if the problem was watchOS 6.2, and if it would work on my dad's watch.
Lastly, I tried adding privacy messages in the Info.plist for both the phone and the watch app. The messages did not display when I re-downloaded the app, but it still did not fix the issue.
(the messages I used were: "Privacy - Nearby Interaction Usage Description" which does not work on watchOS 6.2, but I still kept in the info.plist, and "Privacy - Bluetooth Always Usage Description")
I know Apple Watches are supposed to use the connected phone for Internet and then wifi if a phone is not connected. My watch shows that my phone is connected, and I have tried it with just wifi. I believe my watch can still get an Internet connection because I can still use Siri and even go to the App Store on my watch, which needs an Internet connection. I would appreciate any help. Let me know if I need to add any more information to help clear things up.

In Swift, how to ignore a part of the code when running from an App Extension target?

There's a similar question that works on Objective-C, but I tried the same code in Swift and it never executes, neither in the main app, nor in the action extension.
My situation is similar to the one in the question above, that is, when running from the main app I want to use UIApplication.shared.open to open a link in Safari, but I want to ignore this part of the code on the App Extension.
The problem isn't finding out whether the app is running from an App Extension or not, but ignoring the code when building for the App Extension, so that the compiler does not give me the following error on build:
You could introduce a new Custom Flag (similar to the DEBUG flag) for the extension target. In your Build Settings look for the Custom Flags and add a new one (e.g. "EXTENSION"). Like here in the screenshot, but also do it for release.
In your Code you could then do something like
#if EXTENSION
// here goes code that is only compiled in the extension
#else
// here goes code that is only compiled outside the extension
#endif
Update: Please read the Apple provided documentation on App Extensions:
Some APIs Are Unavailable to App Extensions
Because of its focused role in the system, an app extension is ineligible to participate in certain activities. An app extension cannot:
Access a Application.shared object, and so cannot use any of the methods on that object
- Apple, App Extension Programming Guide
To programmatically find if the it the running extension via code it's really simple, just do this:
let bundleUrl: URL = Bundle.main.bundleURL
let bundlePathExtension: String = bundleUrl.pathExtension
let isAppex: Bool = bundlePathExtension == "appex"
// `true` when invoked inside the `Extension process`
// `false` when invoked inside the `Main process`

Why am I getting a AXServer error when sharing data via Snapchat SDK?

I am using the example xcode project for sharing media between my app and Snapchat directly. I have successfully authenticated my app by this point (it loads bitmoji + user info and I can print the access token). The code that causes the error is invoked after the UIPicker has selected an image:
from line 38 of MediaPickerViewController.swift
fileprivate func shareImage(image: UIImage) {
let snapPhoto = SCSDKSnapPhoto(image: image)
let snapContent = SCSDKPhotoSnapContent(snapPhoto: snapPhoto)
// Send it over to Snapchat. This produced the error below
snapAPI.startSending(snapContent)
}
Then when you pick an image from the gallery:
2020-03-23 17:49:54.487603-0700 CreativeKitSample[20966:5903027]
[AXRuntimeCommon] AX Lookup problem - errorCode:1100 error:
Permission denied portName:'com.apple.iphone.axserver' PID:20969
This is running on my iPhone, debugging over USB. I'm new to Swift development, and my best guess is that my app is developer certificate signed, perhaps it is sandboxed on iOS 13 from communication with prod apps? Or is the AXServer more of a Core UI thing or Accessibility? This project uses Interface Builder/Storyboards. I tried disabling accessibility checkbox on UI elements. I'm at a loss here, searching for AXServer permission errors has not been useful.
some AX errors:
https://github.com/TimOliver/TOCropViewController/issues/402
https://forums.developer.apple.com/thread/120696 (clue that it may be inter-app permission)
https://bugs.webkit.org/show_bug.cgi?id=203618 (sandbox extension?)
I'm an idiot. When I created my Apple Developer Cert and Provisioning Profile, I had a collision with the package identifier, so I changed it in the provisioning settings but neglected to update the snapchat portal's bundle ID. Leaving the question here for anyone else with the same problem.
They could make their error a bit more descriptive...
I got maybe a solution... I'm dismissing before the current View Controller, then present the SFSafariViewController.
Using following Code does help me out! (Xcode 11.3.1, macOS 10.14.6, iOS 13.3)
DispatchQueue.main.async(execute: {
// code goes here
})
// my solution...
DispatchQueue.main.async {
self.present(safariVC, animated: true, completion: nil)
}

Will Firebase Analytics work from the simulator in Xcode?

In an IOS app I have
and I ensured the plist "defeat" entry is not there, and then I have analytics events like
Analytics.logEvent("touchedButton", parameters: nil)
In fact, if I run the app just in the Xcode simulator .. are these events reported to Firebase Analytics and show up?
Or perhaps if you build to an iPhone?
Or does it only work if it's an actual build which has gone through TestFlight?
Surprisingly I couldn't find this info anywhere.
Is it precisely here that such custom events will show:
Yes, both simulator or device will work.
If you haven't already read, read their getting started tutorials, it covers most of it https://firebase.google.com/docs/analytics/ios/start
A couple of points
Make sure when you configure your Firestore settings , you enable analytics
AnalyticsConfiguration.shared().setAnalyticsCollectionEnabled(true)
I do all of this initial settings in AppDelegate
something like
//init Firebase
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
Fabric.with([Crashlytics.self])
let _ = FirebaseConfig.sharedInstance // This is a custom singelton class where I enable the analytics
In Scheme settings for your target you need to add -FIRAnalyticsDebugEnabled
As you can see I have also a disable option there, sometimes analytics goes crazy and spams the console , so I'd like to disable it with . -FIRDebugDisabled
Analytics clusters your events unless you specify it is a custom event.
For example I use following to tag the viewcontroller names
func logEvent(eventTitle:String , eventContent:String)
{
Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
AnalyticsParameterItemID: "AppName-\(eventTitle)" as NSObject,
AnalyticsParameterItemName: eventTitle as NSObject,
AnalyticsParameterContentType: eventContent as NSObject
])
}
But int the firestore these are clustered under select_content section because I used AnalyticsEventSelectContent key when creating the log.
Under main events screen , select_content my view controlers logged with above function
4.There is a specific DebugView in the FirestoreConsole that works with a device, it updates every 60 seconds as long as settings for -FIRAnalyticsDebugEnabled is true in the scheme.
There is a significant delay in the Event Section of Firestore console, I don't know why this happens, but sometimes there is a delay up to 15 - 30 mins. Havent researched that issue yet, it really doesnt bother me.
Just follow https://firebase.google.com/docs/analytics/ios/start
To enable Analytics Debug mode on your development device, specify the following command line argument in Xcode :
-FIRDebugEnabled
It works perfectly for Simulator and device.
Note: For react-native debugging, launch app from xcode with the selected scheme and not with 'yarn ios', then it works perfectly for Simulator also

iOS10 UNNotificationServiceExtension not called

Im implementing new iOS10 extension to use rich notifications. Im trying to test it on push notifications but is not working, I just receive a simple notification and is not going through the extension.
I did all that it's specified in the official sites and some other places:
I have my app up and running with push notifications and the right provisioning profile
I added a new target to my app, a Notification Service Extension
Implemented my own code (it doesn't matter really because is not even entering to the new class)
Also I had to set a provisioning profile for this extension, I just use one with a wildcard, I don't see any documentation specifying if the extension target has to enable push notifications capability, in that case I would need a specific provisioning for this one, at the moment I just use a wildcard prov, anyway it matches (it must match) the profile I use in the app target, and push notifications capability is enabled for the app target only.
I added UNNotificationExtensionCategory and NSExtensionPointIdentifier. Also Im sending the category as part of the push payload from the server.
As I said, I get the notification but never goes through the extension. I see how the OS tries to load the extension but then throws an error with no relevant description to identify the problem:
Dec 31 21:00:00 iPhone SpringBoard(libextension.dylib)[51] <Notice>: calling plugIn beginUsing:
Dec 31 21:00:57 iPhone pkd[86] <Notice>: assigning plug-in com.test.app.NotificationWithAttachmentExtension(1.0) to plugin sandbox
Dec 31 21:03:57 iPhone pkd[86] <Notice>: enabling pid=51 for plug-in com.test.app.NotificationWithAttachmentExtension(1.0) 38BB5FF1-2597-42E0-B950-169DBFA80573 /private/var/containers/Bundle/Application/A8C47706-C0EC-4FB1-ABA7-0118372F6900/testapp.app/PlugIns/NotificationWithAttachmentExtension.appex
Dec 31 21:00:53 iPhone SpringBoard(PlugInKit)[51] <Notice>: plugin com.test.app.NotificationWithAttachmentExtension interrupted
Dec 31 21:03:56 iPhone SpringBoard(PlugInKit)[51] <Notice>: Hub connection error Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.test.app.NotificationWithAttachmentExtension" UserInfo={NSDebugDescription=connection to service named com.test.app.NotificationWithAttachmentExtension}
Jun 29 13:33:36 iPhone SpringBoard(libextension.dylib)[51] <Notice>: PlugInKit error in beginUsing:
Jun 17 23:33:04 iPhone SpringBoard(libextension.dylib)[51] <Notice>: killing invalid plugIn
Dec 31 21:00:00 iPhone SpringBoard(UserNotificationsServer)[51] <Error>: Extension error whilst trying to modify push notification F502-9B36: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.test.app.NotificationWithAttachmentExtension" UserInfo={NSDebugDescription=connection to service named com.test.app.NotificationWithAttachmentExtension}
Dec 31 21:00:00 iPhone SpringBoard(UserNotificationsServer)[51] <Notice>: [com.test.app] Saving notification F502-9B36
Dec 31 21:00:00 iPhone SpringBoard(libextension.dylib)[51] <Notice>: completed calling plugIn beginUsing: for pid: 0
Relevant extension .plist:
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>UNNotificationExtensionCategory</key>
<string>attachmentCategory</string>
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<real>1</real>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
What's wrong or missing?
What also might do the trick is check your deployment target for the extension. Mine was set at 10.2 while the device I was testing on was (still) using 10.1
After altering the deployment target to 10.0 the UNNotificationServiceExtension instance was called perfectly
And if you've done everything correctly, don't forget to attach it to the process.
After running the app that contains the extension:
Set your breakpoint in the extension
Select Debug / Attach to Process by PID or name
Enter the name of the extension target
Trigger the push notification
Finally I have this working correctly, and this is what I remember from this issue.
1) Do not use devices with iOS10 beta version, because one of the problems I had was because I was using a beta version.
2) only the app requres APNS entitlements, this is not required for the privisoning used for the extension.
3) I was using a provisioning profile matching the id of the extension (not wildcard), anyway I cannot confirm if it works fine or not with wildcard.
4) NSExtensionAttributes are not required, just use NSExtensionPointIdentifier and NSExtensionPrincipalClass for the extension .plist. Unless you are using your own layout
5) This is working even using iOS 9 token registration methods.
6) don't forget mutable-content value in the payload coming in the push notification, this is the only mandatory value you need from the server to go through the extension.
I think this covers all the problems I had
If you are using Firebase, then try changing the payload as:
{
“aps” : {
“category” : “SECRET”,
“mutable_content” : true,
“alert” : {
“title” : “Secret Message!”,
“body” : “(Encrypted)”
},
},
“ENCRYPTED_DATA” : “Salted__·öîQÊ$UDì_¶Ù∞è Ω^¬%gq∞NÿÒQùw”
}
The mutable_content field maps to the mutable-content field on APNs.
For more details, go through this link.
Swift 5 Easy way
this is very easy way just do it like this
You must set the deployment target to be the same on all your targets. I wonder why this is not done automatically by XCode :/ .. Apple loves to waste the developers' time smh...
Came here the second time. The first time, this answer helped me, the second time, it didn't. After a lot of (internal) swearing I found out that I had somehow accidentally removed the extension from Embedded Binaries in my main app target. When I added the extension back, my extension would be called again.
So check this:
Click on your app project to the left.
Click on your main app target.
Choose General.
Under Embedded Binaries, make sure your extension is listed, if not, add it.
Had the same problem, what solved it for me was removing the extension from Embedded Binaries and adding it again.
It seems like your plist is mixing 2 plists.
There are 2 extensions in play:
Notification Content Extension - responsible for displaying the content via a view controller
Notification Service Extension - responsible for fetching content in the background before notification is displayed
Here is the plist for Notification Content Extension target:
Here is the plist for Notification Service Extension target:
After trying many of the possible fixes already given without success, it dawned on me that a framework had been mistakenly added to our extension target rather than the unit test target.
Removing the framework and targeting 10.2 allowed my extension to be called once again.
If you are curious as to which framework for whatever reason you can find it here: https://github.com/plu/JPSimulatorHacks
For anyone looking to just trigger a breakpoint in your app’s notification extension, the process is pretty simple in Xcode 11.3:
Select the scheme of the extension (not the parent app scheme).
Select Edit Scheme.
Select the Executable to be your parent app.
Uncheck Debug executable. By unchecking this, you are informing Xcode to debug the extension instead of the parent app. This is key to getting Xcode to stop within the extension’s breakpoints.
Select Automatically under the Launch option.
Run the scheme of the extension (not the parent app).
Send your device a notification. Profit.
Unless some other stuff triggers Xcode’s moodiness, the above settings should ensure that whenever you select and run the extension scheme, the breakpoints within the extension will be hit. You can go back and check Debug executable in step 4 if you want Xcode to stop at breakpoints within the parent app.
Pro-tip: Don’t forget to add "mutable-content": 1 within the notification JSON payload, otherwise you will have a major sad (iOS won’t invoke the extension without that key).
Here’s a picture of my scheme editor dialog for the notification extension:
In my case it was what I forget to select NotificationServiceExtension in Scheme as particular app instead of "my application" where I try to use it. So I've been running "my app" and waited for breakpoints in code of another app (NotificationServiceExtension) and and that's why they never showed up. I tried every suggestions before that.
My issue was two fold. The first probably was that I had set the Info.plist property NSExtensionPrincipalClass to bundle.identifier.NotificationService instead of ProductModuleName.NotificationService. Module name is the default, but I had erroneously changed to the identifier when debugging some other things related to different schemes and different targets.
The second issue was that I tested by running the notification service target. For me it worked much better when running the app target. I saw some other people recommending to use the notification service target to enable debugging. But that works fine when running the app target as well. You'll have to attach the debugger to your notification service manually though.
The way I discovered the above was to create a new test project with minimal code. In hindsight I definitely recommend that approach instead of trying out all the different solutions found on stackoverflow etc
After struggling with this I finally made this work for me by just changing 2 things.
The bundleID of the NotificationServiceExtension target must be a different one. Preferred style com.companyname.appname.notificationservice(whatever). Setting the same bundleID of the app causes a failure in building to device.
The main thing is the deployment target. I was double checking this with app's target but we must also check the deployment target of the newly created NotificationServiceExtenion's target which is by default the latest version. Set that to the minimum iOS version you would like to support but greater than ios 10.
Note: Make sure you have "mutable-content" : 1 in remote payload.
Hope this helps someone.
The public func didReceiveNotificationRequest(request: UNNotificationRequest, withContentHandler contentHandler: (UNNotificationContent) -> Void) method in UNNotificationServiceExtension has changed between swift versions.
Some of the online examples are not up to date.
Make sure the method you're overriding in your custom subclass of UNNotificationServiceExtension
I had:
func didReceive(request: UNNotificationRequest, withContentHandler contentHandler:(UNNotificationContent) -> Void)
which didn't work until i changed to:
func didReceiveNotificationRequest(request: UNNotificationRequest, withContentHandler contentHandler: (UNNotificationContent) -> Void)
Be sure that you have 'None' configuration set. Look at the screenshot. With other parameters didReceive doesn't work.
Configuration
Other possibility is to check category name in plist file of UNNotificationContentExtension.
String value of "UNNotificationExtensionCategory" in plist and "category/click_action" of payload should be same.
I'm currently working on Xcode 12.3 and Swift 5.2, and when I backed off the Main Project Target and Notification Service Extension Target from iOS 14.2 to iOS 13.2, it worked!
The system executes your notification content app extension only when a remote notification’s payload contains the following information:
The payload must include the mutable-content key with a value of 1.
The payload must include an alert dictionary with title, subtitle, or
body information.
Specifying the remote notification payload:
{
“aps” : {
“category” : “SECRET”,
“mutable-content” : 1,
“alert” : {
“title” : “Secret Message!”,
“body” : “(Encrypted)”
},
},
“ENCRYPTED_DATA” : “Salted__·öîQÊ$UDì_¶Ù∞è Ω^¬%gq∞NÿÒQùw”
}
Thanks for all good suggestions, which helped me fixed my mismatch with deployment target and missing mutable-content. But for me I still had issues getting the NSE to be called. I found the solution in the source code from this blog, https://medium.com/gits-apps-insight/processing-notification-data-using-notification-service-extension-6a2b5ea2da17. I was missing Embed App Extensions that copied the extension to my app. Why this was missing, I'm not sure, I have not seen this as a required step in the different tutorials I have read.
The Embed App Extension is just a "Copy File Phase" that has been renamed, as explained here https://stackoverflow.com/a/71031519/3080858
This tutorial was also very helpful setting up the different App Ids, App Groups, Profiles etc:
https://blog.logrocket.com/implement-push-notifications-react-native-onesignal/

Resources