how do I switch between NSViewControllers using NSPageController? - macos

I have had bad luck finding any examples on the web that closely match what I am trying to do. I am trying to using NSPageController to view and switch between multiple NSPageControllers. My steps.
I create a new OS X swift project
I add an object to the ViewController and make it of NSPageController class.
I add two buttons, one I label "Next" and the other one I label "Back" for the transitions.
I link the buttons to the NSPageController object as navigateForward and navigateBack actions.
I create an outlet in the custom NSViewController class for the NSPageController object and add the specific NSPageController delegate methods.
I add two additional view controllers in storyboard and create an identifier for them to reference back in my custom view controller class: Wizard1, Wizard2.
import Cocoa
class ViewController: NSViewController, NSPageControllerDelegate {
#IBOutlet var myPageController: NSPageController!
override func viewDidLoad() {
super.viewDidLoad()
let vc1: AnyObject? = self.storyboard!.instantiateControllerWithIdentifier("Wizard1")
let vc2: AnyObject? = self.storyboard!.instantiateControllerWithIdentifier("Wizard2")
self.myPageController.arrangedObjects.append(vc1!)
self.myPageController.arrangedObjects.append(vc2!)
// Do any additional setup after loading the view.
}
override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
myPageController = NSPageController()
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil?)
}
required init?(coder aDecoder: NSCoder) {
myPageController = NSPageController()
super.init(coder:aDecoder)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func pageController(pageController: NSPageController, identifierForObject object: AnyObject!) -> String! {
return "View"
}
func pageController(pageController: NSPageController, viewControllerForIdentifier identifier: String!) -> NSViewController! {
let vc1: AnyObject? = self.storyboard!.instantiateControllerWithIdentifier("Wizard1")
return vc1 as NSViewController
}
func pageController(pageController: NSPageController, prepareViewController viewController: NSViewController!, withObject object: AnyObject!) {
viewController.representedObject = object
}
func pageControllerDidEndLiveTransition(pageController: NSPageController) {
pageController.completeTransition()
}
func pageControllerWillStartLiveTransition(pageController: NSPageController) {
self.presentViewControllerAsModalWindow(self.storyboard?.instantiateControllerWithIdentifier("Wizard2") as NSViewController)
}
}
The error I get when pressing the Next button is:
-[NSNib initWithNibNamed:bundle:] could not load the nibName: NSPageController in bundle (null).

Perhaps you are trying to load a nib with the wrong name in AppDelegate.m or wherever you are initializing your page controller.
Otherwise you have missed creating a .xib file and to name it NSPageController. When creating a Cocoa Touch Class there is a checkbox to also create an xib file for your class if needed.

This line is responsible for the error:
myPageController = NSPageController()
You're trying to initialize a view controller without a nib, that's why it does not work. By default the NSViewController's name is taken to identify the nib that corresponds to it. In your case it is "NSPageController".

Related

Obtaining a View Controller Reference

I read quite a few questions and answers no this problem. Some are for Ojective C. Some are for iOS. The ones that were close to what I need didn't work.
I've set up a protocol for delegation. It doesn't work. The problem is that delegate variable isn't set. I need the reference to an active controller.
Delegator
protocol SwitchTabDelegate: class {
func selectTab(tab: Int)
}
class ViewController: NSViewController {
weak var delegate: SwitchTabDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func selectCompositions(_ sender: NSButton) {
if let delegate = self.delegate {
delegate.selectTab(tab: 2)
}
else {
print("self.delegate is nil")
}
print("delegate called")
}
}
Delegatee
class TabViewController: NSTabViewController, SwitchTabDelegate {
var viewController : ViewController?;
override func viewDidLoad() {
super.viewDidLoad()
//viewController = storyboard?.instantiateController(withIdentifier: "viewController") as? ViewController
// viewController?.delegate = self
// print(viewController)
}
func selectTab(tab: Int) {
print("In the delegate")
switchToDataTab()
}
func switchToDataTab() {
Timer.scheduledTimer(timeInterval: 0.2, target: self,
selector: #selector(switchToDataTabCont),
userInfo: nil, repeats: false)
}
func switchToDataTabCont(){
self.selectedTabViewItemIndex = 2
}
}
The delegatee is the main NSViewContoller. On the storyboard, it contains two buttons and a Container view controller. Embedded in the container view controller is the TabViewController, the delegatee. You can see in the delegatee where I tried to get a reference. It does get a reference, presumably to the newly instantiated instance. I need a reference to the original view controller that was spun up when the application started.
Answer
I added the following code to the delegator:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
let controller = segue.destinationController as! TabViewController
self.delegate = controller as SwitchTabDelegate
}
That's not how it should work following the design pattern. The delegator should have no knowledge of the delegatee. I've spent way too much time on this issue so a hack is going to do.
When using storyboards, you want to "push" references to children when they are created vs. pulling them from an upstream controller. This is what -prepareForSegue:sender: is used for.

Accessing a content of an IBOutlet from another class

My question is simple , I have an IBoutlet(of a label) declared in a class , I need to change it content from another class , is that possible ?
edit : The view of the IBoutlet is loaded in the other class through a scroll view
the first class :
var historyVariable = ""
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// here a code to set a value to historyVariable when button pressed
Text().call()
}
}
the second class
class Text: UIViewController {
#IBOutlet var History: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
func call() {
History.text = "\(historyVariable)"
}
}
it gives me an error unexpectedly found nil while unwrapping an Optional value when trying to set the text to the History label
Assuming text() is actually Text(), that would create a new, empty Text object. Since it's not part of the controller hierarchy, its view and other outlets aren't loaded at the time you invoke call().
You either need to instantiate the Text controller from a storyboard (or otherwise) and present it so that it's properly initialized or else use one that's already active...if that's your intent.

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.

