-[UITableView indexPathForRowAtPoint:] returns an index path given a CGPoint. However, it only works when the CGPoint lies exactly on a cell. If the point happens to be on the header or footer of a section, it returns nil.
Given a CGPoint, how can I get the section at that point which doesn't fail in the case of header/footer?
I ended up using the following extension method, which relies on -rectForSection:. Depending on your needs, you may want to use -rectForHeaderInSection: or -rectForFooterInSection: instead.
- (NSIndexPath *)xyz_indexPathForRowAtPoint:(CGPoint)point {
NSIndexPath *indexPath = [self indexPathForRowAtPoint:point];
if (indexPath) {
return indexPath;
}
for (NSInteger section = 0; section < [self numberOfSections]; section++) {
if (CGRectContainsPoint([self rectForSection:section], point)) {
return [NSIndexPath indexPathForRow:0 inSection:section];
}
}
return nil;
}
There doesn't seem to be a specific method for that.
If [UITableView indexPathForRowAtPoint:] returns nil, you could add or subtract a few pixels from the point's Y-coordinate (preferably the sum of the footer height and header height), and see if this method returns a value. If it does, you obtain a section number close to the original point, and use the - rectForSection: method or the - rectForFooterInSection: and - rectForHeaderInSection: methods to get the exact section.
Related
So I am trying to select objects from my array, to be able to delete them when I do an IBAction. I tried:
checking if the item is Selected:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (self.editEnabled) {
RDNote *selectedNote = [self.notes objectAtIndex:indexPath.row];
if (selectedNote.isSelected) {
selectedNote.selected = NO;
for (NSIndexPath *indexPathFromArray in self.indexPathsOfSelectedCells) {
if (indexPathFromArray.row == indexPath.row) {
[self.mutableCopy removeObject:indexPathFromArray];
}
}
} else {
selectedNote.selected = YES;
[self.indexPathsOfSelectedCells addObject:indexPath];
}
[self.collectionView reloadData];
IBAction:
- (IBAction)didTapTrashBarButton:(id)sender {
NSMutableArray *mutableNotes = [NSMutableArray arrayWithArray:self.notes];
for (NSIndexPath *indexPath in self.indexPathsOfSelectedCells) {
[mutableNotes removeObjectAtIndex:indexPath.row];
}
self.notes = [NSArray arrayWithArray:mutableNotes];
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithArray:self.indexPathsOfSelectedCells]];
} completion:^(BOOL finished) {
self.indexPathsOfSelectedCells = nil;
[self activateEditMode:NO];
[self saveDataToFile:self.notes];
}];
}
But I am having issues with indexes e.g.:(some times shows me the Error that Object index 2 is not between [0..1]), and there are bugs while selecting multiple Object and Deleting them.
Please help me with an advice for some other method that I can use, a Code will be perfect! Thanks!
This problem arrises because:
Let say you have five objects in array 1,2,3,4,5
you are running a loop for removing object based on indexpath which are selected rows. Now your indexpath contains 1st row and 3rd row.
while executing it for the first time you will remove object 1. Now 2,3,4,5 will left in the array. Now second time your indexpath.row is 3rd. It will remove third object which is 4 but in actual array it was 3.
Your code is crashing sometimes because if you have selected first row and last row. In this case i have selected 1 and 5. Now my indexpaths array will say I have to delect objectsAtIndexes 1 and 5.
While executing the loop I'll remove object at Index 1. Now i will be left with 2,3,4,5. On second iteration it will say remove objectAtIndex 5 where as Index 5 doesn't exist because now we have 4 elements in array.
In such cases best way of doing this is try removing elements in array from the end like remove 5th element first and then go for other one. Run your loop in reverse order.
NSInteger i = [self.indexPathsOfSelectedCells count]-1;
while (i > 0){
NSIndexPath *indexPath = [self.indexPathsOfSelectedCells objectAtIndex:i];
[mutableNotes removeObjectAtIndex:indexPath.row];
i--;
}
So I'm using iCarousel to display some images in my app and I need a label to show current image index when I scroll (e.g. "3/10"). This is my code:
- (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel
{
NSLog(#"index: %i", currentItemIndex);
imageNumber.text = #"";
imageNumber.text = [[[imageNumber.text
stringByAppendingString:[NSString stringWithFormat:#"%i", currentItemIndex+1]]
stringByAppendingString:#"/" ]
stringByAppendingString:[NSString stringWithFormat:#"%i", carousel.numberOfItems]];
}
Although this method is being called whenever I scroll, the currentItemIndex never change. It remains 0.
Does anybody know why is this or can suggest another way I can update the index when the user scrolls the carousel?
First, you need to have
carousel.currentItemIndex
and not currentItemIndex.
Second, you may also implement carouselDidEndScrollingAnimation: delegate and check for the index:
- (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel
{
NSLog(#"index: %i", carousel.currentItemIndex);
}
I'm currently working on an app that populates a UITableView with items from a MPMediaItemCollection. I'm trying to add a UITableViewCellAccessoryCheckmark to the row that matches the title of the currently playing track.
I've done so by creating a mutable array of the track titles, which are also set for my cell's textLabel.text property. (for comparison purposes)
Note: This is all done in - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
MPMediaItem *mediaItem = (MPMediaItem *)[collectionMutableCopy objectAtIndex: row];
if (mediaItem) {
cell.textLabel.text = [mediaItem valueForProperty:MPMediaItemPropertyTitle];
}
[mutArray insertObject:cell.textLabel.text atIndex:indexPath.row];
To the best of my knowledge this all works fine except for the below. At this point, I am trying to get the index of the currently playing tracks title and add the UITableViewCellAccessoryCheckmark to that row.
if (indexPath.row == [mutArray indexOfObjectIdenticalTo:[mainViewController.musicPlayer.nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
Getting to my question, I added all of the above (mostly irrelevant) code because I'm stumped on where I went wrong. When I log indexOfObjectIdenticalTo: it spits out "2147483647" every time, even though there are never more than 5 objects in the array. But why?
If anyone has any tips or pointers to help me fix this it would be greatly appreciated!
2147483647 just mean the object is not found.
From the documentation of -[NSArray indexOfObjectIdenticalTo:]:
Return Value
The lowest index whose corresponding array value is identical to anObject. If none of the objects in the array is identical to anObject, returns NSNotFound.
and NSNotFound is defined as:
enum {
NSNotFound = NSIntegerMax
};
and 2147483647 = 0x7fffffff is the maximum integer on 32-bit iOS.
Please note that even if two NSString have the same content, they may not be the identical object. Two objects are identical if they share the same location, e.g.
NSString* a = #"foo";
NSString* b = a;
NSString* c = [a copy];
assert([a isEqual:b]); // a and b are equal.
assert([a isEqual:c]); // a and c are equal.
assert(a == b); // a and b are identical.
assert(a != c); // a and c are *not* identical.
I believe you just want equality test instead of identity test, i.e.
if (indexPath.row == [mutArray indexOfObject:[....]]) {
Looking at the docs for NSArray
Return Value
The lowest index whose corresponding array value is identical to anObject. If none of the objects in the array is identical to anObject, returns NSNotFound.
So you should probably do a check
NSInteger index = [array indexOfObjectIdenticalTo:otherObject];
if (NSNotFound == index) {
// ... handle not being in array
} else {
// ... do your normal stuff
}
I need to find the pixel-frame for different ranges in a textview. I'm using the - (CGRect)firstRectForRange:(UITextRange *)range; to do it. However I can't find out how to actually create a UITextRange.
Basically this is what I'm looking for:
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
UITextRange*range2 = [UITextRange rangeWithNSRange:range]; //DOES NOT EXIST
CGRect rect = [textView firstRectForRange:range2];
return rect;
}
Apple says one has to subclass UITextRange and UITextPosition in order to adopt the UITextInput protocol. I don't do that, but I tried anyway, following the doc's example code and passing the subclass to firstRectForRange which resulted in crashing.
If there is a easier way of adding different colored UILables to a textview, please tell me. I have tried using UIWebView with content editable set to TRUE, but I'm not fond of communicating with JS, and coloring is the only thing I need.
Thanks in advance.
You can create a text range with the method textRangeFromPosition:toPosition. This method requires two positions, so you need to compute the positions for the start and the end of your range. That is done with the method positionFromPosition:offset, which returns a position from another position and a character offset.
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
CGRect rect = [textView firstRectForRange:textRange];
return [textView convertRect:rect fromView:textView.textInputView];
}
It is a bit ridiculous that seems to be so complicated.
A simple "workaround" would be to select the range (accepts NSRange) and then read the selectedTextRange (returns UITextRange):
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
textView.selectedRange = range;
UITextRange *textRange = [textView selectedTextRange];
CGRect rect = [textView firstRectForRange:textRange];
return rect;
}
This worked for me even if the textView is not first responder.
If you don't want the selection to persist, you can either reset the selectedRange:
textView.selectedRange = NSMakeRange(0, 0);
...or save the current selection and restore it afterwards
NSRange oldRange = textView.selectedRange;
// do something
// then check if the range is still valid and
textView.selectedRange = oldRange;
Swift 4 of Andrew Schreiber's answer for easy copy/paste
extension NSRange {
func toTextRange(textInput:UITextInput) -> UITextRange? {
if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location),
let rangeEnd = textInput.position(from: rangeStart, offset: length) {
return textInput.textRange(from: rangeStart, to: rangeEnd)
}
return nil
}
}
To the title question, here is a Swift 2 extension that creates a UITextRange from an NSRange.
The only initializer for UITextRange is a instance method on the UITextInput protocol, thus the extension also requires you pass in UITextInput such as UITextField or UITextView.
extension NSRange {
func toTextRange(textInput textInput:UITextInput) -> UITextRange? {
if let rangeStart = textInput.positionFromPosition(textInput.beginningOfDocument, offset: location),
rangeEnd = textInput.positionFromPosition(rangeStart, offset: length) {
return textInput.textRangeFromPosition(rangeStart, toPosition: rangeEnd)
}
return nil
}
}
Swift 4 of Nicolas Bachschmidt's answer as an UITextView extension using swifty Range<String.Index> instead of NSRange:
extension UITextView {
func frame(ofTextRange range: Range<String.Index>?) -> CGRect? {
guard let range = range else { return nil }
let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
guard
let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
let end = position(from: start, offset: length),
let txtRange = textRange(from: start, to: end)
else { return nil }
let rect = self.firstRect(for: txtRange)
return self.convert(rect, to: textInputView)
}
}
Possible use:
guard let rect = textView.frame(ofTextRange: text.range(of: "awesome")) else { return }
let awesomeView = UIView()
awesomeView.frame = rect.insetBy(dx: -3.0, dy: 0)
awesomeView.layer.borderColor = UIColor.black.cgColor
awesomeView.layer.borderWidth = 1.0
awesomeView.layer.cornerRadius = 3
self.view.insertSubview(awesomeView, belowSubview: textView)
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
UITextRange *textRange = [[textView _inputController] _textRangeFromNSRange:range]; // Private
CGRect rect = [textView firstRectForRange:textRange];
return rect;
}
Here is explain.
A UITextRange object represents a range of characters in a text
container; in other words, it identifies a starting index and an
ending index in string backing a text-entry object.
Classes that adopt the UITextInput protocol must create custom
UITextRange objects for representing ranges within the text managed by
the class. The starting and ending indexes of the range are
represented by UITextPosition objects. The text system uses both
UITextRange and UITextPosition objects for communicating text-layout
information. There are two reasons for using objects for text ranges
rather than primitive types such as NSRange:
Some documents contain nested elements (for example, HTML tags and
embedded objects) and you need to track both absolute position and
position in the visible text.
The WebKit framework, which the iPhone text system is based on,
requires that text indexes and offsets be represented by objects.
If you adopt the UITextInput protocol, you must create a custom
UITextRange subclass as well as a custom UITextPosition subclass.
For example like in those sources
God evening :-D
i have a a tableview,(using core data) i pub the cell with a "Todo" like: "5 mile Run" and i set the detailed text to #"points value #", X were x is a number set by a slider, and the same time u set name for the Todo.
i have put in a button in the cell, and called it add, and i want to be able to add that number in the detailed text to a "totalPoints" attribute in my core data model.
i can make a fetchRequest for the entity,but how do i make sure that i get that number, and how do i use simple math when the "pointValue" is stored in NSNumber object.
Update :
fixed it :-D
if u add a button to your cell, u can get that indexPath like this :
- (IBAction)buttonTapped:(id)sender {
if (![sender isKindOfClass: [UIButton class]]) return;
UITableViewCell *cell = (UITableViewCell *)[sender superview];
if (![cell isKindOfClass: [UITableViewCell class]]) return;
NSIndexPath *indexPath = [self.tableView indexPathForCell: cell];
// do something with indexPath.row and/or indexPath.section.
this fixed my app, and it´s now in beta testing :-P
Thanks for your help
Skov
In your sample code, there are a couple issues. First, when you are getting the values a, b and e you can just do this:
NSNumber *a = toDo.pointsValue;
(assuming that toDo.pointsValue is an NSNumber).
Secondly, in your if statement you are comparing the values of the pointers, not the values in the objects that the pointers point to.
This code should have the result you want (I assume you initialize toDo in some way) :
ToDo *toDo;
CGFloat x = [toDo.pointsValue floatValue] + [toDo.totalPoint floatValue];
//Note: it is better to use CGFloat rather than plain float
CGFloat y = [toDo.goodiesPoints floatValue];
if (x >= y)
{
[self performSelector:#selector(winView)
withObject:nil
afterDelay:0.5];
}
I am not sure exactly what you mean when you say "how do i make sure that i get that number". What number is "that number"?