How to make sure that a new view controller is visible to the user? - swift

First of all the native macOS application is made into an accessory type application 3 seconds after launching (to show an info screen first, before the app goes into the system menu bar):
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
NSApplication.shared.setActivationPolicy(.accessory)
}
It has a status bar menu created with:
class func createMenu(color: Bool) -> Void {
let statusBar = NSStatusBar.system
self.sharedInstance.storedStatusItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)
self.sharedInstance.storedStatusItem.menu = ABCMenu.statusBarMenu()
}
The user has several options in the status bar menu to open different screens with controls. One of them is shown with:
class func showServiceViewController() -> Void {
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: Bundle.main)
guard let vc = storyboard.instantiateController(withIdentifier: "ABCServiceViewController") as? ABCServiceViewController, let checkedWindow = ABCUIManager.sharedInstance.window else {
return
}
checkedWindow.contentViewController = vc
checkedWindow.setIsVisible(true)
checkedWindow.orderFrontRegardless()
}
The problem is that sometimes the selected view controller is not brought to the background and has to be found underneath many other already opened applications and windows. Most of the time it works fine, but not always.
Are there any better ways to assure that the new view controller is always brought up to the highest level and shown to the user?
Thank you for any suggestions.

I would activate application before showing window (as you have accessory application it is not activated by default and must be done programmatically)
...
NSApplication.shared.activate(ignoringOtherApps: true)
checkedWindow.contentViewController = vc
checkedWindow.setIsVisible(true)
checkedWindow.orderFrontRegardless()
}

Related

How to Make macOS App Window Hidden When Closed and Reopened With Menu Bar Item?

I am developing a macOS app (using Swift & Storyboard) which window behaves like the Adobe Creative Cloud app. And I could not find the optimal solution after hours of research.
This means:
When the app launches, the main window shows up with various menus on the status bar, an icon appears in the dock, and an icon appears in the status bar.
When the user clicks the red X, the main window and the icon in the dock are hidden.
The main app window can be reopened by clicking the status bar icon. And the dock icon reappears.
My storyboard looks like this:
I have tried the following:
By setting Application is agent (UIElement) to YES, I was able to close the main app window while keeping the app alive. However, the app icon does not show up in the dock, and there are no menus in the left side of the status bar.
I was able to launch a new app window by clicking the status bar icon. But doing so simply opens a whole new window regardless of whether a window is already being presented (I only want one window to show up).
let storyboard = NSStoryboard(name: "Main", bundle: nil)
guard let window = storyboard.instantiateController(withIdentifier: .init(stringLiteral: "main")) as? WindowController else { return }
window.showWindow(self)
Much appreciation for anyone who can help!
Don't use the Application is agent approach, but change the activationPolicy of the NSApp.
To dynamically hide the icon after closing the (last) window use this in your AppDelegate:
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
NSApp.setActivationPolicy(.accessory)
return false
}
And use something simular to this to initialise your menubar icon and activate the window including a dock icon:
class ViewController: NSViewController {
var status: NSStatusItem?
override func viewDidLoad() {
super.viewDidLoad()
status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
status?.button?.title = "Test"
status?.button?.action = #selector(activateWindow(_:))
status?.button?.target = self
}
#IBAction func activateWindow(_ sender: AnyObject) {
NSApp.setActivationPolicy(.regular)
DispatchQueue.main.async {
NSApp.windows.first?.orderFrontRegardless()
}
}
}

iOS 14 UISplitViewController (sidebar) with triple column sidebar toggle icon behavior

