NSMutableAttributedString not rendering correctly - swift

I am using a SplitViewController; The master is tableView with a series of RightDetail cells whose Attributed Strings are set via a callback from a value setting popover.
The value passed back and set via a method that sets the detailTextLabel of the cell. It does this within a DispatchQueue.main.async as some of updates to detailTextLabel are made via calls from Network.Framework
The strange thing is when the popover makes the changes the change to the attributed string is not presented correctly. The focus has to change to the next table cell before the correct drawing occurs.
So it is only when focus changes to "Flow Rate" that temperature is rendered correctly, as shown blow.
I have stepped through the following code and checked that the correct value is being written to self.buttonCell?.detailTextLabel?.attributedText
You'll also see that I've added setNeedsLayout() and setNeedsFocusUpdate()
func setDetailValueWithUnit( to index: Int) {
/** sets the detailtext to the value as the specified index and appends units.
it does this within the main dispatchQueue as this method is called by
operationResult which is called by the statemachine.
**/
DispatchQueue.main.async {
let font = self.buttonCell?.detailTextLabel?.font
let finalString = NSMutableAttributedString(string: valueString,attributes: [.font:font!])
if let units = self.units() {
finalString.append(units)
self.buttonCell?.detailTextLabel?.attributedText = finalString
//next 2 lines experimental in an attempt to fix the attributed text not
//being consistently redendered correctly
self.buttonCell?.detailTextLabel?.setNeedsLayout()
self.buttonCell?.detailTextLabel?.setNeedsFocusUpdate()
NSLog("setDetailValueWithUnit: %d", index)
return
} else {
assert(false, "No Units found")
}
}
}

The problem is that what you are doing is completely wrong. You should never be touching a cell's text label font, text, or anything else directly like that. That's not how a table view works. You should set the table's data model and update the table from there, thus causing cellForRowAt: to be called for the visible cells. In that way, only cellForRowAt: configures the cell, and all will be well.

Related

tableViewSelectionDidChange with addSubview and removeFromSuperview in table row

I looked for posts for issues with all 3 functions listed in the title, but my problem is not discussed anywhere. Frankly, my problem is such that it defies logic IMHO.
On selection of a row in my view-based NSTableView, I want to show a "Save" button in my last (aka "Action") column. So on viewDidLoad() I created my orphan button. Then on tableViewSelectionDidChange() I remove it from the superview and add it to my column's NSTableCellView. When there's no row selected, there is no button to show. Simple. Below code works perfectly.
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow
btnSave!.removeFromSuperview()
if selectedRow > -1,
let rowView = rowView(selectedRow),
let actionView = rowView.view(atColumn: Column.action.hashValue) as? NSTableCellView {
actionView.addSubview(btnSave!)
}
}
Wait. Did I say Perfect? It works as long as I use mouse to change the selected row, including selecting below the last row in table which removes selection and any button in previous selected row. However, when I use the keyboard Up/Down keys to change the selection, it does not work.
First I thought the function will get called only if I use mouse to change row selection. However, that's not true. That's true for tableViewSelectionIsChanging as per docs and as per facts. But for tableViewSelectionDidChange docs don't say it will only work when mouse is used and the facts bear that out. I put print statements and function does get called. I stepped through debugger as well. The mind boggling part is - the method to remove button from superview works, but the one to add button as subview does not work - and only if I use keyboard. How is it possible that the same exact code executes but I get two different outcomes?
Adding remaining functions
I use this to get selected row
private func rowView(_ rowIndex: Int, _ make: Bool = false) -> NSTableRowView? {
return tableView.rowView(atRow: rowIndex, makeIfNecessary: make)
}
I call this in viewDidLoad to create my orphan button
private func createButton() {
btnSave = NSButton(frame: NSRect(x: 10, y: 0, width: 22, height: 16))
btnSave?.title = "S"
btnSave?.setButtonType(.momentaryPushIn)
btnSave?.bezelStyle = .roundRect
}

NSAttributedText not working for UILabel in Swift

