Can't change the size of UISearchbar's text field - objective-c

I have a UISearchBar created in InterfaceBuilder and IBOutlet of it called userSearchBar. Now I want to change its width and height in code and can't do it. Here is my code:
for (UIView * const subview in userSearchBar.subviews) {
if ([subview isKindOfClass: NSClassFromString(#"UISearchBarBackground")]) {
[subview removeFromSuperview];
}
if ([subview isKindOfClass: [UITextField class]]) {
UITextField * const textField = (UITextField *)subview;
textField.textColor = [UIColor redColor];
textField.text = #"Enter text";
textField.frame = CGRectMake(0, 0, 250, 50);
}
}
All other changes (text, color, removing bar) works perfectly but changing the size not.

I've come up against exactly this myself, and I found the best way through was to write my own search bar from scratch, subclassing UIView. This gives me legitimate access to all the properties I might want to customise (such as background, colour when considering design or brand), rather than trying hack through like this. It's future proof, not that hard, and not hugely time consuming. It's a fun exercise and will deliver exactly what you want.

Related

change UISearchBar text field background image for iOS-7

I am specifically looking for an iOS-7 answer. If I had an image, the answer would be
[mySearchBar setSearchFieldBackgroundImage:myImage forState:UIControlStateNormal];
But I just want to apply a color. Is there a simple way of doing this with just a color? Again, for iOS-7.
Note: I see a host of very complicated answers here on SO: with traversing, etc. I hoping for something simple and that won't cause my app to be rejected.
If I understand correctly, you are looking for the barTintColor property. You can set the barTintColor property as such,
mySearchBar.barTintColor = [UIColor blackColor];
or
[mySearchBar setBarTintColor:[UIColor blackColor];
Hope that helps!
First you need to find the UITextField inside the search bar and then change its color. Here is the code to get the text field:
for (UIView *subView in searchBar.subviews)
{
for (UIView *secondLevelSubview in subView.subviews){
if ([secondLevelSubview isKindOfClass:[UITextField class]])
{
UITextField *searchBarTextField = (UITextField *)secondLevelSubview;
return searchBarTextField;
}
}
}
return nil;

UISearchBar text color change in iOS 7

How to change text color of UISearchBar in iOS 7?
In iOS 6, I was subclassing the UISearchBar and in layoutSubviews customising the properties of UITextField subview of UISearchBar.
But in iOS 7, UISearchBar doesn't have UITextField as its subview. How to fix this?
In iOS 7 to access Text Field you have to reiterate on level more. Change your code like this
for (UIView *subView in self.searchBar.subviews)
{
for (UIView *secondLevelSubview in subView.subviews){
if ([secondLevelSubview isKindOfClass:[UITextField class]])
{
UITextField *searchBarTextField = (UITextField *)secondLevelSubview;
//set font color here
searchBarTextField.textColor = [UIColor blackColor];
break;
}
}
}
Note : This is Not Public API
OR
You can use appearance Property of UIControls, Like
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setDefaultTextAttributes:#{NSForegroundColorAttributeName:[UIColor redColor]}];
Note: Appearance proxy can be used for iOS 9.0+
OutPut
You can set The tintcolor to apply to key elements in the search bar.
Use tintColor to tint foreground elements.
Use barTintColor to tint the bar background.
In iOS v7.0, all subclasses of UIView derive their behavior for tintColor from the base class. See the discussion of tintColor at the UIView level for more information.
Apple Doc
You can set the text colour by
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setTextColor:[UIColor blueColor]];
For XCode 6 (iOS8 SDK) the following DOESN'T work
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setTextColor:[UIColor redColor]];
But the following DOES work (for deployment to iOS7 and iOS8)
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setDefaultTextAttributes:#{NSForegroundColorAttributeName:[UIColor redColor]}];
While it's true that the UIAppearance protocol is a "public API," it's not true that UITextField supports this.
If you take a look at UITextField.h and look for the string "UI_APPEARANCE_SELECTOR" you'll see that it has no instances of this string. If you look at UIButton, you find quite a few - these are all of the properties that are officially supported by the UIAppearance API. It's somewhat well-known that UITextField is not supported by the UIAppearance API, so the code in Sandeep's answer will not always work and it's actually not the best approach.
This is a useful post with useful links: http://forums.xamarin.com/discussion/175/uitextfield-appearance
The correct approach is unfortunately messy - iterate through the subviews (or subviews of main subview for iOS7) and set it manually. Otherwise you will have unreliable results. But you can just create a category for UISearchBar and add a setTextColor:(UIColor*)color method. Example:
- (void)setTextColor:(UIColor*)color
{
for (UIView *v in self.subviews)
{
if([Environment isVersion7OrHigher]) //checks UIDevice#systemVersion
{
for(id subview in v.subviews)
{
if ([subview isKindOfClass:[UITextField class]])
{
((UITextField *)subview).textColor = color;
}
}
}
else
{
if ([v isKindOfClass:[UITextField class]])
{
((UITextField *)v).textColor = color;
}
}
}
}
Caution : This should lead to App Rejection!
KVC FTW. This did it for me.
UITextField *searchField = [self.searchBar valueForKey:#"_searchField"];
searchField.textColor = [UIColor redColor];
You can set the text attributes like so
[[UITextField appearanceWhenContainedIn:[<YOUR_CONTROLLER_NAME> class], nil] setDefaultTextAttributes:#{NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14.0]}];
Here is working example done in C# using Xamarin:
SearchBar = new UISearchBar ();
foreach (var subView in SearchBar.Subviews) {
foreach (var field in subView.Subviews) {
if (field is UITextField) {
UITextField textField = (UITextField)field;
textField.TextColor = UIColor.White;
}
}
}
Hope this helps someone.
This seems to be the correct answer https://stackoverflow.com/a/19315895/2493073
The Swift version of it is:
UITextField.appearanceWhenContainedInInstancesOfClasses([UISearchBar.self]).textColor = UIColor.blueColor()
This would only work for iOS 9.0, in order to make it work for lower versions you'll need to follow this question. https://stackoverflow.com/a/27807417/2493073
Swift Extension
public extension UISearchBar {
public func setTextColor(color: UIColor) {
let svs = subviews.flatMap { $0.subviews }
guard let tf = (svs.filter { $0 is UITextField }).first as? UITextField else { return }
tf.textColor = color
}
}
In my case, I have multiple UISearchBar objects and they need to change the textField font color. The appearanceWhenContainedIn update one UISearchBar behavior, but another doesn't.
I subclass the UISearchBar and implement custom -(id)initWithFrame: as following, and it works.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.searchBarStyle = UISearchBarStyleMinimal;
self.tintColor = [UIColor whiteColor];
self.barTintColor = [UIColor whiteColor];
[[UITextField appearanceForTraitCollection:self.traitCollection whenContainedIn:[self class], nil] setDefaultTextAttributes:
#{
NSForegroundColorAttributeName : [UIColor whiteColor]
}];
}
return self;
}
UIAppearance Protocol Reference said that,
In other words, the containment statement in
appearanceWhenContainedIn: is treated as a partial ordering. Given a
concrete ordering (actual subview hierarchy), UIKit selects the
partial ordering that is the first unique match when reading the
actual hierarchy from the window down.
So, appearanceWhenContainedIn: won't deal with all UISearchBar in the hierachy of UIWindow. And it suggests that.
Use the appearanceForTraitCollection: and
appearanceForTraitCollection:whenContainedIn: methods to retrieve the
proxy for a class with the specified trait collection.
The easiest way to do it is by putting this code in viewDidAppear or viewWillAppear:
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setDefaultTextAttributes:#{NSForegroundColorAttributeName:[UIColor whiteColor]}];
This works in iOS 8 and Xcode 6, unlike some of the other code. It can mess around with the font and text size, etc, but you can change that in the text attributes.
That changes the text colour for all search bars in your app. If you only want to change one, use the above code, and then in any other views with a search bar, use the same code but set the colour to whatever you want.
you can use search bar inside textfield
UISearchBar * searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 50, 320, 44) ];
searchBar.autocorrectionType = UITextAutocapitalizationTypeWords;
searchBar.delegate = self;
searchBar.searchBarStyle = UISearchBarStyleMinimal;
searchBar.barTintColor = [UIColor redColor];
[[UITextField appearanceWhenContainedIn:[searchBar class], nil]setDefaultTextAttributes:#{NSForegroundColorAttributeName:[UIColor whiteColor]}];
[self.view addSubview:searchBar];
Update in Swift 3
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).textColor = UIColor.black
Even though I would have preferred to use appearance API, it didn't work with iOS8. Here's the least hackish solution I did come with:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (self.shouldEnableSearchBar)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
UITextField *searchBarTextField = [[AFPBaseViewController findSubviewsOfView:self.searchBar ofClass:[UITextField class]] firstObject];
searchBarTextField.textColor = AFPConstantsColorGold;
});
}
}
You could maybe even create a UIView category with this. The reason why this has to be called in viewDidAppear is that UISearchBar is actually contained in a ViewController, and doesn't load all its subviews until it has appeared on screen. It could be added into viewWillAppear too, but I haven't tested it.
+ (NSArray *)findSubviewsOfView:(UIView *)view ofClass:(Class)class
{
NSMutableArray *targetSubviews = [NSMutableArray new];
for (id subview in view.subviews)
{
if ([subview isKindOfClass:class])
{
[targetSubviews addObject:subview];
}
if ([subview subviews].count)
{
[targetSubviews addObjectsFromArray:[self findSubviewsOfView:subview ofClass:class]];
}
}
return targetSubviews.copy;
}
This is the right solution for iOS8:
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setDefaultTextAttributes:#{NSFontAttributeName: [UIFont fontWithName:#"Helvetica" size:14], NSForegroundColorAttributeName:[UIColor lightGrayColor]}];
You have to set the font as well, otherwise, the font size will be wrong.
This class will give you full control over every item in the UISearchBar
import UIKit
class SMTSearchBar: UISearchBar {
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
initialize()
}
convenience init() {
self.init(frame: CGRectZero)
initialize()
}
// Style the view
func initialize() {
// Search Area
let searchField = valueForKey("searchField") as! UITextField
searchField.textColor = Colors.White
searchField.font = UIFont(name: Fonts.MuseoSans500, size: 16)
searchField.backgroundColor = Colors.Black.colorWithAlphaComponent(0.1)
// Icons
let searchIcon = UIImage(named: Icons.Search)?.imageWithTint(Colors.White)
let smallClearIconNormal = UIImage(named: Icons.SmallClear)?.imageWithTint(Colors.White)
let smallClearIconHighLight = UIImage(named: Icons.SmallClear)?.imageWithTint(Colors.White.colorWithAlphaComponent(0.5))
setImage(searchIcon, forSearchBarIcon: .Search, state: .Normal)
setImage(smallClearIconHighLight, forSearchBarIcon: .Clear, state: .Highlighted)
setImage(smallClearIconNormal, forSearchBarIcon: .Clear, state: .Normal)
}
func setPlaceHolder(placeholder: String) {
for subView in subviews{
for subsubView in subView.subviews {
if let textField = subsubView as? UITextField {
textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSForegroundColorAttributeName: Colors.White.colorWithAlphaComponent(0.5)])
}
}
}
}
}
Usage (in navigation bar)
let searchBar:SMTSearchBar = SMTSearchBar()
searchBar.sizeToFit()
searchBar.setPlaceHolder("Search for cool things")
navigationItem.titleView = searchBar
searchBar.becomeFirstResponder()

