Constraint constant changes but layout doesn't get updated? - swift

I have a strange problem: in my table view I want to update a single cell when its UISlider gets modified (value changed control event). At the moment the value of the slider changes, I want to animate a button into the cell (sliding in from the right). I set the constant of the button's constraint to -50 to make sure it's not visible by default, and when the slider's value gets changed, a method gets called which updates the table view, so cellForRowAtIndexPath gets called again for all cells in my table. Simplified it looks something like this:
func valueChanged(sender:UISlider) {
// Reload table view
self.myTableView.reloadData()
// Let table view know the value of the slider got modified
self.didChangeValueOfSlider = true
}
And in cellForRowAtIndexPath I'm keeping track of which cell's slider got changed by using a custom selectedCellIndexPath variable. When the table view gets to the cell that got modified: it runs the following code:
// Check if cell is selected
if indexPath == self.selectedCellIndexPath {
// Check if value of slider was changed
if self.didChangeValueOfSlider == true {
// Value was changed: set constraint back to default
saveButtonTrailingConstraint.constant = CGFloat(15)
self.view.setNeedsUpdateConstraints()
self.view.updateConstraintsIfNeeded()
// Reset slider update status
self.didChangeValueOfSlider = false
}
}
Calling those setNeedsUpdateConstraints() and updateConstraintsIfNeeded() might be overkill or unnecessary, but please note this is my 15th attempt or so to actually get the view to display the updated layout. I used breakpoints to confirm the constant actually changes, after the code above is finished running and everything works perfectly fine. The only thing I can't get working is the updating part. I've tried UIView.animateWithDuration to animate the change, and I've tried methods like layoutIfNeeded() and beginUpdates() and endUpdates of the table view: nothing works. What could be the reason the layout doesn't get updated? And what am I supposed to be calling the layout and update methods on? I've been calling them on self.view but I'm not even sure if you're supposed to be calling it on the view if you're trying to update the layout of a table view cell. Any help would be appreciated.

I figured it out. The problem turned out to be the constraint itself. as I was accessing the wrong one. I defined it as saveButtonTrailingConstraint but I was actually modifying was the saveButton's width constraint (which apparently can't be animated). Instead of accessing saveButton.constraints[indexOfTrailingConstraint] I should have defined saveButton.superview!.constraints[indexOfTrailingConstraint] as the constraint belongs to the superview and not to the button itself. I guess the best thing to take away from this clumsy mistake is to always double-check if you're modifying the correct constraint if you're doing it programmatically, because it doesn't always show on the first eye.

Related

How do I manually set the key-view in a cocoa app?

I have a cocoa app written in Swift. The app has a number of NSControls for user entries. The user can use the tab key to move between the controls, most of them being textfields. I am doing all in Code, I do not use the Interface Builder.
Some of them are dynamic: I have a custom NSControl, let's call it myRow which itself is hosting two ´NSTextFields´. myRow itself is hosted in a superView, let's call it myRowManager. When the user makes an entry this myRow notifies the myRowManager, and another myRow is placed below the first one. When one field's content is deleted by the user the myRowManager then deletes that myRow and re-arranges the remaining myRows. The whole setup is mimicking a table view, where there is always one more row than there are entries so that the user can always add an entry at the end.
Using the tab key when modifying existing entries works just fine.
But the nature of my setup means, that when a row is added or deleted a method .layoutSubviews() is called, there all myRows are removed from the superView (the manager) and then new myRows are created, reflecting the new number and order, which is of course reflecting the underlying data.
But by removing the myRow which just had the focus from the view hierarchy sets the window's initial responder as the key-view, as the current and next element in the key-view loop are no longer present.
I therefore am trying to set the key-view manually to the newly created textfield. To do this, a reference to this textfield is kept by the layoutSubviews method.
Unfortunately I have not been successful so far.
What I am doing now (this is the method of the manager which is called by a myRow when an entry is added):
func rowAddedAtEnd(_ index: Int) {
layoutSubviews()
self.window?.recalculateKeyViewLoop()
self.window?.selectKeyView(following:targetView)
}
What strikes me as odd is that there is no method th set a specific view as the key view. I can either use .selectKeyView(following:) or (preceding:)
But anyway - it does not work. The focus ring just gets reset to the top left element in the window.
How can I solve this?
From the documentation of func selectKeyView(following view: NSView):
Sends the nextValidKeyView message to view and, if that message returns an NSView object, invokes makeFirstResponder(_:) with the returned object.
To select targetView, skip nextValidKeyView and call makeFirstResponder(_:).
self.window?.makeFirstResponder(targetView)

