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

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.

Related

How to override Copy and Paste NSMenuItems for one View Controller Swift macOS

I am writing a macOS application with multiple view controllers, using Storyboards.
In my main View Controller I would like to be able to copy and paste data to the NSPasteboard. The data is related to buttons displayed to the user, and the exact data to be copied varies depending on which button has most recently been pressed/selected.
I would like to be able to override the standard behaviour of the Copy and Paste NSMenuItems when my main View Controller is the front most (key) window, but revert to back to standard behaviour when other windows are in the foreground, as they all contain NSTextFields which can be copied/pasted into.
I have done a lot of googling, and overriding this behaviour is not very well documented. I can achieve it globally by adding an IBAction into the App Delegate, which I could use to call a function in whichever View Controller is key, but this doesn't feel like a very elegant solution.
Currently my IBAction in the App Delegate looks like this:
#IBAction func copy(_ sender: Any) {
if let window = NSApplication.shared.keyWindow {
if let splitView = window.contentViewController as? SplitViewController {
if let controlVC = splitView.controlItem.viewController as? ControlViewController {
controlVC.copyAction(self)
}
}
}
}
Am I missing a neater solution?
Thanks,
Dan

In Xcode, How Can I Add A Tutorial Window to My Mac App, Which Opens Only On First Launch

I'm designing a MacOS app, for which I'd like to have a second window appear on the first time the app is launched (similar to what Apple does on the first time you open iMovie or Pages). I've tried a couple things to no avail. So far, I've created a second group as a target of the original app. The new group contains the tutorial storyboard I designed and a custom class called OnboardingWindowController (a subclass of NSWindowController). I gave the window controller the Storyboard ID of OnboardingWindowController, and added the following method:
class func loadFromNib() -> OnboardingWindowController {
let vc = NSStoryboard(name: "Onboarding", bundle: nil).instantiateController(withIdentifier: "OnboardingWindowController") as! OnboardingWindowController
return vc
}
I'm not sure, however, how to get this to launch without prompting the first time the app is launched and then never again. Thanks in advance!
Add a key to userdefaults check it's value, if it's false then show the tutorial else don't.
if UserDefaults.standard.bool(forKey: "didShowTutorial") {
// Code to proceed without showing tutorial
} else {
UserDefaults.standard.set(true, forKey: "didShowTutorial")
// Code to show the tutorial
}

How to get back to the current window from AppDelegate

In my macOS application, I am following a OAuth-Login procedure.
I am authenticating successfully by receiving a code within a url through my custom url, with which I already can get hold of an access_token.
I start the login procedure with the simple click of a button in a ViewController.
The system I log in to then answers with my registered custom url scheme, providing me with a code I have to then use to get an access_token with POST request. This access_token than can be used to make api calls.
My Problem now is that lacking knowledge of how to implement other techniques, I am currently doing all the latter procedure within the AppDelegate function application(_ application: NSApplication, open urls: [URL]).
From my limited understanding this is the way to handle this, but now
how can I get back from there to get hold of the current view? I am really struggling with the view controller life cycle part of things here I guess...
In AppDelegate, you can get a reference to the current window's ViewController as follows. Replace "MainViewController" with the name of the one you use.
iOS Swift:
if let vc = window?.rootViewController as? MainViewController {
// use `vc` here by calling public functions
// and accessing public properties
}
macOS Swift:
if let vc = NSApplication.shared.mainWindow?.contentViewController as? MainViewController {
// use `vc` here by calling public functions
// and accessing public properties
}
OK, found it: since there is no rootViewController in macOS-land as there is with iOS, it works a little different:
in macOS you can get hold of the window currently "under keyboard" like so:
in application(_:open:), where the redirect_uri gets called:
if let viewController = NSApplication.shared.keyWindow?.contentViewController as? ViewController {
// make changes to the view
}

How can I close a Safari App Extension popover programmatically?

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)
}

Need to use a NSTextView (or NSTextField) for a clickable URL

I'm given an arbitrary NSAttributedString (parsed from markdown, not that it matters here) which may contain URLs that I want to be clickable in a text field within an NSTableView cell. The requirements state that if the user clicks the URL, they be taken to it with the default browser. IF they click anywhere else in the cell, we have default behavior (displaying an additional info popup).
I'm attempting to use a NSTextView to display the content. However, clicking outside the URL but within the view selects the text and eats the mouse click. Making the view not selectable won't allow clicking the URL either. I also don't really want the text to be selectable but that's a minor side problem.
So... I decided to make my view controller an NSTextViewDelegate so I could use some of those callbacks. But my app crashes if I set the NSTextView's delegate property to 'self'. This happens even if I don't implement any of the functions, even though they are all optional.
I'm using Swift 3 and wonder if there's some bug or other issue there? The call stack appears to be sending a textView:willChangeSelectionFromCharacterRanges:toCharacterRanges: message even though it's not implemented. And incidentally, implementing that method isn't helping either.
Any help, or sample code in Swift 3 using the delegate protocol, would be greatly appreciated.
Here's the crash I get by simply setting the delegate property on NSTextView:
By request, here's the code that set's the delegate. Currently I just set it whenever the message changes. This can obviously be optimized but for now I just want to see it work.
var notification: SSNotification! {
didSet {
guard let notificationCellView = self.view as? SSNotificationCellView else { return }
notificationCellView.subjectLabel.stringValue = notification.subject
if let description = notification.message , description != "" {
let attrString = TSMarkdownParser.standard().attributedString(fromMarkdown: description)
notificationCellView.messageLabel.textStorage?.setAttributedString(attrString)
notificationCellView.messageLabel.isHidden = false
notificationCellView.messageLabel.delegate = self
} else {
notificationCellView.messageLabel.isHidden = true
}
}
}
I never did figure out why I was crashing but I was able to come up with a workaround. I was originally trying to make the view controller for the table cell which contained NSTextView be the delegate. I changed it so that the cell's view subclass itself was the delegate and all is well.
I don't get it but it works, so that's what matters.