How to increase the height of NSTableHeaderView?

I need to implement a headerview with specific size and gradient. I have to insert images in certain cells of the headerview.Tried to create the cells for the headerview using the following code,but i was not able to customize the headerview.
[[tableColumn headerCell] setImage:[NSImage imageNamed:#"sampleHeader"]];
If I use the overridden subclass of headerview, I was not able to view the images or text in the header cell.Please provide me any pointers to solve this issue.
I was able to insert images and text by subclassing the NSTableHeaderCell.How to increase height of the NSTableHeaderView?
If I subclass both NSTableHeaderView and NSTableHeaderCell , was not able to view anything in the
headercell.I used the following code for setting headerview and headercell
[tableView setHeaderView:CustomHeaderView];
[tableColumn setHeaderCell:[[[CustomHeaderTableCell alloc] initImageCell:
[NSImage imageNamed:#"sample"]]autorelease]];
I have the same issue as given in the below url
http://lists.apple.com/archives/cocoa-dev/2002/Jun/msg00331.html
You don't need to subclass NSTableHeaderView.
I was able to change the height of the header view using the following snippet in the controller class:
-(void)awakeFromNib {
NSRect frame = tableView.headerView.frame;
frame.size.height = 26;
tableView.headerView.frame = frame;
}
It should be noted that the scroll view takes care of the layout. It automatically changes the frame of the headerView as necessary, but leaves the height intact. Resizing the clip view etc as suggested in the other answer is not necessary.
You can also create a NSTableHeaderView object, initialize it with a frame(rect with height and width) and set that NSTableHeaderView object to your table view.
NSTableHeaderView *tableHeaderView = [[NSTableHeaderView alloc] initWithFrame:NSMakeRect(0, 0, 120, 60)];
[myTableView setHeaderView:tableHeaderView];
[tableHeaderView release];
Following link helped me in solving the issue.
http://lists.apple.com/archives/cocoa-dev/2003/Feb/msg00676.html
You need to set the Frame for NSClipView, NSTableHeaderView and the CornerView
This is how I implemented the same in Code.
for(NSView * subview in [topScrollView subviews])
{
for(NSView * subSubView in [subview subviews])
{
if([[subSubView className] isEqualToString:#"NSTableHeaderView"] && [[subview className] isEqualToString:#"NSClipView"])
{
[subSubView setFrameSize:NSMakeSize(subSubView.frame.size.width, subSubView.frame.size.height+5)];//HeaderView Frame
[subview setFrameSize:NSMakeSize(subview.frame.size.width, subview.frame.size.height+5)];//ClipView Frame
}
}
if ([[subview className] isEqualToString:#"_NSCornerView"])
{
[subview setFrameSize:NSMakeSize(subview.frame.size.width, subview.frame.size.height+5)]; //CornerView Frame
}
}

View-based NSTableView with rows that have dynamic heights

I have an application with a view-based NSTableView in it. Inside this table view, I have rows that have cells that have content consisting of a multi-row NSTextField with word-wrap enabled. Depending on the textual content of the NSTextField, the size of the rows needed to display the cell will vary.
I know that I can implement the NSTableViewDelegate method -tableView:heightOfRow: to return the height, but the height will be determined based on the word wrapping used on the NSTextField. The word wrapping of the NSTextField is similarly based on how wide the NSTextField is… which is determined by the width of the NSTableView.
Soooo… I guess my question is… what is a good design pattern for this? It seems like everything I try winds up being a convoluted mess. Since the TableView requires knowledge of the height of the cells to lay them out... and the NSTextField needs knowledge of it's layout to determine the word wrap… and the cell needs knowledge of the word wrap to determine it's height… it's a circular mess… and it's driving me insane.
Suggestions?
If it matters, the end result will also have editable NSTextFields that will resize to adjust to the text within them. I already have this working on the view level, but the tableview does not yet adjust the heights of the cells. I figure once I get the height issue worked out, I'll use the -noteHeightOfRowsWithIndexesChanged method to inform the table view the height changed… but it's still then going to ask the delegate for the height… hence, my quandry.
This is a chicken and the egg problem. The table needs to know the row height because that determines where a given view will lie. But you want a view to already be around so you can use it to figure out the row height. So, which comes first?
The answer is to keep an extra NSTableCellView (or whatever view you are using as your "cell view") around just for measuring the height of the view. In the tableView:heightOfRow: delegate method, access your model for 'row' and set the objectValue on NSTableCellView. Then set the view's width to be your table's width, and (however you want to do it) figure out the required height for that view. Return that value.
Don't call noteHeightOfRowsWithIndexesChanged: from in the delegate method tableView:heightOfRow: or viewForTableColumn:row: ! That is bad, and will cause mega-trouble.
To dynamically update the height, then what you should do is respond to the text changing (via the target/action) and recalculate your computed height of that view. Now, don't dynamically change the NSTableCellView's height (or whatever view you are using as your "cell view"). The table must control that view's frame, and you will be fighting the tableview if you try to set it. Instead, in your target/action for the text field where you computed the height, call noteHeightOfRowsWithIndexesChanged:, which will let the table resize that individual row. Assuming you have your autoresizing mask setup right on subviews (i.e.: subviews of the NSTableCellView), things should resize fine! If not, first work on the resizing mask of the subviews to get things right with variable row heights.
Don't forget that noteHeightOfRowsWithIndexesChanged: animates by default. To make it not animate:
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[tableView noteHeightOfRowsWithIndexesChanged:indexSet];
[NSAnimationContext endGrouping];
PS: I respond more to questions posted on the Apple Dev Forums than stack overflow.
PSS: I wrote the view based NSTableView
This got a lot easier in macOS 10.13 with .usesAutomaticRowHeights. The details are here: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (In the section titled "NSTableView Automatic Row Heights").
Basically you just select your NSTableView or NSOutlineView in the storyboard editor and select this option in the Size Inspector:
Then you set the stuff in your NSTableCellView to have top and bottom constraints to the cell and your cell will resize to fit automatically. No code required!
Your app will ignore any heights specified in heightOfRow (NSTableView) and heightOfRowByItem (NSOutlineView). You can see what heights are getting calculated for your auto layout rows with this method:
func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
print(rowView.fittingSize.height)
}
Based on Corbin's answer (btw thanks shedding some light on this):
Swift 3, View-Based NSTableView with Auto-Layout for macOS 10.11 (and above)
My setup: I have a NSTableCellView that is laid out using Auto-Layout. It contains (besides other elements) a multi-line NSTextField that can have up to 2 rows. Therefore, the height of the whole cell view depends on the height of this text field.
I update tell the table view to update the height on two occasions:
1) When the table view resizes:
func tableViewColumnDidResize(_ notification: Notification) {
let allIndexes = IndexSet(integersIn: 0..<tableView.numberOfRows)
tableView.noteHeightOfRows(withIndexesChanged: allIndexes)
}
2) When the data model object changes:
tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)
This will cause the table view to ask it's delegate for the new row height.
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
// Get data object for this row
let entity = dataChangesController.entities[row]
// Receive the appropriate cell identifier for your model object
let cellViewIdentifier = tableCellViewIdentifier(for: entity)
// We use an implicitly unwrapped optional to crash if we can't create a new cell view
var cellView: NSTableCellView!
// Check if we already have a cell view for this identifier
if let savedView = savedTableCellViews[cellViewIdentifier] {
cellView = savedView
}
// If not, create and cache one
else if let view = tableView.make(withIdentifier: cellViewIdentifier, owner: nil) as? NSTableCellView {
savedTableCellViews[cellViewIdentifier] = view
cellView = view
}
// Set data object
if let entityHandler = cellView as? DataEntityHandler {
entityHandler.update(with: entity)
}
// Layout
cellView.bounds.size.width = tableView.bounds.size.width
cellView.needsLayout = true
cellView.layoutSubtreeIfNeeded()
let height = cellView.fittingSize.height
// Make sure we return at least the table view height
return height > tableView.rowHeight ? height : tableView.rowHeight
}
First, we need to get our model object for the row (entity) and the appropriate cell view identifier. We then check if we have already created a view for this identifier. To do that we have to maintain a list with cell views for each identifier:
// We need to keep one cell view (per identifier) around
fileprivate var savedTableCellViews = [String : NSTableCellView]()
If none is saved, we need to created (and cache) a new one. We update the cell view with our model object and tell it to re-layout everything based on the current table view width. The fittingSize height can then be used as the new height.
For anyone wanting more code, here is the full solution I used. Thanks corbin dunn for pointing me in the right direction.
I needed to set the height mostly in relation to how high a NSTextView in my NSTableViewCell was.
In my subclass of NSViewController I temporary create a new cell by calling outlineView:viewForTableColumn:item:
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
NSTableColumn *tabCol = [[outlineView tableColumns] objectAtIndex:0];
IBAnnotationTableViewCell *tableViewCell = (IBAnnotationTableViewCell*)[self outlineView:outlineView viewForTableColumn:tabCol item:item];
float height = [tableViewCell getHeightOfCell];
return height;
}
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
IBAnnotationTableViewCell *tableViewCell = [outlineView makeViewWithIdentifier:#"AnnotationTableViewCell" owner:self];
PDFAnnotation *annotation = (PDFAnnotation *)item;
[tableViewCell setupWithPDFAnnotation:annotation];
return tableViewCell;
}
In my IBAnnotationTableViewCell which is the controller for my cell (subclass of NSTableCellView) I have a setup method
-(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation;
which sets up all outlets and sets the text from my PDFAnnotations. Now I can "easily" calcutate the height using:
-(float)getHeightOfCell
{
return [self getHeightOfContentTextView] + 60;
}
-(float)getHeightOfContentTextView
{
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[self.contentTextView font],NSFontAttributeName,nil];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:[self.contentTextView string] attributes:attributes];
CGFloat height = [self heightForWidth: [self.contentTextView frame].size.width forString:attributedString];
return height;
}
.
- (NSSize)sizeForWidth:(float)width height:(float)height forString:(NSAttributedString*)string
{
NSInteger gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
NSSize answer = NSZeroSize ;
if ([string length] > 0) {
// Checking for empty string is necessary since Layout Manager will give the nominal
// height of one line if length is 0. Our API specifies 0.0 for an empty string.
NSSize size = NSMakeSize(width, height) ;
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:size] ;
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:string] ;
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init] ;
[layoutManager addTextContainer:textContainer] ;
[textStorage addLayoutManager:layoutManager] ;
[layoutManager setHyphenationFactor:0.0] ;
if (gNSStringGeometricsTypesetterBehavior != NSTypesetterLatestBehavior) {
[layoutManager setTypesetterBehavior:gNSStringGeometricsTypesetterBehavior] ;
}
// NSLayoutManager is lazy, so we need the following kludge to force layout:
[layoutManager glyphRangeForTextContainer:textContainer] ;
answer = [layoutManager usedRectForTextContainer:textContainer].size ;
// Adjust if there is extra height for the cursor
NSSize extraLineSize = [layoutManager extraLineFragmentRect].size ;
if (extraLineSize.height > 0) {
answer.height -= extraLineSize.height ;
}
// In case we changed it above, set typesetterBehavior back
// to the default value.
gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
}
return answer ;
}
.
- (float)heightForWidth:(float)width forString:(NSAttributedString*)string
{
return [self sizeForWidth:width height:FLT_MAX forString:string].height ;
}
I was looking for a solution for quite some time and came up with the following one, which works great in my case:
- (double)tableView:(NSTableView *)tableView heightOfRow:(long)row
{
if (tableView == self.tableViewTodo)
{
CKRecord *record = [self.arrayTodoItemsFiltered objectAtIndex:row];
NSString *text = record[#"title"];
double someWidth = self.tableViewTodo.frame.size.width;
NSFont *font = [NSFont fontWithName:#"Palatino-Roman" size:13.0];
NSDictionary *attrsDictionary =
[NSDictionary dictionaryWithObject:font
forKey:NSFontAttributeName];
NSAttributedString *attrString =
[[NSAttributedString alloc] initWithString:text
attributes:attrsDictionary];
NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
[[tv textStorage] setAttributedString:attrString];
[tv setHorizontallyResizable:NO];
[tv sizeToFit];
double height = tv.frame.size.height + 20;
return height;
}
else
{
return 18;
}
}
Since I use custom NSTableCellView and I have access to the NSTextField my solution was to add a method on NSTextField.
#implementation NSTextField (IDDAppKit)
- (CGFloat)heightForWidth:(CGFloat)width {
CGSize size = NSMakeSize(width, 0);
NSFont* font = self.font;
NSDictionary* attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSRect bounds = [self.stringValue boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:attributesDictionary];
return bounds.size.height;
}
#end
Have you had a look at RowResizableViews? It is quite old and I haven't tested it but it may nevertheless work.
Here's what I have done to fix it:
Source: Look into XCode documentation, under "row height nstableview". You'll find a sample source code named "TableViewVariableRowHeights/TableViewVariableRowHeightsAppDelegate.m"
(Note: I'm looking at column 1 in table view, you'll have to tweak to look elsewhere)
in Delegate.h
IBOutlet NSTableView *ideaTableView;
in Delegate.m
table view delegates control of row height
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row {
// Grab the fully prepared cell with our content filled in. Note that in IB the cell's Layout is set to Wraps.
NSCell *cell = [ideaTableView preparedCellAtColumn:1 row:row];
// See how tall it naturally would want to be if given a restricted with, but unbound height
CGFloat theWidth = [[[ideaTableView tableColumns] objectAtIndex:1] width];
NSRect constrainedBounds = NSMakeRect(0, 0, theWidth, CGFLOAT_MAX);
NSSize naturalSize = [cell cellSizeForBounds:constrainedBounds];
// compute and return row height
CGFloat result;
// Make sure we have a minimum height -- use the table's set height as the minimum.
if (naturalSize.height > [ideaTableView rowHeight]) {
result = naturalSize.height;
} else {
result = [ideaTableView rowHeight];
}
return result;
}
you also need this to effect the new row height (delegated method)
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
[ideaTableView reloadData];
}
I hope this helps.
Final note: this does not support changing column width.
Here is a solution based of JanApotheker's answer, modified as cellView.fittingSize.height was not returning the correct height for me. In my case I am using the standard NSTableCellView, an NSAttributedString for the cell's textField text, and a single column table with constraints for the cell's textField set in IB.
In my view controller, I declare:
var tableViewCellForSizing: NSTableCellView?
In viewDidLoad():
tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView
Finally, for the tableView delegate method:
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let tableCellView = tableViewCellForSizing else { return minimumCellHeight }
tableCellView.textField?.attributedStringValue = attributedString[row]
if let height = tableCellView.textField?.fittingSize.height, height > 0 {
return height
}
return minimumCellHeight
}
mimimumCellHeight is a constant set to 30, for backup, but never actually used. attributedStrings is my model array of NSAttributedString.
This works perfectly for my needs. Thanks for all the previous answers, which pointed me in the right direction for this pesky problem.
This sounds a lot like something I had to do previously. I wish I could tell you that I came up with a simple, elegant solution but, alas, I did not. Not for lack of trying though. As you have already noticed the need of UITableView to know the height prior to the cells being built really make it all seem quite circular.
My best solution was to push logic to the cell, because at least I could isolate what class needed to understand how the cells were laid out. A method like
+ (CGFloat) heightForStory:(Story*) story
would be able to determine how tall the cell had to be. Of course that involved measuring text, etc. In some cases I devised ways to cache information gained during this method that could then be used when the cell was created. That was the best I came up with. It is an infuriating problem though as it seems there should be a better answer.

