NSTextView, NSFontManager, and changeAttributes() - swift

So I have this app that I'm writing to get familiar with Swift and programming for OSX. It's a note-taking app. The note window consists of an NSTextView and a button that brings up an NSFontPanel.
Changing the font works great. Selecting a size? No problem. Want to change attributes of the font like color, underlining, etc? I'm not at all sure how to get this to work.
Other sources (here and here, for example) seem to suggest that NSTextView should be the target of NSFontManager and that NSTextView has it's own implementation of changeAttributes(). Making NSTextView the target, however, does nothing. When I select text in NSTextView and bring up the font panel, the first selection I make in the fontPanel results in deselection of the text.
Making my view controller the target for NSFontManager and implementing a stub for changeAttributes yields an object of type NSFontEffectsBox which I am unable to find any good documentation for.
Question is... what am I supposed to do with NSFontEffectsBox? If in the fontPanel I select blue text with double underline, I can see those attributes in the debugger but I'm unable to access them programatically.
Here's the relevant code:
override func viewDidLoad() {
super.viewDidLoad()
loadNoteIntoInterface()
noteBody.keyDelegate = self // noteBody is the NSTextView
noteBody.delegate = self
noteBody.usesFontPanel = true
fontManager = NSFontManager.sharedFontManager()
fontManager!.target = self
}
Code for changing the font. This works just fine.
override func changeFont(sender: AnyObject?) {
let fm = sender as! NSFontManager
if noteBody.selectedRange().length>0 {
let theFont = fm.convertFont((noteBody.textStorage?.font)!)
noteBody.textStorage?.setAttributes([NSFontAttributeName: theFont], range: noteBody.selectedRange())
}
}
Stub code for changeAttributes:
func changeAttributes(sender: AnyObject) {
print(sender)
}
So.. my goals are two:
Understand what's going on here
have any changes I make in the fontPanel be reflected in the NSTextView selected text.
Thank you.

So I did manage to find an answer of sorts. Here's how I implemented changeAttributes() in my program:
func changeAttributes(sender: AnyObject) {
var newAttributes = sender.convertAttributes([String : AnyObject]())
newAttributes["NSForegroundColorAttributeName"] = newAttributes["NSColor"]
newAttributes["NSUnderlineStyleAttributeName"] = newAttributes["NSUnderline"]
newAttributes["NSStrikethroughStyleAttributeName"] = newAttributes["NSStrikethrough"]
newAttributes["NSUnderlineColorAttributeName"] = newAttributes["NSUnderlineColor"]
newAttributes["NSStrikethroughColorAttributeName"] = newAttributes["NSStrikethroughColor"]
print(newAttributes)
if noteBody.selectedRange().length>0 {
noteBody.textStorage?.addAttributes(newAttributes, range: noteBody.selectedRange())
}
}
Calling convertAttributes() on sender returns an attribute array, but the names don't seem to be what NSAttributedString is looking for. So I just copy them from old name to new and send them on. This is a good start, but I will probably remove the old keys before adding the attributes.
The question remains, tho.. is this the right way to do things?

Related

Subclassed NumberFormatter in NSTableView not formatting

