Preferences Menu in OS X with Swift - swift

I'm running into a few issues trying to set up a Preferences menu for my app.
I'm currently using Swift and Xcode 7.
I have a Preferences window that opens from the Main menu. However, once it is open is where my issues lie. I have 2 buttons, one that resets the user inputs, and the other which is supposed to save the information.
I've got the reset button set up to make the text inputs default values.
My next goal is to have the save button set the variables with any entered text in the text fields and close the Preferences window. Then, in an ideal world, I'd like to be able to re-open the Preferences to make sure they saved.
Is this possible?
Currently, my program hangs if I click the Save button, or the Preferences window will not reopen if the red X in the corner is clicked.
Here is my current Preferences Window code, not even sure what of this is unnecessary:
import Cocoa
protocol PreferencesWindowDelegate {
func preferencesDidUpdate()
}
class PreferencesWindow: NSWindowController, NSWindowDelegate {
#IBAction func ResetPref(sender: NSButton) {
UserName.stringValue = "Name"
UserEmail.stringValue = "Email"
ManEmail.stringValue = "Man. Email"
SSEmail.stringValue = "SS Email"
}
#IBOutlet var SSEmail: NSTextField!
#IBOutlet var ManEmail: NSTextField!
#IBOutlet var UserEmail: NSTextField!
#IBOutlet var UserName: NSTextField!
#IBAction func SavePref(sender: NSButton) {
}
override var windowNibName : String! {
return "PreferencesWindow"
}
override func controlTextDidBeginEditing(_: NSNotification){
func controlTextDidChange(obj: NSNotification){
}
func windowDidLoad() {
super.windowDidLoad()
self.window?.center()
self.window?.makeKeyAndOrderFront(nil);
NSApp.activateIgnoringOtherApps(true);
}
var delegate: PreferencesWindowDelegate?
func controlTextDidEndEditing(obj: NSNotification){}
func windowWillClose(notification: NSNotification) {
delegate?.preferencesDidUpdate()
}
}
}
Any guidance is appreciated, as this is my first time working in Swift!

Is your preferencesDidUpdate() function defined?
You typically save preferences by using NSUserDefaults, which saves preferences to a .plist file. This is the Cocoa way. To save NSUSerDefaults, you just do
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(myValue, forKey: myKey)

Related

How to close/dismiss/hide a menu by clicking on a button in an embedded view from within it, in Swift?