Why do all backgrounds disappear on UITableViewCell select?

My current project's UITableViewCell behavior is baffling me. I have a fairly straightforward subclass of UITableViewCell. It adds a few extra elements to the base view (via [self.contentView addSubview:...] and sets background colors on the elements to have them look like black and grey rectangular boxes.
Because the background of the entire table has this concrete-like texture image, each cell's background needs to be transparent, even when selected, but in that case it should darken a bit. I've set a custom semi-transparent selected background to achieve this effect:
UIView *background = [[[UIView alloc] initWithFrame:self.bounds] autorelease];
background.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
background.opaque = NO;
[self setSelectedBackgroundView:background];
And although that yields the right look for the background, a weird side effect happens when I select the cell; all other backgrounds are somehow turnt off. Here's a screenshot. The bottom cell looks like it should and is not selected. The top cell is selected, but it should display the black and grey rectangular areas, yet they are gone!
Who knows what's going on here and even more important: how can I correct this?
What is happening is that each subview inside the TableViewCell will receive the setSelected and setHighlighted methods. The setSelected method will remove background colors but if you set it for the selected state it will be corrected.
For example if those are UILabels added as subviews in your customized cell, then you can add this to the setSelected method of your TableViewCell implementation code:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
self.textLabel.backgroundColor = [UIColor blackColor];
}
where self.textLabel would be whatever those labels are that are shown in the picture above
I'm not sure where your adding your selected view, I usually add it in the setSelected method.
Alternatively, you can subclass the UILabel and override the setHighlighted method like so:
-(void)setHighlighted:(BOOL)highlighted
{
[self setBackgroundColor:[UIColor blackColor]];
}
The cell highlighting process can seem complex and confusing if you don't know whats going on. I was thoroughly confused and did some extensive experimentation. Here's the notes on my findings that may help somebody (if anyone has anything to add to this or refute then please comment and I will endeavour to confirm and update)
In the normal “not selected” state
The contentView (whats in your XIB unless you coded it otherwise) is drawn normally
The selectedBackgroundView is HIDDEN
The backgroundView is visible (so provided your contentView is transparent you see the backgroundView or (if you have not defined a backgroundView you'll see the background colour of the UITableView itself)
A cell is selected, the following occurs immediately with-OUT any animation:
All views/subviews within the contentView have their backgroundColor cleared (or set to transparent), label etc text color's change to their selected colour
The selectedBackgroundView becomes visible (this view is always the full size of the cell (a custom frame is ignored, use a subview if you need to). Also note the backgroundColor of subViews are not displayed for some reason, perhaps they're set transparent like the contentView). If you didn't define a selectedBackgroundView then Cocoa will create/insert the blue (or gray) gradient background and display this for you)
The backgroundView is unchanged
When the cell is deselected, an animation to remove the highlighting starts:
The selectedBackgroundView alpha property is animated from 1.0 (fully opaque) to 0.0 (fully transparent).
The backgroundView is again unchanged (so the animation looks like a crossfade between selectedBackgroundView and backgroundView)
ONLY ONCE the animation has finished does the contentView get redrawn in the "not-selected" state and its subview backgroundColor's become visible again (this can cause your animation to look horrible so it is advisable that you don't use UIView.backgroundColor in your contentView)
CONCLUSIONS:
If you need a backgroundColor to persist through out the highlight animation, don't use the backgroundColor property of UIView instead you can try (probably with-in tableview:cellForRowAtIndexPath:):
A CALayer with a background color:
UIColor *bgColor = [UIColor greenColor];
CALayer* layer = [CALayer layer];
layer.frame = viewThatRequiresBGColor.bounds;
layer.backgroundColor = bgColor.CGColor;
[cell.viewThatRequiresBGColor.layer addSublayer:layer];
or a CAGradientLayer:
UIColor *startColor = [UIColor redColor];
UIColor *endColor = [UIColor purpleColor];
CAGradientLayer* gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = viewThatRequiresBGColor.bounds;
gradientLayer.colors = #[(id)startColor.CGColor, (id)endColor.CGColor];
gradientLayer.locations = #[[NSNumber numberWithFloat:0],[NSNumber numberWithFloat:1]];
[cell.viewThatRequiresBGColor.layer addSublayer:gradientLayer];
I've also used a CALayer.border technique to provide a custom UITableView seperator:
// We have to use the borderColor/Width as opposed to just setting the
// backgroundColor else the view becomes transparent and disappears during
// the cell's selected/highlighted animation
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(0, 43, 1024, 1)];
separatorView.layer.borderColor = [UIColor redColor].CGColor;
separatorView.layer.borderWidth = 1.0;
[cell.contentView addSubview:separatorView];
When you start dragging a UITableViewCell, it calls setBackgroundColor: on its subviews with a 0-alpha color. I worked around this by subclassing UIView and overriding setBackgroundColor: to ignore requests with 0-alpha colors. It feels hacky, but it's cleaner than any of the other solutions I've come across.
#implementation NonDisappearingView
-(void)setBackgroundColor:(UIColor *)backgroundColor {
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
if (alpha != 0) {
[super setBackgroundColor:backgroundColor];
}
}
#end
Then, I add a NonDisappearingView to my cell and add other subviews to it:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"cell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
UIView *background = [cell viewWithTag:backgroundTag];
if (background == nil) {
background = [[NonDisappearingView alloc] initWithFrame:backgroundFrame];
background.tag = backgroundTag;
background.backgroundColor = backgroundColor;
[cell addSubview:background];
}
// add other views as subviews of background
...
}
return cell;
}
Alternatively, you could make cell.contentView an instance of NonDisappearingView.
My solution is saving the backgroundColor and restoring it after the super call.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
UIColor *bgColor = self.textLabel.backgroundColor;
[super setSelected:selected animated:animated];
self.textLabel.backgroundColor = bgColor;
}
You also need to do the same thing with -setHighlighted:animated:.
Found a pretty elegant solution instead of messing with the tableView methods. You can create a subclass of UIView that ignores setting its background color to clear color. Code:
class NeverClearView: UIView {
override var backgroundColor: UIColor? {
didSet {
if UIColor.clearColor().isEqual(backgroundColor) {
backgroundColor = oldValue
}
}
}
}
Obj-C version would be similar, the main thing here is the idea
I created a UITableViewCell category/extension that allows you to turn on and off this transparency "feature".
You can find KeepBackgroundCell on GitHub
Install it via CocoaPods by adding the following line to your Podfile:
pod 'KeepBackgroundCell'
Usage:
Swift
let cell = <Initialize Cell>
cell.keepSubviewBackground = true // Turn transparency "feature" off
cell.keepSubviewBackground = false // Leave transparency "feature" on
Objective-C
UITableViewCell* cell = <Initialize Cell>
cell.keepSubviewBackground = YES; // Turn transparency "feature" off
cell.keepSubviewBackground = NO; // Leave transparency "feature" on
Having read through all the existing answers, came up with an elegant solution using Swift by only subclassing UITableViewCell.
extension UIView {
func iterateSubViews(block: ((view: UIView) -> Void)) {
for subview in self.subviews {
block(view: subview)
subview.iterateSubViews(block)
}
}
}
class CustomTableViewCell: UITableViewCell {
var keepSubViewsBackgroundColorOnSelection = false
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
// MARK: Overrides
override func setSelected(selected: Bool, animated: Bool) {
if self.keepSubViewsBackgroundColorOnSelection {
var bgColors = [UIView: UIColor]()
self.contentView.iterateSubViews() { (view) in
guard let bgColor = view.backgroundColor else {
return
}
bgColors[view] = bgColor
}
super.setSelected(selected, animated: animated)
for (view, backgroundColor) in bgColors {
view.backgroundColor = backgroundColor
}
} else {
super.setSelected(selected, animated: animated)
}
}
override func setHighlighted(highlighted: Bool, animated: Bool) {
if self.keepSubViewsBackgroundColorOnSelection {
var bgColors = [UIView: UIColor]()
self.contentView.iterateSubViews() { (view) in
guard let bgColor = view.backgroundColor else {
return
}
bgColors[view] = bgColor
}
super.setHighlighted(highlighted, animated: animated)
for (view, backgroundColor) in bgColors {
view.backgroundColor = backgroundColor
}
} else {
super.setHighlighted(highlighted, animated: animated)
}
}
}
All we need is to override the setSelected method and change the selectedBackgroundView for the tableViewCell in the custom tableViewCell class.
We need to add the backgroundview for the tableViewCell in cellForRowAtIndexPath method.
lCell.selectedBackgroundView = [[UIView alloc] init];
Next I have overridden the setSelected method as mentioned below.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
UIImageView *lBalloonView = [self viewWithTag:102];
[lBalloonView setBackgroundColor:[[UIColor hs_globalTint] colorWithAlphaComponent:0.2]];
UITextView *lMessageTextView = [self viewWithTag:103];
lMessageTextView.backgroundColor = [UIColor clearColor];
UILabel *lTimeLabel = [self viewWithTag:104];
lTimeLabel.backgroundColor = [UIColor clearColor];
}
Also one of the most important point to be noted is to change the tableViewCell selection style. It should not be UITableViewCellSelectionStyleNone.
lTableViewCell.selectionStyle = UITableViewCellSelectionStyleGray;