I'm implementing the iOS 14 (iPadOS 14) sidebar (UISplitViewController with TripleColumn) and having strange "sidebar toggle icon" behavior.
In iOS 13 I'm using the tab bar with some split views and table views so I need the Triple Column instead of the Double Column to work.
For example, using the sidebar in "flights" tab needs three columns:
And there are some tabs with only one column (in iOS 13, it was a table view instead of a split view). I set the supplementary view to nil, and hide the view by calling "hide" method implemented in iOS 14. (See below for code):
The "sidebar toggle icon" on the upper left is automatically displayed. After clicking the toggle icon, the sidebar hides correctly but an "back button" was created on my secondary view(a UITableViewController embedded in a UINavigationController):
Pressing (clicking) the back button has no response. User can still swipe from the left edge of the screen to make sidebar reappear but the "back button" is confusing. My expected behavior is, after the toggle icon selected in sidebar, display the "sidebar toggle icon" instead of the "back button" in the secondary view. And after pressing the "sidebar toggle icon" in secondary view, the sidebar reappears.
Like the Photos app in iOS 14 (iPadOS 14), the toggle button is shown instead of the back button. And clicking the toggle icon will make the sidebar shown again. (but it's a double column split view)
My code:
SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if #available(iOS 14.0, *) {
let main = UIStoryboard(name: "Main", bundle: nil)
let splitViewController = UISplitViewController(style: .tripleColumn)
splitViewController.preferredDisplayMode = .twoBesideSecondary
splitViewController.preferredSplitBehavior = .tile
splitViewController.setViewController(SideBarViewController(), for: .primary)
// fall back for compact screen
splitViewController.setViewController(main.instantiateInitialViewController(), for: .compact)
window.rootViewController = splitViewController
self.window = window
window.makeKeyAndVisible()
}
}
}
SideBarViewController:
// if the first tab (dashboard) was selected
private func selectDashboardTab() {
if #available(iOS 14.0, *) {
let dashboardVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DashboardTab") as? UINavigationController
splitViewController?.preferredPrimaryColumnWidth = 250.0
splitViewController?.preferredDisplayMode = .twoBesideSecondary
splitViewController?.preferredSplitBehavior = .tile
splitViewController?.setViewController(dashboardVC, for: .secondary)
splitViewController?.setViewController(nil, for: .supplementary)
splitViewController?.hide(.supplementary)
}
}
I was able to reproduce the problem... but was unable to circumvent it using the available API. Apple stubbornly decided that the sidebar icon would always be in the supplementary controller when in a 3-column layout, it seems.
That being said, I've coded a hack to fix it. I managed to create a UISplitViewController subclass that "fakes" an editable style property, thus allowing us to set the number of columns (the hack just creates a brand new controller and makes it the new rootViewController).
The hack allows us to switch between 2-colulm and 3-column layout at will, and seems to solve the sidebar icon problem.
Link to the Xcode project below. But here's the gist of it:
class AdaptableSplitViewController: UISplitViewController {
override var style: Style {
get {
super.style
}
set {
guard newValue != style else { return }
let primaryController = viewController(for: .primary)
let supplementaryController = viewController(for: .supplementary)
let secondaryController = viewController(for: .secondary)
let newSplitController = AdaptableSplitViewController(style: newValue)
newSplitController.setViewController(primaryController , for: .primary)
newSplitController.setViewController(secondaryController, for: .secondary)
if newValue == .tripleColumn {
newSplitController.setViewController(supplementaryController, for: .supplementary)
}
UIApplication.shared.windows[0].rootViewController = newSplitController
}
}
}
Let me know if this helps.
Link to the zipped Xcode sample project
Late in the discussion but... I've encountered a similar behavior.
Just before setting you secondary, set it to nil. Strange, I know, but it fixed it for me. Like this:
splitViewController?.setViewController(nil , for: .secondary)
splitViewController?.setViewController(dashboardVC, for: .secondary)

How to communicate to the window controller from view controller of newly, programmatically created tab?