I have created a menu app, using Swift, for Mac OS, within which, a custom view is the only menu item. There's a plus button on this custom view, which opens a window that has a textfield.
When I click on the plus button, the window appears, but the menu does not disappear. The textfield is also not focused. When I type one letter, the letter is not shown in the textfield, but the menu disappears, and the textfield is focused and ready to receive entry.
I want to have the custom view or menu disappear and have the textfield ready to receive keystrokes when I click on the plus button, not after I press an extra key.
How may I achieve that? What am I doing wrong?
Here's my code:
// CustomView.swift
var customWindow: CustomWindow!
override func awakeFromNib() {
customWindow = CustomWindow()
}
#IBAction func plusButtonClicked(_ sender: NSButton) {
customWindow.showWindow(nil)
}
// CustomWindow.swift
override var windowNibName : NSNib.Name? {
return NSNib.Name("CustomWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
self.window?.center()
self.window?.makeKeyAndOrderFront(self)
self.window?.level = .mainMenu + 100
NSApp.activate(ignoringOtherApps: true)
if customTextField.acceptsFirstResponder {
customTextField.window?.makeFirstResponder(customTextField)
}
// CustomMenuContoller.swift
let statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
#IBOutlet weak var CustomMenu: NSMenu!
#IBOutlet weak var customView: CustomView!
var customMenuItem: NSMenuItem!
override func awakeFromNib() {
customMenuItem = CustomMenu.item(withTitle: "TheMenu")
customMenuItem.view = customView
statusBarItem.menu = CustomMenu
}
Inspired by El Tomato's comment, I found the solution.
Given the fact that the plusButtonClicked is limited to its own context, which is the controller within which it resides and all the public variables, I could not call a method on CustomMenu from it. Because CustomMenu in itself is not public. But its containing variable statusBarItem.menu, is public and accessible from all the other views. So I added statusBarItem.menu?.cancelTracking() to plusButtonClicked action and it works.

How do I connect text fields from child window?

I have a single-window (OSX, not iOS) app that works beautifully, but I need to add a Preferences window. I have created the window and linked it to the Preferences menu (to which it opens and displays as expected). However, now I need to interact with the text fields I have set up.
Here is my new window:
Settings.xib
Here is my main window:
MainMenu.xib
Again, I can display it just fine, but I need to be able to set the values of the text fields in the Settings window. Not real sure how to do that? I am trying to stay away from Storyboards at the moment (just trying to keep it simple; maybe phase II). I am storing the values from the Settings window in the Keychain (which works beautifully in MainMenu.xib). Now I just need to be able to enter them and save them from Settings.xib.
My file structure is pretty straightforward. I have AppDelegate.swift which is doing all of the work. Do I (and how do I) connect Settings.xib to AppDelegate? I know I can bind the text fields from Settings.xib to outlets in AppDelegate (but when I do that I get an error: [General] [<NSApplication 0x6080001005a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key accountNumber).
I know this is a n00b question, but it is so simple it is hard to find an applicable answer.
Thanks!
Creating a new window controller was the key. My problem, as I stated in my question, was getting the text fields wired up as functional outlets. I was able to show the window as desired, just couldn't get to the text fields.
I created SettingsWindowController.swift to subclass NSWindowController. My connections are as shown:
Window connections
File's Owner connections
Additionally, in Settings.xib, the Custom Class of File's Owner is set to SettingsWindowController. Then the individual text fields are connected to SettingsWindowController.swift as outlets.
In SettingsWindowController.swift:
`import Cocoa
class SettingsController: NSWindowController, NSWindowDelegate
{
#IBOutlet weak var accountNumber: NSTextField!
#IBOutlet weak var meterNumber: NSTextField!
#IBOutlet weak var webCredentialKey: NSTextField!
#IBOutlet weak var webCredentialPassword: NSTextField!
// allows window creation without needing to specify NIB name
override var windowNibName: String! {
return "Settings"
}
override func windowDidLoad() {
super.windowDidLoad()
// initialization code removed for brevity
}
func windowWillClose(_ notification: Notification) {
// teardown code removed for brevity
NSApplication.shared().stopModal()
}
}'
In AppDelegate.swift,
`var prefs: SettingsController? = nil
#IBAction func OpenPreferences(_ sender: Any) {
DispatchQueue.main.async(execute: { () -> Void in
NSApplication.shared().runModal(for: (self.prefs?.window)!)
})
}`
func applicationDidFinishLaunching(_ aNotification: Notification)
{
// Insert code here to initialize your application
prefs = SettingsController()
}`
Hope this helps someone else.

AppleTV - tvos - Hybrid app using native and TVMLKIT - Can't back to native app

I'm a beginner on TVOS.
I'd like to create an hybrid app on AppleTV using a native app and TVMLKIT.
My native application is just a simple native app with buttons (using swift).
When we click on a button, I launch a a javascript app using TVLMKIT and TVJS.
My TVJS as uses the Player to display a video.
When the video is over, I want to close the TVJS app and back to the native ViewController.
My problem is that when I back to native app, I loose the focus on my native View (the app is frozen).
native ViewController:
import UIKit
import TVMLKit
class ViewController: UIViewController, TVApplicationControllerDelegate {
var window: UIWindow?
var appController: TVApplicationController?
var appControllerContext = TVApplicationControllerContext();
static let TVBaseURL = "http://localhost:9001/"
static let TVBootURL = "\(ViewController.TVBaseURL)/client/js/application.js"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var label: UILabel!
#IBOutlet weak var viewAd: UIView!
#IBAction func clickOnlaunchAd(sender: AnyObject) {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
guard let javaScriptURL = NSURL(string: ViewController.TVBootURL) else {
fatalError("unable to create NSURL")
}
appControllerContext.javaScriptApplicationURL = javaScriptURL
appControllerContext.launchOptions["BASEURL"] = ViewController.TVBaseURL
appController = TVApplicationController(context: appControllerContext, window: window,delegate: self)
}
#IBAction func clickOnChangeText(sender: AnyObject) {
label.text = "changed";
}
func appController(appController: TVApplicationController, didStopWithOptions options: [String : AnyObject]?) {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext){
let notifyEventToNative : #convention(block) (NSString!) -> Void = {
(string : NSString!) -> Void in
print("[log]: \(string)\n")
self.appController?.stop()
}
jsContext.setObject(unsafeBitCast(notifyEventToNative, AnyObject.self), forKeyedSubscript: "notifyEventToNative")
}
}
Just before calling "notifyEventToNative" from my TVJS, I call "navigationDocument.clear();" to clear the TVML view.
I can see my native app but I can't interact with it.
Any ideas?
Thanks.
I also had the same problem. I was opened a TVML document from the UIViewController. And I also lost the focus. So, first of all I can advice you to override var called preferredFocusedView in your ViewController. In this method you can return reference to viewAd. But the better solution would be to wrap your ViewController into the TVML-item (with the TVMLKit framework). In that case I hope that you will have no problems with focus because you will use TVML during the whole application.

