Getting clicked item index in NSPathControl with pathItems instead of pathComponentCells - cocoa

Since NSPathControl.setPathComponentCells(_:) and .clickedPathComponentCell() are deprecated, I'm trying to use pathItems and clickedPathItem instead. Since I'm representing a virtual path, I cannot use the NSPathControl.url setter, but instead set pathItems directly.
The problem is that in the action method it doesn't seem possible to get the index of the clicked path item, nor does it seem possible to associate any kind of data with each path item, since when the action method is called, the actual object instances stored in pathItems and also the one returned by clickedPathItem change every time.
Here is the sample code that reproduces the issue:
class ViewController: NSViewController {
#IBOutlet weak var pathControl: NSPathControl!
override func viewDidLoad() {
super.viewDidLoad()
pathControl.pathItems = ["a", "b", "c"].map({ title in
let item = NSPathControlItem()
item.title = title
return item
})
}
#IBAction func selectPath(_ sender: NSPathControl) {
print(sender.clickedPathItem!.description, sender.clickedPathItem!.title, sender.pathItems.description)
}
}
Here is a sample output (notice how the printed addresses change every time):
<NSPathControlItem: 0x6000012780a0> a [<NSPathControlItem: 0x6000012780a0>, <NSPathControlItem: 0x600001278020>, <NSPathControlItem: 0x600001278090>]
<NSPathControlItem: 0x600001278070> a [<NSPathControlItem: 0x600001278070>, <NSPathControlItem: 0x600001278140>, <NSPathControlItem: 0x6000012780d0>]
<NSPathControlItem: 0x60000124c030> a [<NSPathControlItem: 0x60000124c030>, <NSPathControlItem: 0x60000124c080>, <NSPathControlItem: 0x60000124c070>]

Related

Executing NSApplicationDelegate Code Before ViewController viewDidLoad

