NSCollectionView with Dynamic Width Items Using Auto Layout - cocoa

I'm building an NSCollectionView that scrolls horizontally with items that look something like this:
I have my NSCollectionView item defined in a .xib with auto layout constraints. The width of the item is defined by the width of the label plus the width of the count label (and its surrounding pill). All this is encapsulated by an NSView called "Box."
Here are the constraints for Box:
I have my datasource hooked up and everything seems fine except I can't get the width of the item to change based on the sizes of the views inside it.
From what I've read, I can't rely solely on auto layout to define the size here (I hope I'm wrong about that) much like I would with a dynamic height in an NSTableView. So I'm trying to calculate the size with the sizeForItemAt delegate method from NSCollectionViewDelegateFlowLayout:
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "PhaseItem"), for: indexPath) as! PhaseItem
return item.box.fittingSize
}
When I print the fittingSize to the console, it looks right. But I'm getting a Unable to simultaneously satisfy constraints crash related to the size of Box (it has a subclass of RoundedWrap):
"<NSLayoutConstraint:0x6000033c3890 H:|-(11)-[NSTextField:0x7ff6abf6e9f0] (active, names: '|':Avid.RoundedWrap:0x7ff6abf74840 )>",
"<NSLayoutConstraint:0x6000033c6300 H:[NSTextField:0x7ff6abf6e9f0]-(7)-[Avid.BadgeView:0x7ff6abf6f120] (active)>",
"<NSLayoutConstraint:0x6000033c5400 H:[Avid.BadgeView:0x7ff6abf6f120]-(9)-| (active, names: '|':Avid.RoundedWrap:0x7ff6abf74840 )>",
"<NSLayoutConstraint:0x6000033c39d0 H:|-(8)-[NSTextField:0x7ff6abf6f360] (active, names: '|':Avid.BadgeView:0x7ff6abf6f120 )>",
"<NSLayoutConstraint:0x6000033c2760 H:[NSTextField:0x7ff6abf6f360]-(8)-| (active, names: '|':Avid.BadgeView:0x7ff6abf6f120 )>",
"<NSLayoutConstraint:0x6000033c3a20 H:|-(0)-[Avid.RoundedWrap:0x7ff6abf74840] (active, names: '|':NSView:0x7ff6abf74a80 )>",
"<NSLayoutConstraint:0x6000033c3b60 H:[Avid.RoundedWrap:0x7ff6abf74840]-(0)-| (active, names: '|':NSView:0x7ff6abf74a80 )>",
"<NSAutoresizingMaskLayoutConstraint:0x6000033140f0 h=--& v=--& NSView:0x7ff6abf74a80.width == 0 (active)>"
The crash goes away if I remove the constraint that pins the trailing edge of the Box to the containing view, but then my NSCollectionView items don't appear at all.
Is there an easier way to do all this? How can I use auto layout to set the dynamic width of my NSCollectionViewItems?

It looks like I was on the right track. I just needed to add:
item.view.layoutSubtreeIfNeeded()
Like this
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
///...
item.view.layoutSubtreeIfNeeded()
return item
}
In order for the item size to be accurate.

Related

In a view-based NSTableView, the NSTextField is clipped by an NSImageView