Set maximum characters (to one) in a NSTextfield in Swift

How do i set the maximum amount of characters in multiple NSTextfields (OSX cocoa app, NOT iOS) to one in Swift?
Please explain how to do it, because I'm a complete newbie when it comes to OSX app development and therefore I don't understand short answers like "Use NSFormatter", because I have no idea what it is and how to implement it. Like Examples
There's no built-in way to simply set the maximum, I think because you need to decide what behavior you want. For example, if there's already one character in the field, and the user enters a second character, what should happen? Should the 2nd character be ignored? Replace the first? Beep?
In any case, you can get whatever behavior you want using the NSText Delegate methods. Make your view controller (or whatever object has the logic) a delegate of the text field, and implement the various delegate method(s) to do what you need.
Again, the exact behavior is up to you, but if I were implementing this, I might be inclined to make the text field always use the last character entered (such that, if one character is already present, pressing a second replaces the first). To do that, you'd want to override textDidChange to look at the value of the text field, and modify it if appropriate. Something like:
class ViewController: NSViewController, NSTextFieldDelegate {
#IBOutlet weak var textField: NSTextField!
override func controlTextDidChange(obj: NSNotification) {
if self.textField.stringValue.characters.count > 1 {
self.textField.stringValue = String(self.textField.stringValue.characters.last!)
}
}
}
You don't need to limit the characters a user will enter just only look at the first character entered. In fact, it is probably better since you will always have to handle possible user errors. If you want to you can issue an alert that they entered too many by getting the characters.count. You might want an alert if they don't answer at all. The code below will work as is if you set up a storyboard with 1 NSTextField and one button and connect them. If you have more than one textfield, i.e. like a multiple choice test, just set up all the text fields the same way.
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var firstLetter: NSTextField!
Add as many text fields as you need:
#IBOutlet weak var secondLetter: NSTextField!
#IBOutlet weak var thirdLetter: NSTextField!
etc.
#IBAction func button(sender: AnyObject) {
var firstEntry = firstLetter!.stringValue
var index1 = firstEntry.startIndex
if firstEntry.characters.count > 1 {
runMyAlert("Bad USER! ONLY ONE Character!")
}
if firstEntry == "" { //left it blank
runMyAlert("You need to enter at least one character!")
exit(0) //or you'll crash on next line
}
var nameLetter1:Character = firstEntry[index1]
print( "First Letter == \(nameLetter1) ")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func runMyAlert( alertMessage: String){
var myWindow = NSWindow.self
let alert = NSAlert()
alert.messageText = "ERROR ERROR ERROR"
alert.addButtonWithTitle("OK")
alert.informativeText = alertMessage
alert.runModal()
}
}

Why is my prepareForSegue code activating the wrong button?

