Are React Native macOS menu bar apps possible? - macos

Is there any way to currently create a React Native app that runs in the background within the macOS menu bar?

The question is not recent, but I found it searching for this topic and there is a recent relevant update: with the new desktop efforts from Microsoft, this is now feasible, although still tricky.
Credits to ospfranco for the great work.
The only way to do it is by editing your AppDelegate class and initialize the status button label and its popover content with the Root View content from the React Native application. It's also possible to customize its size, appearance and the button at the bar, but only in Swift code.
func applicationDidFinishLaunching(_ aNotification: Notification) {
let jsCodeLocation: URL
jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "tempomat", initialProperties: nil, launchOptions: nil)
let rootViewController = NSViewController()
rootViewController.view = rootView
popover = NSPopover()
popover.contentSize = NSSize(width: 700, height: 800)
popover.animates = true
popover.behavior = .transient
popover.contentViewController = rootViewController
statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(60))
if let button = self.statusBarItem.button {
button.action = #selector(togglePopover(_:))
button.title = "Tempomat"
}
}
References:
React Native MacOS (Microsoft): https://github.com/microsoft/react-native-macos
Sample code: https://github.com/ospfranco/react-native-macos-menubar-template
Blog post: https://ospfranco.github.io/post/2020/05/23/how-to-make-a-react-native-menu-bar-app-for-mac-os/

As of now, the best way to do this is using the Electron platform.

Related

Open and bring new window to front when pressing NSMenuItem

I have a macOS menu application that is running as an agent (LSUIElement).
I need it to have a companion settings window. There is a "settings" NSMenuItem in the NSMenu, and the requirement is to open an actual window and bring it to the front when pressed.
The window is SwiftUI driven. Here's how it's working:
// main is an NSMenu
main.addItem(
withTitle: "Settings",
action: #selector(AppDelegate.openSettings),
keyEquivalent: "")
#objc func openSettings() {
let detailView = SettingsWindow(); // Swift UI view
let controller = DetailWindowController(rootView: detailView) // See below
controller.window?.title = "Settings";
controller.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
class DetailWindowController<RootView : View>: NSWindowController {
convenience init(rootView: RootView) {
let hostingController = NSHostingController(rootView: rootView.frame(width: 400, height: 500))
let window = NSWindow(contentViewController: hostingController)
window.setContentSize(NSSize(width: 400, height: 500))
self.init(window: window)
}
}
What actually happens
The current behaviour is that the window opens, however it's always behind whatever other windows are currently in the foreground.
I need it to be in the foreground.
NSApp.activate(ignoringOtherApps: true) in the code above is an attempt to achieve this, but that didn't work.
Any help would be amazing. Many thanks.
Figured it out in the end. I needed these 3. The activation policy should be better managed, but for what it's worth here's what works.
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
settingsWindow!.window?.orderFrontRegardless()
In context
#objc func openSettings() {
if(settingsWindow == nil) {
let detailView = ActionWindow();
settingsWindow = DetailWindowController(rootView: detailView)
settingsWindow!.window?.title = "Cloud Brains - Settings";
settingsWindow!.showWindow(nil)
}
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
settingsWindow!.window?.orderFrontRegardless()
}

iPad: White text on one side of the status bar, black text on the other

