Conditionally showing NSViewController at app launch - macos

I'm developing an OSX app where I show first a login/register window if the user hasn't logged in yet.
After login success I show my main view controller.
If the user is already logged in (a token is stored), then the app has to launch directly with the main view controller.
I'm new to OSX development, I googled for this kind of scenario but couldn't find anything.
So I went up with what I think should work. It works sometimes, sometimes I get a blank window.
In the storyboard I let the Main Menu and the Window Controller. I removed the "contains" segue to my main view controller.
In AppDelegate, I put this:
func applicationDidFinishLaunching(aNotification: NSNotification) {
if loggedIn {
self.showViewController(NSStoryboard.mainViewController())
} else {
let loginController = NSStoryboard.loginViewController()
loginController.delegate = self
self.showViewController(loginController)
}
}
private func showViewController(viewController: NSViewController) {
if let mainWindow = NSApplication.sharedApplication().mainWindow {
mainWindow.contentViewController = viewController
} else {
print("Error: No main window!")
}
}
About half of the times the window is empty and I see in the console "Error: No main window!". I thought maybe I can use applicationDidBecomeActive but this is called basically when it comes to the foreground and this is not what I need.
Further, the times when it works, and I log in, then I want to show the main view controller:
func onLoginSuccess() {
self.showViewController(NSStoryboard.mainViewController())
}
And here I also get "Error: No main window!" (always) and nothing happens.
The docs say following about mainWindow being nil:
The value in this property is nil when the app’s storyboard or nib file has not yet finished loading. It might also be nil when the app is inactive or hidden.
But why is the storyboard not finished loading or the app inactive when I'm launching it? And on login success the app is definitely active and in the foreground and the main window is always nil.
What am I doing wrong? How can I implement this workflow? Alternatively I could create a "parent" view controller, have that one connected to the window in the storyboard, and add the login or main view controller as nested view controllers to that. But don't really like having to add a do nothing view controller.
I'm using XCode 7(beta 4), Swift 2, OSX 10.10.4
Edit:
The NSStoryboard methods come from an extension, it looks like this:
extension NSStoryboard {
private class func mainStoryboard() -> NSStoryboard { return NSStoryboard(name: "Main", bundle: NSBundle.mainBundle()) }
private class func signupStoryboard() -> NSStoryboard { return NSStoryboard(name: "LoginRegister", bundle: NSBundle.mainBundle()) }
class func mainViewController() -> ViewController {
return self.mainStoryboard().instantiateControllerWithIdentifier("MainViewController") as! ViewController
}
class func loginViewController() -> LoginViewController {
return self.signupStoryboard().instantiateControllerWithIdentifier("LoginViewController") as! LoginViewController
}
class func registerViewController() -> RegisterViewController {
return self.signupStoryboard().instantiateControllerWithIdentifier("RegisterViewController") as! RegisterViewController
}
}

To put the solution we found in the comments as an answer:
Apparently NSApplication.sharedApplication().mainWindow is a different window than my main window in the storyboard.
So, I created an NSWindowController subclass and assigned it to the window in the storyboard, using the identity inspector.
Then I moved the logic I had in app delegate to this NSWindowController. It looks like this:
class MainWindowController: NSWindowController, LoginDelegate {
override func windowDidLoad() {
if loggedIn {
self.onLoggedIn()
} else {
let loginController = NSStoryboard.loginViewController()
loginController.delegate = self
self.contentViewController = loginController
}
}
func onLoggedIn() {
self.contentViewController = NSStoryboard.mainViewController()
}
func onLoginSuccess() {
self.onLoggedIn()
}
}
* Thanks Lucas Derraugh for pointing me in the right direction!

enum Storyboards: String {
case main = "Main"
case settings = "Settings"
func instantiateVC<T>(_ identifier: T.Type) -> T? {
let storyboard = NSStoryboard(name: rawValue, bundle: nil)
guard let viewcontroller = storyboard.instantiateController(withIdentifier: String(describing: identifier)) as? T else { return nil}
return viewcontroller
}
}
//Need to use like this
//Make sure Storyboard Id and class-name are the same
if let windowController = Storyboards.main.instantiateVC(IDMainController.self) {
windowController.showWindow(nil)
//----- OR -----
self.contentViewController = windowController
} else {
print("Cannot find IDMainController")
}

Related

How to hand over delegate to controller in different places of project

