How can I close a Safari App Extension popover programmatically? - swift

I'm building a Safari App Extension using XCode 8.3 and Swift 3, following the Safari App Extension Programming Guide. The extension includes a popover that appears when the extension's toolbar item is clicked. The popover view contains a few buttons linked to actions the user can perform.
I want clicking one of these buttons to close the popover after its action has been performed. By default, clicking anywhere outside of a popover closes it, but I haven't been able to find any other way to close the popover, either in the guide or in the docs.
I know that NSPopover has a performClose method, but there doesn't appear to be a way to access the popover itself from within the extension: the app extension only lets you provide a SFSafariExtensionViewController, whose contents magically appear within the popover.
I've also tried using dismissViewController as described in this StackOverflow answer, but in my view controller self.presenting is always nil, and self.dismissViewController(self) just crashes the extension with the message:
dismissViewController:: Error: maybe this view controller was not presented?.
Lastly, I noticed a related question about programmatically opening the toolbar item popover has gone unanswered the past 6 months. This leads me to suspect Apple may simply have strict limits on how the popover can be opened and closed. Even if this is the case, it would be nice to know for sure what the limitations are.

I'll add an answer in case anyone stumbles upon this question.
A dissmissPopover() instance method has been added to the SFSafariExtensionViewController class. This can be used to programatically close the popover.
The default template given when creating a Safari App Extension in XCode gives you a SafariExtensionViewController class that extends SFSafariExtensionViewController and holds a shared instance as a static field called 'shared', so you can call the dismissPopover() method from that instance.
For example:
class SafariExtensionHandler: SFSafariExtensionHandler {
func myFunc() {
// do stuff;
SafariExtensionViewController.shared.dismissPopover()
// do other stuff;
}
}

I did it by calling dismiss method like below
#IBAction func onLoginBtnClicked (_ sender: Any) {
NSLog("Button clicked")
self.dismiss(self)
}

Related

How do I offer a "sent action" from a custom class?

I'm developing a custom class in Swift based on NSObject. It's a statusMenu icon/menu helper. When I receive an event for the icon being clicked in my custom class, I want to pass this on in the same way an NSButton allows to create an IBAction to respond to the user clicking the button.
How do I do this?
Code:
I'm registering a selector in my class to listen to clicks:
statusItem.action = #selector(statusBarIconClicked)
The selector receiving this:
#objc func statusBarIconClicked(sender: AnyObject) {
print("clicked clicked!!")
// pass sent action on through a new sent action... how?
}
I want this to be linkable to the user in the same way a button can lead to this:
#IBAction func myClassSaysMenuWasClicked(_ sender: Any) {
// Reacting to that
}
Googled for a good while and found: nothing.
I take it that you're asking about this sort of thing, displayed in the Connections inspector (this is iOS, not macOS, but it's the same idea):
The question would then be: when the user selects an instance of my class in the nib editor in Xcode, I'd like those Sent Events to appear in the Connections inspector so that the user can hook up one of them and use the target-action architecture.
You can do this only if your class is a Control subclass. Thus, for example, in iOS, a custom UIControl subclass displays those sent events in Interface Builder.
If you can't do that, then the programmer won't be able to configure your target-action in Interface Builder. You can certainly implement a target–action architecture, but the programmer will have to set the target and action in code. (You could do half a job of it by making the target an outlet, of course.)
I worked around the comment above and googled further. I found the solution being to change from NSObject to NSController in this line:
class StatusMenuController: NSControl, NSMenuDelegate {
And run this command when I want to trigger the sent action:
if let theAction = self.action { NSApp.sendAction(theAction, to: self.target, from: self) }
The if-command of course checking so that an action is actually set before trying to use it.
I found no ways during my research to add any further sent actions. The way to go here seems to be delegates.

How to programmatically show a window/view controller in Swift 4 for macOS application

I'm trying to programmatically show a window in my macOS application. The reason I want to do it programmatically is because the user clicks a Login button and the resulting function depends on the success of the login. If it was successful, the window is shown; otherwise, it is not.
Here is a screenshot of my Xcode window:
Here's what I'm trying in the code (Alert is a class I created to make showing NSAlert dialogs easier):
#IBAction func btnLogin_Click(_ sender: Any) {
let email = txtEmail.stringValue
if(email.isEmpty) {
Alert.show(parent: view.window!, message: "Email is required.")
return
}
let password = txtPassword.stringValue
if(password.isEmpty) {
Alert.show(parent: view.window!, message: "Password is required.")
return
}
// this does not work, even though I gave the window and view controllers
// both identifiers in the storyboard
self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("wcStores"))
}
Apple's documentation for NSStoryboard.SceneIdentifier is pretty much nonexistent, and other methods I've seen seem to pertain to earlier versions of Swift.
What am I doing wrong?
Alright, so this is not what I originally was trying to do, but this way is much better, and what I was really looking for.
let vcStores = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("vcStores"))
as! NSViewController
self.view.window?.contentViewController = vcStores
This will just replace the window's contents with the contents in the view vcStores as defined in Interface Builder.
This video also helped, albeit for iOS and Swift 3. I was trying to create a new NSWindow at first because I'm so used to doing desktop development in Java Swing or C# Windows Forms. But I love how easy it is to just switch out the window contents like this.

pickerDidSettle() not called