Make a public var without making its class and related functions public?

I'm starting to learn Swift.
I've a viewController that has a var which needs updating from an outside viewController. So I added public to its declaration but my code won't compile because my class is internal (by default). So i make my class public but then it forces me to make all functions inside my class public including viewDidLoad, the tableView dataSource and delegate methods. What am i doing wrong? I don't want anyone else to call my controller's viewDidLoad.
All I wanted to viewControllerA to access a var inside viewControllerB without exposing every function inside viewControllerB to the outside world.
In ObjC, this can be achieved very easily by marking the property readonly in the headerfile and readwrite in the implementation. In this case, I would've the property in the header file so it's read-writable from outside.
here's some pseudo code
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var myTitle: NSString?
override func viewDidLoad() {
super.viewDidLoad()
}
}
// objC part
MyViewController *myViewController = [[MyViewController alloc] initWithNibName:#"MyViewController" bundle:nil];
myViewController.myTitle = #""; // not available
Now if i make myTitle a public var, i get this error
Declaring a public var for an internal class
So I make MyViewController a public class.
Now i get bunch of errors
Method 'tableView(_:numberOfRowsInSection:)' must be declared public
because it matches a requirement in public protocol
'UITableViewDataSource'
You could make a protocol to save and access data across view controllers. Here's one way to do it.
// Make a custom protocol delegate with a method to store the variable. In this case I'll store a boolean.
protocol storeViewControllerBVariableDelegate {
func storeVariable(data: Bool?)
}
// In your view controller A, assign your custom protocol delegate to it and add the new delegate method.
class viewControllerA: UIViewController, storeViewControllerBVariableDelegate {
func storeVariable(data: Bool?) {
self.variableName = data
}
}
// In your view controller A's prepare for segue, assign the stored variable to view controller B if you wanted to pass it forward and backward between view controllers.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let viewControllerB = segue.destinationViewController as! viewControllerB
viewControllerB.variableName = variableName
}
// In your view controller B, initialize a variable and assign it to the delegate.
class viewControllerB: UIViewController {
var variableName: Bool!
var delegate: storeViewControllerBVariableDelegate?
// However you want to save the variable in view controller B, you can do so in an IBAction, viewDidLoad, etc.
#IBAction func saveVariable(sender: UIButton) {
delegate?.storeVariable(self.variableName)
}
}
Here are two solutions that I can think of for passing variables between view controllers
Global Option
ViewController2.swift
import UIKit
var globalVariable = String()
class ViewController1: UIViewConroller {
}
ViewController2.swift
class ViewController2: UIViewController {
overload func viewDidLoad() {
globalVariable = "some string data"
}
}
you can now access that variable globally.
Segue Option
I think a better way to handle sending data back and forth between View Controllers is by using delegates and the prepareForSegue function which is covered in depth here.
You declare your prepareForSegue function like so:
View Controller 1
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "yourIdentifierInStoryboard") {
var yourNextViewController = (segue.destinationViewController as yourNextViewControllerClass)
yourNextViewController.value = yourValue
ViewController 2
class yourNextViewControllerClass {
var value:Int! // or whatever
The you can call it programmatically
self.performSegueWithIdentifier("yourIdentifierInStoryboard", sender: self)
If you want to set values back from your second View Controller, you can use a delegate Method, to respect the original author of this content I'll redirect you to his post:
Read the rest from the original post.

How do I get the return key to perform the same action as a button press in Swift?

I want to know how you allow an action to be made by either pressing the return key on the software keyboard or by tapping a UIButton.
The UI button is already set up to perform an IBAction.
How do I also allow users to press the return key on the keyboard to perform the same action?
Make sure your class extends the UITextFieldDelegate protocol
SomeViewControllerClass : UIViewController, UITextFieldDelegate
You can perform action as follows:
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
//textField code
textField.resignFirstResponder() //if desired
performAction()
return true
}
func performAction() {
//action events
}
UPDATE
If your deployment target is iOS 9.0 or later, you can connect the “Primary Action Triggered” event of your text field to an action, like this:
ORIGINAL
Make your view controller adopt the UITextFieldDelegate protocol.
Set your text field's delegate to your view controller.
Implement textFieldShouldReturn: to call your action.
Swift 4.2 :
Other approach for the textfield created programmatically and doesn't need delegate :
MyTextField.addTarget(self, action: #selector(MyTextFielAction)
, for: UIControl.Event.primaryActionTriggered)
And then perform your action like below :
func MyTextFielAction(textField: UITextField) {
//YOUR CODE can perform same action as your UIButton
}
If your deployment target is iOS 9.0 or later, you can connect the “Primary Action Triggered” event of your text field to an action, like this:
I was not able to get the "Primary Action Triggered" to work as suggested. I used "Editing Did End" and that works for now Screenshot of Editing Did End
Here is a complete example, with both:
button-action to write and also to clear label and text when pressing button repeatedly it alternates both actions
return-in-keyboard when pressing key it triggers action and also resigns first responder
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField1: UITextField!
#IBOutlet weak var label1: UILabel!
var buttonHasBeenPressed = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
textField1.delegate = self
}
#IBAction func buttonGo(_ sender: Any) {
performAction()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
performAction()
return true
}
func performAction() {
buttonHasBeenPressed = !buttonHasBeenPressed
if buttonHasBeenPressed == true {
label1.text = textField1.text
} else {
textField1.text = ""
label1.text = ""
}
}
}

Resources