NSTableView & NSNotificationCenter strange behavior - macos

I got very strange behavior when reloading NSTableView data inside notification observer.
class MainWindowController: NSWindowController, NSTableViewDataSource, NSTableViewDelegate
{
var data: String[] = []
#IBOutlet var filesTableView: NSTableView!
override func awakeFromNib()
{
super.awakeFromNib()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "droppedFiles:", name: DroppedFilesNotification.notificationName, object: nil)
}
func droppedFiles(notification: NSNotification!)
{
data += ["123"]
println(data.count)
filesTableView.reloadData()
}
func numberOfRowsInTableView(tableView: NSTableView!) -> Int
{
return data.count
}
#IBAction func crazyTest(AnyObject)
{
NSNotificationCenter.defaultCenter().postNotificationName(DroppedFilesNotification.notificationName, object: self, userInfo: [DroppedFilesNotification.fileNamesParameterName: ["123"]])
}
}
First call of crazyTest function displays:
1
Seconds call of crazyTest function displays:
2
3
4
Third call of crazyTest function displays numbers 5-13.
If we would remove filesTableView.reloadData() from droppedFiles function then all works fine except table view isn't updated. Any idea why this happens and how to reload table view there?
EDIT:
Also, there is no issue in case calling droppedFiles function directly instead of using NSNotificationCenter. But I'd prefer to use notification center in my application.
Thanks ahead.

If this is a view-based table view which (perhaps implicitly) loads its views from NIBs, then its awakeFromNib method will get called each time the NIB is loaded. From here:
Note: Calling makeViewWithIdentifier:owner: causes awakeFromNib to be
called multiple times in your app. This is because
makeViewWithIdentifier:owner: loads a NIB with the passed-in owner,
and the owner also receives an awakeFromNib call, even though it’s
already awake.
In your case, you're registering for the notification each time. So, you're registering for it many times over and you receive the notification once for each time you registered.

You are not removing the observer. My guess is that you are going into that view controller multiple times and therefore it is registered for the same notification multiple times. Therefore, when the notifications is triggered, the callback gets called multiple times.

Related

How to close window (NSWindowController) by hitting the ESC key?

Issue
I would like the user being able to close a window by hitting the ESC key but I can't get it to work in this specific case, hitting ESC triggers an error sound (the "no you can't do that" macOS bloop) and nothing happens.
Context
I'm making a subclass of NSWindowController which itself creates an instance of a subclass of NSViewController and sets it in a view. Both controllers have their own xib file.
NSWindowController:
final class MyWindowController: NSWindowController, NSWindowDelegate {
#IBOutlet weak var targetView: MainView!
let myVC: MyViewController!
var params: SomeParams!
override func windowDidLoad() {
super.windowDidLoad()
myVC = MyViewController(someParams: params)
myVC.view.setFrameSize(targetView.frame.size)
myVC.view.setBoundsSize(targetView.bounds.size)
targetView.addSubview(myVC.view)
}
override var windowNibName: String! {
return "MyWindowController"
}
convenience init(someParams params: SomeType) {
self.init(window: nil)
self.params = params
}
}
NSViewController:
final class MyViewController: NSViewController {
convenience init(someParams params: SomeType) {
// do stuff with the params
}
override func viewDidLoad() {
super.viewDidLoad()
// configure stuff for the window
}
}
What I've tried
I suppose that my issue is that the MyWindowController NSWindow is the .initialFirstResponder when I would want the content of the targetView (an NSTableView) to be the first responder - this way I could use keyDown, I guess, and send the close command to the window from there. This doesn't seem optimal, though.
I've tried forcing the view controller views into being the first responder by using window?.makeFirstResponder(theView) in the windowDidLoad of MyWindowController but nothing ever changes.
I've also tried adding this to MyWindowController:
override func cancelOperation(_ sender: Any?) {
print("yeah, let's close!")
}
But this only works if the user clicks first on the background of the window then hits ESC, and it still emits the error sound anyway. Which is actually what made me think that the issue was about the first responder being on the window.
Question
How would you achieve that? Of course, I know that the user can already close the window with CMD+W, but I'd really like to sort out this issue nonetheless.
Note that the code example is in Swift but I can also accept explanations using Objective-C.
The documentation of cancelOperation explains how cancelOperation should work:
This method is bound to the Escape and Command-. (period) keys. The key window first searches the view hierarchy for a view whose key equivalent is Escape or Command-., whichever was entered. If none of these views handles the key equivalent, the window sends a default action message of cancelOperation: to the first responder and from there the message travels up the responder chain.
If no responder in the responder chain implements cancelOperation:, the key window searches the view hierarchy for a view whose key equivalent is Escape (note that this may be redundant if the original key equivalent was Escape). If no such responder is found, then a cancel: action message is sent to the first responder in the responder chain that implements it.
NSResponder declares but does not implement this method.
NSWindow implements cancelOperation: and the next responder, the window controller, isn't checked for an implementation of cancelOperation:. The cancel: message does arrive at the window controller. Implementing
- (void)cancel:(id)sender
{
NSLog(#"cancel");
}
will work. The cancel: message isn't inherited from a superclass so autocompletion doesn't suggest it.
This worked for me in Xcode 10 and Swift 4.2:
#objc func cancel(_ sender: Any?) {
close()
}
I tried it before but without the #objc part and it didn't work. So don't omit it.
When I needed such behavior I implemented it by overriding keyDown: of the NSWindow object.
I.e. something like the following:
- (void)keyDown:(NSEvent *)theEvent
{
int k = [theEvent keyCode];
if (k == kVK_Escape)
{
[self close];
return;
}
[super keyDown:theEvent];
}