I have a UINavigationController which includes 4 screens. Each has a back button respectively. Task: by pressing the back button on Controller4 get to Controller2, which will immediately switch to Controller1. I want to do this through a delegate (connection of 2 and 4 controllers), but I don’t understand how I can access the ViewController4.delegate = self property in Controller2. There is already an action for clicking and switching to 3Controller, to do something like
let storyboard = UIStoryboard(name: "VC4", bundle: nil)
guard let vc4 = story.instantiateViewController(withIdentifier: "VC4") as? VC4 else { return }
vc4.delegate = self
}
Please, help. How to link two controllers that are in completely different places in the project through a delegate (or something else)
P.S. This is my first time asking a question, sorry in advance if it's crooked
Example
protocol Controller2Delegate: AnyObject {
func showVC1()
}
class Controller2: UIViewController, Controller2Delegate {
// need to connect between 2 and 4 controller
let story = UIStoryboard(name: "VC4", bundle: nil)
guard let vc4 = story.instantiateViewController(withIdentifier: "VC4") as? VC4 else { return }
vc4.delegate = self //this is not work, and i already have same method for push to VC3
func showVC1() {
//any logic to call VC1 from VC2
//break point never called
}
class Controller4: UIViewController {
weak var delegate: Controller2Delegate?
func GoToVC1() {
//method who's call logic for pop to VC2
delegate?.showVC1
}// breakpoint is called
}

NSWindow opens but buttons doesn't work and I cannot create IBOutlets

I have the following NSWindowController:
import Foundation
import Cocoa
extension NSImage.Name {
static let skyflokLogo = NSImage.Name("skyflokLogo")
}
class LoginWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
}
#IBAction func cancelLogin(_ sender: NSButton) {
print("Jones")
}
#IBAction func confirmLogin(_ sender: NSButton) {
print("lol")
}
}
and I open the window like this:
static func loadLoginWindow() -> NSWindowController {
let win = LoginWindowController(windowNibName: NSNib.Name("LoginWindow"))
win.showWindow(self)
return win
}
And store it in the AppDelegate class:
#IBAction func loginFunction(_ sender: NSMenuItem) {
print("TEST")
testCtrl = UIHelpers.loadLoginWindow()
}
And components the window contains can be seen here:
My problem is that the window opens as it is supposed to, but my IBAction functions does not work and I cannot create IBOutlets by control dragging from the window. Can someone direct me to documentation or help me solve this?
You need to hold your LoginWindow (I personally recommend to append "Controller" to the class name) instance somewhere. Otherwise, the instance disappears as soon as loadLoginWindow() ended, and therefore IBActions cannot be performed.

Where I should put code to customize Cancel button in UISearchBar?

I trying to change Cancel button color in UISearchBar implemented with UISearchController (iOS 8 and greater). This is a code I use:
if self.resultSearchController.active {
for subView in self.resultSearchController.searchBar.subviews {
for subsubView in subView.subviews {
if subsubView.isKindOfClass(UIButton) {
subsubView.tintColor = UIColor.whiteColor()
}
}
}
}
If I paste it in viewDidLoad, it doesn't work, cause I think Cancel button initialize only when SearchController becomes Active.
If I paste code in viewDidLayoutSubviews everything work great, but I'm not sure its a correct way.
So, where I should put this code in TableViewController?
Also, I don't understand, how I can receive notification in my TableViewController that SearchController becomes inactive. In other words where I should put code like this:
if self.resultSearchController.active == false {
//Do something
}
First you should insert delegate methods :-
class HomeViewController: UIViewController,UISearchResultsUpdating, UISearchBarDelegate {
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search here..."
searchController.searchBar.delegate = self
searchController.searchBar.sizeToFit()
searchController.hidesNavigationBarDuringPresentation = true
tableView.tableHeaderView = searchController.searchBar
searchController.searchBar.tintColor = UIColor.whiteColor()
}
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
}
}
then used delegate methods and change cancel button colors and thing what you want
You can try this in AppDelegate's didFinishLaunchWithOptions:.
UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses([UISearchBar.self]).tintColor = UIColor.whiteColor()
PS: This is a generic method and would affect UIBarButtonItem in UISearchBar across app.
Swift 4.2, 4.0+ An answer is added here for a custom search bar that can be customized as below,
You can check the usage of SearchBar class.

Losing reference to NSWindows in AppDelegate

I'm running into issues where I lose reference to NSWindows that are declared in AppDelegate, with the code below.
class AppDelegate: NSObject, NSApplicationDelegate {
var window1: CustomWindow1!
var window2: CustomWindow2!
func setupWindows() {
self.window1 = CustomWindow1()
self.window2 = CustomWindow2()
let vc2 = VIEWOFSOMEVIEWCONTROLLER()
self.window2.contentView.addSubview(vc2.view)
self.window1.grandchildVC = vc2
self.window1.addChildWindow(self.window2!, ordered: NSWindowOrderingMode.Above)
// etc.
}
#IBAction addWindowsAgain(sender: AnyObject) {
// This is where if fails
if self.window1 != nil {
self.window1 == nil
}
}
}
class CustomWindow1() {
var grandchildVC: NSViewController
func deleteChildWindowThenSelf() {
self.grandchildVC.view.window.close()
self.close()
}
}
I am able to call deleteChildWindowThenSelf(), and get rid of the windows as expected, but when calling addWindowsAgain in AppDelegate, if fails with lldb error. Does AppDelegate lose those window vars when close is called, or am I misunderstanding something?
NSWindow has a variable releasedWhenClosed which is true by default for custom created windows unless the window is owned by a window controller.
Set the variable to false.