My Swift 3, Xcode 8.2 MacOS app loads several tables through web services calls. Since the tables are used by one or more of my seven view controllers, I placed them in the AppDelegate.
The problem is that the AppDelegate methods applicationWillFinishLaunching and applicationDidFinishLaunching run after the ViewController viewDidLoad methods.
As a result the table views show no data. I was able to get it to work correctly by calling the appDelegate method that loads the data from one of the ViewController viewDidLoad methods. Since any of the ViewControllers could be invoked on application start up, I would have to add the call to all of them and some sort of flagging method to prevent redundant loads.
My question is: where can I place code that will execute prior to the ViewControllers loading? The code loads data into multiple arrays of dictionary. These arrays are in the AppDelegate.
I read up on #NSApplicationMain and replacing it with a main.swift. I assume none of application objects would have been instantiated at that point so I couldn't call their methods and don't think my code would be valid outside of a class.
The pertinent part of my appDelegate:
class AppDelegate: NSObject, NSApplicationDelegate {
var artists: [[String:Any]]? = nil
var dispatchGroup = DispatchGroup() // Create a dispatch group
func getDataFromCatBox(rest: String, loadFunction: #escaping ([[String: Any]]?) -> Void) {
let domain = "http://catbox.loc/"
let url = domain + rest
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "Get"
let session = URLSession.shared
var json: [[String:Any]]? = nil
dispatchGroup.enter()
session.dataTask(with: request) { data, response, err in
if err != nil {
print(err!.localizedDescription)
return
}
do {
json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [[String: Any]]
}
catch {
print(error)
}
loadFunction(json)
self.dispatchGroup.leave()
}.resume()
}
func loadArtistTable(array: [[String: Any]]?) {
artists = array
}
}
The ViewController code:
override func viewDidLoad() {
super.viewDidLoad()
appDelegate = NSApplication.shared().delegate as! AppDelegate
appDelegate.getDataFromCatBox(rest: "artists.json", loadFunction: appDelegate.loadArtistTable)
appDelegate.dispatchGroup.wait()
artistTable.reloadData()
}
The code works in that the TableView is populated when the window appears. While it's not a lot of code, I would have to duplicate across all my View Controllers.
This a prototype. The production version will have 14 tables and invocations.
I guess my comment should be an answer. So. Why not just make the window containing the table views not be visible on launch? Then in didFinishLaunching, load the table data and then show the window.
I don't think there is any way to do what I want the way it is structured in the question. The ViewController code could be reduced to
appDelegate = NSApplication.shared().delegate as! AppDelegate
appDelegate.getDataFromCatBox(rest: "artists.json", loadFunction: appDelegate.loadArtistTable
by creating a wrapper function in AppDelegate that had the wait in it. It also could contain a flag that indicated that a given table had already been loaded so as not to make a redundant call.
I ended up going with a different approach: I created a super class with singleton subclasses for each table. Now my viewDidLoad method looks like this:
artists.loadTable() // The sublass
artistTable.reloadData()
If any one comes up with a cleaner solution to the original problem, I'll accept their answer in place of mine.

OS X Core Data - Passing a Managed Object Context to a View Controller

I am developing a Mac Application in Xcode 7.3.1. and I am trying to pass a Model Object Context from my AppDelegate to an ArrayController.
I have a class named DataController which creates my Core Data stack. DataController.managedObjectContext holds the Managed Object Context.
My AppDelegate class is as follows:
class AppDelegate: NSObject, NSApplicationDelegate {
var dataController: DataController!
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
// Create an instance of the DataController class.
dataController = DataController()
// Create a reference to the first ViewController embedded in the WindowController.
guard let splitViewController = NSApplication.sharedApplication().windows[0].contentViewController as? ManagedObjectContextSettable
else { fatalError("Wrong view controller type")}
// Set the managedObjectContext property.
splitViewController.managedObjectContext = dataController.managedObjectContext
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
In my storyboard I have embedded a SplitViewController in my WindowController. The SplitViewController has its own custom View Controller class named SplitViewController. Here is the code in the SplitViewController:
class SplitViewController: NSSplitViewController, ManagedObjectContextSettable {
var managedObjectContext: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
// Create a reference to the first ViewController embedded in the WindowController.
let childControllers = self.childViewControllers
print("childControllers.count = \(childControllers.count)")
for childController in childControllers{
if childController.isKindOfClass(TableViewController){
print("Found TableViewController")
guard let tableViewController = childController as? ManagedObjectContextSettable
else { fatalError("Wrong view controller type")}
tableViewController.managedObjectContext = managedObjectContext
}
}
}
}
Within one of the Split View Items is my TableView which has its own View Controller named TableViewController. Here is the code for TableViewController:
class TableViewController: NSViewController, ManagedObjectContextSettable, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
var managedObjectContext: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
//print(managedObjectContext.description)
}
}
In the storyboard I dragged an ArrayController and in the Bindings tab of the Inspector I have set Bind To and selected TableViewController and set the Model Key Path to 'self.managedObjectContext'. Ultimately it's not receiving the Managed Object Context.
I cannot establish if I should override the prepareForSegue function for an embedded ViewController, every example I read is for IOS.
Where am I going wrong please?
applicationDidFinishLaunching can be executed after viewDidLoad. Set managedObjectContext of the childControllers when managedObjectContext of the splitViewController is set.
Bindings use KVO. Change var managedObjectContext to dynamic var managedObjectContext to make the property KVO compliant.
If you are using an array controller with Cocoa bindings you have to override the init(coder:) method and initialize the managed context there to perform the implicit initial fetch. viewDidLoad is too late.
The segue workflow is the same as in iOS. It's even more convenient because there is a property presentingViewController to get the reference to the parent view controller.
The Core Data Manager DataController is supposed to be a singleton to ensure that the managed object context instance is always the same.
This may not be good practice, but it appears to work.
I made my DataController class a singleton class to ensure there was only one Managed Object Context. In my TableViewController I created a managedObjectContext property as follows:
lazy var managedObjectContext = DataController.sharedInstance.managedObjectContext
In my array controller I bind the managed object context parameter to the TableViewController and set the Model Key Path to self.managedObjectContext.
Can this be improved upon?

When is a Cocoa binding 'set'

