It seems in macOS 10.14 Mojave, the only way to create NSImage instances that automatically draw a light and dark version is via asset catalogs and +[NSImage imageNamed:]. However, I need to create dynamic images at runtime and there doesn't seem to be a way to do so without using private API.
Under the hood, it seems a private property _appearanceName has been introduced to NSImageRep that is somehow used to select the correct representation. It should be straight forward to create an NSImage with image representations that have the corresponding _appearanceName set but I would like to avoid this.
I found a simple workaround (posted below) but it doesn't seem to work correctly when the system appearance is changing (i.e. user is switching from light to dark or vice versa) or when used in view hierarchies that have the appearance property set to different appearances (e.g. one view hard-coded to dark mode, another view hard-coded to light mode).
So, how can I manually create a dynamic NSImage that is correctly showing a light or dark version, like the asset catalog images do?
#implementation NSImage (CustomDynamic)
+ (NSImage *)imageWithLight:(NSImage *)light dark:(NSImage *)dark
{
if (#available(macOS 10.14, *)) {
return [NSImage
imageWithSize:light.size
flipped:NO
drawingHandler:^(NSRect dstRect) {
if ([NSImage appearanceIsDarkMode:NSAppearance.currentAppearance]) {
[dark drawInRect:dstRect];
} else {
[light drawInRect:dstRect];
}
return YES;
}
];
} else {
return light;
}
}
+ (BOOL)appearanceIsDarkMode:(NSAppearance *)appearance
{
if (#available(macOS 10.14, *)) {
NSAppearanceName basicAppearance = [appearance bestMatchFromAppearancesWithNames:#[
NSAppearanceNameAqua,
NSAppearanceNameDarkAqua
]];
return [basicAppearance isEqualToString:NSAppearanceNameDarkAqua];
} else {
return NO;
}
}
#end
D'uh, it turned out the code posted in the question works just fine! The drawing handler is in fact called at appropriate times and does handle all the appearance situations.
However, I had code that scaled and cached those images and it was still using the ancient [image lockFocus]; … [image unlockFocus]; way of drawing images instead of using +[NSImage imageWithSize:flipped:drawingHandler:].
Related
I cannot figure out how to update the background color of my custom NSView when the user switches in and out of dark mode.
I've read the documentation and followed instructions here:detecting darkmode
The strange thing is that I can get all the subviews to behave correctly, but for some strange reason I can't get the background color of the main view to change. The background color of the view looks correct when I start the app in either mode, but when I switch between modes while the app is running it doesn't update to the new theme.
Would be grateful for any suggestions.
Inside the Custom NSView I have the method
- (void) viewDidChangeEffectiveAppearance
{
self.needsDisplay = YES;
}
and inside the drawRect I have a do a simple color change before continuing with drawing in the view
NSAppearance *currentAppearance = [NSAppearance currentAppearance];
if (#available(*, macOS 10.14)) {
if(currentAppearance.name == NSAppearanceNameDarkAqua) {
red = 0.5*red+0.5;
green = 0.5*green+0.5;
blue = 0.5*blue+0.5;
}
}
Here is a screenshot of darkmode before (the way it should look)
Dark Mode Before
Here is a screenshot of light mode after user switch
Light Mode After
Here is a screenshot of lightmode before (the way it should look)
Light Mode Before
And here is a screenshot of darkmode after user switch
Dark Mode After
ps The reason I'm baffled and have little code to post is that the correct behavior is supposed to happen automatically with little effort. I even deleted the view from the nib and rebuilt it thinking maybe some setting got corrupted, but that didn't solve the problem.
Update: I found the source of the problem. This method gets called in windowDidLoad
- (void) setTransparent:(BOOL)transparent
{
if(transparent) {
[self.window setOpaque:NO];
NSColor *backgroundColor = [NSColor windowBackgroundColor];
backgroundColor = [backgroundColor colorWithAlphaComponent: .75];
[self.window setBackgroundColor:backgroundColor];
self.window.alphaValue = 0.75;
}
else {
[self.window setOpaque:YES];
NSColor *backgroundColor = [NSColor windowBackgroundColor];
backgroundColor = [backgroundColor colorWithAlphaComponent: 1];
[self.window setBackgroundColor:backgroundColor];
self.window.alphaValue = 1;
}
}
I get the expected behavior if I comment out the call to this method.
Why did this cause me to lose the automatic behavior of background color change when the user changes between light and dark mode?
My guess is that you’re not actually using the standard color for the background color.
You are using [NSColor windowBackgroundColor], however then making a copy with a different alpha component (via colorWithAlphaComponent), making it no longer a standard color.
My guess is a lot of the automatic ‘just works’ behaviour happens when you use the standard color definitions. As a test, could you try removing the colorWithAlphaComponent calls (where you are adding the transparency) from your settransparent method and see whether it works? if it does, you might need to find another way to add transparency to your view if you want the automatic behaviour.
With OSX 10.10 beta 3, Apple released their dark tint option. Unfortunately, it also means that pretty much all status bar icons (with the exception of Apple's and Path Finder's that I've seen), including mine, remain dark on a dark background. How can I provide an alternate image for when dark tint is applied?
I don't see an API change on NSStatusBar or NSStatusItem that shows me a change, I'm assuming it's a notification or something reactive to easily make the change as the user alters the tint.
Current code to draw the image is encased within an NSView:
- (void)drawRect:(NSRect)dirtyRect
{
// set view background color
if (self.isActive) {
[[NSColor selectedMenuItemColor] setFill];
} else {
[[NSColor clearColor] setFill];
}
NSRectFill(dirtyRect);
// set image
NSImage *image = (self.isActive ? self.alternateImage : self.image);
_imageView.image = image;
}
TL;DR: You don't have to do anything special in Dark Theme. Give NSStatusItem (or NSStatusBarButton) a template image and it will style it correctly in any menubar context.
The reason why some apps' status items (such as PathFinder's) already work in Dark Theme is because they're not setting their own custom view on the StatusItem, but only setting a template image on the StatusItem.
Something like:
_statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
NSImage *image = [NSImage imageNamed:#"statusItemIcon"];
[image setTemplate:YES];
[_statusItem setImage:image];
This works exactly as you'd expect in Mavericks and earlier, as well as Yosemite and any future releases because it allows AppKit to do all of the styling of the image depending on the status item state.
Mavericks
In Mavericks (and earlier) there were only 2 unique styles of the items. Unpressed and Pressed. These two styles pretty much looked purely black and purely white, respectively. (Actually "purely black" isn't entirely correct -- there was a small effect that made them look slightly inset).
Because there were only two possible state, status bar apps could set their own view and easily get the same appearance by just drawing black or white depending on their highlighted state. (But again note that it wasn't purely black, so apps either had to build the effect in the image or be satisfied with a hardly-noticeable out of place icon).
Yosemite
In Yosemite there are at least 32 unique styling of items. Unpressed in Dark Theme is only one of those. There is no practical (or unpractical) way for an app to be able to do their own styling of items and have it look correct in all contexts.
Here are examples of six of those possible stylings:
Status items on an inactive menubar now have a specific styling, as opposed to a simple opacity change as in the past. Disabled appearance is one other possible variation; there are also other additional dimensions to this matrix of possibilities.
API
Arbitrary views set as NSStatusItem's view property have no way to capture all of these variations, hence it (and other related API) is deprecated in 10.10.
However, seed 3 introduces new API on NSStatusItem:
#property (readonly, strong) NSStatusBarButton *button NS_AVAILABLE_MAC(10_10);
This piece of API has a few purposes:
An app can now get the screen position (or show a popover from) a status item without setting its own custom view.
Removes the need for API like image, title, sendActionOn: on NSStatusItem.
Provides a class for new API: i.e. looksDisabled. This allows apps to get the standard disabled/off styling (like Bluetooth/Time Machine when off) without requiring a custom image.
If there's something that can't be done with the current (non- custom view) API, please file an enhancement request for it. StatusItems should provide behavior or appearances in a way that it standard across all status items.
More discussion is at https://devforums.apple.com/thread/234839, although I've summarized most everything here.
I end up did something like following to my custom drag and drop NSStatusItemView: (Using Swift)
var isDark = false
func isDarkMode() {
isDark = NSAppearance.currentAppearance().name.hasPrefix("NSAppearanceNameVibrantDark")
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
isDarkMode()
// Now use "isDark" to determine the drawing colour.
if isDark {
// ...
} else {
// ...
}
}
When the user changed the Theme in System Preferences, the NSView will be called by the system for re-drawing, you can change the icon colour accordingly.
If you wish to adjust other custom UI outside this view, you can either use KVO to observer the isDark key of the view or do it on your own.
I created a basic wrapper around NSStatusItem that you can use to provide support for 10.10 and earlier with custom views in the status bar. You can find it here: https://github.com/noahsmartin/YosemiteMenuBar The basic idea is to draw the custom view into a NSImage and use this image as a template image for the status bar item. This wrapper also forwards click events to the custom view so they can be handled the same way as pre 10.10. The project contains a basic example of how YosemiteMenuBar can be used with a custom view on the status bar.
Newest swift code set image template method is here:
// Insert code here to initialize your application
if let button = statusItem.button {
button.image = NSImage(named: "StatusIcon")
button.image?.isTemplate = true // Just add this line
button.action = #selector(togglePopover(_:))
}
Then it will change the image when dark mode.
When your application has drawn any GUI element you can get its appearance via [NSAppearance currentAppearance] which itself has a name property that holds something like
NSAppearanceNameVibrantDark->NSAppearanceNameAqua->NSAppearanceNameAquaMavericks
The first part is the appearance’s name, which is also available as a constant in NSAppearanceNameVibrantDark or NSAppearanceNameVibrantLight.
I don’t know if there’s a way to get just the first part, but I think this does the trick for now.
Example code:
-(void)awakeFromNib {
NSStatusItem* myStatusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
myStatusItem.title = #"Hello World";
if ([[[NSAppearance currentAppearance] name] containsString:NSAppearanceNameVibrantDark]) {
myStatusItem.title = #"Dark Interface";
} else {
myStatusItem.title = #"Light Interface";
}
}
But just in case you do want to monitor the status changes you can. I also know there is a better way to determine lite/dark mode than what's been said above, but I can remember it right now.
// Monitor menu/dock theme changes...
[[NSDistributedNotificationCenter defaultCenter] addObserver: self selector: #selector(themeChange:) name:#"AppleInterfaceThemeChangedNotification" object: NULL];
//
-(void) themeChange :(NSNotification *) notification
{
NSLog (#"%#", notification);
}
How do I hide the mapview when I have an overlay on top of the mapview in iOS7? This snippet of code used to work in iOS6 but when i upgrade my app to iOS7 it cease to work.
NSArray *views = [[[self.mapView subviews] objectAtIndex:0] subviews];
[[views objectAtIndex:0] setHidden:YES];
Any suggestions or feedback?
With what incanus said with MKTileOverlay, it is like this in the view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *tileTemplate = #"http://tile.stamen.com/watercolor/{z}/{x}/{y}.jpg";
MKTileOverlay *overlay = [[MKTileOverlay alloc] initWithURLTemplate:tileTemplate];
overlay.canReplaceMapContent = YES;
[self.mapView addOverlay:overlay];
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(37.54827, -121.98857)];
self.mapView.delegate = self;
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
{
MKTileOverlayRenderer *renderer = [[MKTileOverlayRenderer alloc] initWithOverlay:overlay];
return renderer;
}
If you need control over how the overlay feeds the data, you need to subclass MKTileOverlay and override loadTileAtPath:result:
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *tile = [self someHowGetTileImageIntoNSDataBaseOnPath:path];
if (tile) {
result(tile, nil);
} else {
result(nil, [NSError errorWithDomain: CUSTOM_ERROR_DOMAIN code: 1 userInfo:nil]);
}
}
The MKOverlay protocol requires boundingMapRect:, which should returns MKMapRect for the rectangular region that this overlay covers. However, I personally found that if I override it myself, it voids the prior canReplaceMapContent = YES setting as Apple probably does not like to show a blank gray map. So I just let MKTileMapOverlay handles it instead.
If your overlay is not actually tiles, then MKTileOverlay does not really apply. But I think you probably can fake it but always reporting nil data within loadTileAtPath:result:, and add your real overlay via another overlay. Another option would be just cover the whole world with black polygon overlay, but then the unsuspecting user would possibly be unknowingly streaming more data than he/she likes.
MapKit isn't really designed for direct access to the map view subviews outside of true overlays (e.g. turning off Apple's map underneath).
Two ideas:
Consider using the new iOS 7 MKTileOverlay class along with the canReplaceMapContent property. This has the effect of turning off Apple's underlying map.
Consider a similar but separate library such as the MapBox iOS SDK which can emulate the look of MapKit but has greater flexibility for styling (and also supports back to iOS 5).
I have no idea why you would want to do it but instead of counting the number of subviews, you should just ask the mapView for the number of overlays it has
if ([[mapView overlays] count] > 0)
{
....
}
Having tried many methods I still haven't found a good and full-proof way of preventing the usual "maps" from being shown behind custom map tiles that I am using. Ultimately I want my app to have a map page consisting only of a custom map.
I am really looking for a solution that is pure iOS and doesn't require any 3rd party software but it would appear difficult.
I have tried 3 methods already:
Number 1, hiding the background map via it's view:
NSArray *views = [[[self.mapView subviews] objectAtIndex:0] subviews];
[[views objectAtIndex:0] setHidden:YES];
this however doesn't work on a certain new operating system coming out very soon! The whole screen goes blank. The Apple Developer Forum hasn't provided a solution either
Number 2, Using another blank overlay (e.g. MKCircle) to cover the background map. This works however when scrolling or zooming out quickly, sometimes the overlay flickers off and you can briefly see the background map behind so not ideal.
Number 3, and this is what I have been working on for a few days now is to simply prevent the user from zooming out. Most documented methods tend to use regionDidChangeAnimated or regionWillChangeAnimated, however these do not seem to suddenly stop the map zooming out when pinching - they wait until the pinch movement has finished before taking effect so again it means the background map can be viewed briefly.
So now I am stumped, unless of course I have missed something with these other two methods.
So any help would be much appreciated!
Add this:
-(void)viewDidAppear:(BOOL)animated
{
MKTileOverlay *overlay = [[MKTileOverlay alloc] init];// initWithURLTemplate:tileTemplate];
overlay.canReplaceMapContent = YES;
[map addOverlay:overlay];
overlay = nil;
}
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *tile =nil;
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay: (id<MKOverlay>)overlay
{
if ([overlay isKindOfClass:[MKTileOverlay class]])
{
MKTileOverlayRenderer *renderer = [[MKTileOverlayRenderer alloc] initWithOverlay:overlay];
[renderer setAlpha:0.5];
return renderer;
}
}
It will replace the map content from the background. It worked very well in my case where I am adding an overlay on the whole map and hiding the real map from the user.
You can't do this in current releases without a third-party library like MapBox. However, the future OS release that you speak of lets you do this.
I tried the new Customizing API for iOS 5 and have some problems I don`t understand. The way I do it:
UITabBar *tabBar = [rootController tabBar];
if ([tabBar respondsToSelector:#selector(setBackgroundImage:)])
{
[tabBar setBackgroundImage:[UIImage imageNamed:#"tabbar_bg.png"]];
tabBar.selectionIndicatorImage = [UIImage imageNamed:#"over.png"];
tabBar.tintColor = [UIColor colorWithRed:56.0/255.0 green:63.0/255.0 blue:74.0/255.0 alpha:1.0];
tabBar.selectedImageTintColor = [UIColor colorWithRed:94.0/255.0 green:102.0/255.0 blue:114.0/255.0 alpha:1.0];
}
The problem is shown on the image below:
The border ist my Problem... and it only occurs if I try to use it with nice ( :P ) colors.. if I try it with white it looks like this:
Do you have any ideas how to fix it?
If you create a subclass of UITabBarItem and implement the methods
- (UIImage *)selectedImage
- (UIImage *)unselectedImage
You can return whatever images you want from these and they won't have any styling effects applied.
Technically these are private methods, but you aren't calling them, you are overriding them, and I've seen plenty of apps use this technique without being rejected.
You can also use a category to override these methods for all tabbaritems in your app. A good trick is to just override selectedImage to return image, like this:
- (UIImage *)selectedImage
{
return self.image;
}
That way, all of your tab bar items will use whatever image you supply without applying any effects for the selectedImage, but will still use the default grey styling for the unselectedImage. Note that this means that you supply an image with colours for the tab bar items, not just a mask image as normal.