TL;DR:
In macOS 10.11, a view based NSTableView containing an NSTextField and an NSImageView right under the textfield, with some rows having an image and others not, some texts are clipped after having scrolled the table down then up.
Before scrolling:
After scrolling:
When I say "clipped" it means that the text view is at its minimum height as defined with autolayout, when it should be expanded instead.
Note that this faulty behavior only happens in macOS 10.11 Mavericks. There's no such issues with macOS 10.12 Sierra.
Context
macOS 10.11+, view based NSTableView.
Each row has a textfield, and an image view just below the textfield.
All elements have autolayout constraints set in IB.
Goal
The text view has to adapt its height vertically - the image view should move accordingly, staying glued to the bottom of the textfield.
Of course the row itself also has to adapt its height.
Sometimes there's no image to display, in this case the image view should not be visible.
This is how it's supposed to render when it works properly:
Current implementation
The row is a subclass of NSTableCellView.
In the cell the textfield is set with an attributed string.
In tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat, I make a dummy textView that I use to find the actual textfield's height, and return an amended row height accordingly. I also check if there's an image to display and if it's the case I add the image height to the row height.
let ns = NSTextField(frame: NSRect(x: 0, y: 0, width: defaultWidth, height: 0))
ns.attributedStringValue = // the attributed string
var h = ns.attributedStringValue.boundingRect(with: ns.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading]).height
if post.hasImage {
h += image.height
}
return h + margins
In tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? I prepare the actual cell contents:
getUserAvatar { img in
DispatchQueue.main.async {
cell.iconBackground.layer?.backgroundColor = self.prefs.colors.iconBackground.cgColor
cell.iconBackground.rounded(amount: 6)
cell.iconView.rounded(amount: 6)
cell.iconView.image = img
}
}
cell.usernameLabel.attributedStringValue = xxx
cell.dateLabel.attributedStringValue = xxx
// the textView at the top of the cell
cell.textLabel.attributedStringValue = xxx
// the imageView right under the textView
if post.hasImage {
getImage { img in
DispatchQueue.main.async {
cell.postImage.image = img
}
}
}
Issues
When scrolling the tableView, there's display issues as soon as one or several rows have an image in the image view.
Clipped text
Sometimes the text is clipped, probably because the empty image view is masking the bottom part of the text:
Normal
Clipped
IMPORTANT: resizing the table triggers a redraw and fixes the display issue...
What I've tried
Cell reuse
I thought the main issue was because of cell reuse by the tableView.
So I'm hiding the image field by default in tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?, only unhiding if there's an image to set, and I do it on the main thread:
guard let cell = tableView.make(withIdentifier: "xxx", owner: self) as? PostTableCellView else {
return nil
}
DispatchQueue.main.async {
cell.postImage.isHidden = true
cell.postImage.image = nil
}
// set the text view here, the buttons, labels, etc, then this async part runs:
downloadImage { img in
DispatchQueue.main.async {
cell.postImage.isHidden = false
cell.postImage.image = img
}
}
// more setup
return cell
But the imageView is still blocking the text view in some cases.
And anyway, sometimes the text is clipped at the first display, before any scrolling is done...
CoreAnimation Layer
I thought maybe the cells need to be layer backed so that they're correctly redisplayed, so I've enabled CoreAnimation layer on the scrollView, the tableView, the tableCell, etc. But I hardly see any difference.
A/B test
If I remove the imageView completely and only deal with the textfield everything works ok. Having the imageView is definitely what is causing issues here.
Autolayout
I've tried to handle the display of the row contents without using autolayout but to no avail. I've also tried to set different constraints but clearly I suck at autolayout and didn't manage to find a good alternative to my current constraints.
Alternative: NSAttributedString with image
I've tried to remove the image view and have the image added at the end of the attributed string in the text view instead. But the result is often ugly when it works - and for most times it just doesn't work (for example when the image can't be downloaded in time to be added to the attributed string, leaving the cell without text or image at all and at a wrong height).
Question
What am I doing wrong? How could I fix these issues? My guess is that I should change the autolayout constraints but I don't see a working solution.
Or maybe would you do this entirely differently?
Text field has "Preferred Width" field in IB which allows tune up correlation of intrinsic size with auto-layout calculated size.
Changing this IB property value to "First Runtime Layout Width" or "Explicit" often helps resolving similar issues.
This resolved a lot of my issues in past.

Xcode - Blank Space After UITableView

What is the best/correct way to get a UITV Cells to resize based on the display they're being viewed on? Basically, I would like the same number of rows displayed on screen, regardless of the device being used. Is this done in code or am i missing something in the storyboard?
Please see my example below (this is what i see in storyboard):
This is I see at the bottom of the screen on an iPhone 6 simulator:
This is what I see at the bottom of the screen on an iPhone 4s simulator:
I'm pretty new to Xcode so appreciate this is probably a well documented question... but i've been at it for a while now and i'm at the point where i need a little help -- so any help appreciated.
Thanks in advance.
You will have to handle this in your UITableViewController subclass, there isn't a way to do what you want to do in the storyboard.
Assuming the following:
The table view is fixed in portrait mode
The table view has only one section
Adding this method to your UITableViewController subclass will return a dynamic height based on the height of the table view:
(Swift)
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat{
let numCells = self.tableView(self.tableView, numberOfRowsInSection:0)
let cellHeight = self.tableView.frame.size.height / CGFloat(numCells)
return cellHeight
}
(Objective-C)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger numCells = [self tableView:self.tableView numberOfRowsInSection:0];
CGFloat cellHeight = self.tableView.frame.size.height / numCells;
return cellHeight;
}

Set NSTextField frame height in tableView viewForTableColumn not working

I have a .xib with 2-3 NSTextFields displayed in it (as shown below). I would like to resize one of these NSTextFields so it best fits the content it is displaying. My problem is that I cannot change the size of the Text Field in the viewForTableColumn at all.
I have successfully set my heightOfRow. That is working nicely.
I have turned off AutoLayout for the field as it wasn't quite working properly for me.
Now in tableView viewForTableColumn I have the following code:
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?
{
let cell = tableView.makeViewWithIdentifier("MessageView", owner: self) as! MessageView!
var newFrame = cell.messageSubject.frame
newFrame.size.height = 200
cell.messageSubject.frame = newFrame
return cell
}
As you can see the frame for the Subject field remains unaltered.
Interestingly the size of the frame is always the default size set in the Interface builder even when AutoLayout is turned on.
My conclusion is that I'm trying to do this in the wrong method.
I have been able to resize the frame in a simple non-table based viewController with no problem.