I have subclassed a NumberFormatter for my specific use and it's working great when I call it from code. Here's the subclass:
class MyNumberFormatterUnitPrice: NumberFormatter {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init() {
super.init()
setup()
}
private func setup() {
self.format = "#,##0.00####;(#,##0.00####)"
self.numberStyle = .currencyAccounting
}
}
I'm doing this because, while it's possible to set the positive and negative formats in the storyboard, you can't set those and "currencyAccounting". However, when I create a NumberFormatter in the storyboard, choose this subclass and then put it under my text cell for the column I would like to format, it appears to get overridden within:
tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
In here, I'm currently setting the cell by this logic:
let cellIdentifier = tableColumn!.identifier.rawValue
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(cellIdentifier), owner: nil) as? NSTableCellView {
...
I believe that I'm getting the tableviewcell from the tableview by its identifier and not creating a new one, so that should have its textfield along with it. I read the documentation and this appears to be the right way to do this. If I set the numberformatter after I've gotten the cell here, it works. But I don't want to do it this way because I don't want a giant case statement to set specific cell properties - I'd like to do this in the storyboard editor.
Any advice on how to use the storyboard editor to set the numberformatter?
Edit: Further to this, I have put a breakpoint inside where I am setting the stringvalue of the textfield, and the text is being set as follows:
textfield.stringValue = text
Looking at where the number formatter is in the debugger, it's set at the table column level - that doesn't seem right. But it's definitely still there and hasn't been written over somehow.
(lldb) e ((tableColumn?.dataCell as! NSTextFieldCell).formatter as! NumberFormatter).format
(String) $R14 = "#,##0.00####;0.00;(#,##0.00####)"
And the textfield's formatter is nil... weird.
(lldb) e textfield.formatter
(Formatter?) $R26 = nil
I'm going to go back and check the storyboard to see if maybe I dropped the formatter in the wrong place.
OK that got it. I'm obviously a noob to MacOS development and I just trusted that dropping the number formatter into the column I wanted was going to put it at the cell level... that's NOT true!.
In this image you can see the right and the wrong way to do this if you're expecting to format the cell. The right way is highlighted. You have to open the hierarchy up right down to the text field for the cell. The wrong way is above and you'll see it under the "text cell" which is actually the text cell for the column.
Thanks for everyone who stuck with me and tried help me on this. I hope this answer helps others in the future avoid several days of frustration like I just had!
Also as #Willeke points out, there's no real need to subclass the numberformatter if all you're doing is setting these properties. I'm going to do it because I have lots of cells that I want to share a common format but if all you're doing is formatting one cell, it's not needed.

Get reference to current object

My goal is to safe a reference from the button, label or textfield inside a variable.
The problem is that I don't know on which control the user tapped.
I am having a simple application which looks like this:
The user can touch any control.
It is easy enough with just those three controls because I can drag in a action. But if I am having many of them I can't handle them all over the action methods. Is there a general way in which I can safe a reference to the control in a variable so that I can know which of the controls is the active one?
Edit
As suggested I am using a function and assigning the variable to the sender of the function. This is how it looks in code:
var currentObject: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
myTextfield.action = #selector(myAction)
}
func myAction(sender: NSTextField)
{
print("aktuell: \(sender)")
currentObject = sender
}
As you can see this only works for a NSTextfield. Is there a way in which the function works for every control?
Set the tag attribute for each item, and then you can check sender.tag to identify which object is calling it.
To set the tag, select the Attributes inspector in Storyboard (upper right side - middle button of Utilities) and look for this section:

view.endEditing causing app to freeze on some textFields

Problem solved. See end of post.
Sorry if this is a bit long but I'm hoping I've included as much info to get this solved.
Brief overview of problem: Enter value in a textField using my custom keypad. Tap done button(should trigger view.endEditing) and some textFields will cause the app to freeze, most the time Xcode won't even throw an error but instead just restart the app, but i did catch one once(pic below). It works as expected on some textFields.
So I have a view controller with a bunch of textFields for the user to fill out which then performs calculations.
I have made a custom Keypad which essentially is the decimal pad with a "Done" button. I did this by making an keyboard.xib file and a keyboard.swift file.
Heres a snapshot of the error, I've included a whole bunch of my code below incase I'm using a method that isn't the best.
This is how the keyboard.swift file looks:
import UIKit
// The view controller will adopt this protocol (delegate)
// and thus must contain the keyWasTapped method
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
func backspace()
}
class keyboard: UIView {
// This variable will be set as the view controller so that
// the keyboard can send messages to the view controller.
weak var delegate: KeyboardDelegate?
// MARK:- keyboard initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
let xibFileName = "Keyboard" // xib extention not included
let view = NSBundle.mainBundle().loadNibNamed(xibFileName, owner: self, options: nil)[0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
// MARK:- Button actions from .xib file
#IBAction func keyTapped(sender: UIButton) {
// When a button is tapped, send that information to the
// delegate (ie, the view controller)
self.delegate?.keyWasTapped(sender.titleLabel!.text!) // could alternatively send a tag value
}
#IBAction func backspace(sender: UIButton) {
self.delegate?.backspace()
}
#IBAction func Done(sender: UIButton) {
self.delegate?.keyDone()
}
}
In the viewController I'm pretty sure I've included all the necessary things to access the keyboard seeing as it works for some textFields. Such as:
class myViewController: UITableViewController,UITextFieldDelegate, KeyboardDelegate
Then in viewDidLoad set each textField delegate:
self.textField1.delegate = self
self.textField2.delegate = self
self.textField3.delegate = self
// initialize custom keyboard
let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: numpad.height))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
textField1.inputView = keyboardView
textField2.inputView = keyboardView
textField3.inputView = keyboardView
Then this function (which seems to me to be the problem):
func keyDone() {
view.endEditing(true)
//activeTextField.resignFirstResponder()
print("please dont freeze")
}
I have checked all the connections, they seem to be fine.
Let me know if I can add any more info to help work it out.
Many Thanks.
Solved!!!
I suppose ill just put it down to beating my head over it rather than taking a break from the screen! Still I'm confused why it wasn't given a more specific error.
The problem was that in some cases one of the functions was dividing by zero (this is undefined... not possible) but a good thing to take from this(thank you Olivier) is the Instruments Tools to help find where abouts the code was losing its mind. This tutorial helped me understand how to use instruments! So once I could see where it was going crazy I set up a bunch of print statements to watch the values as they went into the 'problem' calculation, where I found the denominator to be zero. Bit of rearranging the code around to avoid this and problem solved!
This error message is basically saying that there is a memory issue, try running the code with instruments (Allocations in particular) this might reveal is there is something amiss with your keyboard
Edit 2: for anyone finding this error message in future (actual solution in this case)
Double check any code code running after keyDone() to see if there are any infinite loops or situations that would cause the compiler to assume an infinite amount of memory is required. In this case a line of code was dividing by zero, causing a fatal memory error (unable to allocate the N/A value it generated)