NSProgressIndicator on Modal Sheet Doesn't Animate

My main window controller has a toolbar item that triggers the presentation of a modal sheet. The sheet is supposed to display the progress of a lengthy, asynchronous process (e.g., sync local data with a server).
However, I can not get the (indeterminate) progress indicator to animate.
This is the action that triggers the modal sheet:
var syncProgressWindowController: SyncProgressWindowController!
// ...
#IBAction func syncWithServer(_ sender: AnyObject) {
// (Actual HTTP code not implemented)
syncProgressWindowController = SyncProgressWindowController()
syncProgressWindowController.loadWindow()
guard let modalWindow = syncProgressWindowController.window else {
return
}
self.window?.beginSheet(modalWindow, completionHandler: { (response) in
// THIS GETS EXECUTED.
// However, the code below has no effect:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
// self.syncProgressWindowController.progressIndicator is
// NOT nil, despite windowDidLoad() not being called
// (see below)
})
}
The modal sheet window controller (class SyncProgressWindowController above) is defined like this:
#IBOutlet weak var progressIndicator: NSProgressIndicator!
convenience init() {
self.init(windowNibName: "SyncProgressWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
// Breakpoints here don't work, logs don't print to the console.
// Not called? But outlet _is_ set (see above).
}
The xib file (SyncProgressWindow.xib) has:
File's Owner Identity/Class set to "SyncProgressWindowController"
Window has New Referencing Outlet to File's Owner's window
Window has delegate outlet wired to "File's Owner" (just in case - but delegate methods don't seem to get called either).
Window has "Visible at Launch" unchecked (and is therefore displayed modally with no problems).
Progress has New Referencing Outlet wired to File's Owner's progressIndicator.
However:
SyncProgressWindowController's windowDidLoad() does not get called (Execution does not stop at breakpoints there and logs aren't printed).
Despite that, the property/outlet progressIndicator is set somehow, because the app does not crash when I attempt to animate it, with code like this:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
What am I missing?
completionHandler will be fired when you close sheet by endSheet(_:returnCode:) So you start indicator before sheet will be closed.
I'm not good in xib files, but when i disabled row with loadWindow, windowDidLoad was called. I'm not sure it's right way.

How do I access the undoManager for my Mac OS app in swift?

I am simply trying to get undo working for the actions a user performs in my app. By default, any text editing the user does has the benefit of undo, but any actions that are done otherwise (from my code) does not.
I can see the documentation explains that I need to get an instance of NSUndoManager and call registerUndoWithTarget, but I am stumped with the first step: getting the undoManager from within my ViewController. Since ViewController is a UIResponder, I tried this:
if let undoManager = self.undoManager {
undoManager.registerUndoWithTarget(self, selector: Selector("removeLatestEntry:"), object: "test")
}
Since that binding returns nil, I thought maybe the ViewController doesn't have the undoManager, so I looked for it in the window:
if let window = NSApplication.sharedApplication().mainWindow {
if let undoManager = window.undoManager {
undoManager.registerUndoWithTarget(self, selector: Selector("removeLatestEntry:"), object: "test")
}
}
Alas, the window binding also returns nil. Sorry, I am very new to this. Can anyone point me in the right direction? Am I supposed to implement my own undoManager or something? There is clearly an undoManager somewhere because anything a user does manually in my textField is getting undo behavior. It seems like this would be a singleton that I could access easily from a ViewController.
--
Edit: BTW, the code above was placed in viewDidLoad and removeLatestEntry is just a function in my ViewController that takes a string and prints it at this point.
To use undoManager, the ViewController needs to be first responder. So:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
becomeFirstResponder()
}
Then, from wherever your action is defined that needs to be reversed, you register your undo and pass it whatever it needs to undo that action. So in my case:
func addEntry(activity: String) {
// some other stuff…
undoManager!.registerUndoWithTarget(self, selector: Selector("removeLatestEntry:"), object: activity)
}