I've got an NSArrayController set in my storyboard where I set the mode to Entity Name with a name of Client, and bound the managed object context, selection indexes, and sort descriptors. My NSPopupButton links to that array controller and when I run I see all the elements I expect on the button.
Now I made a strong #IBOutlet in my code and I'm trying to access the contents:
let objs = clientArrayController.arrangedObjects as! [Client]
print("I have \(objs.count) clients")
I tried that code in viewDidLoad, viewWillAppear and viewDidAppear. They all say 0 clients. Clearly that's not possible as I have the clients showing in the UI.
What am I doing wrong here?
viewDidLoad, viewWillAppear, and viewDidAppear are possibly all called before your array controller collects its data from the core data store - hence they (correctly) report a count of zero. To get word of any changes to your array controller's arrangedObjects array you could use one of the aforementioned methods to install an observer that watches this object and reports any changes:
// MyPopUpController.swift
var ArrayControllerArrangedObjectsObservationContext = "arrayController.arrangedObjects"
func viewDidLoad() {
arrayController.addObserver(self,
forKeyPath: "arrangedObjects",
options: .New | .Old,
context: &ArrayControllerArrangedObjectsObservationContext)
}
You're given the opportunity to respond to changes in observeValueForKeyPath...
// MyPopUpController.swift
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
switch context {
case &ArrayControllerArrangedObjectsObservationContext:
// Check counts here
default:
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
If you only need the observer on start-up, after you've checked the counts, and done whatever you need to do you should then remove the observer:
// MyPopUpController.swift
arrayController.removeObserver(self,
forKeyPath: "arrangedObjects",
context: &ArrayControllerArrangedObjectsObservationContext)

'#selector' refers to a method that is not exposed to Objective-C

The new Xcode 7.3 passing the parameter via addTarget usually works for me but in this case it's throwing the error in the title. Any ideas? It throws another when I try to change it to #objc
Thank you!
cell.commentButton.addTarget(self, action: #selector(FeedViewController.didTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
The selector it's calling
func didTapCommentButton(post: Post) {
}
In my case the function of the selector was private. Once I removed the private the error was gone. Same goes for fileprivate.
In Swift 4
You will need to add #objc to the function declaration. Until swift 4 this was implicitly inferred.
You need to use the #objc attribute on didTapCommentButton(_:) to use it with #selector.
You say you did that but you got another error. My guess is that the new error is that Post is not a type that is compatible with Objective-C. You can only expose a method to Objective-C if all of its argument types, and its return type, are compatible with Objective-C.
You could fix that by making Post a subclass of NSObject, but that's not going to matter, because the argument to didTapCommentButton(_:) will not be a Post anyway. The argument to an action function is the sender of the action, and that sender will be commentButton, which is presumably a UIButton. You should declare didTapCommentButton like this:
#objc func didTapCommentButton(sender: UIButton) {
// ...
}
You'll then face the problem of getting the Post corresponding to the tapped button. There are multiple ways to get it. Here's one.
I gather (since your code says cell.commentButton) that you're setting up a table view (or a collection view). And since your cell has a non-standard property named commentButton, I assume it's a custom UITableViewCell subclass. So let's assume your cell is a PostCell declared like this:
class PostCell: UITableViewCell {
#IBOutlet var commentButton: UIButton?
var post: Post?
// other stuff...
}
Then you can walk up the view hierarchy from the button to find the PostCell, and get the post from it:
#objc func didTapCommentButton(sender: UIButton) {
var ancestor = sender.superview
while ancestor != nil && !(ancestor! is PostCell) {
ancestor = view.superview
}
guard let cell = ancestor as? PostCell,
post = cell.post
else { return }
// Do something with post here
}
Try having the selector point to a wrapper function, which in turn calls your delegate function. That worked for me.
cell.commentButton.addTarget(self, action: #selector(wrapperForDidTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
-
func wrapperForDidTapCommentButton(post: Post) {
FeedViewController.didTapCommentButton(post)
}
As you know selector[About] says that Objective-C runtime[About] should be used. Declarations that are marked as private or fileprivate are not exposed to the Objective-C runtime by default. That is why you have two variants:
Mark your private or fileprivate method declaration by #objc[About]
Use internal, public, open method access modifier[About]

NSCollectionView does show nothing

I've tried to follow this guide:
Quick Start for Collection Views
using an NSImageView in the Collection View Item.
Nothing shows up, neither if i set the image with a Image Well neither if i set the array via code.
So i tried to do it programmatically, using
func representedObject(representedObject: AnyObject)
{
super.representedObject = representedObject
photoImageView.image = (representedObject as! NSImage)
println("\(representedObject)")
}
in the Collection View Item (subclassed).
If I don't subclass Collection View Item Xcode tells me that there is no prototype set, if i subclass it it tells that "could not load the nibName"... (it's in the storyboard with correct identity set)
I can't have this Collection View to work :-(
Anyway, i like the bindings... so i'd like to achieve the correct result with bindings..
I checked and rechecked every passage in the document at the link and everything seems fine. the main difference is that the document uses the app delegate, i'm using a view controller.
i translated KVC methods in swift, i think they are correct since i know them have been called. Here them are:
func insertObject(p: ClientPhoto, inClientPhotoArrayAtIndex index: Int) {
images.insertObject(p, atIndex: index)
}
func removeObjectFromClientPhotoArrayAtIndex(index: Int) {
images.removeObjectAtIndex(index)
}
func setClientPhotoArray(a: NSMutableArray) {
images = a
}
func clientPhotoArray() -> NSArray {
return images
}
Their are basically 2 ways to work with NSCollectionView. 1 is to set the itemPrototype property and the other is to override newItemForRepresentedObject. The override method is more flexible and has the advantage that you using the technique below you can create the nscollectionviewitem in storyboard and all the outlets will be set correctly. Here is an example of how I use it:
class TagsCollectionView: NSCollectionView {
// ...
override func newItemForRepresentedObject(object: AnyObject!) -> NSCollectionViewItem! {
let viewItem = MainStoryboard.instantiateControllerWithIdentifier("tagCollectionViewItem") as! TagCollectionViewItem
viewItem.representedObject = object
return viewItem
}

Resources