Move View Behind Keyboard When textfield selected

Recently I used this to Move View Behind Keyboard When textfield is selected
http://www.ioscreator.com/tutorials/move-view-behind-keyboard-ios8-swift
What if I need to move view only for some inputs, others are near top and don't need it? I tries the following code but no success in that.
var searcher
func keyboardWillShow(notification: NSNotification) {
if (searcher.tag == 1){
if let userInfo = notification.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
kbHeight = keyboardSize.height
self.animateTextField(true)
}
}
}
}
Thanks in advance!
I would recommend you to use AutoKeyboardScrollView library instead of adding code manually. This library already does what you need with the expected behaviour.
As seen on the project's readme:
Another great library for this is IQKeyboardManager. https://github.com/hackiftekhar/IQKeyboardManager
Simple to use and will do this globally for your entire app. Easy configuration and it adds previous, next and done functionality
If you're using cocoapods, just add
pod 'IQKeyboardManager-Swift'
Check out the docs on the GitHub page for the config options

Simple NSPageController example throws an unknown subview warning and stops working

I'm trying to get a very basic NSPageController to work (in book mode, not history mode). It will successfully transition once, and then stop working.
I suspect I'm creating the NSImageViews I'm loading into it wrong, but I can't figure out how.
The storyboard has a the SamplePageController which holds in initial hard-coded NSImageView.
I suspect I'm missing something really obvious here, since all of the tutorial's I've found for NSPageController are in Objective C not swift, and tend to focus on the history view mode.
The code is:
import Cocoa
class SamplePageController: NSPageController, NSPageControllerDelegate {
private var images = [NSImage]()
#IBOutlet weak var Image: NSImageView!
//Gets an object from arranged objects
func pageController(pageController: NSPageController, identifierForObject object: AnyObject) -> String {
let image = object as! NSImage
let image_name = image.name()!
let temp = arrangedObjects.indexOf({$0.name == image_name})
return "\(temp!)"
}
func pageController(pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
let controller = NSViewController()
let imageView = NSImageView(frame: Image.frame)
let intid = Int(identifier)
let intid_u = intid!
imageView.image = images[intid_u]
imageView.sizeToFit()
controller.view = imageView
return controller
// Does this eventually lose the frame since we're returning the new view and then not storing it and the original ImageView is long gone by then?
// Alternatively, are we not sizing the imageView appropriately?
}
override func viewDidLoad() {
super.viewDidLoad()
images.append(NSImage(named:"text")!)
images.append(NSImage(named:"text-2")!)
arrangedObjects = images
delegate = self
}
}
In this case your pageController.view is set to your window.contentView and that triggers the warning. What you need to do is add a subview in the window.contentView and have your pageController.view point to that instead.
The reason for the warning is that since NSPageController creates snapshots (views) of your content history, it will add them at the same level as your pageController.view to transition between them: that means it will try to add them to pageController.view.superview.
And if your pageController.view is set to window.contentView, you are adding subviews to the window.contentView.superview, which is not supported:
New since WWDC seed: NSWindow has never supported clients adding subviews to anything other than the contentView.
Some applications would add subviews to the contentView.superview (also known as the border view of the window). NSWindow will now log when it detects this scenario: "NSWindow warning: adding an unknown subview:".
Applications doing this will need to fix this problem, as it prevents new features on 10.10 from working properly. See titlebarAccessoryViewControllers for official API.