In one of my Watch Extension's interface controllers I have several WKInterfacePicker elements, and I need to know when the user has selected a value. According to documentation, WKInterfaceController should be able to implement pickerDidSettle(_:) method that has the corresponding picker element as parameter. For some reason the method never gets called when I use the pickers. Here is the basic structure of my implementation:
override func pickerDidSettle(picker: WKInterfacePicker) {
// Code inside this block is not called
}
If I mark the function with an #IBAction attribute and connect them with the picker elements in interface builder, the instance method works. However, this apparently prevents me to assign picker actions that receive all the picker values through which the user is scrolling.
#IBAction
override func pickerDidSettle(picker: WKInterfacePicker) {
// This function gets called, but blocks other actions
}
My interface controller inherits from WKInterfaceController and conforms to two custom protocols. How should I implement the method?
Edit: The issue was related to a possible bug in WatchKit, where pickerDidSettle(_:) will not be called without an existing #IBAction connection to the controller. I assume it is a bug, because related instance methods pickerDidFocus(_:) and pickerDidResignFocus(_:) work independent of the connection.
Sometimes this issue occurs, when something gets 'out of sync' between Xcode and the Simulator.
Just close the Simulator and clean and rebuild your app (via 'Product/Clean Build Folder') to recreate the 'sync.

Use NSPanel for user input. Not opening up again

I want to display a 'NSPanel' for the user to input a name for a new folder. Why a NSPanel? Because it looks awesome! It hosts one TextField and one PushButton to confirm the name. It shall also close the window when clicked.
It displays when the "add" button gets clicked in my menu. It also closes when the "done" button gets clicked in the NSPanel. But when I click "add" again it doesn't show up anymore. That also occurs when I close it via the normal "close button" in the title bar. So it is not explicitly related to the "done"-PushButton. I also tested implementing func windowWillClose(notification: NSNotification) which also doesn't get triggered in either cases. What could be the problem? Also, does it somehow need to be a "new" window every time? Or am I using this correctly for user input? I mean it just gets instantiated once and then "shown" and "unshown" or am I wrong?
So I did a new Cocoa-Class - Subclass of NSWindowController - and let xCode create a .xib for that also. In that .xib I "designed" the NSPanel. I ticked visible at launch without that the window wouldn't appear when the menu button gets clicked. I also hooked up an IBOutlet for the NSPanelin my Cocoa Class. My Class at the moment looks like this:
import Cocoa
class NamingHUD: NSWindowController, NSWindowDelegate {
#IBOutlet var insertNameWindow: NSPanel!
#IBOutlet weak var nameTextField: NSTextField!
override var windowNibName : String! {
return "NamingHUD"
}
override func windowDidLoad() {
super.windowDidLoad()
insertNameWindow.center()
insertNameWindow.makeKeyAndOrderFront(nil)
NSApp.activateIgnoringOtherApps(true)
}
#IBAction func userSetName(sender: NSButton) {
print("Close button clicked")
insertNameWindow.close()
}
}
In my Main Class I declared it as a variable like this:
var namingHUD:NamingHUD!
and then in override func awakeFromNib() as:
namingHUD = NamingHUD()
as well as in a click handler like:
#IBAction func addClicked(sender: NSMenuItem) {
namingHUD.showWindow(nil)
}
Now. When I click and addClicked() gets called the window shows up as expected. Fine! I enter a name and hit the "done" button and it closes the window properly. Also Fine! But when I click again, say to add another folder, the window doesn't show up anymore. I also created a Preferences Window the exact same way. But with a Window instead of a NSPanel inside. That totally works as it should.
So I clearly confused something or forget something. What could it be? I openly admit that it is the first time I am working with any kind of window outside of following a tutorial. So I clearly didn't grasp the whole concept of it. I read up about windows in Apples Developer Guide and it kinda makes sense. But... well, doesn't work at the moment. Am I "misusing" the NSPanel? Shouldn't be the case as it inherits from NSWindow or?
Did you connect the window outlet of NamingHUD to your awesome panel? Nibs are loaded lazily:
namingHUD = NamingHUD() // init the controller but doesn't load the nib
...
namingHUD.showWindow(nil) // now you are loading it for the first time
It works the first time because showWindow() loads the nib and show the window referenced by the window outlet. Your panel shows up because it's set to "Visible at launch". Your of course had no window to show.
Subsequent clicks don't load the nib file again, only order the window outlet to show up. That's why your panel did not show again. FYI: an NSPanel is a subclass of NSWindow so it has everything that NSWindow has, and then some more.

Swift and watchkit: pushControllerWithName not being called at all

I have always used the pushControlledWithName method in swift/watchkit to move to another interface controller, basically like this:
self.pushControllerWithName("newinterfacecontroller", context: nil)
In some of my projects, when I put this in a function (like where the user presses a button) it simply doesn't get called at all. No errors, just as if the code isn't there at all. If I create a new test project and try it it works. I am baffled as to what's going on here.
Example of what happens:
#IBAction func button1Action() {
println("test")
self.pushControllerWithName("newinterfacecontroller", context: nil)
}
Pressing the button will print "test" in the console, but it doesn't try to move to the new interface controller (with identifier "newinterfacecontroller") at all.
I think you've figured this out from the comments, but page-based interfaces are technically modals and not navigation-stack interfaces.
You can present modals from anywhere, but you can only push onto a navigation stack from a non-modal.