how to Load initial window controller from storyboard?

I have gone through many questoions but none of them snaswers my query.
I am trying to load initial window programmatically
Here is what I have done.
I have added main.swift as-
import Cocoa
private func runApplication(
application: NSApplication = NSApplication.sharedApplication(),
delegate: NSApplicationDelegate? = AppDelegate(),
bundle: NSBundle = NSBundle.mainBundle(),
nibName: String = "MainMenu",
var topLevelObjects: NSArray? = nil) {
setApplicationDelegate(application, delegate)
}
private func setApplicationDelegate(application: NSApplication, delegate: NSApplicationDelegate?) -> NSApplication {
if let delegate = delegate {
application.delegate = delegate
}
return application
}
runApplication()
Appdelegate.swift is-
import Cocoa
//#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var appControl:AppFlow?
func applicationDidFinishLaunching(aNotification: NSNotification) {
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
override init() {
//
self.appControl = AppFlow()
super.init()
}
}
And in AppFlow I am trying to load window controller from storyboard.-
import Cocoa
class AppFlow{
let initialStoryBoard:NSStoryboard?
override init() {
self.initialStoryBoard = NSStoryboard(name: "Main" , bundle : nil)
super.init()
var windowController = (self.initialStoryBoard?.instantiateControllerWithIdentifier("mainWindow")) as! NSWindowController
windowController.window?.makeKeyAndOrderFront(nil)
}
}
But I am not able to launch initial window controller and view controller. App starts and terminates automatically, no window is presented to user.
What I am doing wrong? Thanks for your help.
Here is what I did in order to load initial window from storyboard (as well as MainMenu) programmatically without attribute #NSApplicationMain and function NSApplicationMain(_, _)
File: AppConfig.swift (Swift 4)
struct AppConfig {
static var applicationClass: NSApplication.Type {
guard let principalClassName = Bundle.main.infoDictionary?["NSPrincipalClass"] as? String else {
fatalError("Seems like `NSPrincipalClass` is missed in `Info.plist` file.")
}
guard let principalClass = NSClassFromString(principalClassName) as? NSApplication.Type else {
fatalError("Unable to create `NSApplication` class for `\(principalClassName)`")
}
return principalClass
}
static var mainStoryboard: NSStoryboard {
guard let mainStoryboardName = Bundle.main.infoDictionary?["NSMainStoryboardFile"] as? String else {
fatalError("Seems like `NSMainStoryboardFile` is missed in `Info.plist` file.")
}
let storyboard = NSStoryboard(name: NSStoryboard.Name(mainStoryboardName), bundle: Bundle.main)
return storyboard
}
static var mainMenu: NSNib {
guard let nib = NSNib(nibNamed: NSNib.Name("MainMenu"), bundle: Bundle.main) else {
fatalError("Resource `MainMenu.xib` is not found in the bundle `\(Bundle.main.bundlePath)`")
}
return nib
}
static var mainWindowController: NSWindowController {
guard let wc = mainStoryboard.instantiateInitialController() as? NSWindowController else {
fatalError("Initial controller is not `NSWindowController` in storyboard `\(mainStoryboard)`")
}
return wc
}
}
File main.swift (Swift 4)
// Making NSApplication instance from `NSPrincipalClass` defined in `Info.plist`
let app = AppConfig.applicationClass.shared
// Configuring application as a regular (appearing in Dock and possibly having UI)
app.setActivationPolicy(.regular)
// Loading application menu from `MainMenu.xib` file.
// This will also assign property `NSApplication.mainMenu`.
AppConfig.mainMenu.instantiate(withOwner: app, topLevelObjects: nil)
// Loading initial window controller from `NSMainStoryboardFile` defined in `Info.plist`.
// Initial window accessible via property NSWindowController.window
let windowController = AppConfig.mainWindowController
windowController.window?.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
app.run()
Note regarding MainMenu.xib file:
Xcode application template creates storyboard with Application Scene which contains Main Menu. At the moment seems there is no way programmatically load Main Menu from Application Scene. But there is Xcode file template Main Menu, which creates MainMenu.xib file, which we can load programmatically.
This is not how you start (and maintain) an application's main run loop. See #NSApplicationMain. This causes the main run loop to be set up and run until it's terminated. There's no need for a main.swift file any longer, as you can just put this into your app delegate's file directly.
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
}
Xcode's new application project template does this for you.

Resources