The Home app on iPadOS 14 displays black text on the left side of the status bar, and white text on the right side. How is this achieved? Can it be done via public APIs?
Can it be done via public APIs?
Yes, it's all defined in UIBarStyle. Since each uiviewcontroller added to a splitview is embedded into its own UINavigationController, you can set the barstyle for each uiviewscontroller.navigationController!.navigationBar.barStyle
For white text, choose .black, and .default is black text.
myHomeViewController.navigationController!.navigationBar.barStyle = .black
you could also create your own viewcontrollers and define they're preferredstatusbarstyle to be .lightContent or .darkContent by overriding the childForStatusBarStyle as per the docs.
You can override the preferred status bar style for a view controller
by implementing the childForStatusBarStyle method.
As per Apples wwdc20 Build for iPad. Home was rebuilt using the new features of UISplitViewController.
Minus the button and fancy background. Home split view is a UISplitViewController initialized with the doubleColumn style. UISplitViewController(style: .doubleColumn)
Example of creating a split view in code only in SceneDelegate.swift of a new project:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
//UISplitViewControllerDelegate
splitViewController.delegate = self
//set which view is what
splitViewController.setViewController(sidebarViewController, for: .primary)
splitViewController.setViewController(myHomeViewController, for: .secondary)
//setup of sidebar and detail controllers
sidebarViewController.navigationController?.navigationBar.prefersLargeTitles = true
myHomeViewController.navigationController?.navigationBar.prefersLargeTitles = true
sidebarViewController.title = "Home"
myHomeViewController.title = "Home"
//appear side by side when a column is shown, use tile style.
splitViewController.preferredSplitBehavior = .tile
//'color' myHomeViewController statusbar to white, by setting barStyle = .black
myHomeViewController.navigationController!.navigationBar.barStyle = .black
//start styling your navigation controllers, As i've set preferesLargeTitles, style the navBars largeTitleTextAttributes
myHomeViewController.navigationController!.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
//style viewcontroller
myHomeViewController.view.backgroundColor = .blue
// always have primary and secondary sidebyside
//splitViewController.preferredDisplayMode = .oneBesideSecondary
splitViewController.show(.primary)
splitViewController.show(.secondary)
window = UIWindow(windowScene: windowScene)
window?.rootViewController = splitViewController
window?.makeKeyAndVisible()
}
Other helpful resources I found:
TLDW wwdc20 notes
UISplitViewController apple docs
This can be done only if the root view controller is a UISplitViewController. I've set one up with the new splitview column styles in iOS 14, but the older way should work too.
Then you need to get the navigationController from each viewController and set the barStyle on its navigationBar to .default or .black
After placing your splitViewController as the root viewController of your UIWindow in your scene function, set the style as below:
var embeddedSplitViewController: UISplitViewController?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
self.makeSplitViewController()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = self.embeddedSplitViewController
self.window = window
window.makeKeyAndVisible()
self.embeddedSplitViewController?.viewController(for: .primary)?.navigationController?.navigationBar.barStyle = .black
self.embeddedSplitViewController?.viewController(for: .secondary)?.navigationController?.navigationBar.barStyle = .default
}
}
func makeSplitViewController() {
let splitViewController = UISplitViewController(style: .doubleColumn)
// I have SwiftUI Views here but they could be any UIViewController
let primaryViewController = UIHostingController(rootView: SidebarView())
splitViewController.setViewController(primaryViewController, for: .primary)
splitViewController.setViewController(UIHostingController(rootView: DetailView()), for: .secondary)
let compactViewController = UIHostingController(rootView: SidebarView())
splitViewController.setViewController(compactViewController, for: .compact)
splitViewController.preferredPrimaryColumnWidth = 320
self.embeddedSplitViewController = splitViewController
}
This worked for me, and the statusBar reflected the barStyle and updated accordingly when showing and hiding the primary viewController.

Swift - How to have NSSharingServicePicker pops in a specific button?

I'm learning Swift and creating my first project for OS X. I would like to have the NSSharingServicePicker popover displayed over a button "shareButton" when the user pushed that button.
I create the NSSharingServicePicker object with this code:
var text = "Hello, world!"
var share = NSSharingServicePicker(items: [text])
Now, how I have to customise this code to display the popover?
share.showRelativeToRect(rect: NSRect, ofView: NSView, preferredEdge: NSRectEdge)
I tried this but it shows the popover in a wrong position and the console shows me an error.
let rect = shareButton.frame
let edge : NSRectEdge = NSRectEdge.MaxY
let view = shareButton
share.showRelativeToRect(rect, ofView: view, preferredEdge: edge)
"NSSharingServicePicker showRelativeToRect: ofView: preferredEdge:] should not be called on mouseUp
Please configure the sender with -[NSControl sendActionOn:NSLeftMouseDownMask];"
First of All, to get rid of the NSLeftMouseDownMask warning thing, change the behavior of the button when your app starts up. Put this code within viewDidLoad(), for example.
youShareButton.sendAction(on: NSLeftMouseDownMask)
And this is what I did for displaying the picker view on the button
let shareItems = [aURL]
let sharingPicker:NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems)
let sharingPicker:NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems)
sharingPicker.show(relativeTo: yourShareButton.bounds, of: yourShareButton, preferredEdge: .minY)
Ok, I resolved by replacing:
let rect = shareButton.frame
with
let rect = NSZeroRect

Resizing NSWindow to match view controller size in storyboard

I am working on Xcode 6.1.1 on OSX 10.10. I am trying out storyboards for Mac apps. I have a NSTabViewController using the new NSTabViewControllerTabStyleToolbar tabStyle and it is set as the default view controller for the window controller. How do I make my window resize according to the current selected view controller?
Is it possible to do entirely in Interface Builder?
Here is what my storyboard looks like:
The auto layout answer is half of it. You need to set the preferredContentSize in your ViewController for each tab to the fitting size (if you wanted the tab to size to the smallest size satisfying all constraints).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = view.fittingSize
}
If your constraints are causing an issue below try first with a fixed size, the example below sets this in the tab item's view controller's viewWillAppear function (Swift used here, but the Objective-C version works just as well).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
If that works, fiddle with your constraints to figure out what's going on
This solution for 'toolbar style' tab view controllers does animate and supports the nice crossfade effect. In the storyboard designer, add 'TabViewController' in the custom class name field of the NSTabViewController. Don't forget to assign a title to each viewController, this is used as a key value.
import Cocoa
class TabViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
// Add size of first tab to tabViewSizes
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
super.viewDidLoad()
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(from: fromViewController, to: toViewController, options: [.crossfade, .allowUserInteraction], completionHandler: completion)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title, let window = view.window else {
return
}
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
}
else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}
The window containing a toolbar style tab view controller does resize without any code if you have auto layout constraints in your storyboard tab views (macOS 11.1, Xcode 12.3). I haven't tried other style tab view controllers.
If you want to resize with animation as in Finder, it is sufficient to add one override in your tab view controller. It will resize the window with system-calculated resize animation time and will hide the tab view during resize animation:
class PreferencesTabViewController: NSTabViewController {
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
guard let window = view.window else {
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
return
}
let fromSize = window.frame.size
let toSize = window.frameRect(forContentRect: toViewController.view.frame).size
let widthDelta = toSize.width - fromSize.width
let heightDelta = toSize.height - fromSize.height
var toOrigin = window.frame.origin
toOrigin.x += widthDelta / 2
toOrigin.y -= heightDelta
let toFrame = NSRect(origin: toOrigin, size: toSize)
NSAnimationContext.runAnimationGroup { context in
context.duration = window.animationResizeTime(toFrame)
view.isHidden = true
window.animator().setFrame(toFrame, display: false)
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
} completionHandler: { [weak self] in
self?.view.isHidden = false
}
}
}
Please adjust closure syntax if you are using Swift versions older than 5.3.
Use autolayout. Set explicit size constraints on you views. Or once you have entered the UI into each tab view item's view set up the internal constraints such that they force view to be the size you want.

