I have developed a macOS personal finance app in SwiftUI that uses NSTableViews and NSOutlineViews through the NSViewControllerRepresentable interface between SwiftUI and AppKit.
I have found that the AppKit view capabilities are far superior to anything I can create using SwiftUI lists plus they run faster and support TypeSelect. Passing data into the Appkit ViewControllers is straightforward but getting information out seems to be less so.
Apple developer documentation suggests that I should use a Coordinator class and I have tried to do so but to no avail. The delegated method that I need (func tableViewSelectionDidChange(_ notification: Notification)) fails to run (from inside my Coordinator class).
For the moment I am exchanging data and actions using NotificationCenter.default and that is working well but I am keen to use the 'official' method. I have found a macOS application that uses Coordinator successfully (at https://www.markusbodner.com/til/2021/02/08/multi-line-text-field-with-swiftui-on-macos/) but I cannot get my Coordinator class to work as a NSTableViewDelegate even though it builds and runs without errors (Xcode 12.4).
All I want to do for a start is to get an NSViewController that contains a NSTableView to pass the NSTableView.selectedRow into its parent SwiftUI view via a Coordinator, whenever the user selects a new row in the NSTableView. Can anyone help (with some sample code, if possible, please)?
I seem to have found the solution to my own problem. I had been allocating my NSTableViewDelegate to the Coordinator in the makeNSViewController function. However, it appears that my NSTableView is not being instantiated until the code that populates the contents of my NSTableView's NSTableViewDataSource has been run (in the updateNSViewController function). As a result there is no NSTableViewDelegate to allocate.
Moving tableVC.tableView?.delegate = context.coordinator into the updateNSViewController function, following the code that populates the contents of my NSTableViewDataSource, makes my NSTableViewDelegate work as intended and tableView.selectedRow values are now passed to my parent SwiftUI view via func tableViewSelectionDidChange(_ notification: Notification) successfully. Hurrah!
It may be useful to note that I am using Cocoa bindings in my NSTableView and have found that it is necessary to leave empty #IBAction func stubs (ctrl-dragged form Interface Builder as normal) in the ViewController (the File's Owner) and place copies of those funcs (populated with code, of course) in my Coordinator class to get their code to execute as intended.
The resulting overall code is much neater than my old code, which used notifications to pass actions and data between SwiftUI and AppKit - so the 'official' method looks to be best.
Related
Please note this is not an iOS question.
I have an NSView-based app (i.e. not document-based), and I’d like to bolt on a printing subsystem. I can get NSViews in my main controller to print ok. However, I want to have a special view constructed just for printing. The view should not show in the app’s window. The view contains two NSTextFields, two NSTextViews, and 5 labels.
I cannot seem to figure out a way to do this. I have tried various forms of these examples:
Add an NSView to my main view window? Seems logical, but it’s awkward in a storyboard, (I can’t position the view in the storyboard).
Programmatically create a custom NSView with a xib?
For this, I’ve tried:
#IBOutlet weak var printView: NSView!
….
let printOperation = NSPrintOperation(view: printView!)
This results in the comprehensive "fatal error: unexpectedly found nil while unwrapping an Optional value” message.
The outlets are configured correctly (I think)
A seperate ViewController? If so, how can I avoid having two print buttons — one to call the print controller, and the second, to print the PrintController’s view.
I’ve tried reading the Apple docs, but they are not the way I learn best. There are also no Print documents in Swift that I've found. I’ve waded through many SE questions, but have come up blank. Could you point me towards a solution please.
I think the specific problem here is that you're never causing the view to be loaded.
You can double check whether this is the case by overriding the viewDidLoad method on the controller. If it's never called, your view is never loaded from the nib.
Normally the UI machinery takes care of all that when you display a view controller, which is why it can be so confusing when it doesn't happen.
You should be able to trigger the view load by accessing the view property on the controller. e.g.
_ = self.view // Touch the view to force it to load
https://developer.apple.com/documentation/appkit/nsviewcontroller/1434401-view has some additional information.
You can also call loadView() directly although that's usually frowned upon.
This is how I'm currently doing it but I'm wondering if this is how Apple suggests. I've read some debates about that.
let appDelegate : AppDelegate = NSApplication.sharedApplication().delegate as AppDelegate
if let moc = appDelegate.managedObjectContext {
// do stuff here
}
So that's just to get it from the AppDelegate to the first viewController. From there I'm guessing that using segues is the way to go to pass around the managedObjectContext?
Using the above code is pretty annoying because I'm typing that into every method in the viewController that needs the MOC. It's even more annoying when I have a function with a return statement and all my code that uses the MOC is inside the body of the if statement since that creates errors stating that there is/may not be a return. Is there a better way to do that, like make it more global?
EDIT:
My ViewController.swift file has this header:
import Cocoa
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
and contains a Table View
My AppDelegate.swift file has:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
Apple sample code stores and creates the Core Data stack in the app delegate, but that doesn't mean it's right. In fact, it's entirely wrong. The app delegate shouldn't own the data store. Apple does it, and most people follow along, because it's convenient. You should really have a custom class which owns and creates the Core Data stack.
This custom class needs to be instantiated somewhere, that could be in the app delegate and then passed to your root view controller, or it could be in the root view controller itself. This custom class could also be a singleton (because you don't want multiple instances of it).
Each view controller should have a public property for the MOC which is set by its creator (part of the segue code). In this way each controller isn't going and getting the MOC, the MOC dependency is being injected. This helps keep the relationships clean and aids in unit testing. You also don't need the let to check you got the MOC back - if it isn't there it's a development issue (and if it couldn't be created you should never have created / pushed the view controller...).
I'm fairly new to Mac development and am slightly confused by the new "storyboard" feature in Xcode 6. What I'm trying to do is segue from one view controller to another in the same window. As of right now, all the different NSViewControllerSegues present the view controller in a new window, be it a modal or just another window. What I'd like to do is just segue within the same window, much in the same way one would on iOS (though an animated transition is not crucial). How would this be achieved?
If you provide a custom segue (subclass of NSStoryboardSegue) you can get the result you are after. There are a few gotchas with this approach though:
the custom segue will use presentViewController:animator so you will need to provide an animator object
because the presented view is not backed by a separate Window object, you may need to provide it with a custom NSView just to catch out mouse events that you don't want to propagate to the underlying NSViewController's view
there's also a Swift-only glitch regarding the custom segue's identifier property you need to watch out for.
As there doesn't seem to be much documentation about this I have made a small demo project with custom segue examples in Swift and Objective-C.
I also have provided some more detail in answer to this question.
(Reviving this as it comes up as first relevant result on Google and I had the same problem but decided against a custom segue)
While custom segues work (at least, the code given in foundry's answer worked under Swift 3; it needs updating for Swift 4), the sheer amount of work involved in writing a custom animator suggests to me that their main use case is custom animations.
The simple solution to changing the content of a window is to create an NSWindowController for your window, and to set its contentViewController to the desired viewController. This is particularly useful if you are following the typical pattern of storyboards and instantiate a new ViewController instance every time you switch.
However.
The NSStoryboard documentation says, quite clearly in macOS, containment (rather than transition) is the more common notion for storyboards which led me to look again at the available tools.
You could use a container view for this task, which adds a NWViewController layer instead of the NSWindowController outlined above. The solution I've gone with is to use an NSTabViewController. In the attributes inspector, set the style to 'unspecified', then select the TabView and set its style to 'tabless'.
To change tabs programatically, you set the selectedTabViewItemIndexof your TabViewController.
This solution reuses the same instance of the ViewControllers for the tab content, so that any data entered in text fields is preserved when the user switches to the other 'tab'.
Simple way with no segues involved to replace the current view controller in the same window:
if let myViewController = self.storyboard?.instantiateController(withIdentifier: "MyViewController") as? MyViewController {
self.view.window?.contentViewController = myViewController
}
I am working on a new Cocoa project using Swift, Core Data and storyboards, and have come across a problem that makes no sense to me. After some fairly extensive hunting around, including on this site, I have come to the conclusion that I must be missing something obvious, but cannot figure out what. Here is what I have done so far:
1.Create a new project, Cocoa Application, using Swift, Storyboards, and Core Data.
2. Create an entity in the .xcdatamodeld. Let’s call it Dataset.
3. Create a subclass of NSSplitViewController (for what i want to do in the rest of the program).
4. Set the window content of the main window to and instance of myVC. I checked, and it loads up and displays fine.
5. In the viewController.swift, get the managedObjectContext like so:
#IBOutlet var moc:NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
let appDelegate = NSApplication.sharedApplication().delegate as AppDelegate
moc = appDelegate.managedObjectContext
println("mainsplitviewcontroller moc:")
println(moc)
println("mainsplitviewcontroller psc:")
println(moc.persistentStoreCoordinator)
NSLog("Main split view loaded")
}
(yes, I have about dependency injection, but I want to solve this problem first).
In IB, put a managedObjectContext object in the View Controller instance.
In IB, connect the myVC outlet for the variable moc to the managedObjectContext.
In IB, create an array controller. Set it To Entity. Entity Name is Dataset. Turn on Prepares Content.
Either as an outlet or a binding, connect the array controller to the MOC. Using outlet, just dragging from managed object context in it's right-click popup to the icon for the MOC created in 6 above. For bindings, the old fashioned way, going to the bindings tab, and under Parameters, Bind to: (view controller), Model Key Path: moc. (moc is from 5 above)
Then, I build and run. and I get the error: "Cannot perform operation since managed object context has no persistent store coordinator."
This happens whichever way I try to do 9, above.
Now, the thing is, from my println statements, the objects referred to in both the app delegate and the viewcontroller are the same, both for the managed object context and for the persistent store controller, as below:
appdelegate moc:
appdelegate psc:
mainsplitviewcontroller moc:
mainsplitviewcontroller psc:
I wish I could show images, but I am new here and so cannot do that. Am I doing something clearly wrong? I thought I understood the process: make sure the VC can access the MOC, then put the MOC object into the VC's window in IB, make it an outlet, and connect it to an array controller. Why would the swift file for the view controller seem to show that the PSC is the same as the app delegate, but in IB, the array controller think the MOC has no PSC at all?
Thanks for reading!
I don't know if this is going to help, but I'm not surprised that your project shows that error. You have two managed object contexts - one created by your app delegate, and one created by the storyboard. Your interface code is connecting to that second MOC, which is not connected to your persistent store.
I know the question is a bit generic but I guess my issue is generic as well.
I'm developing a small application in my free time and I decided to do it with Cocoa. It's nice, many things works almost automagically, but sometimes it's quite hard to understand how the framework works.
Lately I'm facing a new problem. I want to manage all the windows of the application from a single class, a front controller basically. I have a main menu and an "Import data" function. When I click it I want to show another window containing a table and call a method for updating the data. The problem is that this method is inside the class that implements the NSTableViewDataSource protocol.
How can I have a reference to that class? And more important, which should be the right way to do it? Should I extend the NSWindow class so that I can receive an Instance of NSWindow that can control the window containing the table (and then call the method)?
I may find several ways to overcome this issue, but I'd like to know which one is the best practice to use with cocoa.
PS: I know that there are tons of documentations files, but I need 2 lives to do everything I'd like to, so I thought I may use some help asking here :)
The problem is that this method is inside the class that implements the NSTableViewDataSource protocol.
How can I have a reference to that class?
These two sentences don't make sense, but I think I understand what you're getting at.
Instead of subclassing NSWindow, put your import window's controlling logic – including your NSTableViewDataSource methods – into a controller class. If the controller corresponds to a window, you can subclass NSWindowController, though you don't have to.
You can implement -importData: as an IBAction in your application delegate, then connect the menu item's selector to importData: on First Responder. That method should instantiate the import window controller and load the window from a nib.
In your import window controller's -awakeFromNib or -windowDidLoad method, call the method which updates the data.
Added:
Here's the pattern I'd suggest using in your app delegate:
#property (retain) ImportWindowController *importWC;
- (IBAction) showImportWindow:(id) sender {
if (!self.importWC)
self.importWC =
[[ImportWindowController alloc] initWithWindowNibName:#"ImportWindow"];
[self.importWC refreshData];
[self.importWC.window makeKeyAndOrderFront:sender];
}