I am showing the user location on a mapView with:
self.mapView.showsUserLocation = YES;
The user gets prompted the AlertView where he can choose whether to allow to use the current location or not.
If he presses yes everything is ok and I do not worry about it.
But if he presses NO I would like to zoom to a specific region.
So how do I know whether the MKMapView is allowed to use the current location?
I found the solution where I would create my own CLLocationManager and its delegate to see if it returns an denied error.
But this does not quite feel right, why introduce a new CLLocationManger if I do not need it.
Isn't there an other way?
You don't need a delegate. Just use the CLLocationManager class method authorizationStatus:
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized) {
// allowed
} else {
// not allowed
}
The possible values are:
typedef enum {
kCLAuthorizationStatusNotDetermined = 0,
kCLAuthorizationStatusRestricted,
kCLAuthorizationStatusDenied,
kCLAuthorizationStatusAuthorized
} CLAuthorizationStatus;
Related
In macOS 10.14 users can choose to adopt a system-wide light or dark appearance and I need to adjust some colours manually depend of the current mode.
Since the actual appearance object you usually get via effectiveAppearance is a composite appearance, asking for its name directly probably isn't a reliable solution.
Asking for the currentAppearance usually isn't a good idea, either, as a view may be explicitly set to light mode or you want to know whether a view is light or dark outside of a drawRect: where you might get incorrect results after a mode switch.
The solution I came up with looks like this:
BOOL appearanceIsDark(NSAppearance * appearance)
{
if (#available(macOS 10.14, *)) {
NSAppearanceName basicAppearance = [appearance bestMatchFromAppearancesWithNames:#[
NSAppearanceNameAqua,
NSAppearanceNameDarkAqua
]];
return [basicAppearance isEqualToString:NSAppearanceNameDarkAqua];
} else {
return NO;
}
}
You would use it like appearanceIsDark(someView.effectiveAppearance) since the appearance of a specific view may be different than that of another view if you explicitly set someView.appearance.
You could also create a category on NSAppearance and add a - (BOOL)isDark method to get someView.effectiveAppearance.isDark (better chose a name that is unlikely to be used by Apple in the future, e.g. by adding a vendor prefix).
I have used the current appearance checking if the system is 10.14
+ (BOOL)isDarkMode {
NSAppearance *appearance = NSAppearance.currentAppearance;
if (#available(*, macOS 10.14)) {
return appearance.name == NSAppearanceNameDarkAqua;
}
return NO;
}
And to detect the change of mode in a view the methods are:
- (void)updateLayer;
- (void)drawRect:(NSRect)dirtyRect;
- (void)layout;
- (void)updateConstraints;
And to detect the change of mode in a view controller the methods are:
- (void)updateViewConstraints;
- (void)viewWillLayout;
- (void)viewDidLayout;
Using notification:
// Monitor menu/dock theme changes...
[NSDistributedNotificationCenter.defaultCenter addObserver:self selector:#selector(themeChanged:) name:#"AppleInterfaceThemeChangedNotification" object: nil];
-(void)themeChanged:(NSNotification *) notification {
NSLog (#"%#", notification);
}
For more information Dark Mode Documentation
Swift 4
func isDarkMode(view: NSView) -> Bool {
if #available(OSX 10.14, *) {
return view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
return false
}
For me neither of these answers worked, if I wanted a global state, not per view, and I didn't have access to the view, and I wanted to be notified for updates.
The solution was to ask for NSApp.effectiveAppearance in the main thread, or at least after the current callback method has returned to the system.
So, first I have to register, following the directions of Saúl Moreno Abril, with a code like
[NSDistributedNotificationCenter.defaultCenter addObserver:self selector:#selector(themeChanged:) name:#"AppleInterfaceThemeChangedNotification" object: nil];
then on the callback method write something like
-(void)themeChanged:(NSNotification *) notification {
[self performSelectorOnMainThread:#selector(themeChangedOnMainThread) withObject:nil waitUntilDone:false];
}
and then the actual code:
- (void) themeChangedOnMainThread {
NSAppearance* appearance = NSApp.effectiveAppearance;
NSString* name = appearance.name;
BOOL dark = [appearance bestMatchFromAppearancesWithNames:#[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]] == NSAppearanceNameDarkAqua;
}
Also the answer from Borzh helped, but is seemed more fragile than the others.
There are actually 8 possible appearances for a view, and 4 of them are for ordinary use. That is,
NSAppearanceNameAqua the Light Mode,
NSAppearanceNameDarkAqua the Dark Mode,
NSAppearanceNameAccessibilityHighContrastAqua Light Mode with increased contrast (set from Accessibility),
NSAppearanceNameAccessibilityHighContrastDarkAqua Dark Mode with increased contrast.
A direct comparison
appearance.name == NSAppearanceNameDarkAqua;
may fail to detect the dark mode if it is with increased contrast. So, always use bestMatchFromAppearancesWithNames instead.
And it is even better to take account of the high-contrast appearances for better accessibility.
To know if the app appearance is Dark, use next code:
+ (BOOL)isDarkMode {
NSString *interfaceStyle = [NSUserDefaults.standardUserDefaults valueForKey:#"AppleInterfaceStyle"];
return [interfaceStyle isEqualToString:#"Dark"];
}
When first calling requestAuthorizationToShareTypes:readTypes:completion: of the HKHealthStore with the set of HKQuantityType I want permissions to, I can see this modal view requesting authorization from the user to read and share every type of object for which the application may require access.
I'm trying to figure out if there is a way to prompt the user this modal view, besides the first time i'm calling: requestAuthorizationToShareTypes:readTypes:completion:.
I tried calling the requestAuthorizationToShareTypes:readTypes:completion: every time with the same set but after the first call its not prompting anymore. When trying to change the set with a new type that wasn't there before I can successfully prompt this screen, but I don't think calling this method each time with a new HKQuantityType in the set is the right way (as there is a limit to the amount of types exists).
Is it even possible?
Thanks for any help whatsoever.
UPDATE
I'll add some code snippet of the call:
[self.healthStore requestAuthorizationToShareTypes:writeDataTypes readTypes:readDataTypes completion:^(BOOL success, NSError *error) {
if (!success) {
NSLog(#"You didn't allow HealthKit to access these read/write data types. The error was: %#.", error);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// Update the user interface based on the current user's health information.
});
}];
where writeDataTypesz and readDataTypes are NSSet returned from the following methods:
// Returns the types of data that I want to write to HealthKit.
- (NSSet *)dataTypesToWrite
{
HKQuantityType *heightType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeight];
HKQuantityType *weightType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyMass];
return [NSSet setWithObjects: heightType, weightType, nil];
}
// Returns the types of data that I want to read from HealthKit.
- (NSSet *)dataTypesToRead
{
HKQuantityType *heightType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeight];
HKQuantityType *weightType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyMass];
HKCharacteristicType *birthdayType = [HKCharacteristicType characteristicTypeForIdentifier:HKCharacteristicTypeIdentifierDateOfBirth];
HKCharacteristicType *biologicalSex = [HKCharacteristicType characteristicTypeForIdentifier:HKCharacteristicTypeIdentifierBiologicalSex];
return [NSSet setWithObjects:heightType,weightType,birthdayType,biologicalSex, nil];
}
Changing the set to include NEW types each time I call requestAuthorizationToShareTypes:readTypes:completion: results in opening this view with all the types that I ever asked permissions to (not necessarily in this specific call):
By design, it is not possible to re-prompt the user for authorization. If you would like the user to consider changing their authorizations for your app, ask them to go to Settings or the Health app to do so.
By the time being we can't do it. However, it makes sense to be able to do so because switching the access rights is not as simple as tap the "ok" or "cancel" button. You may assume that everyone knows how to tap the switch and turn things on, but trust me, you'll be wrong if you assume that way.
I'd file a bug in apple's radar to let them know that their design is not flawless. We should be able to prompt the user again. If user is annoyed, he can always delete our app, and I feel it's much better than reading a really long "how to enable the access in Health.app" list and getting confused, and that's what a normal user would do.
I have an NSOpenPanel with an accessoryView; in this view the user chooses a couple of radio button to change the allowed types. When the panel opens, the right files are enabled, the other disabled. Ok, good.
Now the user changes the radio buttons, the viewController of the accessoryView observe the changes in the radio button matrix and changes consequently the allowedTypes of the NSOpenPanel.
After that, following Apple documentation, it calls -validateVisibleColumns, but nothing visible changes in the panel. That is: the right files seems disabled: I can choose them but they are in grey!
Another wrong effect: I select a file (enabled), change the file type, the (now wrong) file remains selected, with the OK button enabled: but this is the wrong file type! It seems that the change happens but the interface doesn't know!
My code is (selected is bound to the matrix of radio button):
- (void)observeValueForKeyPath.....
{
NSString *extension = (self.selected==0) ? #"txt" : #"xml";
[thePanel setAllowedFileTypes:#[extension, [extension uppercaseString]]];
[thePanel validateVisibleColumns];
}
I first tried to insert a call
[thePanel displayIfNeeded]
then I tried with
[thePanel contentView] setNeedsDisplay]
with no results. I also tried to implement the panel delegate method panel:shouldEnableURL:, that should be called by validateVisibleColumns: I just found that it was called just once, at the opening of NSOpenPanel.
Can someone have an idea why this happens? I tried all this with sandboxed and not-sandboxed applications, no difference. I'm developing on ML with 10.8 sdk.
Edit
By now the only way to avoid the problem is to implement panel:validateURL:error, but this is called after the user clicked 'open' and it's very bad.
I have the exact same problem, under 10.9, non-sandboxed, and have spent the better part of this DAY trying to find a solution!
After A LOT of tinkering and drilling down through the various classes that make up the NSOpenPanel (well NSSavePanel really) I did find a way to force the underlying table to refresh itself:
id table = [[[[[[[[[[[[_openPanel contentView] subviews][4] subviews][0] subviews][0] subviews][0] subviews][7] subviews][0] subviews][1] subviews][0] subviews][0] subviews][0] subviews][2];
[table reloadData];
Of course, the best way to code this hack would be to walk down the subview list ensuring the right classes are found and eventually caching the end table view for the subsequent reloadData calls.
I know, I know, this is a very ugly kludge, however, I can not seem to find any other answer to fix the issue, other than "file a bug report". Which, from what I can see online people have been doing since 1.8! :(
EDIT:
Here is the code I am now using to make my NSOpenPanel behave correctly under 10.9:
- (id) openPanelFindTable: (NSArray*)subviews;
{
id table = nil;
for (id view in subviews) {
if ([[view className] isEqualToString: #"FI_TListView"]) {
table = view;
break;
} else {
table = [self openPanelFindTable: [view subviews]];
if (table != nil) break;
}
}
return table;
}
- (void) refreshOpenPanel
{
if (_openPanelTableHack == nil)
_openPanelTableHack = [self openPanelFindTable: [[_openPanel contentView] subviews]];
[_openPanelTableHack reloadData];
[_openPanel validateVisibleColumns];
}
This code requires two instance variables _openPanel and _openPanelTableHack to be declared in order to work. I declared _openPanel as NSOpenPanel* and _openPanelTableHack is declared as id.
Now, instead of calling [_openPanel validateVisibleColumns] I call [self refreshOpenPanel] to force the panel to update the filenames as expected. I tried caching the table view when the NSOpenPanel was created, however, it seems that once you "run" the panel the table view changes, so I have to cache it on the first update instead.
Again, this is a GIANT hack, however, I do not know how long it will take Apple to fix the issue with accessory views and the file panels, so for now, this works.
If anyone has any other solutions that are not huge kludges please share! ;)
An implementation in swift of Eidola solution.
Biggest difference is that I search for a NSBrowser (sub)class rather than a specific class name. Tested on 10.10 (not sandboxed).
private weak var panelBrowser : NSBrowser? //avoid strong reference cycle
func reloadBrowser()
{
if let assumedBrowser = panelBrowser
{
assumedBrowser.reloadColumn(assumedBrowser.lastColumn)
}
else if let searchResult = self.locateBrowser(self.panel?.contentView as! NSView)
{
searchResult.reloadColumn(searchResult.lastColumn)
self.panelBrowser = searchResult //hang on to result
}
else
{
assertionFailure("browser not found")
}
}
//recursive search function
private func locateBrowser(view: NSView) -> NSBrowser?
{
for subview in view.subviews as! [NSView]
{
if subview is NSBrowser
{
return subview as? NSBrowser
}
else if let result = locateBrowser(subview)
{
return result
}
}
return nil
}
Edit:
Ok, so the code above will not work all the time. If it's not working and a file is selected (you can see the details/preview), then you have to reload the last to one column instead of the last column. Either reload the last two columns (make sure there are at least 2 columns) or reload all columns.
Second problem: if you reload the column, then you lose the selection. Yes, the selected files/directory will still be highlighted, but the panel will not return the correct URL's.
Now I am using this function:
func reloadBrowser()
{
//obtain browser
if self.panelBrowser == nil
{
self.panelBrowser = self.locateBrowser(self.panel?.contentView as! NSView)
}
assert(panelBrowser != nil, "browser not found")
//reload browser
let panelSelectionPatch = panelBrowser.selectionIndexPaths //otherwise the panel return the wrong urls
if panelBrowser.lastColumn > 0
{
panelBrowser.reloadColumn(panelBrowser.lastColumn-1)
}
panelBrowser.reloadColumn(panelBrowser.lastColumn)
panelBrowser.selectionIndexPaths = panelSelectionPatch
}
Just upgrade to xcode 6.3 (on Yosemite 10.10.3) and its ok. Apple fixed the bug (no more need Eidola Hack).
I'm trying to figure out how to limit my NSDocument based application to one open document at a time. It is quickly becoming a mess.
Has anyone been able to do this in a straightforward & reliable way?
////EDIT////
I would like to be able to prompt the user to save an existing open document and close it before creating/opening a new document.
////EDIT 2
I'm now trying to just return an error with an appropriate message if any documents are opening -- however, the error message is not displaying my NSLocalizedKeyDescription. This is in my NSDocumentController subclass.
-(id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError{
if([self.documents count]){
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:#"Only one document can be open at a time. Please close your document." forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:#"Error" code:192 userInfo:dict];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
It won't be an easy solution, since it's a pretty complex class, but I would suggest that you subclass NSDocumentController and register your own which disables opening beyond a certain number of documents. This will allow you to prevent things like opening files by dropping them on the application's icon in the dock or opening in the finder, both of which bypass the Open menu item.
You will still need to override the GUI/menu activation code to prevent Open... from being available when you have a document open already, but that's just to make sure you don't confuse the user.
Your document controller needs to be created before any other document controllers, but that's easy to do by placing a DocumentController instance in your MainMenu.xib and making sure the class is set to your subclass. (This will cause it to call -sharedDocumentController, which will create an instance of yours.)
In your document controller, then, you will need to override:
- makeDocumentForURL:withContentsOfURL:ofType:error:
- makeUntitledDocumentOfType:error:
- makeDocumentWithContentsOfURL:ofType:error:
to check and see if a document is already open and return nil, setting the error pointer to a newly created error that shows an appropriate message (NSLocalizedDescriptionKey).
That should take care of cases of drag-and-drop, applescript,etc.
EDIT
As for your additional request of the close/save prompt on an opening event, that's a nastier problem. You could:
Save off the information (basically the arguments for the make requests)
Send the -closeAllDocumentsWithDelegate:didCloseAllSelector:contextInfo: with self as a delegate and a newly-created routine as the selector
When you receive the selector, then either clear out the saved arguments, or re-execute the commands with the arguments you saved.
Note that step 2 and 3 might need to be done on delay with performSelector
I haven't tried this myself (the rest I've done before), but it seems like it should work.
Here's the solution I ended up with. All of this is in a NSDocumentController subclass.
- (NSInteger)runModalOpenPanel:(NSOpenPanel *)openPanel forTypes:(NSArray *)extensions{
[openPanel setAllowsMultipleSelection:NO];
return [super runModalOpenPanel:openPanel forTypes:extensions];
}
-(NSUInteger)maximumRecentDocumentCount{
return 0;
}
-(void)newDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(newDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super newDocument:sender];
}
}
- (void)newDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super newDocument:(__bridge id)contextInfo];
}
-(void)openDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super openDocument:sender];
}
}
- (void)openDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super openDocument:(__bridge id)contextInfo];
}
Also, I unfortunately needed to remove the "Open Recent" option from the Main Menu. I haven't figured out how to get around that situation.
When doing CLLocationManager, is there a delegate method that gets called when a user clicked the "Allow" or "Don't allow" prompt that request to use Location?
I tried this but this doesn't get called after a user "Allow" or "Don't allow".
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
Also, is there a variable that will tell me what the user selected?
I tried the below, but that always returns true.
locationManager.locationServicesEnabled
Thank you,
Tee
There is a delegate method for that
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized) {
// user allowed
}
}
[CLLocationManager locationServicesEnabled] only tells your if the location service are enabled on the device.
[CLLocationManager authorizationStatus] returns the actual status you're looking for.
You'll have to implement didFailWithError: method:
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
if ([error domain] == kCLErrorDomain) {
// We handle CoreLocation-related errors here
switch ([error code]) {
// "Don't Allow" on two successive app launches is the same as saying "never allow". The user
// can reset this for all apps by going to Settings > General > Reset > Reset Location Warnings.
case kCLErrorDenied:
case kCLErrorLocationUnknown:
default:
break;
}
} else {
// We handle all non-CoreLocation errors here
}
}
EDIT: Looking at CLLocationManager's reference I've found this:
+ (CLAuthorizationStatus)authorizationStatus
Return Value
A value indicating whether the application is authorized
to use location services.
Discussion The authorization status of a given application is managed
by the system and determined by several factors. Applications must be
explicitly authorized to use location services by the user and
location services must themselves currently be enabled for the system.
This authorization takes place automatically when your application
first attempts to use location services.
locationManager.locationServicesEnabled indicates whether location services is available, but not necessarily mean they are allowed for your app.
Use CLLocationManager.authorizationStatus() if you need to find out the status at a point in time, or implement
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
Take note since iOS 8, the authorization request does not takes place automatically when your application first attempts to use location services. You need to call requestWhenInUseAuthorization() explicitly before you call startUpdatingLocation() on your CLLocationManager instance.
And make sure you have the NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription key in Info.plist, depending on the type of authorization you are after. If these are missing, there are no errors, no logs, no hints, no nothing that will point you in the right direction :)