How to present a modal atop the current view in Swift

(Xcode6, iOS8, Swift, iPad)
I am trying to create a classic Web-like modal view, where the outside of the dialog box is "grayed-out." To accomplish this, I've set the alpha value of the backgroundColor of the view for the modal to 0.5, like so:
self.view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
The only problem is that when the modal becomes full-screen, the presenting view is removed. (Ref Transparent Modal View on Navigation Controller).
(A bit irritated at the concept here. Why remove the underlying view? A modal is, by definition, to appear atop other content. Once the underlying view is removed, it's not really a modal anymore. it's somewhere between a modal and a push transition. Wa wa wa... Anyway..)
To prevent this from happening, I've set the modalPresentationStyle to CurrentContext in the viewDidLoad method of the parent controller, and in Storyboard... but no luck.
self.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
How do I prevent the presenting view from being removed when the modal becomes full screen?
tyvm.. more info below.
Also in Storyboard, like so (Presentation: Current Context)
Thx for your help... documentation below:
First, remove all explicit setting of modal presentation style in code and do the following:
In the storyboard set the ModalViewController's modalPresentation style to Over Current context
Check the checkboxes in the Root/Presenting ViewController - Provide Context and Define Context.
They seem to be working even unchecked.
You can try this code for Swift:
let popup : PopupVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopupVC") as! PopupVC
let navigationController = UINavigationController(rootViewController: popup)
navigationController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.presentViewController(navigationController, animated: true, completion: nil)
For swift 4 latest syntax using extension:
extension UIViewController {
func presentOnRoot(`with` viewController : UIViewController){
let navigationController = UINavigationController(rootViewController: viewController)
navigationController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(navigationController, animated: false, completion: nil)
}
}
How to use:
let popup : PopupVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopupVC") as! PopupVC
self.presentOnRoot(with: popup)
The only problem I can see in your code is that you are using CurrentContext instead of OverCurrentContext.
So, replace this:
self.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
for this:
self.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
This worked for me in Swift 5.0. Set the Storyboard Id in the identity inspector as "destinationVC".
#IBAction func buttonTapped(_ sender: Any) {
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let destVC = storyboard.instantiateViewController(withIdentifier: "destinationVC") as! MyViewController
destVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
destVC.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(destVC, animated: true, completion: nil)
}
The problem with setting the modalPresentationStyle from code was that you should have set it in the init() method of the presented view controller, not the parent view controller.
From UIKit docs: "Defines the transition style that will be used for this view controller when it is presented modally. Set
this property on the view controller to be presented, not the presenter. Defaults to
UIModalTransitionStyleCoverVertical."
The viewDidLoad method will only be called after you already presented the view controller.
The second problem was that you should use UIModalPresentationStyle.overCurrentContext.
The only way I able to get this to work was by doing this on the presenting view controller:
func didTapButton() {
self.definesPresentationContext = true
self.modalTransitionStyle = .crossDissolve
let yourVC = self.storyboard?.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
let navController = UINavigationController(rootViewController: yourVC)
navController.modalPresentationStyle = .overCurrentContext
navController.modalTransitionStyle = .crossDissolve
self.present(navController, animated: true, completion: nil)
}
I am updating a simple solution. First add an id to your segue which presents modal. Than in properties change it's presentation style to "Over Current Context". Than add this code in presenting view controller (The controller which is presenting modal).
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let Device = UIDevice.currentDevice()
let iosVersion = NSString(string: Device.systemVersion).doubleValue
let iOS8 = iosVersion >= 8
let iOS7 = iosVersion >= 7 && iosVersion < 8
if((segue.identifier == "chatTable")){
if (iOS8){
}
else {
self.navigationController?.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
}
}
}
Make sure you change segue.identifier to your own id ;)

Resources