ios 14 PHPhotoLibrary.requestAuthorization not calling handler - ios

I had a code to request authorization that worked perfectly on iOS <= 13. When iOS 14 launched, PHPhotoLibrary.requestAutorization with Selected Photos stopped triggering the handler
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
DispatchQueue.main.async {
Now the completion is not being called when the user selects Selected Photos. It starts working only after app restart, and more than that, it does not show limited library picker when it does work, so I have to call PHPhotoLibrary.shared().presentLimitedLibraryPicker manualy
I tried doing it the new way with
if #available(iOS 14, *) {
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
switch status {
case .authorized:
...
case .limited:
but it doesn't get called here either
I've been battling with this issue for days now, what could be the problem here? I have a feeling that something might be obstructing the views being called, but this code is being called with a press of a UIButton on a UIViewController inside a UITabBarViewController, so I have no idea.

The limited library picker is displayed only once per session:
By default, the system automatically prompts the user to update their limited library selection once per app life cycle.
Also, application shouldn't asks access for photos library before will be relaunched, to check current access level you should use authorizationStatus(for:).
If status is equal to notDetermined, your application should asks access.
If status is limited, you can ask access to display the picker automatically to the user. Also, to allow the iOS to show the picker automatically, your application's plist should does not contain PHPhotoLibraryPreventAutomaticLimitedAccessAlert with true value.
If your application asked access before in current life cycle, the iOS, will no display the picker. Also, I think, the iOS contains some bug with the picker, and will not called the handler.
Here, you can find example of an application that uses new API.

Related

How to detect result from authorization dialog Core Motion?

I'm working on an app that utilizes Core Motion. During the initial onboarding of the app, it asks the user for permission, and we basically want to update the UI based on the response of that popup (Allowed/Denied). Where for notifications and location services this seems easy to do, it doesn't like a request permission API exists for Core Motion, instead it just triggers the popup when starting updating on a manager like we do now:
let motionManager = CMMotionActivityManager()
motionManager.startActivityUpdates(to: OperationQueue.main) {
// do stuff
}
Ideally I want to be able to detect a change in CMMotionActivityManager.authorizationStatus(), but so far haven't been able to come up with a working solution other than implement a timer that checks this property, which I don't feel is a particularly nice solution.
I tried making either authorizationStatus() or the manager as a whole an observable using Combine but that doesn't seem to trigger any updates.
If the user has denied permission, she cannot then grant permission without going to the Settings app. So you don't need to check on a timer. It should suffice to check at app launch and on notifications applicationWillEnterForeground and didBecomeActiveNotification.

How to check if permission is allowed from settings iOS [duplicate]

This question already has answers here:
Why does viewWillAppear not get called when an app comes back from the background?
(7 answers)
Closed 4 years ago.
I am working on project that needs to load the users contacts. For this I need the contacts information. Thus I am using Contacts Framework. It is easy to use and really very fast also. I am new to iOS so I just used the code snippet of getting contacts information.
What I have done: But I have problem, and that is when the user install the application and go the respective ViewController, the ViewController shows Dialog for permission. There user can Deny and Allow the permission. My app works well when user allows the permission but does not work in other way. So I used a function to check if user has given my app the permission or not.
So I read that when user has not granted the permission we can not do anything. Except we can take him to settings where he can allow the permission and get back the app. here is the code I am using to go to the settings app.
if let appSettings = URL(string: UIApplicationOpenSettingsURLString + Bundle.main.bundleIdentifier!) {
if UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings)
}
}
Problem:
now my problem is critical, and that is I know I can take user to
settings view, now what if user still do not allow the Permission and
just get back to our App, in this case how to check if user has given
us permission or not??
I am new to iOS and swift so please help me through example. I have searched a lot but did not find anything. In Android there are callback and also onResume could be used, but in iOS I used ViewWillAppear thinking as equivalent of onResume but that did not work.
If you had a refresh button you could do something like the following:
#IBAction func refreshButtonPressed(_ sender: UIButton) {
ContactsService.sharedInstance.CNContactStore.requestAccess(to: .contacts, completion: { granted, error in
if !granted {
//TODO: - Do something (e.g)
self.[method to display an access denied alert to the user]
return
} else {
[insert your code here]
}
}
Also, as another user has stated in a reply to your original question: Why does viewWillAppear not get called when an app comes back from the background? may be of interest to you. You may want to call the contents of the ContactsService.sharedInstance.CNContactStore.requestAccess( .... { } block inside a function and hook that up to a listener for the UIApplication.willEnterForegroundNotification
https://developer.apple.com/documentation/contacts
https://developer.apple.com/documentation/contacts/cncontactstore
https://developer.apple.com/documentation/contacts/cncontactstore/1402873-requestaccess

How to check application state under swift UI Test

Some background
I am currently writing a UI Test for a settings pane, and click on buttons to enable certain permissions such as push notifications and location services.
However, if the alert for the permission has been displayed before (regardless of the user allowing or denying access to the permission), the alert will not display again, and will just take the user to the settings app. Unfortunately, these settings do not reset, meaning the first time I run the UI tests, alerts will show; and on all subsequent UI test runs, the buttons will take me to the settings app unless I reset the device before the tests begin.
My issue
Thus, my test needs to know if the app went into the background, and attempt to foreground it to continue the testing:
if app.state == background {
foregroundApp()
}
// continue with other tests
Is there any way to determine if the app is in the background?
What I tried
I researched methods to determine the state of the application (running/background/etc) from a UI test, and was not able to find much. I tried to check whether certain elements exist:
if (app.navigationBars.element.exists) ...
but this gives me runtime errors[1] if the user is taken to the settings page because the app under test is in the background, and the test cannot lookup the navigationBars (or other elements).
I tried using some of the methods from Facebook's private headers for XCUIApplication() and XCUIElement().
XCUIApplication().state always returns 3 no matter what state the app is currently in, and any attempts to call XCUIApplication().resolve() to foreground the app give me the same errors as before[1]
I tried to rewrite the logic to foreground the app before resuming the tests, but methods such as XCUIApplication().launch() kill the app before restarting, which I cannot do. Only siri service seems to work for me, but I cannot access the siri service through the corporate proxy, and modifying proxy permissions is not possible.
Is there any other way to check the app state?
Errors
[1] This error is printed every time I try to do something involving state. I do not call snapshotView anywhere, and thus the suggestion to use afterScreenUpdates is useless.
Failure to get snapshot within 15.0s
Cannot snapshot view (<UIKeyboardImpl: 0x7febcc75d000; frame = (0 0;
414 226); layer = <CALayer: 0x608000625720>>) with
afterScreenUpdates:NO, because the view is not in a window. Use
afterScreenUpdates:YES.`
tl;dr
I need to check whether the app I am UI testing has entered the background (i.e. user pressed the home button). Checking for existence of particular elements such as navigation bars doesn't work, neither do most methods from Facebook's private headers for XCUIApplication/XCUIElement. Foregrounding the app also causes issues, and relaunching the app is not an option; neither is siri service.
You can do this in Swift 4, using XCUIApplication.state, which will give you information about the state of the app - whether it's in the foreground or background etc. however, it's not possible to find this information in Swift 3 and below. Essentially, UI testing in Swift 3 doesn't support leaving the app.

EKEventStore requestAccess() is failing to display the user prompt in iOS 9

I've run into a strange case with requesting Calendar access in iOS 9. The logic works fine on all iOS 10 devices.
In our case, we've added a button on a toolbar that the user taps to add an Event to their Calendar.
So, we don't check for permission to access the calendar until the first time the user taps the "Add to Calendar" button. Like this:
#IBAction func addToCalendarPressed(_ sender: UIBarButtonItem) {
eventStore.requestAccess(to: .event, completion: { (granted, error) in
if (granted) && (error == nil) {
// Present the EKEventEditViewController
} else {
// Do something else (show an alert)
}
})
}
In iOS 10, the app displays the prompt for Calendar access as you'd expect.
In iOS 9.x, the prompt doesn't display, the completion handler fires immediately, and granted returns false.
Worse, even though the "Calendar permission" check appears to have completed, you can't go into Settings and manually enable Calendar access. As far as the device is concerned, the app never made the permission check, so the app never appears under Settings > Privacy > Calendar.
I've tried to move the check into viewWillAppear or viewDidLoad instead of waiting for the user to tap the button, and it still doesn't work. I've pulled some sample apps with the event check and they work in iOS 9. I just haven't been able to figure out why the prompt fails in this instance.
The app also checks for location (but not on this screen), and those work correctly in iOS 9.
We found the issue. Somehow our project didn't have CFBundleDisplayName in the info.plist. At a later point in development, opening the project in Xcode 8 added the property, but left the name blank.
Both the Calendar permission and Photos permission prompts for iOS 9 use that value when displaying the application name in the permission prompt.
The fix was to simply set $(PRODUCT_NAME) for CFBundleDisplayName in our info.plist.

NSUserDefaultsDidChangeNotification and Today Extensions

I am developing an iPhone app with a Today Extension. The app has a Model module that loads from/saves toNSUserDefaults. Since I want this information to be available to both the main app and the extension, I use an app group:
let storage = NSUserDefaults(suiteName: "group.etc.etc.etc...")
Both the app and the extension can access the information without any problem.
The main app occasionally might create a local notification to present to the user. That notification has two actions associated with it (UIUserNotificationAction). One of those actions triggers some code run on the background on the main app. That code changes the NSUserDefaults information and triggers a synchronization. My code is something like this:
func application(application: UIApplication, handleActionWithIdentifier id: String?, forLocalNotification not: UILocalNotification, completionHandler: () -> ()) {
// Interact with model here
// New information gets saved to NSUserDefaults
userDefaultsStorage.synchronize()
completionHandler()
}
Now, on the Today Ext. I naturally observe any changes made to the information on NSUserDefaults so that I can reload the interface on the widget:
override func viewDidLoad() {
super.viewDidLoad()
// ...
NSNotificationCenter.defaultCenter().addObserverForName(NSUserDefaultsDidChangeNotification, object: nil, queue: NSOperationQueue.mainQueue()) { _ in
self.reload()
}
}
Now, here's my issue:
The main app schedules a UILocalNotification. I open the today view and look at my today widget.
When the notification fires, a banner appears on the top of the screen.
I slide down on that banner to reveal the two actions and I select the one that I mentioned earlier (the today widget is still live and on screen).
I know for a fact that the action runs correctly in the background, and that the changes are being made to the information on NSUserDefaults.
However, even though the today widget has been active and on screen all this time, no reload action is triggered. After further investigation, I can confirm that the NSUserDefaultsDidChangeNotification is not being fired (I placed a breakpoint and it did not trigger, and did some other checks as well).
I know the changes are being made by the notification action because if I force a reload of the widget (by closing and opening the today view) the widget updates correctly.
I have seen various tutorials online where the first thing they say is to listen to this notification and update the widget so that "the widget is in sync with NSUserDefaults". But the thing is that AFAICT this notification is absolutely useless! How come??
Note 1: When I change the information on NSUserDefaults from within the today widget the notification fires correctly.
Note 2: Debugging a today widget is absolutely horrible, btw. It is always necessary to tell Xcode to "Attach to process by name..." before it can react to breakpoints and crashes. And iOS is constantly creating a new process for the widget so I have to constantly tell Xcode to attach again.
From doc here:
Cocoa includes two types of notification centers:
The NSNotificationCenter class manages notifications within a single
process. The NSDistributedNotificationCenter class manages
notifications across multiple processes on a single computer.
Apparently the containing app and today extension are different processes, since when you debug today extension you want to attach containing app process, but NSNotificationCenter only work within a single process.
In order to communicate between containing app and extensions, you can use
Darwin Notify Center CFNotificationCenterthat works like NSDistributedNotificationCenter, which is only available for osx.
The idea is use a file inside the group folder that they share. in containing app, you write the data you want to send into the file, then post a CFNotification, which will be received by today extension.
In today extension, use CFNotificationCenterAddObserver to observer the CFNotification, upon receiving it, callback will be called, in which a NSNotification has to be posted due to callback is a C style function and "userInfo" cannot be passed in the CFNotification, after receiving this NSNotification object, it starts to read data from the file, which is used to update the today extension view in Notification center.
You can use this github code to implement force loading the today extension view. It works for me.
Here is a great post on this. http://www.atomicbird.com/blog/sharing-with-app-extensions
Another option is to use setHasContent function. When you schedule a local identifier, set has content to false to hide the view, in handleActionWithIdentifier set it to true to show the view. This way, when you stay in notification center, you will not see the view for a moment, but when you see it, it will be the updated data.
let widgetController = NCWidgetController.widgetController()
widgetController.setHasContent(false, forWidgetWithBundleIdentifier: "YourTodayWidgetBundleIdentifier")
But I think the whole problem is a rare case, which doesn't need to be fixed since you can get the updated data reloading the notification center or switch to notification tab and switch back to today tab.

Resources