NSAppearance is not updating when toggling dark mode - macos

I have a macOS app that runs only in the macOS status bar. I changed the "Application is agent (UIElement)" property in the Info.plist to "YES":
<key>LSUIElement</key>
<true/>
I have a timer that prints out the appearance's name every 5 seconds like this:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
let appearance = NSAppearance.currentDrawing()
print(appearance.name)
}
Problem
The name doesn't actually change when I toggle dark/light mode in system settings. It always prints the name of the appearance that was set when the application launched.
Is there a way to listen to system appearance changes?
Goal
My end goal is actually to draw an NSAttributedString to an NSImage, and use that NSImage as the NSStatusItem button's image.
let image: NSImage = // generate image
statusItem.button?.image = image
For the text in the attributed string I use UIColor.labelColor that is supposed to be based on the system appearance. However it seems to not respect the system appearance change.
When I start the application in Dark Mode and then switch to Light Mode:
When I start the application in Light Mode and then switch to Dark Mode:
Side note
The reason why I turn the NSAttributedString into an NSImage and don't use the NSAttributedString directly on the NSStatusItem button's attributedTitle is because it doesn't position correctly in the status bar.

The problem with drawing a NSAttributedString is, that NSAttributedString doesn't know how to render dynamic colors such as NSColor.labelColor. Thus, it doesn't react on appearance changes. You have to use a UI element.
Solution
I solved this problem by passing the NSAttributedString to a NSTextField and draw that into an NSImage. Works perfectly fine.
func updateStatusItemImage() {
// Use UI element: `NSTextField`
let attributedString: NSAttributedString = ...
let textField = NSTextField(labelWithAttributedString: attributedString)
textField.sizeToFit()
// Draw the `NSTextField` into an `NSImage`
let size = textField.frame.size
let image = NSImage(size: size)
image.lockFocus()
textField.draw(textField.bounds)
image.unlockFocus()
// Assign the drawn image to the button of the `NSStatusItem`
statusItem.button?.image = image
}
React on NSAppearance changes
In addition, since NSImage doesn't know about NSAppearance either I need to trigger a redraw on appearance changes by observing the effectiveAppearance property of the button of the NSStatusItem:
observation = statusItem.observe(\.button?.effectiveAppearance, options: []) { [weak self] _, _ in
// Redraw
self?.updateStatusItemImage()
}

Related

NSPopover application does not always appear where it should

I am working on an NSPopover application. I am using raywenderlich tutorial as a starting place.
The issue I am having is that when the popover opens and the system status bar is closed (such as when using multiple full-screen apps on a laptop) the popover shows in the bottom left of the screen.
Is there a way to force the system status bar to open and stay open while the popover is open?
We had a similar issue, and ended up detecting when the system menubar was minimized:
[NSMenu menuBarVisible]
As far as keeping your window visible, you might look into sharing your window style on the fly using NSBorderlessWindowMask | NSNonactivatingPanelMask
The problem is that when the status bar isn't visible the statusItem / button has a weird position so it's in the left side of the screen.
A possibly solution to this could be to save the position when the popover is first opened and keep showing it relative to that point. In this answer they place the popover relative to an invisible window. This we need because when we display the popover relative to the statusItem / button, the position is weird if the status is not visible.
So if you save the window as a variable and show the popover relative to this you will end up with something like this:
static let popover = NSPopover()
var invisibleWindow: NSWindow!
func showPopover(sender: Any?) {
if let button = AppDelegate.statusItem.button {
if (invisibleWindow == nil) {
invisibleWindow = NSWindow(contentRect: NSMakeRect(0, 0, 20, 1), styleMask: .borderless, backing: .buffered, defer: false)
invisibleWindow.backgroundColor = .red
invisibleWindow.alphaValue = 0
// find the coordinates of the statusBarItem in screen space
let buttonRect:NSRect = button.convert(button.bounds, to: nil)
let screenRect:NSRect = button.window!.convertToScreen(buttonRect)
// calculate the bottom center position (10 is the half of the window width)
let posX = screenRect.origin.x + (screenRect.width / 2) - 10
let posY = screenRect.origin.y
// position and show the window
invisibleWindow.setFrameOrigin(NSPoint(x: posX, y: posY))
invisibleWindow.makeKeyAndOrderFront(self)
}
AppDelegate.popover.show(relativeTo: invisibleWindow.contentView!.frame, of: invisibleWindow.contentView!, preferredEdge: NSRectEdge.minY)
NSApp.activate(ignoringOtherApps: true)
}
}

How can I create a label text with shadow?

