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);
}
Related
UITextField has a .placeholder text property, for showing info before text has been added to the field, up until now it's always been clear and visible, but in iOS13 dark mode was introduced and now placeholder text is practically unreadable in a white UITextField (I am explicitly making it white via .backgroundColor = [UIColor whiteColor]).
My question is, what are some practical solutions to fix this throughout my project, I could manually change the placeholder color on any UITextField manually, by simply setting an attributedPlaceholder string, that may take a while, is there a way to disable dark mode settings just on UITextFields specifically but not for other elements?
It turns out Apple has provided a way to override this on various elements (or even your entire app's UIWindow) with the following (Objective-C):
if (#available(iOS 13.0, *)) {
textField.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
I applied it to all UITextFields via swizzle, to turn it off on EVERYTHING in your project, just use this in your appDelegate didFinishLaunching method but replace textField with _window
(IMPORTANT EDIT: with the newest version of xCode _window seems to have been dropped and now app projects create something called a SceneDelegate and the overrideUserInterfaceStyle has to be applied to that somehow, but I'm new to scene delegates and don't know how they work so I can't offer much help there, to disable scenedelegate and return to traditional AppDelegate management of the UIWindow, see here: https://stackoverflow.com/a/57467270/2057171)
in swift
Paste the below code to appdelegate file
if #available(iOS 13.0, *) {
window!.overrideUserInterfaceStyle = .light
}
It will work fine.
I would consider not explicitly setting the text field background to white.
You can more robustly support dark and light mode by using UI Element colors described here: https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors
For one of my text fields I did something like this:
if #available(iOS 13, *) {
self.searchBarTextField.textColor = UIColor.label
self.searchBarTextField.backgroundColor = UIColor.secondarySystemBackground
} else {
self.searchBarTextField.backgroundColor = UIColor(white: 1.0, alpha: 1.0)
}
From the code above, now the background of your textfield will dynamically change when the user changes their light vs dark mode setting. And the text color will change with it. And by placeholder text color will be handled by OS. You could override if you needed: https://developer.apple.com/documentation/uikit/uicolor/3173134-placeholdertext
pls check this
[nameTextField,corpIDTextField,passwordTextField,conformPasswordTextField,genderTextField,YOBTextField,mobileNoTextField].forEach {
$0?.delegate = self
if #available(iOS 13.0, *) {
$0?.overrideUserInterfaceStyle = .light
}
}
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.
My app has an option that allows the user to choose between the standard "full-size" window titlebar/toolbar and the "compact" titlebar/toolbar made available in the NSWindow 10.10 API. Specifically, I'm using the -titleVisibility method to set either NSWindowTitleVisible or NSWindowTitleHidden depending on the user's preference. If the user checks the "Compact Titlebar" checkbox, NSWindowTitleHidden is applied to the window, otherwise, the window uses the default style. When the checkbox value changes, the value is stored in the app's user defaults, and the window is updated/redrawn.
Everything works great until the application is relaunched. Each time the app starts up, the window grows by exactly how much space is saved by switching from the default window style (NSWindowTitleVisible) to the new style (NSWindowTitleHidden). So restarting the app 5 – 6 times will make the window flush with the menubar and the dock, depending on how big the window was when the checkbox was initially checked.
In other words, it doesn't seem like the window's frame is being updated in NSUserDefaults when the property is set. Is there a workaround for this, or am I just overlooking something? Any advice would be muy helpful.
Thanks!
A better (and confirmed working) solution was posted at https://openradar.appspot.com/18510665 by pointum:
The problem is that window size is restored by the system using -[NSWindow setFrameUsingName:] before titleVisibility is set. Solution:
Remove "Autosave Name" value in Interface Builder.
Set it in code right after setting titleVisibility using -[NSWindow setFrameAutosaveName:].
Try setting the titleVisibility property to the number 1 in the User Defined Runtime Attributes
1 is the corresponding value for NSWindowTitleHidden
typedef NS_ENUM(NSInteger, NSWindowTitleVisibility) {
/* The default mode has a normal window title and titlebar buttons. */
NSWindowTitleVisible = 0,
/* The always hidden mode hides the title and moves the toolbar up into the area previously occupied by the title. */
NSWindowTitleHidden = 1,
} NS_ENUM_AVAILABLE_MAC(10_10);
However this would print a message to the console complaining that NSWindow is not key value coding-compliant for the key titleVisibility on OS X versions previous to 10.10
Simple fix for now is to save and restore the window's frame manually, here's how I do it:
In your app delegate, when application terminates, save the window's frame
- (void)applicationWillTerminate:(NSNotification *)notification
{
[[NSUserDefaults standardUserDefaults] setObject:NSStringFromRect(self.windowController.window.frame) forKey:#"WindowFrameKey"];
}
In your window controller's -awakeFromNib method, restore the frame
- (void)awakeFromNib
{
if([NSWindow instancesRespondToSelector:#selector(setTitleVisibility:)])
{
// Hide Titlebar
[self.window setTitleVisibility:NSWindowTitleHidden];
NSString *winFrameString = [[NSUserDefaults standardUserDefaults] stringForKey:#"WindowFrameKey"];
if(winFrameString != nil)
{
NSRect savedRect = NSRectFromString(winFrameString);
if(!NSEqualRects(self.window.frame, savedRect))
{
[self.window setFrame:savedRect display:YES animate:NO];
}
}
}
I know that for elements of classes UIButton and UIBarButtonItem they automatically assume window.tintColor as the main colour, which results of an immediate change in case I set a new tintColor to window at any time in the app.
I was wondering if there is any way to make UILabel elements to follow the same pattern, where once created they automatically assumer its default colour as window.tintColor and if changing window.tintColor at any time within my app runtime would also result in changing the UILabel tintColour automatically?
I hope that makes sense.
UILabels are a subclass of UIView, so when you are running in iOS 7 they will have a tintColor property and will inherit that color from their parent view if their tint color is set to nil (which is default).
From Apple's Documentation:
By default, a view’s tint color is nil, which means that the view uses its parent’s tint. It also means that when you ask a view for its tint color, it always returns a color value, even if you haven’t set one.
However, you also ask "if changing window.tintColor at any time within my app runtime would also result in changing the UILabel tintColour automatically?" Apple advises you to not change the tint color when items are on screen:
In general, it’s best to change a view’s tint color while the view is offscreen.
I would guess this is because there is no guarentee that all the various UI elements will detect the tintColor change and update their visible views. However, the UIView documentation suggests a workaround if you want to update tintColor while your UILables are on screen:
To refresh subview rendering when this property changes, override the tintColorDidChange method.
So just make sure to call tintColorDidChange on any views currently on screen whose tint color should update when the tintColor of their parent view changes.
But why don't your UILabel's update their color?
So the above helps you set and update your various tintColor's, but you're not seeing any effect - why?
Well that has to do with what Apple designed Tint to indicate. From the Human Interface Guidelines:
color gives users a strong visual indicator of interactivity
Apple got rid of borders and gradients around interactive elements and replaced them with color - specifically tintColor. The whole idea behind tintColor is that things users can tap on get it, and things they can't tap on don't.
UILabel is not an interactive element - it is a text description - and so Apple will let you set a tintColor on it (as any UIView has a tintColor) but setting that tintColor will not change how it is drawn.
So what should you do? First, be aware that making more than just buttons take on the tint color could be a poor UI choice for your app - iOS 7 users and Apple app reviewers both will be expecting those rules to be followed.
So are you forced to keep your UILabel free from color then?
No - especially if you do things "right". Apple explains:
In a content area, add a button border or background only if necessary. Buttons in bars, action sheets, and alerts don’t need borders because users know that most of the items in these areas are interactive. In a content area, on the other hand, a button might need a border or a background to distinguish it from the rest of the content.
I would suggest you consider the UI of your app. If you really want your non-intereactive elements to have the same tintColor as your interactive elements, then make sure you use something more, like a border or background color, so your users (and Apple app reviewers) know what is clickable and what is not.
As to how you should update the colors, you can either manually set the textColor property to be whatever color you want, or you'll need to make your own subclass of UILabel that overrides - (void)tintColorDidChange to update the textColor property when notifications are sent out - allowing you to have a UILabel whose text updates to match the tintColor of its parent.
I hope this helps!
Found above explanation to be helpful - particularly the pointer to tintColorDidChange(). However, getting it to work out right didn't work at first, but finally came up with a code example that does. Basically, I had a tableView with cells containing images and labels. The images would update with a change in tintColor, but not the labels - which didn't look right. Better for either both to change or neither change. The code below is for the cell in the tableView. This was written with Swift 4.2.
//
// ImageLabelCell.swift
// -- provides an example of changing "tintColor" of UILabel to behave like other elements.
// -- made problem a bit more complex by having a "selected" cell be in inverse colors - so also deal with background.
//
import UIKit
enum CellTypes: Int, CaseIterable { case cell1, cell2, cell3, cell4
// This type provides a demonstration of a way to associate different titles and images to populate the UILabel and UIImageView in the cell.
var title: String {
return "\(self)".uppercased()
}
var imageName: String {
return "\(self)"
}
}
class ImageLabelCell: UITableViewCell {
#IBOutlet weak var lblString: UILabel?
#IBOutlet weak var imgView: UIImageView!
fileprivate var type = CellTypes.cell1
fileprivate var cellSelected = false
}
extension ImageLabelCell {
func getFgBgColors() -> (UIColor, UIColor) { // get foreground, background colors
let white = UIColor.white
var useTint = UIColor.blue // Use your app color here. Just ensures useTint is not nil.
if let tint = tintColor {
useTint = tint
}
if cellSelected {
return (white, useTint) // Selected cell is white on colored background
} else {
return (useTint, white) // Unselected cell is colored on white background
}
}
func configureCell(type: CellTypes, andSelected selected: Bool) {
// Save properties we may use again later
self.type = type
self.cellSelected = selected
// Set label text and image
lblString?.text = type.title
imgView.image = UIImage(named: type.imageName)
// Set colors
let (fgColor, bgColor) = getFgBgColors()
imgView.tintColor = fgColor
self.contentView.backgroundColor = bgColor
lblString?.textColor = fgColor
}
override func tintColorDidChange() {
// This gets called when the program tint color changes to gray (or back again) such as when popups appear/disappear.
let (fgColor, bgColor) = getFgBgColors()
// NOTE: need to set text color and background color. Imageview can take care of itself.
lblString?.textColor = fgColor
self.contentView.backgroundColor = bgColor
}
}
Simple solution may work for some usecases - use let fakeButton = UIButton(type: .system) which automatically adjust to window.tintColor.
Important thing is to set .system type which automatically match the window.tintColor.
You may also set fakeButton.isEnabled = false to prevent user interacting with the button. However, we didn't set target-action so the button is already the fake one.
I am trying to add a UIButton to the view of a MPMoviePlayerController along with the standard controls. The button appears over the video and works as expected receiving touch events, but I would like to have it fade in and out with the standard controls in response to user touches.
I know I could accomplish this by rolling my own custom player controls, but it seems silly since I am just trying to add one button.
EDIT
If you recursively traverse the view hierarchy of the MPMoviePlayerController's view eventually you will come to a view class called MPInlineVideoOverlay. You can add any additional controls easily to this view to achieve the auto fade in/out behavior.
There are a few gotchas though, it can sometimes take awhile (up to a second in my experience) after you have created the MPMoviePlayerController and added it to a view before it has initialized fully and created it's MPInlineVideoOverlay layer. Because of this I had to create an instance variable called controlView in the code below because sometimes it doesn't exist when this code runs. This is why I have the last bit of code where the function calls itself again in 0.1 seconds if it isn't found. I couldn't notice any delay in the button appearing on my interface despite this delay.
-(void)setupAdditionalControls {
//Call after you have initialized your MPMoviePlayerController (probably viewDidLoad)
controlView = nil;
[self recursiveViewTraversal:movie.view counter:0];
//check to see if we found it, if we didn't we need to do it again in 0.1 seconds
if(controlView) {
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[controlView addSubview:backButton];
} else {
[self performSelector:#selector(setupAdditionalControls) withObject:nil afterDelay:0.1];
}
}
-(void)recursiveViewTraversal:(UIView*)view counter:(int)counter {
NSLog(#"Depth %d - %#", counter, view); //For debug
if([view isKindOfClass:NSClassFromString(#"MPInlineVideoOverlay")]) {
//Add any additional controls you want to have fade with the standard controls here
controlView = view;
} else {
for(UIView *child in [view subviews]) {
[self recursiveViewTraversal:child counter:counter+1];
}
}
}
It isn't the best solution, but I am posting it in case someone else is trying to do the same thing. If Apple was to change the view structure or class names internal to the control overlay it would break. I am also assuming you aren't playing the video full screen (although you can play it fullscreen with embeded controls). I also had to disable the fullscreen button using the technique described here because the MPInlineVideoOverlay view gets removed and released when it is pressed: iPad MPMoviePlayerController - Disable Fullscreen
Calling setupAdditionalControls when you receive the fullscreen notifications described above will re-add your additional controls to the UI.
Would love a more elegant solution if anyone can suggest something other than this hackery I have come up with.
My solution to the same problem was:
Add the button as a child of the MPMoviePlayerController's view;
fade the button in and out using animation of its alpha property, with the proper durations;
handle the player controller's touchesBegan, and use that to toggle the button's visibility (using its alpha);
use a timer to determine when to hide the button again.
By trial-and-error, I determined that the durations that matched the (current) iOS ones are:
fade in: 0.1s
fade out: 0.2s
duration on screen: 5.0s (extend that each time the view is touched)
Of course this is still fragile; if the built-in delays change, mine will look wrong, but the code will still run.