I'm learning how to program and am playing with a Swift project in Xcode. The main storyboard has two view controllers. The first view controller is simply called ViewController and the second view controller is called HelpScreenViewController.
In ViewController I have a "help" button that switches the user to HelpScreenViewController. This button uses a segue called "goToHelpScreenSegue".
In HelpScreenViewController I have three buttons:
"Close" button to dismisses the view (no segue used)
"Send Feedback" button to generate a new email in the Mail app (no segue used)
"Reset Game" button to call a function that is coded within the first ViewController. This third button uses a segue called "resetGameSegue".
What I'm trying to do is...
...Get the "Reset Game" button on the HelpScreenViewController to reset the game by calling a function that's coded within the first view controller.*
To try and get this to work the way I want, I've used the following code:
WITHIN first main ViewController
import UIKit
import iAd
import AdSupport
import AVFoundation //audio
import GameplayKit
class ViewController: UIViewController, ADBannerViewDelegate, MyResetGameProtocol {
#IBOutlet weak var Banner: ADBannerView!
#IBOutlet weak var buttonA: UIButton!
#IBOutlet weak var buttonB: UIButton!
#IBOutlet weak var buttonC: UIButton!
#IBOutlet weak var buttonD: UIButton!
#IBOutlet weak var labelQuestion: UILabel!
#IBOutlet weak var labelScore: UILabel!
#IBOutlet weak var labelTotalQuestionsAsked: UILabel!
#IBOutlet weak var labelFeedback: UILabel!
#IBOutlet weak var buttonNext: UIButton!
var score :Int! = 0
var totalquestionsasked :Int! = 0
var allEntries : NSArray!
var shuffledQuestions: [AnyObject]!
var nextQuestion = -1
var currentCorrectAnswerIndex : Int = 0
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.Banner?.delegate = self
self.Banner?.hidden = true
LoadAllQuestionsAndAnswers()
if #available(iOS 9.0, *) {
shuffledQuestions = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(allEntries as [AnyObject])
nextQuestion++
LoadQuestion(nextQuestion)
// Fallback on earlier versions
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
LoadQuestionPreiOS9(randomNumber)
}
LoadScore()
AdjustInterface()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let viewController = segue.destinationViewController as! HelpScreenViewController
viewController.controller = self
}
func ResetGame() {
PlaySoundReset()
score = 0
totalquestionsasked = 0
SaveScore()
LoadScore()
}
func PlaySoundReset()
{
let alertSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("pcbeep", ofType: "wav")!)
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: alertSound)
} catch {
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
func SaveScore()
{
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setInteger(score, forKey: "Score")
defaults.setInteger(totalquestionsasked, forKey: "Out of")
}
func LoadScore()
{
let defaults = NSUserDefaults.standardUserDefaults()
score = defaults.integerForKey("Score")
totalquestionsasked = defaults.integerForKey("Out of")
labelScore.text = "Score: \(score)"
labelTotalQuestionsAsked.text = "out of \(totalquestionsasked)"
}
and so on....
WITHIN the second HelpScreenViewController
import UIKit
protocol MyResetGameProtocol {
func ResetGame()
}
class HelpScreenViewController: UIViewController, MyResetGameProtocol {
var controller: MyResetGameProtocol? // reference to the delegate alias First Controller
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
#IBAction func SendFeedback(sender: AnyObject) {
UIApplication.sharedApplication().openURL(NSURL(string: "mailto:feedback#felice.ws?")!)
}
#IBAction func DismissView(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil) }
#IBAction func buttonResetGame(sender: AnyObject) {
controller.ResetGame()
}
}
Now, at the moment with the above code what happens is that if the user taps the "help" button in the first main ViewController (i.e. goToHelpScreenSegue), not only does it take the user to the help screen, but it also calls the function I want activated when the user taps on the "Reset Game" button instead. That is, at the moment, it's the "help" button that resets the game before taking the user to the help screen.
Now, within the help screen, the first two buttons work normally (but they're not using segues). Tapping on the third button (the Reset Game one) simply returns the user back to the main screen. It doesn't call the function, doesn't reset the game.
I've lost count of the times I've changed the code around to try and get it to work right, but I've obviously missed something really obvious.
In particular, I've tried using the following code instead within the main ViewController:
override func prepareForSegue(segue: UIStoryboardSegue?, sender: AnyObject?) {
if segue?.identifier == "resetGameSegue" {
let viewController = segue!.destinationViewController as! HelpScreenViewController
viewController.controller = self
}
However, this results in nothing happening. What I mean is that the button on the main screen works properly (taking the user to the help screen and not incorrectly calling the resetGame function). And, within the help screen the first two buttons work as expected, but the "Reset Game" button just returns the user to the first screen but without calling the ResetGame function.
I also tried removing the IBActions from both my code and the connections inspector for the "Reset Game" button, but that made no difference either.
Any assistance would be most appreciated as I'm just not getting it! :(
I'm agree with MikeG, that you should probably learn about how delegates should be implemented. But the thing you're doing wrong inside this code is that you're not actually calling ResetGame() function on your delegate. Try to implement your #IBAction function in this way:
#IBAction func buttonResetGame(sender: AnyObject) {
controller?.ResetGame()
}
And yeah, if I understand your logic correctly your HelpScreenViewController should not implement MyResetGameProtocol cause your ViewController is the one who's implementing it.