I am currently making a XIB Menu Bar application that displays a notification using this code:
func showNotification(message:String, title:String = "App Name") -> Void {
let notification = NSUserNotification()
notification.title = title
notification.informativeText = message
NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(notification)
}
And calling it like this:
showNotification("\(song)\n\(artist)",title: "Now Playing")
The notification works when the Menu Bar application is hidden away (not shown), however when the user has it shown, the notification does not show.
Is there a way to show the notification while the application is in view?
By default when your application is active, notifications delivered by your app are not shown. To get the expected behaviour, you have to use the user notification center delegate like below :
extension AppController: NSUserNotificationCenterDelegate {
private func setupUserNotificationCenter() {
let nc = NSUserNotificationCenter.defaultUserNotificationCenter()
nc.delegate = self
}
public func userNotificationCenter(center: NSUserNotificationCenter, shouldPresentNotification notification: NSUserNotification) -> Bool {
return true
}
}
Related
Hello dear developers,
I'm currently stuck due to a problem with In-App AppStore rating (SKStoreReviewController).
Here is the situation, I've a screen "FirstScreen" with a button. When I tap on it, I'm going to the next screen "SecondScreen" and an in app alert for AppStore rating pop over.
I'm trying to find a solution for my UITests in order to dismiss this Alert.
I tried many solutions but I'm looking for one which do not depends on string (I don't want to localize the content of this Alert):
override func setUp() {
app = XCUIApplication()
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
let allowButton = alert.buttons.element(boundBy: 1)
if allowButton.exists {
allowButton.tap()
}
}
}
I also tried to add an interaction ("app.swipeUp()") when I'm coming to "SecondScreen" in order to trigger this handler.
I've also tried another solution, as I know when this alert will be triggered:
let dismissButton = XCUIApplication(bundleIdentifier: "com.apple.springboard").buttons.element(boundBy: 1)
if dismissButton.exists {
dismissButton.tap()
}
No one worked and I'm still stuck :(
Does anybody found a solution in order to dismiss this alert ?
Thanks
Swiping up doesn't work but ironically swiping down does. Here is a very simplistic example
import UIKit
import StoreKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
SKStoreReviewController.requestReview()
}
}
}
import XCTest
class UITests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}
func test() {
let app = XCUIApplication()
app.launch()
sleep(5)
app.swipeDown()
sleep(3)
}
}
In my app, I added a toggleSidebar item to the NSToolbar.
#if targetEnvironment(macCatalyst)
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.toggleSidebar, NSToolbarItem.Identifier.flexibleSpace, AddRestaurantButtonToolbarIdentifier]
}
}
#endif
However, when I compile my app to Catalyst, the button is disabled. Does anybody know what else I need to do to hook it up?
If you look at the documentation for .toggleSidebar/NSToolbarToggleSidebarItemIdentifier you will see:
The standard toolbar item identifier for a sidebar. It sends toggleSidebar: to firstResponder.
Adding that method to your view controller will enable the button in the toolbar:
Swift:
#objc func toggleSidebar(_ sender: Any) {
}
Objective-C:
- (void)toggleSidebar:(id)sender {
}
Your implementation will need to do whatever you want to do when the user taps the button in the toolbar.
Normally, under a real macOS app using an NSSplitViewController, this method is handled automatically by the split view controller and you don't need to add your own implementation of toggleSidebar:.
The target needs changed to self, this is shown in this Apple sample where it is done for the print item but can easily be changed to the toggle split item as I did after the comment.
/** This is an optional delegate function, called when a new item is about to be added to the toolbar.
This is a good spot to set up initial state information for toolbar items, particularly items
that you don't directly control yourself (like with NSToolbarPrintItemIdentifier).
The notification's object is the toolbar, and the "item" key in the userInfo is the toolbar item
being added.
*/
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .print {
addedItem.toolTip = NSLocalizedString("print string", comment: "")
addedItem.target = self
}
// added code
else if itemIdentifier == .toggleSidebar {
addedItem.target = self
}
}
}
And then add the action to the scene delegate by adding the Swift equivalent of this:
- (IBAction)toggleSidebar:(id)sender{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
[UIView animateWithDuration:0.2 animations:^{
splitViewController.preferredDisplayMode = (splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModePrimaryHidden ? UISplitViewControllerDisplayModePrimaryHidden : UISplitViewControllerDisplayModeAllVisible);
}];
}
When configuring your UISplitViewController, set the primaryBackgroundStyle to .sidebar
let splitVC: UISplitViewController = //your application's split view controller
splitVC.primaryBackgroundStyle = .sidebar
This will enable your NSToolbarItem with the system identifier .toggleSidebar and it will work automatically with the UISplitViewController in Mac Catalyst without setting any target / action code.
This answer is mainly converting #malhal's answer to the latest Swift version
You will need to return [.toggleSidebar] in toolbarDefaultItemIdentifiers.
In toolbarWillAddItem you will write the following (just like the previous answer suggested):
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .toggleSidebar {
addedItem.target = self
addedItem.action = #selector(toggleSidebar)
}
}
}
Finally, you will add your toggleSidebar method.
#objc func toggleSidebar() {
let splitController = self.window?.rootViewController as? MainSplitController
UIView.animate(withDuration: 0.2) {
splitController?.preferredDisplayMode = (splitController?.preferredDisplayMode != .primaryHidden ? .primaryHidden : .allVisible)
}
}
A few resources that might help:
Integrating a Toolbar and Touch Bar into Your App
Mac Catalyst: Adding a Toolbar
The easiest way to use the toggleSidebar toolbar item is to set primaryBackgroundStyle to .sidebar, as answered by #Craig Scrogie.
That has the side effect of enabling the toolbar item and hiding/showing the sidebar.
If you don't want to use the .sidebar background style, you have to implement toggling the sidebar and validating the toolbar item in methods on a class in your responder chain. I put these in a subclass of UISplitViewController.
#objc func toggleSidebar(_ sender: Any?) {
UIView.animate(withDuration: 0.2, animations: {
self.preferredDisplayMode =
(self.displayMode == .secondaryOnly) ?
.oneBesideSecondary : .secondaryOnly
})
}
#objc func validateToolbarItem(_ item: NSToolbarItem)
-> Bool {
if item.action == #selector(toggleSidebar) {
return true
}
return false
}
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()
}
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.
I'm new to Swift (but not to programming). I have simple app that provides an alert based on specific conditionals. I would like to execute a function (or even just set a variable) when one of the buttons is pressed. Ideally, I just need one button, but if for whatever reason, only the notification.actionButtonTitle can have a handler, that's fine with me.
My notification code is currently in a Swift file as a helper.
import Foundation
class NotificationHelper {
static func sampleNotification(notification: NSUserNotification) {
let notificationCenter = NSUserNotificationCenter.default
notification.identifier = "unique-id-123"
notification.hasActionButton = true
notification.otherButtonTitle = "Close"
notification.actionButtonTitle = "Show"
notification.title = "Hello"
notification.subtitle = "How are you?"
notification.informativeText = "This is a test"
notificationCenter.deliver(notification)
}
}
Currently in AppDelegate, this is defined:
let notification = NSUserNotification()
…and I call the notification like this:
NotificationHelper.sampleNotification(notification: notification)
The resulting notification works, as you can see in the screenshot below. However, I cannot seem to listen to the button action. I have tried adding this to the AppDelegate as well as the NotificationHelper file, but I did not have any success with it:
func userNotificationCenter(center: NSUserNotificationCenter, didActivateNotification notification: NSUserNotification) {
print("checking notification response")
}
Any idea of what I'm missing?
Thanks!
You'll need to assign something as the delegate of the NSUserNotificationCenter:
NSUserNotificationCenter.default.delegate = self
If you add this to your AppDelegate and make your AppDelegate conform to NSUserNotificationCenterDelegate:
class AppDelegate: NSObject, NSUserNotificationCenterDelegate {
}