I'm using a TableView and have a custom TableViewCell that I've added a subview to.
The problem is that I need the subview's height to change sometimes and therefore, the table's contentView would have to be updated as well as the row's height.
The subview of the custom TableViewCell is represented by the yellow background.
These images show what's currently happening in my simulator.
On Load
After the event that causes the subview's height to increase
What's the best approach to take with something like this?
Should I use constraints? And if so, what kind of constraints should I use? Would I have to then reload the tableview too every time the subview's size changes?
Here is the code I'm currently using for my custom TableViewCell:
import UIKit
class CustomCell: UITableViewCell {
var newView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.newView = UIView(frame: self.frame)
self.newView.backgroundColor = .yellowColor()
self.addSubview(newView)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.newView.frame.size.width = self.frame.size.width // because self.frame.width is different than it was in the init method
}
func somethingHappenedThatMySubviewHasToIncreaseInHeight() {
self.newView.frame.size.height = self.frame.size.height + 40
}
}
The best approach is to use Auto Layout and self-sizing cells. Setup constraints in storyboard for your custom cell.
You will not need to reload the tableView. Each cell will automatically adjust its height, based on how much vertical space its subview takes.
For more information, see the detailed walkthrough by smileyborg in his answer to Using Auto Layout in UITableView for dynamic cell layouts & variable row heights.
Related
I setup a trivial view based NSTableView, the view is a simple NSTextField used as a label:
func tableView(_ tv: NSTableView, viewFor tc: NSTableColumn?, row: Int)
-> NSView?
{
let v = (tv.makeView(withIdentifier: viewID, owner: nil) as? NSTextField)
?? NSTextField()
v.isSelectable = false
v.isEditable = false
v.stringValue = data[row] // [String]
v.identifier = viewID
return v
}
and then I enable dragging of the items using this delegate method:
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int)
-> NSPasteboardWriting?
{
return MyPasteboardItem(value: data[row])
}
This works, but when I drag the row, I get an empty representation of the textfield:
(in a different setup things like image views and buttons get drawn, but the NSTextField also ends up white).
I highly suspect this is due to the NSTextField being backed by a TextLayer which doesn't get drawn if the tableview captures an image of the view hierarchy being dragged.
What is a good way to fix this? I considered implementing draw(), but well.
Update: If I do an own NSTextField subclass and override draw(), it indeed starts to work:
final class MyTextField : NSTextField {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
}
Looking at the thing in the view debugger shows that the Layer switches from NSTextLayer to _NSViewBackingLayer when draw is overridden.
But I assume this is not exactly desirable? Is there a better way to accomplish this?
Complete sample: https://gist.github.com/helje5/48728983951ab3362af43b967c554475
Setting drawsBackground=false on the textfield fixed it for me.
If you are using xib file then untick "Draws Background" on the Text Field or in your viewFor: method something like:
v.drawsBackground = false;
I ended up with this in an NSTextField subclass, not sure whether it is a good idea:
override public func draw(_ dirtyRect: NSRect) {
// This switches from the UXLabel being backed by `NSTextLayer` to
// `_NSViewBackingLayer`, which may not be desirable.
// BUT: This enables proper drawing of the Drag&Drop cell.
super.draw(dirtyRect)
}
like below Image..
I tried the below code...
import UIKit
class UploadsController: UICollectionViewController , Dimmable {
let dimLevel: CGFloat = 0.5
let dimSpeed: Double = 0.5
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.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
dim(.In, alpha: dimLevel, speed: dimSpeed)
}
#IBAction func unwindFromUploadEdit(segue: UIStoryboardSegue)
{
dim(.Out, speed:dimSpeed)
}
And connect the AnotherViewController present modally through the button on the cell of the CollectionViewController..
But it does not work..
after click on the button the AnotherViewController open/overlay on the entire CollectionViewController instead of cell of the CollectionViewController...
Plz. suggest me how can I do this I am new in Swift
Look at the frame of AnotherViewController after it is presented. Compare that to the frame of the cell in the CollectionViewController. You want the width and height to the be the same so that it only covers up that view. The x and y points are a bit more tricky. There is a method on UIView called convertRect:toView: . The origin of the cell is relative to the collection view. The origin of the AnotherViewController should be relative to the entire CollectionViewController's view. Use the mentioned method to determine where the cell is relative to its ViewController and use that information to make sure the overlay is the same size and in the same location of the cell.
After searching through SO and online, I'm struggling to figure out a concept that I thought would be relatively simple. Essentially, I have a table in an OS X Swift app, with several columns, and it is currently populating data. I am trying to discern how I can set the background color of each "row" (ideally with alternating colors, but I'll start with just one color). My MasterViewController file is like so;
import Cocoa
class MasterViewController: NSViewController {
var minions = [Minion]()
func setupSampleMinion() {
minions = Minion.fetchMinionData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
// MARK: - NSTableViewDataSource extension MasterViewController: NSTableViewDataSource {
func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
return self.minions.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
// 1
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
let minion = self.minions[row]
// 2
if tableColumn!.identifier == "MyColumn" {
// 3
cellView.imageView!.image = NSImage(named: "minion.name!")
cellView.textField!.stringValue = minion.name!
return cellView
}
return cellView
}
}
func tableView(tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let myCustomView = MyRowView()
return myCustomView
}
class MyRowView: NSTableRowView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
self.backgroundColor = NSColor(red: 0.76, green: 0.82, blue: 0.92, alpha: 1)
NSRectFill(dirtyRect)
}
}
// MARK: - NSTableViewDelegate extension MasterViewController: NSTableViewDelegate {
}
While I THINK I have some of the coding right, this does not seem to set the background color if the row in any way. Any thoughts or overall guidance would be most appreciated. Thank you!
If you just want the rows to use the standard alternating colors for rows, there's a simple checkbox in the Attributes inspector for the table view in IB to enable that.
To use a non-standard background color, you want to set the row view's backgroundColor, but not inside of drawRect(). If you change properties of a view that affect how it draws inside of drawRect(), that will probably mark the view as needing display, which will provoke another call to drawRect(), etc.
It should work to just set it in the delegate's tableView(_:didAddRowView:forRow:) method. That's documented in the description of the backgroundColor property.
With regard to your attempt at overriding drawRect(): setting the row view's backgroundColor will presumably affect how the superclass draws. So, setting it after calling through to super is unlikely to help. It definitely won't affect the subsequent NSRectFill() call. That function relies on the fill color set for the current graphics context, which is implicit. You would change that by calling someColor.set().
Buy, anyway, there should be no need to override drawRect() given that you can set the backgroundColor. If you want to achieve some background drawing beyond what's possible by just setting a color, you should override drawBackgroundInRect() and not drawRect(), anyway.
Finally, your implementation of tableView(tableView:rowViewForRow:) should call the table view's makeViewWithIdentifier(_:owner:) method first, before creating a new view. And it should set the identifier on any new view it does create. That allows the table view to maintain a reuse queue of views, to avoid constantly destroying and recreating views.
let label = UILabel(frame: CGRectMake(20.0, 20.0, 15.0, 15.0))
label.backgroundColor = UIColor.flamingoColor()
UIView.animateWithDuration(3.0, animations: {
cell.addSubview(label)
label.backgroundColor = UIColor.yellowColor()
})
This code I put inside tableView:cellForRowAtIndexPath: -> result is a subview that has a yellow background, but I haven't seen any animation. The same is for method tableView:didEndDisplayingCell:
Which method should I use or what kind of animation should I put there to see any animations inside UITableViewCell. I know that I cannot animate existing labels, because these are with constraints.
Firstly, you should move cell.addSubview(label) to outside of the animation block and add the label to the contentView of the cell.
Back to your problem. You can't animate the background colour of a UILabel. You could use a UIView, of the same size, behind the UILabel for the same effect though.
To get the UIView to change colour once it's appeared I used a UITableViewCell subclass:
class AnimatedCell : UITableViewCell {
let animatedView = UIView()
/* Setup by adding animatedView to the contentView and setting the
animated view's initial frame and colour.
*/
func animate() {
UIView.animateWithDuration(3.0) {
animatedView.backgroundColor = UIColor.redColor()
}
}
Then in your UITableViewController subclass, when the view controller is presented the UITableView will be populated so you need to animate those cells with:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated: Bool)
for cell in tableView.visibleCells() as! [UITableViewCell] {
if let animatedCell = cell as? AnimatedCell {
animatedCell.animate()
}
}
}
Then, for cells that appear when you scroll:
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if let animatedCell = cell as? AnimatedCell {
a.animateLabel()
}
}
Also, you can animate with AutoLayout, you need to change the constant on the NSLayoutConstraint you want to animate. This gives a good example: iOS: How does one animate to new autolayout constraint (height)
My app consists of an NSScrollView whose document view contains a number of vertically stacked NSTextViews — each of which resizes in the vertical direction as text is added.
Currently, this is all managed in code. The NSTextViews resize automatically, but I observe their resizing with an NSViewFrameDidChangeNotification, recalc all their origins so that they don't overlap, and resize their superview (the scroll view's document view) so that they all fit and can be scrolled to.
This seems as though it would be the perfect candidate for autolayout! I set NSLayoutConstraints between the first text view and its container, the last text view and its container, and each text view between each other. Then, if any text view grows, it automatically "pushes down" the origins of the text views below it to satisfy contraints, ultimately growing the size of the document view, and everyone's happy!
Except, it seems there's no way to make an NSTextView automatically grow as text is added in a constraints-based layout? Using the exact same NSTextView that automatically expanded as text was entered before, if I don't specify a constraint for its height, it defautls to 0 and isn't shown. If I do specify a constraint, even an inequality such as >=20, it stays stuck at that size and doesn't grow as text is added.
I suspect this has to do with NSTextView's implementation of -intrinsicContentSize, which by default returns (NSViewNoInstrinsicMetric, NSViewNoInstrinsicMetric).
So my questions: if I subclasses NSTextView to return a more meaningful intrinsicContentSize based on the layout of my text, would my autolayout then work as expected?
Any pointers on implementing intrinsicContentSize for a vertically resizing NSTextView?
I am working on a very similar setup — a vertical stack of views containing text views that expand to fit their text contents and use autolayout.
So far I have had to subclass NSTextView, which is does not feel clean, but works superbly in practice:
- (NSSize) intrinsicContentSize {
NSTextContainer* textContainer = [self textContainer];
NSLayoutManager* layoutManager = [self layoutManager];
[layoutManager ensureLayoutForTextContainer: textContainer];
return [layoutManager usedRectForTextContainer: textContainer].size;
}
- (void) didChangeText {
[super didChangeText];
[self invalidateIntrinsicContentSize];
}
The initial size of the text view when added with addSubview is, curiously, not the intrinsic size; I have not yet figured out how to issue the first invalidation (hooking viewDidMoveToSuperview does not help), but I'm sure I will figure it out eventually.
I had a similar problem with an NSTextField, and it turned out that it was due to the view wanting to hug its text content tightly along the vertical orientation. So if you set the content hugging priority to something lower than the priorities of your other constraints, it may work. E.g.:
[textView setContentHuggingPriority:NSLayoutPriorityFittingSizeCompression-1.0 forOrientation:NSLayoutConstraintOrientationVertical];
And in Swift, this would be:
setContentHuggingPriority(NSLayoutConstraint.Priority.fittingSizeCompression, for:NSLayoutConstraint.Orientation.vertical)
Here is how to make an expanding NSTextView using Auto Layout, in Swift 3
I used Anchors for Auto Layout
Use textDidChange from NSTextDelegate. NSTextViewDelegate conforms to NSTextDelegate
The idea is that textView has edges constraints, which means whenever its intrinsicContentSize changes, it will expand its parent, which is scrollView
import Cocoa
import Anchors
class TextView: NSTextView {
override var intrinsicContentSize: NSSize {
guard let manager = textContainer?.layoutManager else {
return .zero
}
manager.ensureLayout(for: textContainer!)
return manager.usedRect(for: textContainer!).size
}
}
class ViewController: NSViewController, NSTextViewDelegate {
#IBOutlet var textView: NSTextView!
#IBOutlet weak var scrollView: NSScrollView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
activate(
scrollView.anchor.top.constant(100),
scrollView.anchor.paddingHorizontally(30)
)
activate(
textView.anchor.edges
)
}
// MARK: - NSTextDelegate
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
print(textView.intrinsicContentSize)
textView.invalidateIntrinsicContentSize()
}
}
Class ready for copying and pasting. Swift 4.2, macOS 10.14
class HuggingTextView: NSTextView, NSTextViewDelegate {
//MARK: - Initialization
override init(frame: NSRect) {
super.init(frame: frame)
delegate = self
}
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
super.init(frame: frameRect, textContainer: container)
delegate = self
}
required init?(coder: NSCoder) {
super.init(coder: coder)
delegate = self
}
//MARK: - Overriden
override var intrinsicContentSize: NSSize {
guard let container = textContainer, let manager = container.layoutManager else {
return super.intrinsicContentSize
}
manager.ensureLayout(for: container)
return manager.usedRect(for: container).size
}
//MARK: - NSTextViewDelegate
func textDidChange(_ notification: Notification) {
invalidateIntrinsicContentSize()
}
}