Call function in view controller from UITableViewCell

I have a problem which drives me nuts, how is it possible to invoke a function in a view controller from within a UITableViewCell. - I am doing that in Swift.
So I have a View Controller - called mainVC
in this mainVC I have declared a tableView which holds a UITableViewCell (Custom/ Dynamic) in that tableView cell I want call a function which is declared in the mainVC. If I do like self.superview?.myFunc doesn't work.
I hope someone can help me...
It is not as easy as that unfortunately.
What you want to look into is NSNotificationCenter or even better: - Delegates.
If you want a better understanding of Delegates I'd recommend this tutorial: http://www.raywenderlich.com/75289/swift-tutorial-part-3-tuples-protocols-delegates-table-views
If you don't want to read all that, I'll try my best to explain it here.
Let's say you have two view controllers: mainVC and otherVC.
Above the class definition in otherVC, add the following code:
protocol OtherVCDelegate {
func cellClicked()
}
Inside the class in otherVC, add the following property:
var delegate: OtherVCDelegate?
In the didSelectRow (or where you execute the code), add the following code:
self.delegate?.cellClicked()
Now, back in mainVC: Make mainVC conform to this delegate by adding it to the class like this:
class MainViewController: UIViewController, DetailDelegate {
}
In MainViewController add the delegate function and insite that one put your function you want to execute:
func cellClicked() {
println("Cell was clicked")
myFunc()
}
The last thing you have to do is make MainViewController the delegate of OtherViewController. This has to be done where you access the otherVC from the mainVC. Let's say it is done in prepareForSegue:
if segue.identifier == "showDetail" {
(segue.destinationViewController as DetailViewController).delegate = self
}
It is a little bit complicated at first, but once you get a grasp of it it's a walk in the park.

Xcode6/Swift - How to implement an iAdBannerView in multiple view controllers?

Before I get started I am aware this question has been asked many times before, however all of them refer to xcode5/objective-C, not swift. I am only new to app development so I haven't been able to understand the objective-c and use it in swift.
I have got an adBannerView working on my first view controller, however how do I then take this banner and use it across my other 2 view controllers? Do I use the prepareForSegue function (and if so, how)?
My code for the adBannerView I currently have (from here)
//...
import iAd
class ViewController: UIViewController, ADBannerViewDelegate {
//link adBanner
#IBOutlet var adBannerView: ADBannerView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.canDisplayBannerAds = true
self.adBannerView.delegate = self
self.adBannerView.hidden = true
}
func bannerViewWillLoadAd(banner: ADBannerView!) {
NSLog("bannerViewWillLoadAd")
}
func bannerViewDidLoadAd(banner: ADBannerView!) {
NSLog("bannerViewDidLoadAd")
self.adBannerView.hidden = false
}
func bannerViewActionDidFinish(banner: ADBannerView!) {
NSLog("bannerViewDidLoadAd")
//optional resume paused game code
}
func bannerViewActionShouldBegin(banner: ADBannerView!, willLeaveApplication willLeave: Bool) -> Bool {
NSLog("bannerViewActionShouldBegin")
//optional pause game code
return true
}
func bannerView(banner: ADBannerView!, didFailToReceiveAdWithError error: NSError!) {
NSLog("bannerView")
}
//...
Thanks :)
When I have faced this problem my solution was to create a ParentViewController, and the others view controllers inheritate from him. In the parent view controller I create an outlet for a view which will contain iAdView, and in the .xib file of each view controller I link the iAdView container view to the created outlet. After that I create a singleton which has all the iAdView functionality and a property which is the iAdView. In the viewDidAppear of the parent I ask the singleton for the iAdView and add it as a subview of the iAd view container. Doing this that way in the view controllers you are not going to see any code of the iAdView, because all will be in the ParentViewController and in the singleton (lets call it iAdManager). Hope it helps.

Resources