Discard changes inside UITableView do not reflect on the cell with UITextView that is not visible but active

I have UITableView with UITableViewCells that contains UITextView. And I have logic for temporary data and saved data. So, when user edit that cells everything is working, including discarding changes that I do with swapping temporary data and saved data. Then I do reloadData on table view and everything is showed correctly.
My problem is when the user clicks on the textfield and scrolls the table view until the "active" text view cell is not visible and the changes are then discarded. What happens is that all is discarded, but the active cell has the latest data that is not discarded. If I put "cursor" on some other cell and then discard changes again, it'll be ok, all cells that aren't active will discard data.
So, discarding data is not working only on cell that is not visible but still has active uitextview.
I tried to get "latest" active cell and manually reload them but it's not working. I have tried to resignFirstResponder() from tableview and then discarding data and reloading tableview but it's not working.
I expect all text views inside the tableview to have old data that is not saved when I click discard changes.
Cell that is still active (with an active text view) and is not visible on the screen won't discard changes, it will still have old data (but it will not be active anymore)
EDIT:
Here is how everything is working.
Here is how my code is working (very simplified).
So, I have
ViewModel -> ViewController -> Cell
On viewModel i have:
var data = ["Some", "things", "are", "good"]
tempData = []
on init I put tempData = data
(we are here talking about structs so value types, and everything is working as I said regarding that)
on cellForRow I have
cell.data = viewModel.data[indexPath.row]
and If I click discard changes on viewController I just do this in viewModel:
data = tempData
and on ViewController
tableView.reloadData()
and everything is working.
Only thing that is not working is that active cell that is not on the screen. It's not affected by changes in "core" dataset.
So maybe there's a connection about UITableView can't refresh cells that have active UITextView and are not visible at the moment of reloadData()? I don't know about that.
EDIT2:
cellForRow
guard let cell: ExampleTableViewCell = tableView.dequeueReusableCell(),
let data = viewModel.data[safe: indexPath.row] else {
return UITableViewCell()
}
cell.model = (name: data.name, surname: data.surname)
cell.onTextViewDidChange = { [weak self] text in
self?.tableView.beginUpdates()
self?.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
self?.viewModel.update(text: text, index: indexPath.row)
}
return cell
One of the key features of UITableView is that it reuses the same instances of the cells over and over again, only keeping as many unique instances as it takes to simulate there being infinite instances. Notice how in tableView(_: cellForRowAt: ) you do not initialize a cell, you dequeue it. This is because UIKit manages when the cells are initialized for the first time and when they are "recycled", hence RecyclerView on Android.
Without being able to look at your code, I would guess that the issue is that your temporary data is stored inside of an instance of the cell and when the cell scrolls offscreen, something is happening in your tableView(_: cellForRowAt) function which tells the cell to display a different cell's data and doesn't save the temporary data. My suggestion for a fix if this is the case would be to store the temporary data in your UIViewController rather than in the cell. It is also better MVC to store this kind of data in the controller rather than in the view.

(Swift) How to change Label text outside of cell (where the event occurs)?

