NSSplitViewController Child View Not Added - macos

I've created a simple NSViewController and want to add a split view with just one child view. The split view should be controlled by a NSSplitViewController, because I'd like to use the NSSplitItem's facilities for collapsing/expanding split items. After adding a child view controller, the split item is created, but no child view is added to the view tree.
override func viewDidLoad() {
super.viewDidLoad()
let splitViewController = NSSplitViewController()
view.addSubview(splitViewController.splitView)
let myController = MyController(nibName: "MyController", bundle: nil)
splitViewController.addChildViewController(myController)
printTree(view)
}
func printTree(view: AnyObject, _ n: Int = 1) {
if let view = view as? NSView {
NSLog("\(n): \(view)")
for child in view.subviews {
printTree(child, n + 1)
}
}
}
Output:
1: <NSView: 0x618000120140>
2: <NSSplitView: 0x6180001205a0>
Why does the split view have no child view?
To compare, here's the version without split view:
override func viewDidLoad() {
super.viewDidLoad()
let myController = MyController(nibName: "MyController", bundle: nil)
view.addSubview(myController.view)
printTree(view)
}
Output:
1: <NSView: 0x6100001203c0>
2: <NSView: 0x6000001208c0> <-- here's my child view
3: <NSButton: 0x600000140580>
And adding the child view directly as a subview to the split view doesn't work either:
A SplitView managed by a SplitViewController cannot have its subviews modified
So, my question is, why is the child view not added to the view tree inside the split view?

"You're doing it wrong"
You're using base class methods when NSSplitViewController has a very particular API.
See: https://developer.apple.com/library/prerelease/mac/samplecode/Exhibition/Listings/Exhibition_GalleryWindowController_swift.html for an example.
You want the addSplitViewItem: method.

I figured it out. My mistake was that I added the splitView instead of the view:
// this won't work:
self.view.addSubview(splitViewController.splitView)
// this will work:
self.view.addSubview(splitViewController.view)
BTW: using splitViewController.addChildViewController(myController) as I did before is just a shorter way of saying the following:
let item = NSSplitViewItem(viewController: myController)
splitViewController.addSplitViewItem(item)
which didn't work for me because of my mistake described above.

Related

Add completion handler to presentViewControllerAsSheet(NSViewController)?

