How do I refresh menu items in a Swift app? - swift

I have a menubar app and need to refresh one of the menu items when the user opens the menu.
I have a function that pulls the current IP's being used by the machine and stores them in a variable: addresses. The menu is called via override func awakeFromNib(). We have a timer function that I've tried using to update the addresses variable, but can't figure out how to get the menu itself to update with the new data in the variable.
I've tried using a didSet on addresses to update the variable, and I've added the function that updates addresses to a Timer function, but that doesn't update the menu, only the variable.
Here's the code to load the menu:
override func awakeFromNib() {
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateProcessTimer), userInfo: nil, repeats: true)
statusItem.menu = statusMenu
statusItem.image = icon
// get computer information
let compInfo = ComputerInfo()
let addresses = compInfo.getIPAddresses()
let compName = compInfo.getComputerName()
// present the computer info
if let computerNameMenuItem = self.statusMenu.item(withTitle: "computerName") {
computerNameMenuItem.title = compName ?? "unknown"
}
if let computerIPMenuItem = self.statusMenu.item(withTitle: "ipAddress") {
computerIPMenuItem.title = addresses
}
}
As it stands, it sets the menu items when the app loads, but that's it. I'd like to find a way to update the computerIPMenuItem.title every time the user clicks the menu.
--ADDITIONAL INFO--
The menu class is an NSObject, NSApplicationDelegate that calls a nib file. In the nib we have a menubar with NSMenuItem place-holders with the titles referenced above as computerName and ipAddress. Not sure if that helps clarify why some of the traditional override func calls aren't working.

You need to set statusMenu delegate to self:
statusMenu.delegate = self
Then you can use this handler to update the menu before it is displayed:
func menu(NSMenu, update: NSMenuItem, at: Int, shouldCancel: Bool) -> Bool {
if(update.tag == 1) { //where '1' should be the tag of the menu item you want to update
//update your menu item
update.title = "New Title";
}
}
And the main problem in your code is that you are getting the items by title where instead you should get them by tag so you can retrieve them even when title is updated

Another option will be:
override func layoutSubviews() {
super.layoutSubviews()
updateWhatYouNeed()
}

Related

Create a Window on a status bar app for macOS

