Subclassed NumberFormatter in NSTableView not formatting - swift

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.

Related

Swift: Why is UILabel not displaying data from dataSource into the collection view cells?

My goal is to display the letters below in individual cells inside of a collection view. I am attempting to use a UILabel to display them on the screen. This is the code providing the data to be displayed:
let dataSource: [String] = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]
The UILabel was created in a separate file called MyCollectionViewCell.swift. The code below links the label on the storyboard to the source code. It then creates a function that takes each letter (given to it from the dataSource array in the CollectionViewController.swift file) and assigns it to the label so that it can be displayed on the screen.
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet var notesLabel: UILabel!
func setup(with note: String){
notesLabel?.text = note
}
}
Lastly, the code below (written in CollectionViewController.swift) is supposed to go through each cell and pass in the value from the dataSource array into each cell from the collection view. The .setup() method assigns the string value to the UILabel:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.identifier, for: indexPath) as! MyCollectionViewCell
cell.setup(with: dataSource[indexPath.row])
return cell
}
This was working when I followed this video:
https://www.youtube.com/watch?v=TQOhsyWUhwg&t=527s
However, since the moment I deleted the original UILabel and created my own, I have not been able to get the UILabel to display the data from the dataSource array. The image below shows the output I am getting when I run the application:
Look at the screenshot of the application here
I have a feeling this may have something to do with a subview not being properly added to its parent view, or something along those lines. However, I am still a beginner with Swift so I'm not sure if that's what I am doing wrong.
I have tried:
Changing this line to "item" instead of "row": cell.setup(with: dataSource[indexPath.row])
Created an instance of MyCollectionViewCell inside of the "cellForItemAt" function and tried to assign the value of "cell" to the setup function
Created the UILabel in CollectionView.swift instead of MyCollectionViewCell.swift
Replaced "cell.setup(with: dataSource[indexPath.row])" with "cell.setup(with: "test")" to verify if the String "test" would display on each cell, but it did not.
Took away the question mark in "notesLabel?.text = note" and it came back NIL
At this point I have tried a lot more things than that, but did not note them all as I was trying them. However, nothing has worked so far. I've been stuck with this problem for a few days now. If someone can help I will greatly appreciate it.
I solved this issue by creating a XIB file for the cell.
However, the more I am learning about Xcode, I realized that the error could possibly be found in the XML file associated to the Storyboard (Interface Builder).
At the time of this error, I thought that the storyboard was possibly creating a file, but I did not know where to find it. I now know that the XML source code file is found here:
Main.Storyboard -> Open As -> Source Code
This is where the code for the connections made from the storyboard to the view controller are found.
Ex: <outlet .property="name of object connected" />
I possibly just needed to rename the property of the outlet to match my UILabel, and that would have also solved the problem. I hope this can help you if you're having the same issue. Comment or contact me if you have any questions.

How to add UITextView to bottom of UITableView

I have a project where I have to show a lot of information on one screen. It is not too much information, but it is complicated to achieve. For the purpose of this question I suggest to look at this screenshot that will illustrate what I want to achieve.
I currently set up this screen with a UIViewController containing a UITableView that is pinned to the top, bottom, leading and trailing anchor of the view using AutoLayout for the middle section. This works great. I then added a tableHeaderView which contains the information about the user on top. Now I have just added a second UITableView in the tableFooterView, and surprisingly, this works like a charm too. I had a bit of trouble with the height, but I managed to get it done in the viewDidLayoutSubviews() method. But that's bit off topic.
I am now at the final stage where I want to add a UITextView together with a UIButton (in a UIView container) to allow users to add comments. At first, I added this in the tableFooterView and it worked, but as soon as the content was too short (e.g. only one row in the middle and no comments yet), the UITextView would appear in the middle of the screen (directly under the contents of the UITableView). I read up on this and figured it is the expected behavior of the tableFooterView, so I am now trying to figure out a way on how to add this custom view to add comments that will always be on the bottom if the content is not filling the entire screen, but will also scroll with the contents if the content is larger than the screen size. (Ideally, I would want to be able to grow or shrink the UITextView when a user enters text - might be relevant in case someone suggests contentInsets).
Any suggestions? Should I add a subview directly to the UITableView (which is, as far as I read, not recommended)? Should I work with contentInsets on the UITableView and add the UIView container as a subview of my main UIViewController? I'm a bit lost after searching multiple solutions without finding the right one, so I hope you guys can help me out. Cheers!
Adding a UITextView is tricky because it's subclass of UIScrollView, just like UITableView, and adding a scroll view to another scroll view is not only difficult for the gesture recognisers to handle, but also for auto-layout.
What would make your life easier is to have just 1 table view in your view controller with different sections, and each section has a different type of cell.
What I like to do for this is declare an enum in my view controller:
class MyViewController: UIViewController {
private enum Section: Int, CaseIterable {
case foo
case bar
}
}
extension MyViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return Section.allCases.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = Section(rawValue: section) else { preconditionFailure() }
switch section {
case .foo:
return 1
case .bar:
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else { preconditionFailure() }
let cell: UITableViewCell
switch section {
case .foo:
cell = ...
case .bar:
cell = ...
return cell
}
}
For the cells with long text you can just use a default cell style (in your XIB/storyboard) and set
cell.textLabel?.numberOfLines = 0
The table view should be able to automatically size itself to fit the entire text.
The only reason you should need to use a UITextView in your scenario is if you want the user to be able to edit the text. Otherwise save yourself a lot of pain and go with a UILabel.
You should add separate UIView at the bottom of main screen and add UITextView as a subview in it.
Add TableView above this container view so that it scrolls and textview is always visible at the bottom.

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)

Customise UITabBarController.moreNavigationController UITableViewCells

As stated in the title, I'm trying to customise the cells in the moreNavigationController. I've read a number of SO posts asking a similar question but most are outdated and seem to be hacks.
I'm assuming this is a very common situation so there must be a 'tidy' way of doing this surely?
I've looked into overriding one of the tableviews delegate methods but I couldn't figure that out assuming it's even possible. I've also tried the following:
override func viewDidLoad() {
super.viewDidLoad()
let moreNavController = self.moreNavigationController
if let moreTableView = moreNavController.topViewController?.view as? UITableView {
for cell in moreTableView.visibleCells {
cell.textLabel?.textColor = AppDarkColor
cell.imageView?.image = cell.imageView?.image?.imageWithRenderingMode(.AlwaysTemplate)
}
}
}
But moreTableView.visibleCells is empty at least when the UITabBarController is loaded. Maybe I need to move this somewhere else but I'm not sure where or if this is the right approach.
Experimented putting the code in viewWillAppear. It works for me (at least). Hope it works for you!

NSTextView, NSFontManager, and changeAttributes()

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?