I just want to make sure that the UITableView doesn't crash if there is an invalid indexPath.
You can do it by checking both section and row of indexPath is valid.
NSIndexPath* indexPath = YOUR_INDEX_PATH;
// If |isValid| is true, |indexPath| is valid, if not, |indexPath| is invalid
BOOL isValid = [TABLE_VIEW numberOfSections] > indexPath.section &&
[TABLE_VIEW numberOfRowsInSection:indexPath.section] > indexPath.row;
if (isValid) {
NSLog(#"Valid"); // Do whatever you want if |indexPath| is valid
} else {
NSLog(#"Not valid");
}
You could add a test to your view controller.
- (BOOL)isValidIndexPath:(NSIndexPath *)indexPath {
return (indexPath.section < self.tableView.numberOfSections &&
indexPath.row < [self.tableView numberOfRowsInSection:indexPath.section]);
}
Then when you need to check the index path:
if ([self isValidIndexPath:indexPath]) {
...
}
It would be even better as a category, so all table views and controllers could use it.
#interface UITableView (IndexPathTest)
- (BOOL)isValidIndexPath:(NSIndexPath *)indexPath;
#end
#implementation UITableView (IndexPathTest)
- (BOOL)isValidIndexPath:(NSIndexPath *)indexPath {
return (indexPath.section < self.numberOfSections &&
indexPath.row < [self numberOfRowsInSection:indexPath.section]);
}
#end
Then for any table view controller:
if ([self.tableView isValidIndexPath:indexPath]) {
...
}
So In my project I have one HistoryTableViewController which contains 2 sections. One section is the PeopleYouOwe and the other section is a group of PeopleWhoOweYou. How would I be able to hook up this one tableviewcontroller and link it to a detail view that will display different data according to my sections?
Try below approach using prepareForSegue -
creat tableView outlet named myTableView
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
NSIndexPath *path = [self.myTableView indexPathForSelectedRow];
if (path.section == 0)
{
if ([segue.identifier isEqualToString:#"PeopleYouOwe"])
{
PeopleYouOwe *peopleYouOweVC = segue.destinationViewController;
// you can get data of cell as array[path.row]
}
}
else if (path.section == 1)
{
if ([segue.identifier isEqualToString:#"PeopleWhoOweYou"])
{
PeopleWhoOweYou *peopleWhoOweYou = segue.destinationViewController;
// you can get data of cell as array[path.row]
}
}
}
You use the delegate methods provided with UITableViews like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger row = indexPath.row;
NSInteger section = indexPath.section;
if(section == 0) // example
{
// do whatever
}
else if (section == 1)
{
// do something else
}
}
If this is not what you're looking for you will need to provide more details and ideally some code to show what you already have. Your question is vague.
If you prefer prepareForSegue, you can use something like:
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
then take action based on section and row.
I am using titleForHeaderInSection to show a header for a UITableView section. It worked fine with the iOS6 SDK, but the iOS7 SDK shows the header in all CAPS.
I guess it's part of Apple's updated Human Interface Guidelines; all examples in there show headers in all caps. Also, all section headers in Settings on my iPhone are in all caps.
However, I wonder if there is a way around that. Normally, I wouldn't mind showing caps if that improves consistency, but when I need to show people's names in a section header, it's a bit awkward.
Anybody any idea how to get around to capitalization?
Yes, we had very much a similar problem to this, and my own solution was as follows:
The Apple UITableViewHeaderFooterView documentation (the link to it is irritatingly long but you can find it easily with your favourite search engine) says you can access the textLabel of the header view without having to format your own view via viewForHeaderInSection method.
textLabel A primary text label for the view. (read-only)
#property(nonatomic, readonly, retain) UILabel *textLabel Discussion
Accessing the value in this property causes the view to create a
default label for displaying a detail text string. If you are managing
the content of the view yourself by adding subviews to the contentView
property, you should not access this property.
The label is sized to fit the content view area in the best way
possible based on the size of the string. Its size is also adjusted
depending on whether there is a detail text label present.
With some additional searching the best place to modify the label text was the willDisplayHeaderView method (suggested in How to implement `viewForHeaderInSection` on iOS7 style?).
So, the solution I came up with is pretty simple and just does a transformation of the text label string, after it's actually been set by titleForHeaderInSection:
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
//I have a static list of section titles in SECTION_ARRAY for reference.
//Obviously your own section title code handles things differently to me.
return [SECTION_ARRAY objectAtIndex:section];
}
and then simply call the willDisplayHeaderView method to modify how it looks:
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
if([view isKindOfClass:[UITableViewHeaderFooterView class]]){
UITableViewHeaderFooterView *tableViewHeaderFooterView = (UITableViewHeaderFooterView *) view;
tableViewHeaderFooterView.textLabel.text = [tableViewHeaderFooterView.textLabel.text capitalizedString];
}
}
You can put in your own 'if' or 'switch' clauses in there as the section number is also passed to the method, so hopefully it'll allow you to show your user/client name in capitalised words selectively.
solution which i found is add Title in "titleForHeaderInSection" method
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return #"Table Title";
}
and then call the willDisplayHeaderView method to Update :
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
UITableViewHeaderFooterView *header = (UITableViewHeaderFooterView *)view;
header.textLabel.textColor = [UIColor darkGrayColor];
header.textLabel.font = [UIFont boldSystemFontOfSize:18];
CGRect headerFrame = header.frame;
header.textLabel.frame = headerFrame;
header.textLabel.text= #"Table Title";
header.textLabel.textAlignment = NSTextAlignmentLeft;
}
In Swift,
override func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let headerView = view as! UITableViewHeaderFooterView
headerView.textLabel.text = "My_String"
}
If you want to just keep the string as it is, you could simply do this:
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section {
if ([view isKindOfClass:[UITableViewHeaderFooterView class]] && [self respondsToSelector:#selector(tableView:titleForHeaderInSection:)]) {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.textLabel.text = [self tableView:tableView titleForHeaderInSection:section];
}
}
Swift solution:
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
if let headerView = view as? UITableViewHeaderFooterView, self.respondsToSelector(Selector("tableView:titleForHeaderInSection:")) {
headerView.textLabel?.text = self.tableView(tableView, titleForHeaderInSection: section)
}
}
One solution I found is to utilize UITableViewHeaderFooterView.
Instead of
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return #"some title";
}
Do
- (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
static NSString *identifier = #"defaultHeader";
UITableViewHeaderFooterView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier];
if (!headerView) {
headerView = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:identifier];
}
headerView.textLabel.text = #"some title";
return headerView;
}
The annoying downside is that the table view will no longer automatically adjust the section header height for you. So if your header heights varies, you'll have to do something like this:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
id headerAppearance = [UILabel appearanceWhenContainedIn:[UITableViewHeaderFooterView class], nil];
UIFont *font = [headerAppearance font];
CGFloat viewWidth = CGRectGetWidth(tableView.frame);
CGFloat xInset = 10;
CGFloat yInset = 5;
CGFloat labelWidth = viewWidth - xInset * 2;
CGSize size = [sectionInfo.name sizeWithFont:font constrainedToSize:CGSizeMake(labelWidth, MAXFLOAT)];
return size.height + yInset * 2;
}
I really don't like hard-coding layout information (the inset) this way, as it might break in the future version. If anyone has a better solution to get/set the header height, I'm all ears.
Thanks #Animal451. This is more generic solution that would work with any type of header string.
// We need to return the actual text so the header height gets correctly calculated
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerString;
}
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
UITableViewHeaderFooterView *header = (UITableViewHeaderFooterView *)view;
[header.textLabel setText:self.headerString]; //String in the header becomes uppercase. Workaround to avoid that.
}
To avoid duplication the header string can be declared anywhere else. I have done it in the -viewDidLoad method.
In addition to #Animal451's post.
For swift 3 you can use
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard section == 0 ,
let tableViewHeaderFooterView = view as? UITableViewHeaderFooterView
else { return }
tableViewHeaderFooterView.textLabel?.text = "Your awesome string"
}
And then ignore - titleForHeaderInSection:
Keep in mind that this code is for 1st section only. If you want to go through all of your sections, you'll need to add support for them
Swift 5
Static TableViewController & storyboard section title.
The below code will work the first letter capitalized.
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let titleView = view as! UITableViewHeaderFooterView
titleView.textLabel?.text = titleView.textLabel?.text?.capitalized
}
Simplest Solution for was below: Swift 2.x
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
header.textLabel?.text = header.textLabel?.text?.capitalizedString
}
Swift 3.x:
if let headerView = view as? UITableViewHeaderFooterView {
headerView.textLabel?.text? = headerView.textLabel?.text?.capitalized ?? ""
}
The solution which worked for me was to create a simple UILabel Instance Variable to use as the section header view. (Your needs may be different, but for me, I only needed to have text in my header...)
Then, inside viewForHeaderInSection, I set up the label as required, and return this label as the header view.
Then, whenever I want to update the text in the header, I simply change the label's text directly.
In the .h file:
#property (nonatomic, retain) UILabel *sectionHeaderLabel;
then in the .m file:
- (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
return [self refreshMySectionHeaderLabel]; // I created this handy little method to update the header text
}
// Get the latest text for the section header...
-(UILabel *) refreshMySectionHeaderLabel {
// Initialise the first time.
if( sectionHeaderLabel == nil ) {
sectionHeaderLabel = [[UILabel alloc] initWithFrame: CGRectNull];
}
// ...
// May also set other attributes here (e.g. colour, font, etc...)
// Figure out the text...
sectionHeaderLabel.text = #"Your updated text here...";
return sectionHeaderLabel;
}
When I want to update my section header, I simply call:
[self refreshMySectionHeaderLabel];
...or you could simply call:
sectionHeaderLabel.text = #"The new text...";
Take note: I only have 1 section (section 0). You may need to account for multiple sections...
One liner solution, based on #Dheeraj D answer:
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
(view as? UITableViewHeaderFooterView)?.textLabel?.text = (view as? UITableViewHeaderFooterView)?.textLabel?.text?.capitalized
}
SWIFT
In implementation, i found out that you need to specify section header text in both titleForHeaderInSection & willDisplayHeaderView function, othervise header hides.
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let sectionHeader = datasource[section].sectionHeader
// Header Title
return sectionHeader
}
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard let header = view as? UITableViewHeaderFooterView else { return }
header.textLabel?.textColor = UIColor.darkGray
header.textLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold)
header.textLabel?.frame = header.frame
// Header Title
header.textLabel?.text = datasource[section].sectionHeader
}
}
Use the method viewForHeaderInSection to create the header title.
In viewDidLoad register a UITableViewHeaderFooterView
tableView.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "HeaderFooterViewID")
add delegate
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterViewID")
var config = header?.defaultContentConfiguration()
let title = "My Header Title"
let attribute = [ NSAttributedString.Key.foregroundColor: UIColor.blue ]
config?.attributedText = NSAttributedString(string: title, attributes: attribute)
header?.contentConfiguration = config
return header
}
You can adjust the attributed text more, reference
With RubyMotion / RedPotion, paste this into your TableScreen:
def tableView(_, willDisplayHeaderView: view, forSection: section)
view.textLabel.text = self.tableView(_, titleForHeaderInSection: section)
end
I think what's happening is that somewhere in there the text gets set to ALL CAPS. This little trick RESETS the text back to whatever it was originally. Works like a charm for me!
I have a tableview controller that displays a row of cells. Each cell has 3 buttons. I have numbered the tags for each cell to be 1,2,3. The problem is I don't know how to find on which cell a button is being pressed. I'm currently only getting the sender's tag when one of the buttons has been pressed. Is there a way to get the cell row number as well when a button is pressed?
You should really be using this method instead:
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
Swift version:
let buttonPosition = sender.convert(CGPoint(), to:tableView)
let indexPath = tableView.indexPathForRow(at:buttonPosition)
That will give you the indexPath based on the position of the button that was pressed. Then you'd just call cellForRowAtIndexPath if you need the cell or indexPath.row if you need the row number.
If you're paranoid, you can check for if (indexPath) ... before using it just in case the indexPath isn't found for that point on the table view.
All of the other answers are likely to break if Apple decides to change the view structure.
Edit: This answer is outdated. Please use this method instead
Try this:
-(void)button1Tapped:(id)sender
{
UIButton *senderButton = (UIButton *)sender;
UITableViewCell *buttonCell = (UITableViewCell *)[senderButton superview];
UITableView* table = (UITableView *)[buttonCell superview];
NSIndexPath* pathOfTheCell = [table indexPathForCell:buttonCell];
NSInteger rowOfTheCell = [pathOfTheCell row];
NSLog(#"rowofthecell %d", rowOfTheCell);
}
Edit: If you are using contentView, use this for buttonCell instead:
UITableViewCell *buttonCell = (UITableViewCell *)senderButton.superview.superview;
I would recommend this way to fetch indexPath of cell which has any custom subview - (compatible with iOS 7 as well as all previous versions)
-(void)button1Tapped:(id)sender {
//- (void)cellSubviewTapped:(UIGestureRecognizer *)gestureRecognizer {
// UIView *parentCell = gestureRecognizer.view.superview;
UIView *parentCell = sender.superview;
while (![parentCell isKindOfClass:[UITableViewCell class]]) { // iOS 7 onwards the table cell hierachy has changed.
parentCell = parentCell.superview;
}
UIView *parentView = parentCell.superview;
while (![parentView isKindOfClass:[UITableView class]]) { // iOS 7 onwards the table cell hierachy has changed.
parentView = parentView.superview;
}
UITableView *tableView = (UITableView *)parentView;
NSIndexPath *indexPath = [tableView indexPathForCell:(UITableViewCell *)parentCell];
NSLog(#"indexPath = %#", indexPath);
}
This doesn't require self.tablview either.
Also, notice the commented code which is useful if you want the same through a #selector of UIGestureRecognizer added to your custom subview.
There are two ways:
#H2CO3 is right. You can do what #user523234 suggested, but with a small change, to respect the UITableViewCellContentView that should come in between the UIButton and the UITableViewCell. So to modify his code:
- (IBAction)button1Tapped:(id)sender
{
UIButton *senderButton = (UIButton *)sender;
UITableViewCellContentView *cellContentView = (UITableViewCellContentView *)senderButton.superview;
UITableViewCell *tableViewCell = (UITableViewCell *)cellContentView.superview;
UITableView* tableView = (UITableView *)tableViewCell.superview;
NSIndexPath* pathOfTheCell = [tableView indexPathForCell:tableViewCell];
NSInteger rowOfTheCell = pathOfTheCell.row;
NSLog(#"rowofthecell %d", rowOfTheCell);
}
If you create a custom UITableViewCell (your own subclass), then you can simply call self in the IBAction. You can link the IBAction function to your button by using storyboard or programmatically when you set up the cell.
- (IBAction)button1Tapped:(id)sender
{
UITableView* tableView = (UITableView *)self.superview;
NSIndexPath* pathOfTheCell = [tableView indexPathForCell:self];
NSInteger rowOfTheCell = pathOfTheCell.row;
NSLog(#"rowofthecell %d", rowOfTheCell);
}
I assume you add buttons to cell in cellForRowAtIndexPath, then what I would do is to create a custom class subclass UIButton, add a tag called rowNumber, and append that data while you adding button to cell.
Another simple way:
Get the point of touch in tableView
Then get index path of cell at point
The index path contains row index
The code is:
- (void)buttonTapped:(id)sender {
UITapGestureRecognizer *tap = (UITapGestureRecognizer *)sender;
CGPoint point = [tap locationInView:theTableView];
NSIndexPath *theIndexPath = [theTableView indexPathForRowAtPoint:point];
NSInteger theRowIndex = theIndexPath.row;
// do your stuff here
// ...
}
Swift 3
Note: This should really go in the accepted answer above, except that meta frowns upon such edits.
#IBAction func doSomething(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint(), to: tableView)
let index = tableView.indexPathForRow(at: buttonPosition)
}
Two minor comments:
The default function has sender type as Any, which doesn't have convert.
CGPointZero can be replaced by CGPoint()
One solution could be to check the tag of the button's superview or even higher in the view hierarchy (if the button is in the cell's content view).
I would like to share code in swift -
extension UITableView
{
func indexPathForCellContainingView(view1:UIView?)->NSIndexPath?
{
var view = view1;
while view != nil {
if (view?.isKindOfClass(UITableViewCell) == true)
{
return self.indexPathForCell(view as! UITableViewCell)!
}
else
{
view = view?.superview;
}
}
return nil
}
}
In swift:
#IBAction func buttonAction(_ sender: UIButton) {
guard let indexPath = tableView.indexPathForRow(at: sender.convert(CGPoint(), to: tableView)) else {
return
}
// do something
}
I have an NSView containing multiple subviews. One of those subviews is transparent and layered on top.
I need to be able to click through this view down to the subviews below (so that the view below gets first responder status), but all the mouse events get stuck on the top view (alpha is 1, because I draw stuff in it - so it should only click through transparent areas).
I actually expected this to work, since normally it does. What's wrong?
Here's another approach. It doesn't require creating a new window object and is simpler (and probably a bit more efficient) than the findNextSiblingBelowEventLocation: method above.
- (NSView *)hitTest:(NSPoint)aPoint
{
// pass-through events that don't hit one of the visible subviews
for (NSView *subView in [self subviews]) {
if (![subView isHidden] && [subView hitTest:aPoint])
return subView;
}
return nil;
}
I circumvented the issue with this code snippet.
- (NSView *)findNextSiblingBelowEventLocation:(NSEvent *)theEvent {
// Translate the event location to view coordinates
NSPoint location = [theEvent locationInWindow];
NSPoint convertedLocation = [self convertPointFromBase:location];
// Find next view below self
NSArray *siblings = [[self superview] subviews];
NSView *viewBelow = nil;
for (NSView *view in siblings) {
if (view != self) {
NSView *hitView = [view hitTest:convertedLocation];
if (hitView != nil) {
viewBelow = hitView;
}
}
}
return viewBelow;
}
- (void)mouseDown:(NSEvent *)theEvent {
NSView *viewBelow = [self findNextSiblingBelowEventLocation:theEvent];
if (viewBelow) {
[[self window] makeFirstResponder:viewBelow];
}
[super mouseDown:theEvent];
}
Here's a Swift 5 version of figelwump's answer:
public override func hitTest(_ point: NSPoint) -> NSView? {
// pass-through events that don't hit one of the visible subviews
return subviews.first { subview in
!subview.isHidden && nil != subview.hitTest(point)
}
}
Here's a Swift 5 version of Erik Aigner's answer:
public override func mouseDown(with event: NSEvent) {
// Translate the event location to view coordinates
let convertedLocation = self.convertFromBacking(event.locationInWindow)
if let viewBelow = self
.superview?
.subviews // Find next view below self
.lazy
.compactMap({ $0.hitTest(convertedLocation) })
.first
{
self.window?.makeFirstResponder(viewBelow)
}
super.mouseDown(with: event)
}
Put your transparent view in a child window of its own.