when ever a _UIApplicationDidRemoveDeactivationReasonNotification notification is posted
(which is an apple private NSNotification) it holds a NSNumber in it's userInfo,
does anyone know, what does each number mean?
the key is _UIApplicationDeactivationReasonUserInfoKey
all I know so far number 5 means app resigned active for InAppPurchase-verification
(which is an OS message and makes your app inactive until user clicks 'Cancel' or 'Buy')
So I needed this and I got a few values decoded.
I am looking at another similar notification _UIApplicationWillAddDeactivationReasonNotification
with same value key _UIApplicationDeactivationReasonUserInfoKey
here they are (for ios6/7):
// notification/control center overlay
kDeactivationReasonControlOverlay = 1,
// double tapped for the task manager mode
kDeactivationReasonTaskManager = 2,
// app is going inactive, eg: locked, enter background, power off screen, phone call, voice control (not siri)
kDeactivationReasonLockScreen = 3,
// system alerts, eg: privacy permissions, IAP dialogs
kDeactivationReasonSystemAlert = 5,
// siri overlay
kDeactivationReasonSiriOverlay = 6,
However I don't have a test case which i can reproduce 4.
And I don't know if 0 or values above 6 are valid values, if anyone knows please comment!
Related
I have an iOS app where I use only local notifications.
One of the features for this app is that I can create several notifications at once (the number of notifications can be from 1 to 20). Notification trigger is time stamp only.
I always add an attachment to each notification - a picture that is always being presented in the application database.
When a certain time comes, the notification trigger is triggered and this notification is shown to the user, usually once a day.
Sometimes (about 20% of cases) a picture is not displayed in the notification (does not matter if the screen is locked or not). This behavior is present in iOS12 - iOS14.
I did the following checks, which were successful:
no errors occurred while creating and adding an notification to UNUserNotificationCenter
check if a temporary URL image exists before creating UNNotificationAttachment
check if the picture exceeds 10Mb
check if an attachment exists for each pending Notification Requests that has already been added to the notification center
check if there is an access to the attachment of each pending Notification Requests that has already been added to the notification center
I've investigated an interesting case on iOS 13, after restarting the device, no one previously generated notification will show pictures. The debug shows that all UNNotificationAttachments in pending Notification Requests are present, but I do not have access to them. Obviously, not only I but also the OS do not have access. Very strange undocumented behavior.
I assume that over time the OS loses access to the UNNotificationAttachment files, but how to understand this? OS is not coping with its own security?
// how I check access for files in attachments
private func checkSavedAttachments() {
UNUserNotificationCenter.current().getPendingNotificationRequests {
(allScheduled) in
let attachments = allScheduled.map{ $0.content.attachments }.reduce([], +)
var accessDeniedCount = 0
attachments.forEach {
(attach) in
if attach.url.startAccessingSecurityScopedResource() {
print(attach.url)
}
else {
accessDeniedCount += 1
}
attach.url.stopAccessingSecurityScopedResource()
}
if accessDeniedCount > 0 {
fatalError()
}
}
}
Thanks for any help!
I would like to use the didEnterRegion method in association with the RegionBootstrap or MonitorNotifier in my application. Currently I'm using the RegionBootstrap but perhaps the MonitorNotifier is better for my application.
In particular I'm adding an iBeacon parser to the beaconmanager and then setting "Id1" of a region to look for the UUID portion of my iBeacon and setting "Id2" and "Id3" to Null. Though they are set to Null in the Region, I would like to be able to parse the information from those locations upon entering the didEnterRegion method. I'm using "Id2" (Major) and "Id3" (Minor) to provide random identification parameters of the beacons.
This information along with a portion of the data from the UUID would then be sent in a notification to the phone user. When testing, I'm entering the didEnterRegion method but the data that is provided is only that which matches the set region of "Id1". If someone could provide any insight at all, it would be greatly appreciated!
I would also like to receive the didEnterRegion method for the same iBeacon every 10 seconds, but with testing it appeared that once that particular iBeacon was seen once, didEnterRegion wouldn't get a subsequent call again. Any way to clear that the iBeacon was captured so that subsequent captures could happen?
I'm trying to keep the battery usage as low as possible and when using the scanRecord data from a onNonBeaconLEScan to parse the information, I'm noticing significant battery drain even when setting the foreground and background time "BetweenScanPeriod" to something really large. I really only need to see that the iBeacon entered the region and pull the information, then 10 seconds later do it again.
Intended application flow -
User enters region of beacon with matching UUID (ID1)
Beacon information from ID2 and ID3 are parsed and sent along with ID1 to user via notification
10 seconds later user receives another notification with same data
repeat until person leaves region or iBeacon stops transmitting
The simplest way to get the information you need is to enable ranging in the didDetermineStateForRegion callback:
public void didDetermineStateForRegion(int state, Region region) {
beaconManager.startRangingBeaconsInRegion(region);
beaconManager.addRangeNotifier(this);
}
public void didRangeBeaconsInRegion(Region region, List<Beacon> beacons) {
for (Beacon beacon : beacons) {
Identifier id2 = beacon.getId2();
Identifier id3 = beacon.getId3();
// Now do something with id2 and id3
}
}
The didRangeBeaconsInRegion callback will be made every 1100 ms with default settings, but you can change this to be 10 seconds if you wish with a line like this the first time you access the BeaconManager:
beaconManager.setScanPeriod(10000l);
beaconManager.setBetweenScanPeriod(0l);
In terms of battery, if you want to be getting scan updates every 10 seconds, you will be using a lot of battery, because this means doing almost constant bluetooth scans. In the background, you may wish to back off and do a 10 second scan only once every 5 minutes with this:
beaconManager.setBackgroundScanPeriod(10000l);
beaconManager.setBackgroundBetweenScanPeriod(290000l);
BackgroundPowerSaver powerSaver = new BackgroundPowerSaver();
Starting iOS 10.3, Apple is limiting the review prompt (Rate This App) to 3 times a year and it can be turned off in the user's settings.
Q: How do we detect that the 3 times limit has reached or if the user has turned off RTA so in the app I won't show a popup saying: "Did you like the app? If yes, can you write a review? [Yes/No]" because then, if the user taps Yes, nothing will show up.
There is really not much information here from the official documentation: https://developer.apple.com/reference/storekit/skstorereviewcontroller
Although you should call this method when it makes sense in the user experience flow of your app, the actual display of a rating/review request view is governed by App Store policy. Because this method may or may not present an alert, it's not appropriate to call it in response to a button tap or other user action.
Preamble
Asking users if they like the app might lead to your app being rejected. Here is an example:
https://twitter.com/pietbrauer/status/791883047373246464
In case the link dies here is an excerpt of Apples response:
3.2.2 ... your app includes content and features that can manipulate the user reviews or chart rankings on the App Store. Specifically, your app filters user reviews and only directs users who intend to rate your app 4 - 5 stars to complete a rating on the App Store...
I personally believe that this can be a valid tactic if you genuinely try to resolve the users issue, and still give them an opportunity to leave a review afterwards, but the question remains if Apple will see it that way.
Possible solution
Show popup asking the user if they enjoy/like/etc using the app.
Try using [SKStoreReviewController requestReview] to get a review.
Check if the number of windows has changed, indicating that a popup has been shown. The caveat here is that this is not 100% reliable since some other event can cause the number of windows to change.
If the number of windows stays the same use deep linking to forward the user to the app store. The docs for SKStoreReviewController suggest using action=write-review as a query parameter to go directly to the reviews page.
Here is a simple implementation:
// make sure we the current iOS version supports in app reviews
if ([SKStoreReviewController class])
{
NSUInteger windowCount = [UIApplication sharedApplication].windows.count;
[SKStoreReviewController requestReview];
// give the review controller some time to display the popup
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if (windowCount < [UIApplication sharedApplication].windows.count)
{
// assume review popup showed instead of some other system alert
// for example show "thank you"
}
else
{
// open app store to leave review
NSURL *reviewUrl = [NSURL URLWithString:#"{your-app-url}?action=write-review"];
[[UIApplication sharedApplication] openURL:reviewUrl];
}
});
}
Note: I have not submitted this code to the App Store, so this is only theoretical.
Well, you could try to fire the request and see but as long as there's no callback as well as no other official way how to detect whether the rating alert has been displayed at the time you call the requesting method.
There is a way around however – one of the StoreKit classes can be swizzled so you can observe when the Rating dialog is opened.
UIWindow-inspecting ways mentioned around may be useful as well, but swizzling on a method call is probably more reliable.
You can also use some rating managers like AppRating available as a pod, which manage the stuff for you, but only on a naive level by counting the calls and remembering it.
I am using this solution in production code - so far no rejections from Apple:
NSUInteger windowCount = [UIApplication sharedApplication].windows.count;
// SKStoreReviewController only available for >= 10.3, if needed check for class existence
[SKStoreReviewController requestReview];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
BOOL shown = windowCount < [UIApplication sharedApplication].windows.count;
if (shown) {
//popup was shown
}
};
Building on the previous answers to "sniff" the window count for changes, here's a version that's working in Swift 5.4, on iOS 10.3 through 14.4:
func currentWindowCount() -> Int { UIApplication.shared.windows.count }
let initialWindowCount = currentWindowCount()
if #available(iOS 14.0, *) {
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
} else {
SKStoreReviewController.requestReview()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let actuallyShowedPrompt = initialWindowCount < currentWindowCount()
if actuallyShowedPrompt {
// do stuff
}
}
I am writing UI test cases using the new Xcode 7 UI Testing feature. At some point of my app, I ask the user for permission of camera access and push notification. So two iOS popups will show up: "MyApp Would Like to Access the Camera" popup and "MyApp Would Like to Send You Notifications" popup. I'd like my test to dismiss both popups.
UI recording generated the following code for me:
[app.alerts[#"cameraAccessTitle"].collectionViews.buttons[#"OK"] tap];
However, [app.alerts[#"cameraAccessTitle"] exists] resolves to false, and the code above generates an error: Assertion Failure: UI Testing Failure - Failure getting refresh snapshot Error Domain=XCTestManagerErrorDomain Code=13 "Error copying attributes -25202".
So what's the best way of dismissing a stack of system alerts in test? The system popups interrupt my app flow and fail my normal UI test cases immediately. In fact, any recommendations regarding how I can bypass the system alerts so I can resume testing the usual flow are appreciated.
This question might be related to this SO post which also doesn't have an answer: Xcode7 | Xcode UI Tests | How to handle location service alert?
Thanks in advance.
Xcode 7.1
Xcode 7.1 has finally fixed the issue with system alerts. There are, however, two small gotchas.
First, you need to set up a "UI Interuption Handler" before presenting the alert. This is our way of telling the framework how to handle an alert when it appears.
Second, after presenting the alert you must interact with the interface. Simply tapping the app works just fine, but is required.
addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire
The "Location Dialog" is just a string to help the developer identify which handler was accessed, it is not specific to the type of alert.
I believe that returning true from the handler marks it as "complete", which means it won't be called again. For your situation I would try returning false so the second alert will trigger the handler again.
Xcode 7.0
The following will dismiss a single "system alert" in Xcode 7 Beta 6:
let app = XCUIApplication()
app.launch()
// trigger location permission dialog
app.alerts.element.collectionViews.buttons["Allow"].tap()
Beta 6 introduced a slew of fixes for UI Testing and I believe this was one of them.
Also note that I am calling -element directly on -alerts. Calling -element on an XCUIElementQuery forces the framework to choose the "one and only" matching element on the screen. This works great for alerts where you can only have one visible at a time. However, if you try this for a label and have two labels the framework will raise an exception.
Objective - C
-(void) registerHandlerforDescription: (NSString*) description {
[self addUIInterruptionMonitorWithDescription:description handler:^BOOL(XCUIElement * _Nonnull interruptingElement) {
XCUIElement *element = interruptingElement;
XCUIElement *allow = element.buttons[#"Allow"];
XCUIElement *ok = element.buttons[#"OK"];
if ([ok exists]) {
[ok tap];
return YES;
}
if ([allow exists]) {
[allow tap];
return YES;
}
return NO;
}];
}
-(void)setUp {
[super setUp];
self.continueAfterFailure = NO;
self.app = [[XCUIApplication alloc] init];
[self.app launch];
[self registerHandlerforDescription:#"“MyApp” would like to make data available to nearby Bluetooth devices even when you're not using app."];
[self registerHandlerforDescription:#"“MyApp” Would Like to Access Your Photos"];
[self registerHandlerforDescription:#"“MyApp” Would Like to Access the Camera"];
}
Swift
addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
alert.buttons["Allow"].tap()
alert.buttons["OK"].tap()
return true
}
Gosh.
It always taps on "Don't Allow" even though I deliberately say tap on "Allow"
At least
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
allows me to move on and do other tests.
For the ones who are looking for specific descriptions for specific system dialogs (like i did) there is none :) the string is just for testers tracking purposes. Related apple document link : https://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor
Update : xcode 9.2
The method is sometimes triggered sometimes not. Best workaround for me is when i know there will be a system alert, i add :
sleep(2)
app.tap()
and system alert is gone
God! I hate how XCTest has the worst time dealing with UIView Alerts. I have an app where I get 2 alerts the first one wants me to select "Allow" to enable locations services for App permissions, then on a splash page the user has to press a UIButton called "Turn on location" and finally there is a notification sms alert in a UIViewAlert and the user has to select "OK". The problem we were having was not being able to interact with the system Alerts, but also a race condition where behavior and its appearance on screen was untimely. It seems that if you use the alert.element.buttons["whateverText"].tap the logic of XCTest is to keep pressing until the time of the test runs out. So basically keep pressing anything on the screen until all the system alerts are clear of view.
This is a hack but this is what worked for me.
func testGetPastTheStupidAlerts() {
let app = XCUIApplication()
app.launch()
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
app.buttons["TURN ON MY LOCATION"].tap()
}
The string "Allow" is completely ignored and the logic to app.tap() is called evreytime an alert is in view and finally the button I wanted to reach ["Turn On Location"] is accessible and the test pass
~Totally confused, thanks Apple.
The only thing I found that reliably fixed this was to set up two separate tests to handle the alerts. In the first test, I call app.tap() and do nothing else. In the second test, I call app.tap() again and then do the real work.
On xcode 9.1, alerts are only being handled if the test device has iOS 11. Doesn't work on older iOS versions e.g 10.3 etc. Reference: https://forums.developer.apple.com/thread/86989
To handle alerts use this:
//Use this before the alerts appear. I am doing it before app.launch()
let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
//1st alert
_ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
if alwaysAllowButton.exists {
alwaysAllowButton.tap()
return true
}
return false
}
//Copy paste if there are more than one alerts to handle in the app
#Joe Masilotti's answer is correct and thanks for that, it helped me a lot :)
I would just like to point out the one thing, and that is the UIInterruptionMonitor catches all system alerts presented in series TOGETHER, so that the action you apply in the completion handler gets applied to every alert ("Don't allow" or "OK"). If you want to handle alert actions differently, you have to check, inside the completion handler, which alert is currently presented e.g. by checking its static text, and then the action will be applied only on that alert.
Here's small code snippet for applying the "Don't allow" action on the second alert, in series of three alerts, and "OK" action on the remaining two:
addUIInterruptionMonitor(withDescription: "Access to sound recording") { (alert) -> Bool in
if alert.staticTexts["MyApp would like to use your microphone for recording your sound."].exists {
alert.buttons["Don’t Allow"].tap()
} else {
alert.buttons["OK"].tap()
}
return true
}
app.tap()
This is an old question but there is now another way to handle these alerts.
The system alert isn't accessibly from the app context of the app you are launched in, however you can access the app context anyway. Look at this simple example:
func testLoginHappyPath() {
let app = XCUIApplication()
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
In a vacuum with a simulator already launched and permissions already granted or denied, this will work. But if we put it in a CI pipeline where it gets a brand new simulator, all of the sudden it won't be able to find that Username field because there's a notification alert popping up.
So now there's 3 choices on how to handle that:
Implicitly
There's already a default system alert interrupt handler. So in theory, simply trying to typeText on that first field should check for an interrupting event and handle it in the affirmative.
If everything works as designed, you won't have to write any code but you'll see an interruption logged and handled in the log, and your test will take a couple seconds more.
Explicitly via interruptionmonitor
I won't rewrite the previous work on this, but this is where you explicitly set up an interruptionmonitor to handle the specific alert being popped up - or whatever alerts you expect to happen.
This is useful if the built-in handler doesn't do what you want - or doesn't work at all.
Explicitly via XCUITest framework
In xCode 9.0 and above, you can switch between app contexts fluidly by simply defining multiple XCUIApplication() instances. Then you can locate the field you need via familiar methods. So to do this explicitly would look like the following:
func testLoginHappyPath() {
let app = XCUIApplication()
let springboardApp = XCUIApplication(bundleidentifier: "com.apple.springboard")
if springboardApp.alerts[""FunHappyApp" would like permission to own your soul."].exists {
springboardApp.alerts.buttons["Allow"].tap()
}
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
Sounds like the approach to implementing camera access and notifications are threaded as you say, but not physically managed and left to chance when and how they are displayed.
I suspect one is triggered by the other and when it is programatically clicked it wipes out the other one as well (which Apple would probably never allow)
Think of it you're asking for a users permission then making the decision on their behalf? Why? Because you can't get your code to work maybe.
How to fix - trace where these two components are triggering the pop up dialogues - where are they being called?, rewrite to trigger just one, send an NSNotification when one dialogue has been completed to trigger and display the remaining one.
I would seriously discourage the approach of programatically clicking dialogue buttons meant for the user.
How can I set local notifications with out forcing user to open app.
I need my app set a local notification for sunrise and sunset, but I don't want to ask people open app.
I know I can have up to 64 notifications via scheduleLocalNotification, but I need to set it for a year so I should be able to run app in background and set alarms for future sunrises and sunsets in background.
The simple answer is you can't. Your app can't run whenever it wants in the background; it can't schedule a timer to wake itself up to post more notifications when they are due.
The only way you could come close to something like this is by having a server which send a background push notification to your app as a wake-up call when a new batch of 64 notifications are coming close to needed to be posted.
However this would be relying on the fact the user doesn't terminate your app. If the user does then you'd have to send a non-background push notification to the user and hope they click on it to launch your app.
Android Awareness API has recently announced new features that provide a simple solution for your use-case (that avoids you having to explicitly manage location request or computing sunrise times). The way to achieve what you're trying to do is to create and register a TimeFence specified relative to sunrise/sunset.
For example:
// Create TimeFence
AwarenessFence sunriseFence =
TimeFence.aroundTimeInstant(TimeFence.TIME_INSTANT_SUNRISE,
0, 5 * ONE_MINUTE_MILLIS);
// Register fence with Awareness.
Awareness.FenceApi.updateFences(
mGoogleApiClient,
new FenceUpdateRequest.Builder()
.addFence("fenceKey", sunriseFence, myPendingIntent)
.build())
.setResultCallback(new ResultCallback<Status>() {
#Override
public void onResult(#NonNull Status status) {
if (status.isSuccess()) {
Log.i(TAG, "Fence was successfully registered.");
} else {
Log.e(TAG, "Fence could not be registered: " + status);
}
}
});
You will get callbacks when the fence evaluates to TRUE at sunrise, and when it evaluates back to FALSE at 5-min after sunrise.
Please check Fence API code snippets docs for how to add your custom app logic.