Warning: macOS dev beginner here.
I have a menu bar app (with no dock). Most of the app's functionality is in the menu (and implementation is in AppDelegate), but I need a separate window that will open once I click one of the menu items.
I want to use SwiftUI, Swift 5, Xcode 11.3.
I haven't found an appropriate way to do this. Which files and similar need to be created? How to open this window programatically?
#objc func openPreferences() {
// open a new window here...
}
You have to create a window programatically. I have attached sample code of one of my apps:
private var windowController: NSWindowController?
fileprivate func createWindow()
{
let storyboard = NSStoryboard(name: "Main", bundle: nil)
self.windowController = storyboard.instantiateInitialController() as? NSWindowController
// This is example code to show how to customize the hosted view controller. You can pass additional arguments here (may an important global variables that is declared in the AppDelegate).
if let contentController = windowController?.contentViewController as? MyWindowViewController
{
// Do some assignments here
// contentController.variable = ....
// self.windowViewController = contentController // Maybe save for later use.
}
}
#objc fileprivate func open()
{
if self.windowViewController == nil
{
self.createWindow()
}
self.windowController?.showWindow(self)
NSApp.activate(ignoringOtherApps: true) // Bring window to front.
}
I have linked the open() function to a button call (hence the #objc keyword). I think that you already did this, so my open() function would be your openPreferences function.

How to close/dismiss/hide a menu by clicking on a button in an embedded view from within it, in Swift?

I have created a menu app, using Swift, for Mac OS, within which, a custom view is the only menu item. There's a plus button on this custom view, which opens a window that has a textfield.
When I click on the plus button, the window appears, but the menu does not disappear. The textfield is also not focused. When I type one letter, the letter is not shown in the textfield, but the menu disappears, and the textfield is focused and ready to receive entry.
I want to have the custom view or menu disappear and have the textfield ready to receive keystrokes when I click on the plus button, not after I press an extra key.
How may I achieve that? What am I doing wrong?
Here's my code:
// CustomView.swift
var customWindow: CustomWindow!
override func awakeFromNib() {
customWindow = CustomWindow()
}
#IBAction func plusButtonClicked(_ sender: NSButton) {
customWindow.showWindow(nil)
}
// CustomWindow.swift
override var windowNibName : NSNib.Name? {
return NSNib.Name("CustomWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
self.window?.center()
self.window?.makeKeyAndOrderFront(self)
self.window?.level = .mainMenu + 100
NSApp.activate(ignoringOtherApps: true)
if customTextField.acceptsFirstResponder {
customTextField.window?.makeFirstResponder(customTextField)
}
// CustomMenuContoller.swift
let statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
#IBOutlet weak var CustomMenu: NSMenu!
#IBOutlet weak var customView: CustomView!
var customMenuItem: NSMenuItem!
override func awakeFromNib() {
customMenuItem = CustomMenu.item(withTitle: "TheMenu")
customMenuItem.view = customView
statusBarItem.menu = CustomMenu
}
Inspired by El Tomato's comment, I found the solution.
Given the fact that the plusButtonClicked is limited to its own context, which is the controller within which it resides and all the public variables, I could not call a method on CustomMenu from it. Because CustomMenu in itself is not public. But its containing variable statusBarItem.menu, is public and accessible from all the other views. So I added statusBarItem.menu?.cancelTracking() to plusButtonClicked action and it works.

Unable to update NSTouchBar programmatically

I am currently developing a very simple Live Scores MAC OSX app for personal use where I show a bunch of labels (scores) on the touch bar. What I am trying to achieve in a few steps:
Fetch live soccer scores from a 3rd party API every 30 seconds
Parse the scores and make them into labels
Update the touch bar with new scores
[Please note here that this app will not be published anywhere, and is only for personal use. I am aware of the fact that Apple strictly advises against such type of content in the Touch Bar.]
Here is the code that I wrote following basic Touch Bar tutorial from RW (https://www.raywenderlich.com/883-how-to-use-nstouchbar-on-macos). Skeleton of my code is picked from the RW tutorial:
In WindowController (StoryBoard entry point), override makeTouchBar like this:
override func makeTouchBar() -> NSTouchBar? {
guard let viewController = contentViewController as? ViewController else {
return nil
}
return viewController.makeTouchBar()
}
In ViewController, which is also the Touch Bar Delegate, implement the makeTouchBar fn:
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .scoresBar
touchBar.defaultItemIdentifiers = [.match1, .flexibleSpace, .match2, ... , .match10]
return touchBar
}
NSTouchBarDelegate in ViewController. scores is where I store my fetched scores (See 5). I return nil for views if scores aren't fetched yet:
extension ViewController: NSTouchBarDelegate {
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
if (<scores not fetched yet>) {
return nil
}
// matchNum is the match number for which I am showing scores for
let customViewItem = NSCustomTouchBarItem(identifier: identifier)
customViewItem.view = NSTextField(labelWithString: self.scores[matchNum ?? 0])
return customViewItem
}
}
To fetch scores periodically I am running a scheduled task Timer in viewDidLoad() of my viewcontroller like this:
_ = Timer.scheduledTimer(timeInterval: 30.0, target: self, selector: #selector(ViewController.fetchScores), userInfo: nil, repeats: true)
And finally, this is my fetchScores function that also makes a call to update the Touch Bar:
#objc func fetchScores() {
let url = "<scores api end point>"
Alamofire.request(url).responseJSON { response in
if let json = response.result.value {
// update self.scores here and fill it with latest scores
if #available(OSX 10.12.2, *) {
//self.touchBar = nil
self.touchBar = self.makeTouchBar() // This is where I am calling makeTouchBar again to update Touch Bar content dynamically
}
}
}
My understanding from the code above is that once I make a call to makeTouchBar in fetchScores and assign it to my viewcontroller's touchBar property, it should ideally call touchBar(:makeItemForIdentifier) delegate function to update the Touch Bar view (SO thread on this). But in my case, touchBar(:makeItemForIdentifier) is never called. The only time touchBar(:makeItemForIdentifier) is called is the first time, when makeTouchBar is called from my WindowController (See point 1 above). And since scores have not been retrieved yet, my touch bar remains empty.

Way to check data modification across many viewcontroller without using global variables?

I have an app which contains several viewControllers. On the viewDidAppear() of the first VC I call a set of functions which populate some arrays with information pulled from a database and then reload table data for a tableView. The functions all work perfectly fine and the desired result is achieved every time. What I am concerned about is how often viewDidAppear() is called. I do not think (unless I am wrong) it is a good idea for the refreshing functions to be automatically called and reload all of the data every time the view appears. I cannot put it into the viewDidLoad() because the tableView is part of a tab bar and if there are some modifications done to the data in any of the other tabs, the viewDidLoad() will not be called when tabbing back over and it would need to reload at this point (as modifications were made). I thought to use a set of variables to check if any modifications were done to the data from any of the other viewControllers to then conditionally tell the VDA to run or not. Generally:
override func viewDidAppear(_ animated: Bool) {
if condition {
//run functions
} else{
//don't run functions
}
}
The issue with this is that the data can be modified from many different viewControllers which may not segue back to the one of interest for the viewDidAppear() (so using a prepareForSegue wouldn't work necessarily). What is the best way to 'check' if the data has been modified. Again, I figured a set of bool variables would work well, but I want to stay away from using too many global variables. Any ideas?
Notification Center
struct NotificationName {
static let MyNotificationName = "kMyNotificationName"
}
class First {
init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived), name: NotificationName.MyNotificationName, object: nil)
}
func notificationReceived() {
// Refresh table view here
}
}
class Second {
func postNotification() {
NotificationCenter.default.post(name: NotificationName.MyNotificationName, object: nil)
}
}
Once postNotification is called, the function notificationReceived in class First will be called.
Create a common global data store and let all the view controllers get their data from there. This is essentially a global singleton with some accompanying functions. I know you wanted to do this without global variables but I think you should consider this.
Create a class to contain the data. Also let it be able to reload the data.
class MyData {
static let shared = MyData()
var data : SomeDataType
func loadData() {
// Load the data
}
}
Register to receive the notification as follows:
static let dataChangedNotification = Notification.Name("DataChanged")
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
// Establish a way for call activity to notify this class so it can update accordingly
NotificationCenter.default.addObserver(self, selector: #selector(handleDataChangedNotification(notification:)), name: "DataChanged", object: nil)
}
func handleDataChangedNotification(notification: NSNotification) {
// This ViewController was notified that data was changed
// Do something
}
func getDataToDisplay() {
let currentData = MyData.shared.data
// do something
}
// Any view controller would call this function if it changes the data
func sendDataChangeNotification() {
let obj = [String]() // make some obj to send. Pass whatever custom data you need to send
NotificationCenter.default.post(name: type(of: self).dataChangedNotification, object: obj)
}

