I'm very simply trying to be notified when a Pass is added to a passbook.
One thing to note is that I'm also attempting to use a Pass that I generated using a different apple dev account than what my app is using. I'm trying to figure out if that is part of the problem or not.
This is one VC in a 3-tab application.
ViewController.m:
#interface ViewController ()
{
PKPassLibrary *_passLibrary;
NSArray *_passes;
}
#end
"viewDidLoad":
- (void)viewDidLoad
{
//init passbook
_passLibrary = [[PKPassLibrary alloc] init];
_passes = [_passLibrary passes];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(passLibraryDidChange:) name:PKPassLibraryDidChangeNotification object:_passLibrary];
}
and my notification handler:
- (void)passLibraryDidChange:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"%#", #"passes added");
});
}
When I run the app in the iOS Simulator, all works as expected and I can see the log output to the console in Xcode.
When I run on the device, the notification is not called when the Pass is added. On the device, I can't even list any passes.
What's even more strange, is that when I go to delete the Pass from passbook, then re-enter the app, the notification will be called.
ps: I really hope it's something simple I'm missing here.
EDIT: updated with more information and a more complete code sample
You don't seem to be retaining a handle to your PKPassLibrary instance. Create a strong #property on your UIViewController subclass. Alloc+Init the property and configure the notification listening in the viewDidLoad (so it'll only get done once).
It might be more appropriate to set this on the UIApplication somewhere, but that depends on the app logic and how your ViewController fits into your UI (for example does it get replaced or released while you still need access to PassKit notifications?)…
Nick.
P.S. Any reason you're NSLogging on the GCD main queue so explicitly? Is that just left over from some UI feedback?
P.P.S. Seeing the notification when you re-enter the app makes perfect sense since that's when viewWillAppear will get called and a new PKPassLibrary instance will immediately fire the notification that's waiting. Not sure why it worked in the simulator - must be accidental.
It came down to having the pass type identifiers created from the same source that the provisioning profile came from.
I then had to take the new Pass Certificates and add them to my keychain, then re-create the actual passes using the new pass type identifiers.
Meanwhile, In Xcode..in the summary section of my target in the Entitlements section, I can refresh the Passes and see the new pass type id's come in.
Once the app runs, I now get properly notified of any change (addition/subtraction/etc).
I also needed to make sure my _passLibrary was being properly retained.
Related
I tested:
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
which is for putting app on background and it works.
How do I put app back on foreground?
I tried:
UIControl().sendAction(#selector(URLSessionTask.resume), to: UIApplication.shared, for: nil)
But eventually it crashes...
Thank you
Update:
Since you've indicated that you're looking for any technical solution, even those not compatible with the App Store or Apple's terms, this should be possible using the Private API LSApplicationWorkspace: openApplicationWithBundleID. Try something like this:
Create a .h file and set up an interface to the LSApplicationWorkspace class and list the required method. You will need to #import "PrivateHeaders.h" in your bridging header.
//
// PrivateHeaders.h
//
#ifndef PrivateHeaders_h
#define PrivateHeaders_h
#interface LSApplicationWorkspace : NSObject
- (bool)openApplicationWithBundleID:(id)arg1;
#end
#endif /* PrivateHeaders_h */
You should then be able to call this function and pass in the Bundle Identifier of your app as an string.
//
// SomeClass.swift
//
import MobileCoreServices
let workspace = LSApplicationWorkspace()
/**
Launch an App given its bundle identifier
- parameter bundleIdentifier: The bundle identifier of the app to launch
- returns: True if app is launched, otherwise false
*/
func openApp(withBundleIdentifier bundleIdentifier: String) -> Bool {
// Call the Private API LSApplicationWorkspace method
return workspace.openApplication(withBundleID: bundleIdentifier)
}
Original:
What you are doing is likely a violation of the iOS Human Interface Guidelines (although the "Don’t Quit Programmatically" is no longer specifically defined), so as the comments have said, it is not suited to the App Store. Regardless, once your app is suspended in this way, I don't expect that there is a way to resume it programmatically, unless you can hook into a Background Operation to run URLSessionTask.resume, but I have not tested it and am unsure whether it can work.
Apps can be launched (and hence brought into the foreground) programmatically from another app or today extension by using a Custom URL Scheme, or via a Push Notification. It isn't possible to launch the app from the Background Operation via a URL Scheme, since it is part of the UIKit framework, which must be run in the main thread.
In summary, I think your best option is to try to use a Notification. This just means that the user will need to click on the notification to bring your app back into the foreground.
Closing/opening the app should be done explicitly by the user. Any other way of closing or opening the app is not supported by Apple and will be rejected when uploaded to app store. iOS Human Interface Guideline states:
Don’t Quit Programmatically
Never quit an iOS application
programmatically because people tend to interpret this as a crash.
However, if external circumstances prevent your application from
functioning as intended, you need to tell your users about the
situation and explain what they can do about it. Depending on how
severe the application malfunction is, you have two choices.
*Display
an attractive screen that describes the problem and suggests a
correction. A screen provides feedback that reassures usersthat
there’s nothing wrong with your application. It puts usersin control,
letting them decide whether they want to take corrective action and
continue using your application or press the Home button and open a
different application
*If only some of your application's features are
not working, display either a screen or an alert when people activate
the feature. Display the alert only when people try to accessthe
feature that isn’t functioning
Just as a follow up to Jordan's excellent answer I want to give an explanation for why your code works in the first place and why that alone will get your app rejected, even without any functionality to make it active again and bring it to the foreground.
As maddy pointed out in a comment, you're basically calling a method from UIApplication's private API. This works due to the Objective-C runtime's dynamic linking. You might wonder "But I am using Swift, what does that have to do with Objective-C?" The answer lies in #selector mechanism. A Selector is basically just a symbol that the Objective-C runtime looks up in a table to get a method it invokes (for you). This is why it's technically not correct to say you "call a method" when you do something like myObjectInstance.someMethod(). The correct way to phrase that would be to "send a message" to the object, because that's what is happening in the runtime. The target-action mechanism is build around that. The sendAction(_: Selector?, to: Any?) method does the same thing. So in effect your code does the following:
Get the symbol that corresponds to URLSessionTask's suspend() method.
Tell the shared instance of UIApplication to invoke the method that it has for that symbol.
Now usually that would result in a crash with the typical "unknown selector sent to instance..." error message. But here, by sure coincidence UIApplication also has a method for that instance (or rather, the runtime also has one of its methods listed in its table for that symbol). You kind of "found" a method that is not declared in its public header. You successfully circumvented a compile-time check for this and invoke a method that is part of a private API. This is explicitly forbidden in the Apple Developer Program License Agreement
Besides all that, I would strongly advise against trying to design an app that way in the first place. As maddy pointed out it's also likely considered to violate the HIGs. Even if you're not trying to do anything malicious and properly explain the feature in your app's description, that won't make Apple let it slide (I assume). Personally, as a user, I'd also find it annoying if the app did something the system already has a specific mechanic for in a different manner, at least in terms of app's coming to background and foreground.
I don't think it can be done without user interaction
The option is you can generate a push notification to tell the user to bring the application to foreground
When the operating system delivers push notification and the target application is not running in the foreground, it presents the notification.
If there is a notification alert and the user taps or clicks the action button (or moves the action slider), the application launches and calls a method to pass in the local-notification object or remote-notification payload.
I tested:
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
which is for putting app on background and it works.
How do I put app back on foreground?
I tried:
UIControl().sendAction(#selector(URLSessionTask.resume), to: UIApplication.shared, for: nil)
But eventually it crashes...
Thank you
Update:
Since you've indicated that you're looking for any technical solution, even those not compatible with the App Store or Apple's terms, this should be possible using the Private API LSApplicationWorkspace: openApplicationWithBundleID. Try something like this:
Create a .h file and set up an interface to the LSApplicationWorkspace class and list the required method. You will need to #import "PrivateHeaders.h" in your bridging header.
//
// PrivateHeaders.h
//
#ifndef PrivateHeaders_h
#define PrivateHeaders_h
#interface LSApplicationWorkspace : NSObject
- (bool)openApplicationWithBundleID:(id)arg1;
#end
#endif /* PrivateHeaders_h */
You should then be able to call this function and pass in the Bundle Identifier of your app as an string.
//
// SomeClass.swift
//
import MobileCoreServices
let workspace = LSApplicationWorkspace()
/**
Launch an App given its bundle identifier
- parameter bundleIdentifier: The bundle identifier of the app to launch
- returns: True if app is launched, otherwise false
*/
func openApp(withBundleIdentifier bundleIdentifier: String) -> Bool {
// Call the Private API LSApplicationWorkspace method
return workspace.openApplication(withBundleID: bundleIdentifier)
}
Original:
What you are doing is likely a violation of the iOS Human Interface Guidelines (although the "Don’t Quit Programmatically" is no longer specifically defined), so as the comments have said, it is not suited to the App Store. Regardless, once your app is suspended in this way, I don't expect that there is a way to resume it programmatically, unless you can hook into a Background Operation to run URLSessionTask.resume, but I have not tested it and am unsure whether it can work.
Apps can be launched (and hence brought into the foreground) programmatically from another app or today extension by using a Custom URL Scheme, or via a Push Notification. It isn't possible to launch the app from the Background Operation via a URL Scheme, since it is part of the UIKit framework, which must be run in the main thread.
In summary, I think your best option is to try to use a Notification. This just means that the user will need to click on the notification to bring your app back into the foreground.
Closing/opening the app should be done explicitly by the user. Any other way of closing or opening the app is not supported by Apple and will be rejected when uploaded to app store. iOS Human Interface Guideline states:
Don’t Quit Programmatically
Never quit an iOS application
programmatically because people tend to interpret this as a crash.
However, if external circumstances prevent your application from
functioning as intended, you need to tell your users about the
situation and explain what they can do about it. Depending on how
severe the application malfunction is, you have two choices.
*Display
an attractive screen that describes the problem and suggests a
correction. A screen provides feedback that reassures usersthat
there’s nothing wrong with your application. It puts usersin control,
letting them decide whether they want to take corrective action and
continue using your application or press the Home button and open a
different application
*If only some of your application's features are
not working, display either a screen or an alert when people activate
the feature. Display the alert only when people try to accessthe
feature that isn’t functioning
Just as a follow up to Jordan's excellent answer I want to give an explanation for why your code works in the first place and why that alone will get your app rejected, even without any functionality to make it active again and bring it to the foreground.
As maddy pointed out in a comment, you're basically calling a method from UIApplication's private API. This works due to the Objective-C runtime's dynamic linking. You might wonder "But I am using Swift, what does that have to do with Objective-C?" The answer lies in #selector mechanism. A Selector is basically just a symbol that the Objective-C runtime looks up in a table to get a method it invokes (for you). This is why it's technically not correct to say you "call a method" when you do something like myObjectInstance.someMethod(). The correct way to phrase that would be to "send a message" to the object, because that's what is happening in the runtime. The target-action mechanism is build around that. The sendAction(_: Selector?, to: Any?) method does the same thing. So in effect your code does the following:
Get the symbol that corresponds to URLSessionTask's suspend() method.
Tell the shared instance of UIApplication to invoke the method that it has for that symbol.
Now usually that would result in a crash with the typical "unknown selector sent to instance..." error message. But here, by sure coincidence UIApplication also has a method for that instance (or rather, the runtime also has one of its methods listed in its table for that symbol). You kind of "found" a method that is not declared in its public header. You successfully circumvented a compile-time check for this and invoke a method that is part of a private API. This is explicitly forbidden in the Apple Developer Program License Agreement
Besides all that, I would strongly advise against trying to design an app that way in the first place. As maddy pointed out it's also likely considered to violate the HIGs. Even if you're not trying to do anything malicious and properly explain the feature in your app's description, that won't make Apple let it slide (I assume). Personally, as a user, I'd also find it annoying if the app did something the system already has a specific mechanic for in a different manner, at least in terms of app's coming to background and foreground.
I don't think it can be done without user interaction
The option is you can generate a push notification to tell the user to bring the application to foreground
When the operating system delivers push notification and the target application is not running in the foreground, it presents the notification.
If there is a notification alert and the user taps or clicks the action button (or moves the action slider), the application launches and calls a method to pass in the local-notification object or remote-notification payload.
I am trying to get actionable notifications working and I have gotten the actions to display when the notification is expanded, but I cannot get the delegate function to be called when I select an action. I am declaring self.notificationCenter.delegate = self in application(_:didFinishLaunchingWithOptions:), and, if I'm understanding correctly, when an action is selected it should call userNotificationCenter(_:didReceive:withCompletionHandler:), passing in the UNNotificationResponse object. But that method never gets triggered in my code. Any ideas why that may be happening?
The method you have mentioned above gets called as soon as you receive notification.
The method which is executed after clicking on action on notification is,
application:handleActionWithIdentifier:forRemoteNotification:completionHandler:
I figured out the issue. To handle notifications with content you have to add a service extension as a new target in the project. The last thing I ran was the service extension so the debugger wasn't stopping at breakpoints in the main app. I didn't know service extensions are essentially treated like completely separate apps. Since I was still in the process of working on the content of that method, what was in there was not working and it was hitting the breakpoints, so I thought it was not calling that function like it should.
I'm doing some prefetching of data to reduce waiting time experience. In viewDidLoad of UIViewController A, I initiate an asynchronous function from a downloader class to call to the server to grab some small pieces of data. The downloader class stores this information in some Core Data tables. Then, when UIViewController B comes along, it uses that data to populate a table.
When I was running this on a simulator, it worked without exception. But as soon as I put it on a real phone and untethered the phone from the computer, the app crashed. When I got to VC B, I click on a button to open up that table. That button click would just hang, and the app would never recover. I checked the crash log and got the 0x8badf00d error. I'm doing my testing on an old iPhone 4, which could be part of my problem, but the app needs to run on all iPhone versions, not just newer ones.
OK, so I suspected why this was happening - the data wasn't back yet. As a quick and dirty way to test this, I put in a boolean user default that I set to NO from VC A. Then, when the downloader class got the data, it set that same variable to YES. In VC B in viewDidLoad, I put in a busy loop to make it wait until the value had been set to YES. When I ran the app, it instantly cleared the hanging problem, and the wait time in VC B was instant. The user would never know that this was present.
Despite this momentary success, I think that this approach of mine is terrible! Is there a better way than using user defaults? I don't know how to employ a delegate pattern here, and notifications might not work either. I realize that I am blocking the UI thread, but this is intentional. I can't let the user open that table before there is data available. Is there a more elegant approach that is more consistent with Objective C patterns?
I can post code if it will help. Thank you!
I would probably go with notifications for something like this and do your downloading in the background (I couldn't tell if you were doing it in the background or not).
To run the download code in the background do something like
[[DownloadManager instance] performSelectorInBackground:#selector(doDownload) withObject:nil];
In the doDownload class post a notification when it is done
//Downloading code
...
[[NSNotificationCenter defaultCenter] postNotificationName:#"DownloadsDidFinish" object:self];
In your VC B listen for the notification.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(downloadDidFinish:) name:#"DownloadsDidFinish" object:nil];
In downloadDidFinish: you may want to do a performSelectorOnMainThread depending on if you care if you are on a background thread or not at this. You can also post the notification on the main thread by doing the performSelectorOnMainThread at the time of posting the notification.
You can have ViewController B as a delegate with downloadedStarted and downloadedStopped methods. And you can start and stop spinning wheel animation in these methods:
[busyIndicator startAnimating];
I am trying to save my data when I exit or minimize my application, but it seems like applicationwillenterbackground is never called. Do I need to do anything specific?
I have set the <UIApplicationDelegate> in detailview.h class but still no luck.
By the way, I am using Master-DetailView templete if this is relavent.
Use -applicationDidEnterBackground: to save user data. This should also be done in -applicationWillTerminate: which might get called if the battery dies, I guess. See this question for more info: ApplicationWillTerminate in iOS 4.0
For my app, I extensively researched this issue and I found out that besides implementing both above mentioned methods I had to check whether the device supported multi-tasking, otherwise data would get saved twice (e.g. on an iPhone 3G). So here is how that part of my code looks like:
- (void) applicationDidEnterBackground:(UIApplication*)application
{
// This is called on all iOS 4 devices, even when they don't support multi-tasking.
// But we want to avoid saving data twice, so we check.
if ([[UIDevice currentDevice] respondsToSelector:#selector(isMultitaskingSupported)])
[model serializeEntries];
}
- (void) applicationWillTerminate:(UIApplication*)application
{
// This is always called on devices that don't support multi-tasking,
// but also in low-memory conditions and when the app has to be quit for some
// other reason.
[model serializeEntries];
}