square table view cell constraint

i have a problem with auto layout. I have UIImageView inside a cell. The image is square and i set the cell height to be 320. but when i use iPad simulator, the cell height isn't change. it look like still use 320 height.
I've try to set Bottom Margin and Top margin to be < or equal to 12 and set the priority to 1000 which is make the image in the correct size but the cell height isn't change.
question :
how i can set the cell height based on the device?
i use Swift.
thanks.
You can set the constant of your constraint to your customized height.
Or use a custom cell height and then override the method func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat to return the customized height based on your device type. And set the top, bottom, leading and trailing values to its superview bounds.
The second solution is as follows: set your constraints to the bounds
Then use a custom height
And finally override the data source method
NBlet iPad = (UIDevice.currentDevice().userInterfaceIdiom == .Pad)

Achieving this layout for a viewcontroller using autolayout

I am working on a viewcontroller and I would like to try to achieve something like the picture below. I'd like to do this so it looks great on any device with regards to aspect ratio.
The top is a container, the middle is a collectionview, and the bottom is a uitableview.
What i'm trying to preserve is the aspect ratios. My thought to do this was the following:
For the first box, set the leading, trailing, and top margins to be to the container (guideline). Set the bottom one to be the box below (the larger middle box). Set the aspect ratio as well.
For the middle box, set the leading/trailing margins to the guidelines, and set the bottom to the box below. Also set the aspect ratio.
For the last box, set the leading, trailing, bottom (to the guideline) and also the aspect ratio.
I also set to pin widths equally
After doing this, it preserves my ratios correctly but it throws a ton of errors and warnings. Any ideas as to why this would be cranky at me? The crashing/warning report:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7f8a66031bc0 V:[UITableView:0x7f8a65837c00(73)]>",
"<NSLayoutConstraint:0x7f8a6605c150 UITableView:0x7f8a65837c00.width == 7.78082*UITableView:0x7f8a65837c00.height>",
"<NSLayoutConstraint:0x7f8a6604e970 UICollectionView:0x7f8a65838400.leading == UIView:0x7f8a66031eb0.leadingMargin>",
"<NSLayoutConstraint:0x7f8a6604e9c0 UICollectionView:0x7f8a65838400.trailing == UIView:0x7f8a66031eb0.trailingMargin>",
"<NSLayoutConstraint:0x7f8a6604ea10 UICollectionView:0x7f8a65838400.width == UITableView:0x7f8a65837c00.width>",
"<NSLayoutConstraint:0x7f8a63c4ccf0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7f8a66031eb0(320)]>"
)
Thanks so much!
Say, you want top view's height to be 20% of the main view and middle view's height to be 50% of the main view. You can do this programatically like this:
[topView setTranslatesAutoresizingMaskIntoConstraints: NO];
[middleView setTranslatesAutoresizingMaskIntoConstraints: NO];
[bottomView setTranslatesAutoresizingMaskIntoConstraints: NO];
NSDictionary *views = #{#"topView": topView, #"middleView": middleView, #"bottomView": bottomView};
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: #"H:|[topView]|" options: 0 metrics: nil views: views]];
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: #"H:|[middleView]|" options: 0 metrics: nil views: views]];
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: #"H:|[bottomView]|" options: 0 metrics: nil views: views]];
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: #"V:|[topView][middleView][bottomView]|" options: 0 metrics: nil views: views]];
[self.view addConstraint: [NSLayoutConstraint constraintWithItem: topView attribute: NSLayoutAttributeHeight relatedBy: NSLayoutRelationEqual toItem: self.view attribute: NSLayoutAttributeHeight multiplier: 0.2f constant: 0.0f]];
[self.view addConstraint: [NSLayoutConstraint constraintWithItem: middleView attribute: NSLayoutAttributeHeight relatedBy: NSLayoutRelationEqual toItem: self.view attribute: NSLayoutAttributeHeight multiplier: 0.5f constant: 0.0f]];
You need not set aspect height for the bottom view. You only need to pin the bottom view with the bottom edge of the main view.
If you want to do in Interface Builder, you can do this way:
For the top box, add 'Leading', 'Trailing' and 'Top' constraints to the superview. Also, add 'Equal Heights' constraint to the superview and modify the multiplier to the required value (Refer the last image).
For the middle box, add 'Leading' and 'Trailing' constraints to the superview. Add 'Top' constraint to the top box. Also, add 'Equal Heights' constraint to the superview and modify the multiplier to the required value.
For the last box, add 'Leading', 'Trailing' and 'Bottom' constraints to the superview. Add 'Top' constraint to the middle box.

Resources