I'm using Swift 3 on an iOS 10.3 iPhone 6s.
I have an extension method on NSAttributedString:
extension NSAttributedString {
class func attributedString(title: String, titleFont: UIFont, body: String, bodyFont: UIFont) -> NSAttributedString {
let titleAttributes = [NSFontAttributeName : titleFont]
let bodyAttributes = [NSFontAttributeName : bodyFont]
let title = NSAttributedString(string: "\(title): ", attributes: titleAttributes)
let body = NSAttributedString(string: "\(body)", attributes: bodyAttributes)
let text = NSMutableAttributedString()
text.append(title)
text.append(body)
return text
}
}
And I have this UITableViewCell configure method that gets called when setting up the cell in cellForRowAtIndexPath:
class MyCustomCell: UITableViewCell {
...
func configure() {
...
myLabel.attributedText = NSAttributedString.attributedString(title: t, titleFont: tFont, body: b, bodyFont: bFont)
}
}
Somehow the label is blank on the screen.
The params are not optional (in other words, t and b are not and cannot be nil).
If I tap the cell, it opens a screen and then when I come back, the label is showing the text correctly. I've tried adding things like myLabel.setNeedsDisplay(), setNeedsDisplay(), setNeedsLayout(), etc. to the end of the configure method, but nothing's working.
What am I doing wrong?
Other attributed string issues have been because people used things like regular NSFontAttributeName with the documentAttributes: init method. That's not what I'm doing here.
UPDATE:
If I pause with the debugger to inspect the UI, here's what the label shows:
<UILabel: 0x1041734b0; frame = (20 0; 297 0); text = 'Title: body'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x174298290>>
I don't understand why it's got a height of 0. The label's vertical content hugging and content compression resistance priorities are set to 1000 in the storyboard, which normally works great when I use a UILabel's text property. Maybe the vertical hugging and compression resistance priorities don't work correctly with attributed text?
UPDATE 2:
I just changed the constraints by setting the label's vertical content hugging and content compression resistance priorities back to default and giving the label a height constraint of 20. Now the label shows correctly!
I don't like this solution because I want the label to automatically have a height of 0 if there is no text and, if there is text, I want the label's height to match the font size (which can vary depending on user preference's in my app). In other words, I don't want to have to manage the label's height constraint.
Update 3:
Something I didn't show in my configure method...at the beginning I have a guard statement where I clear the label's attributed text if there's no title or body to show. My goal is for the label to hide. However, if there's a title and body, then I proceed to the line I showed in the configure method above.
When I remove this line in the guard statement, the label is visible with a valid height. The behavior is the same when I use the label's text property, which I didn't realize.
I tried using setNeedsLayout() and layoutIfNeeded() to reload the label so it would set its size correctly when I give it text, but it's not working.
Summary
I want to be able to set the vertical content hugging and content compression resistance priorities to 1000 from the storyboard so that the label's height depends on its content. And when I set the label's attributedText to nil, or "", or "Title: body", I want it to automatically set its height correctly. Is this possible? Am I misunderstanding the content priorities?
I see you got it solved, but thought I'd put this here for anyone else stumbling on this.
Remember when updating the UI to always do it on the main thread. When you reload the cell in cellForRowAtIndexPath I believe your code is being called on the main thread.
When you call it elsewhere, it's a good idea to do it like so:
dispatchQueue.main.async {
configure()
}
So it turns out that my problem was that I was also calling configure from the view controller outside of the context of the cellForRowAtIndexPath: method, and it wasn't reloading the cell's height (even though the label's height had changed since I had set text on it).
So, I needed to add code to reload the cell in the table view:
NSIndexPath *indexPath = ...; // Get index path for the cell I want to update.
VCInboxCell *cell = (MyCustomCell *) [self.table cellForRowAtIndexPath:indexPath];
if (cell) {
[cell configure];
[self.conversationsTable reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
In other words, I was setting up the cell with configure in cellForRowAtIndexPath:, but at that point the label's text was nil (because the model's data for the label wasn't set yet). Then, when the model's data changed for the label, I was calling configure again, but not updating the row, so the label was being squashed due to constraints in the cell, even though it wanted to have a height of 19.5 according to its intrinsicContentSize (based on its text value).
Reloading the cell updates the height and all is well.

Tracking the position of a NSCell on change

I have a NSTableView and want to track the position of its containing NSCells when the tableView got scrolled by the user.
I couldn’t find anything helpful. Would be great if someone can lead me into the right direction!
EDIT:
Thanks to #Ken Thomases and #Code Different, I just realized that I am using a view-based tableView, using tableView(_ tableView:viewFor tableColumn:row:), which returns a NSView.
However, that NSView is essentially a NSCell.
let cell = myTableView.make(withIdentifier: "customCell", owner: self) as! MyCustomTableCellView // NSTableCellView
So I really hope my initial question wasn’t misleading. I am still searching for a way how to track the position of the individual cells/views.
I set the behaviour of the NSScrollView (which contains the tableView) to Copy on Scroll in IB.
But when I check the x and y of the view/cells frame (within viewWillDraw of my MyCustomTableCellView subclass) it remains 0, 0.
NSScrollView doesn't use delegate. It uses the notification center to inform an observer that a change has taken place. The solution below assume vertical scrolling.
override func viewDidLoad() {
super.viewDidLoad()
// Observe the notification that the scroll view sends out whenever it finishes a scroll
let notificationName = NSNotification.Name.NSScrollViewDidLiveScroll
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll(_:)), name: notificationName, object: scrollView)
// Post an intial notification to so the user doesn't have to start scrolling to see the effect
scrollViewDidScroll(Notification(name: notificationName, object: scrollView, userInfo: nil))
}
// Whenever the scroll view finished scrolling, we will start coloring the rows
// based on how much they are visible in the scroll view. The idea is we will
// perform hit testing every n-pixel in the scroll view to see what table row
// lies there and change its color accordingly
func scrollViewDidScroll(_ notification: Notification) {
// The data's part of a table view begins with at the bottom of the table's header
let topEdge = tableView.headerView!.frame.height
let bottomEdge = scrollView.bounds.height
// We are going to do hit-testing every 10 pixel. For best efficiency, set
// the value to your typical row's height
let step = CGFloat(10.0)
for y in stride(from: topEdge, to: bottomEdge, by: step) {
let point = NSPoint(x: 10, y: y) // the point, in the coordinates of the scrollView
let hitPoint = scrollView.convert(point, to: tableView) // the same point, in the coordinates of the tableView
// The row that lies that the hitPoint
let row = tableView.row(at: hitPoint)
// If there is a row there
if row > -1 {
let rect = tableView.rect(ofRow: row) // the rect that contains row's view
let rowRect = tableView.convert(rect, to: scrollView) // the same rect, in the scrollView's coordinates system
let visibleRect = rowRect.intersection(scrollView.bounds) // the part of the row that visible from the scrollView
let visibility = visibleRect.height / rowRect.height // the percentage of the row that is visible
for column in 0..<tableView.numberOfColumns {
// Now iterate through every column in the row to change their color
if let cellView = tableView.view(atColumn: column, row: row, makeIfNecessary: true) as? NSTableCellView {
let color = cellView.textField?.textColor
// The rows in a typical text-only tableView is 17px tall
// It's hard to spot their grayness so we exaggerate the
// alpha component a bit here:
let alpha = visibility == 1 ? 1 : visibility / 3
cellView.textField?.textColor = color?.withAlphaComponent(alpha)
}
}
}
}
}
Result:
Update based on edited question:
First, just so you're aware, NSTableCellView is not an NSCell nor a subclass of it. When you are using a view-based table, you are not using NSCell for the cell views.
Also, a view's frame is always relative to the bounds of its immediate superview. It's not an absolute position. And the superview of the cell view is not the table view nor the scroll view. Cell views are inside of row views. That's why your cell view's origin is at 0, 0.
You could use NSTableView's frameOfCell(atColumn:row:) to determine where a given cell view is within the table view. I still don't think this is a good approach, though. Please see the last paragraph of my original answer, below:
Original answer:
Table views do not "contain" a bunch of NSCells as you seem to think. Also, NSCells do not have a position. The whole point of NSCell-based compound views is that they're much lighter-weight than an architecture that uses a separate object for each cell.
Usually, there's one NSCell for each table column. When the table view needs to draw the cells within a column, it configures that column's NSCell with the data for one cell and tells it to draw at that cell's position. Then, it configures that same NSCell with the data for the next cell and tells it to draw at the next position. Etc.
To do what you want, you could configure the scroll view to not copy on scroll. Then, the table view will be asked to draw everything whenever it is scrolled. Then, you would implement the tableView(_:willDisplayCell:for:row:) delegate method and apply the alpha value to the cells at the top and bottom edges of the scroll view.
But that's probably not a great approach.
I think you may have better luck by adding floating subviews to the scroll view that are partially transparent, with a gradient from fully opaque to fully transparent in the background color. So, instead of the cells fading out and letting the background show through, you put another view on top which only lets part of the cells show through.
I just solved the issue by myself.
Just set the contents view postsBoundsChangedNotifications to true and added an observer to NotificationCenter for NSViewBoundsDidChange. Works like a charm!

Store previous value from stepper in swift

The logic from here Stepper value reset after loaded from coredate doesn't work with me plus its in obj C.
I have a label and stepper, i need the value in the label to act as a counter almost. Currently, when i close and reopen the views the core data works and the value from before shows in the label, but when i tap the stepper (after coming back to the view with the stepper) the value in the label counts from 0 again, i need it to add on to the previous value!
This must be easy but i can't seem to figure it out!
Thanks for any help in advance
#IBAction func stepperTapped(sender: UIStepper) { // when the user taps the stepper
label.text = String(Int(sender.value))
someObject.theCounterProperty = Int(sender.value)
do {
try context!.save()
} catch {
print("error")
}
}
Make sure the label and the counter are up to date when the view reappears. In viewWillAppear set the value from the Core Data object.
label.text = String(someObject.theCounterProperty)
counter.value = Double(someObject.theCounterProperty)

UIView is nil after moving to the selected index via UITabBarController

I am using a UITabBarController in a music playing app. I have 2 indexes in my tabBarController, index 0 (tableViewController) and index 1 (nowPlayingViewController). Index 0 contains a UITableViewController that displays song names, and index 1 contains a UIViewController that is supposed to display the now playing page, including labels of the current playing song and artist.
When a user taps on a cell (a song name) on tableViewController i do two things, first I give the title of the song that was selected to a third class, a ControllerClass and then I move the tab bar over to index 1, which is the now playing page...
//this is inside tableViewController class
let _ControllerClass = ControllerClass()
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
_ControllerClass.nowPlayingSongInfo(trackTitles[indexPath.row])
self.tabBarController?.selectedIndex = 1
}
So far so good. The nowPlayingViewController is the delegate of the ControllerClass. When ControllerClass receives the new song title, it calls a delegate method that is supposed to set the text of a UILabel on the nowPlayingViewController. Here is the method inside ControllerClass that calls back with the new title as a String.
func newNowPlayingSongTitle(songTitle: String){
delegate?.configureLabels(songTitle)
}
this also works fine, all good. I receive the callback successfully on nowPlayingViewController which I know because I am able to print the song title when the callback is received, like so...
func configureLabels(songTitle: String){
print("song title is \(songTitle)")
//successfully prints the correct song title
}
HOWEVER, my issue is this... I need to not just print out the new song title, I need to set my UILabel's text property on my nowPlayingViewController equal to the new song title that i receive in the callback. BUT, when i try this...
func configureLabels(songTitle: String){
print("song title is \(songTitle)")
self.songLabel.text = songTitle
//crashes on the line above error = "unexpectedly found nil while unwrapping an optional value"
}
the app crashes due to unexpectedly found nil while unwrapping... . Apparently the UILabel is nil, even though I know i have set it up properly since I can set its text property from other places.
I also tried this,
func configureLabels(songTitle: String){
let view = UIView(frame: CGRectMake(100, 100, 100, 100))
self.view.addSubview(view)
}
to see if the entire view itself is nil, and again it crashes with the same error, so indeed self.view is nil.
What is going on here? I am able to set the labels and images in this view from elsewhere, however in the situated illustarted above the UILabel and the entire view itself are nil. How to fix this problem? Any suggestions would be great, thank you.
Note: I have tried self.loadView() but that did not help either
My issue was that my delegate was set to a different instance of nowPlayingViewController, than the instance that was currently in the tabBarController. To fix this, instead of doing
let np = nowPlayingViewController()
ControllerClass.delegate = np
I did
let np = self.tabBarController?.viewControllers![1] as! nowPlayingViewController
then ControllerClass.delegate = np
the reason this is necessary with a UITabBarController is because the views that are embedded within the tabBarController are loaded once and then remain loaded, thus I needed to set the UILabels on the nowPlayingViewController for the proper instance of the nowPlayingViewController, which is the one that was embedded in the tabBarController, since that instance was the one being displayed. Sure hope someone else finds this useful!