I'm trying to create a label text with a shadow created automatically by the system. In iOS I know that you can use an Attributed style for the label, but in OS X I can't find this option. Could you help?
You can do that with Core Animation (a.k.a. CALayer). Views in iOS are layer-backed by default, but NSView on Mac OS X are much older and requires you to turn on layer manually.
The easiest way is to go to the View Effects Inspector (Cmd + Alt + 8):
If you want to add the shadow programmatically:
override func awakeFromNib() {
let shadow = NSShadow()
shadow.shadowOffset = NSMakeSize(5,5)
shadow.shadowBlurRadius = 5
shadow.shadowColor = NSColor.blackColor()
myLabel.superview?.wantsLayer = true
myLabel.shadow = shadow
}

Swift - cycling through background images

I'm completely new to coding and I'm trying to learn Swift. I'm trying to cycle through background images for an app. The images I have are named 1.jpg, 2.jpg, 3.jpg, 4.jpg. When I try run the simulator, the "super.viewDidLoad()" line is highlighted green with the comment, "Thread 1, Breakpoint 3.1." Here's my code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var imageList = [UIImage]()
for i in 1...4 {
let imageName = "\(i).jpg"
var image = UIImage(named:imageName)
imageList.append(image)
}
self.myImageView.animationImages = imageList
self.myImageView.animationDuration = 4.0
self.myImageView.startAnimating()
It sounds like you've set a breakpoint there. If there's a blue pointer to the left of the line, click it to disable it. Or, use the keyboard shortcut command-Y to disable all breakpoints.

UITextField not scrolling horizontally

I am trying to make a small calculator app.
When a UIButton is pressed, the Button title is added to a UITextField.
kind of:
myuitextfield.text = [myuitextfield.text stringByAppendingString:[button currentTitle];
When I reach the end of my textfield, the text gets truncated. How can I disable this, so the textfield starts scrolling automatically and allows adding more characters?
I tried every possible option in Interface Builder, without any luck.
Isn't the UITextField supposed to scroll automatically? I can see this behavior when a native keyboard is used and text is entered.
I have chosen UITextField, as I need only 1 Line.
To illustrate the Problem:
When I enter text using my custom UIButtons text gets truncated
When I tap the UITextField and enter text using the keyboard I can enter unlimited text and the text is not truncated.
If you are facing this issue on iOS7, I've managed to fix it after been inspired by this post. In my case I had a field for entering an email address and after reaching the edge, the user could carry on typing but the text would be invisible (off-field).
First, add a callback to your UITextField so that you can track a text change to the field:
[self.field addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
Then evaluate the size in pixels of the entered string as it is typed and change the text alignment from left to right when reaching the edge of the field area:
- (void)textFieldDidChange:(NSNotification *)aNotif{
float maxNumPixelsOnScreen = 235; // Change this value to fit your case
CGSize maximumSize = CGSizeMake(maxNumPixelsOnScreen + 10, 1);
NSString *aString = self.field.text;
CGSize stringSize = [aString sizeWithFont:fieldFont
constrainedToSize:maximumSize
lineBreakMode:NSLineBreakByWordWrapping];
self.field.textAlignment = NSTextAlignmentLeft;
if (stringSize.width >= maxNumPixelsOnScreen)
self.field.textAlignment = NSTextAlignmentRight;
}
Note:
self.field is the offending UITextField
maximumSize: I'm adding 10 the the width to be slightly over the limit defined
fieldFont is the UIFont used to render the text field
Hope it helps!
you have to add UITextview and limit the number of lines to 2.Textfield doesnt work with two lines.Textview is same as textfields except the delegates and some properties differ.

IKImageView issue on 10.5.8

I am using Monobjc and Mono to develop an application where the user can zoom in on images. The very first image shows up fine. But when the user navigates to the next image, it shows up blank with a white background. And the subsequent images also show up blank. Now when the same app is executed on 10.6 and 10.7, the images show up perfectly.
There is no additional code that I have written for setting up IKImageView. It is set up in InterfaceBuilder but that's pretty much it.
My code is -
//ImgeData is the binary format of the image.
NSData data = new NSData(ImageData);
NSBitmapImageRep imageRep = new NSBitmapImageRep(data);
NSImage image = new NSImage(new NSSize(imageRep.PixelsWide, imageRep.PixelsHigh));
image.AddRepresentation(imageRep);
image.Retain();
NSData imgDat = image.TIFFRepresentation;
if(imgDat != null)
{
var imgSrc = CGImageSource.CreateWithData(imgDat, null);
var imgRef = CGImageSource.CreateImageAtIndex(imgSrc, 0, null);
if (_ikImageView == null)
{
return;
}
_ikImageView.SetImageImageProperties(imgRef,null);
CGImage.Release(imgRef);
}
UPDATE - I realized that I can see the hand over the blank area. Is it possible the image has gotten hidden ?.
MORE Update - My Window contains NSView which inturn contains IKImageView. Now the entire IKImageView is replaced with a white patch. But if I move the mouse over the area, I see a hand coming up. This tells me that the image is getting rendered but IKImageView is not able to display itself.

Resources