In my window controller, I implement:
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .above)
window.makeKey()
}
}
In the view controller, I have this code:
let window = self.view.window?.windowController as? WindowController
Also tried:
let window = NSApp.mainWindow?.windowController as? WindowController
If I don't have any tabs, it's able to get the window controller. But on new tabs, it does grab the window controller.
Similarly, I've unsuccessfully tried sending an action to the WindowController:
NSApp.sendAction(#selector(WindowController.pageLabelChange), to: nil, from: label)
Works for the original window, but not for any newly created tabs.
How do the newly created view controller objects communicate with the window controller?
Edit:
For more context in how I am using this code: It's basically a PDFView that's embedded in a window. The window has a tool bar that displays the page number. Using any of the above code, I can set the current page number of the PDFView, but when there's a tab, it does not work. Using the .PDFViewPageChanged notification, I call my func
NSApp.sendAction(#selector(WindowController.pageLabelChange), to: nil, from: pdfView)
Edit 2:
I've created a GitHub with a test project that shows the problem I have. You should be able to see that when you launch the project, the + button will add a number to the textfield in the tab bar. But if you go to the View menu > Add tab, it creates a new tab, but the + does nothing.
You create new window controller on stack so it is destroyed right after return, that is the reason. You need to store somewhere reference to created tab window controller and manage it (the place of storage is up to your app logic).
Here is simple demo that makes code work
Tested with Xcode 11.4 / macOS 10.15.5
final class WindowController: NSWindowController {
#IBOutlet weak var testField: NSTextField!
var tabControllers = [WindowController]() // << storage for child controllers
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .above)
window.makeKey()
tabControllers.append(wc) // keep reference in storage
// TODO: - you are responsible to manage wc, eg: remove
// from storage on window close.
}
}
}

How to keep NSWindow.FrameAutosaveName out of scope of tabbed windows?

I have a NSWindowControllerclass in an app that can be used to show a standalone window or to show a tabbed window attached to a main window, which has a different window controller. If the user opens the window as standalone, then I want to use the same window position and size as the last standalone window session. It's comparable to opening a code view in Xcode in a new window.
So I have a self.windowFrameAutosaveName = NSWindow.FrameAutosaveName("Detail App Window") line in windowDidLoad. This works, but the problem is that it also works when the window is shown as a tab in the main window. Next time the user opens the same window as standalone, the size of the main window is being used.
Question: how can I keep self.windowFrameAutosaveName outside a tabbed window scenario?
I create the window controller like this:
let storyboard = NSStoryboard(name: "Detail", bundle: nil)
if let controller = storyboard.instantiateInitialController() as? DetailController {
document.addWindowController(controller)
if tabbed,
let mainWindow = document.windowControllers.first?.window,
let detailWindow = controller.window {
mainWindow.addTabbedWindow(detailWindow, ordered: .above)
}
else {
controller.showWindow(nil)
}
}
The code in windowDidLoad of the DetailController above is:
override func windowDidLoad() {
super.windowDidLoad()
self.shouldCascadeWindows = true
self.windowFrameAutosaveName = NSWindow.FrameAutosaveName("Detail App Window")
}
As you see, I use self.shouldCascadeWindows = true, and this must also keep working. I have experimented with detecting being in a tab scenario, but without any complete success. Also, moving the windowFrameAutosaveName before the controller.showWindow(nil) works in itself, but the windows are no longer loaded with shouldCascadeWindows = true.
Interesting, even Xcode doesn't do that, so maybe it's a limitation of AppKit.

Launching ViewController as sheet from windowDidLoad

I'm trying to show a storyboard as a sheet right after the window has loaded.
override func windowDidLoad() {
super.windowDidLoad()
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Init"), bundle: nil)
let controller = storyboard.instantiateInitialController() as! NSViewController
self.window!.contentViewController?.presentViewControllerAsSheet(controller)
}
Unfortunately, the sheet is shown out of position and behind the window.
When I run the same code inside a button, everything works fine.
Screenshot
How do i correctly show a storyboard sheet after loading a window?
I have more knowledge of the iOS ecosystem but I suppose that you should show new windows only after the origin window has been shown. windowDidLoad is called when the view has been loaded, not when it has been shown.
Therefore, you should probably put your code into viewDidAppear of the contentViewController.