I am attempting to present a sheet configuration view (AddSoundEffect) for my main window/view controller (I'm using storyboards), and when the configuration view controller is dismissed, take the values entered in the AddSoundEffect view and pass that back to the main view. My current code in the main view controller:
presentViewControllerAsSheet(self.storyboard!.instantiateControllerWithIdentifier("AddSoundEffect") as! AddSoundViewController
And in the AddSoundViewController.swift file, the code to dismiss it is:
self.dismissViewController(self)
To pass the data, I have a class-independent tuple that I save data to. How do I add a completion handler to presentViewControllerAsSheet, and (optionally) is there a better way to pass the data between view controllers?
Setup: Xcode version 6.4, OS X 10.10.4
Delegation pattern is the easiest way for you.
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
protocol AddSoundViewControllerDelegate: class {
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect)
}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
weak var delegate: AddSoundViewControllerDelegate?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.delegate?.soundViewController(self, didAddSoundEffect: soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController, AddSoundViewControllerDelegate {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.delegate = self
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect) {
// This method is called only when new sound effect is added
}
}
Another way is to use closures:
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
var completionHandler: ((SoundEffect) -> ())?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.completionHandler?(soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.completionHandler = { [weak self] (soundEffect) -> () in
// Called when new sound effect is added
}
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
}
Or many other ways like sending notification, ... Whatever suits your needs. But delegation pattern or closures is the best way to go in this specific case.
I missed that your question is about NSViewController. This example is for iOS, but same pattern can be used on OS X without any issues.
The easiest way to detect sheet opening or closing is to use the Sheet Notifications:
class ViewController: NSViewController, NSWindowDelegate {
override func viewDidLoad(){
NSApplication.sharedApplication().windows.first?.delegate = self
}
func windowDidEndSheet(notification: NSNotification) {
}
func windowWillBeginSheet(notification: NSNotification) {
}
}

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.

Xcode Swift: How to stop variables from changing back after each viewDidLoad, how to save and update data from different ViewControllers?

I have some vars in my Main VC and when user clicks a button in another VC the prepareForSegue passes along a new value to the Main VC and updates a label.
But when the user clicks again it's back to initial value, so it doesn't increment since the value is set back in the viewDidLoad?
MainVC:
var statsHealth:Int = 0
override func viewDidLoad() {
super.viewDidLoad()
healthLabel.text = String("Health: \(statsHealth)/10")
}
Another VC:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "startSegue") {
let startVC = segue.destinationViewController as ViewController
startVC.statsHealth += 1
}
It's displayed as 0, then 1 but then 0 again and then 1 instead of 2,3,4 etc.
Any ideas?
BR
Nils
Perhaps not the most 'Swift' way to do it, but certainly works well....
Create a file called Variables.swift which will hold all your 'universal' variables (if these are going to be on every page, I see no reason this isn't the 'best' way to do it - certainly it is the most simple to understand!)
in Variables.swift, hold all your universal variables
struct Variables {
static var statsHealth = 0
.....
}
Then, in each other page, access them at any time
healthLabel.text = String("Health: \(Variables.statsHealth)/10")
or set them
Variables.statsHealth += 1
So based on your description, I assume the view controller structure is like this:
AnotherVC -> MainVC
MainVC is presented on top of AnotherVC. When you go back to AnotherVC, did you dismiss MainVC completely? If so, then every time you go from AnotherVC to MainVC, it initiate a new ViewController, and the variables you saved before doesn't exist anymore.
If you want to keep this structure and change variables in MainVC, keep a reference of mainVC in AnotherVC. Then instead of connecting in storyboard, you may want to present it programmatically.
class AnotherVC {
var mainVC: MainVC?
func presentMainVC() {
var targetVC = UIViewController()
if self.mainVC != nil {
targetVC = self.mainVC
} else {
let storyboard = UIStoryboard(name: "Your-storyboard-name", bundle: nil)
targetVC: MainVC = storyboard.instantiateViewControllerWithIdentifier("The-main-VC-identifier") as MainVC
self.mainVC = targetVC
}
//you can change your variable here
mainVC.statsHealth += 1
self.presentViewController(self.mainVC, animated: true, completion: nil)
}
If you mainVC is on top of AnotherVC in any case, you can just revert the reference direction.

Passing data from textfield to next view

I am trying to develop a basic app in Xcode 6.1 and Swift language.
I cant seem to find any IOS 8, Xcode6 and Swift resources yet that help with this. Everything is still xcode 5 and IOS 7.
I have the whole story board done with images buttons and segues. just need to script it.
There are 6 segues all with segue "show".
I am just trying to capture one text field per view controller. upon button press for the segue to the next view controller.
the prepareForSegue thing confuses me.
how do I capture the data from a text field save it to a variable on button press and send that variable info to the next view controller?
That text field I want saved to a variable or let that I can have the final view controller access all the data input from each view controller and then do some basic math.
Any body can help with this?
Assuming that you have two view Controllers here is how you will pass value to the second view controller.
class ViewControllerA: UIViewController {
#IBOutlet var textField: UITextField = nil
...
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "segueTest") {
var svc = segue!.destinationViewController as ViewControllerB;
svc.passedValue = textField.text
}
}
}
class ViewControllerB: UIViewController {
var passedValue: String?
...
override func viewDidLoad() {
...
if let myPassedData = passedValue {
println(myPassedData)
}
or use
let myPassedData = passedValue ?? ""
println(myPassedData)
}
}
}

How to get selected item of NSOutlineView without using NSTreeController?

How do I get the selected item of an NSOutlineView with using my own data source.
I see I can get selectedRow but it returns a row ID relative to the state of the outline. The only way to do it is to track the expanded collapsed state of the items, but that seems ridiculous.
I was hoping for something like:
array = [outlineViewOutlet selectedItems];
I looked at the other similar questions, they dont seem to answer the question.
NSOutlineView inherits from NSTableView, so you get nice methods such as selectedRow:
id selectedItem = [outlineView itemAtRow:[outlineView selectedRow]];
Swift 5
NSOutlineView has a delegate method outlineViewSelectionDidChange
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
}
}
Bonus Tip: You can also get the parent item of the selected item like this:
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
// Get the parent item
if let parentItem = outlineView.parent(forItem: item){
}
}
}
#Dave De Long: excellent answer, here is the translation to Swift 3.0
#objc private func onItemClicked() {
if let item = outlineView.item(atRow: outlineView.clickedRow) as? FileSystemItem {
print("selected item url: \(item.fileURL)")
}
}
Shown is a case where item is from class FileSystemItem with a property fileURL.

Resources