I am unable to find a solution for this on my own, please help.
I have a table view with -/+ buttons inside that increase or decrease a value.
In the footer cell of the table is a label for the "total sum", the added value of all the cell values. When the value inside of a cell is decreased or increased, I want the footer cell text to change accordingly.
Any pointers how I go about that? I have tried to write a global function, but then I can't access the label anymore... tried it as a class method (static), but I can't seem to get it to work.
Can I somehow hook up the action that occurs in the UITableViewCell to trigger an action in the UITableView?
Thank you!
In the UITableViewDelegate associated to your table view, you should call the reloadRowsAtIndexPaths(indexPaths: [NSIndexPath], withRowAnimation animation: UITableViewRowAnimation) method to update your the row you wish to update.
Than it your UITableViewDataSource provide the footer cell with the good new value.

NSTableView with custom cell view and NSArrayController bindings drawing a blank string

It took me forever to figure out how to set up editing on a custom cell view for an NSTableView. Thanks to StackOverflow I figured that much out. P.S. I was doing all of this in Interface Builder.
I have a single column table in which the cell is a custom multi-control NSTableCellView, with:
name (in bold)
description
detail
It's all text. Set up editability on the name only. The table is sorted by the name.
When I edit the name, it has the effect on the bound model that I expect. The table even re-sorts correctly. However, it's displaying incorrectly. The description and detail (not editable) still show up correctly, but the name (which was edited) is a blank. When I inspect the model, it has the correct updated value. But the cell view itself is incorrect.
This doesn't happen all the time--it typically happens if the cell is re-sorted to the top or bottom of the table, but that may be a red herring and may instead have to do with NSTableView cell caching or something.
I hacked up a workaround in which I assign a delegate to the NSTextField (automatically generated for the NSTableCellView) and intercept the textShouldEndEditing event. For some reason this event is getting triggered twice for a given edit (after I press "enter" in the text field)--once for the actual edit where fieldEditor.string is different from the model name, followed by another event where fieldEditor.string is the same as the model name. If I return false for my textShouldEndEditing handler in the latter case, then the cell contents end up being drawn correctly. That's the hack.
I feel like I'm doing something wrong here though, and that shouldn't be necessary.
Is the textShouldEndEditing event supposed to be fired twice?

UITableView's indexPathsForVisibleRows incorrect?

When I check for a table's visible indexPaths with 'indexPathsForVisibleRows' during UIScrollViewDelegate's scrollViewDidScroll and scrollViewDidEndDragging, it seems to be accurate.
But sometimes just when scrolling and dragging is ending and 'cellForRowAtIndexPath' is invoked, the call to 'indexPathsForVisibleRows' returns 0. There are rows visible on-screen so I know it can't be 0. As soon as this happens, I can invoke UITableView::visibleCells and get a non-zero value.
Why does this discrepancy exist?
Did you tried calling methods in below sequence:
[MyTable visibleCells];
[MyTable indexPathsForVisibleRows];
There is a bug in iOS with indexPathsForVisibleRows. Use above two line code to get correct visible rows.
in swift, this works:
let viscells = tableView.visibleCells
let isThisCellVisible = (tableView.indexPathsForVisibleRows ?? []).contains(indexPath)
but this does not
_ = tableView.visibleCells
let isThisCellVisible = (tableView.indexPathsForVisibleRows ?? []).contains(indexPath)
visibleCells and NSFetchedResultsController
be careful how you access visibleCells. the UI will freeze indefinitely if you [attempt] to access the table view's visibleCells while they were in the process of being updated, which is not allowed.
e.g. how you use tableView.beginUpdates()/tableView.endUpdates(), or controllerWillChangeContent(_:)/controllerDidChangeContent(_:), etc
full debug message if this happens is:
[Assert] Attempted to access the table view's visibleCells while they were in the process of being updated, which is not allowed. Make a symbolic breakpoint at UITableViewAlertForVisibleCellsAccessDuringUpdate to catch this in the debugger and see what caused this to occur. Perhaps you are trying to ask the table view for the visible cells from inside a table view callback about a specific row?