Bar button Item to open multiple URLs?

I recently saw the code down below. Is it possible to link a bar button item to multiple URLs? i.e. by tapping the button, I can then choose which website I wish to go to at runtime? Or can I only link a bar button to one URL?
override func viewDidLoad() {
super.viewDidLoad()
googleButton.addTarget(self, action: "didTapGoogle", forControlEvents: .TouchUpInside)}
and
#IBAction func didTapGoogle(sender: AnyObject) {
UIApplication.sharedApplication().openURL(NSURL(string:"http://www.google.com")!)}
Use it as sidebar menu.
This library keeps your sidebar menu over viewcontroller and navigation bar.
https://github.com/balram3429/BTSimpleSideMenu
It is in objective c, but you can use it by bridging. there are other side bar menus are also available in swift, just make sure it has to be open over view controller and navigation bar.
Import this 2 in bridging file.
//#import "BTSimpleSideMenuClass.h"
//#import "BTSimpleMenuItemClass.h"
Make object
var objBTSimpleSideMenuClass = BTSimpleSideMenuClass()
Import delegate
class YourClassName: UIViewController, BTSimpleSideMenuDelegate {
}
From ViewDidLoad or ViewWillAppear call this method and pass array with name, image etc
func setupOptionMenu(noOfItems : NSMutableArray)
{
objBTSimpleSideMenuClass.delegate = self
let ary : NSMutableArray = []
for var i = 0; i < noOfItems.count; i++
{
let item = BTSimpleMenuItemClass.init(title: noOfItems[i] as! String, image: nil) { (success, item) -> Void in
self.methodOptionMenuTap1()
//self.methodOptionMenuTap2()
//self.methodOptionMenuTap3()
}
ary.addObject(item)
}
let swiftArray = ary as NSArray
objBTSimpleSideMenuClass = BTSimpleSideMenuClass.init(item: swiftArray as [AnyObject], addToViewController: self)
}
Hope this will help you a lot.
All the best.