I'm trying to learn about iOS development, and followed Apple's own tutorial for making a To-Do list. After the tutorial, they recommended that I continued to build the app, implement more features and so on.
What I'm trying to achieve is when a user clicks on a cell, it expands and shows extra content. For example:
Buy groceries <- "item1"
Do homework <- "item2"
Go for a jog <- "item3"
Turns into this when tapped:
Buy groceries <- "item1"
Buy apples <- "subitem1"
Buy bread <- "subitem2"
Buy milk <- "subitem3"
Do homework <- "item2"
Go for a jog <- "item3"
Right now I have a XYZToDoSubItem class and a XYZToDoItem class. XYZToDoItem inherits Name, Completed-state and Creation-date from XYZToDoSubItem. In addition it has an Mutable Array containing Subitems.
I have also implemented a set of other functions:
When a user "holds" a cell it enters edit mode
When a user double taps a cell, it set completion state to YES and adds a checkmark
So the single-tap cannot interfere with these functions.
I also have a "listener" for single taps (not complete viewDidLoad function):
-(void) viewDidLoad {
UITapGestureRecognizer *stp = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
stp.numberOfTapsRequired = 1;
stp.numberOfTouchesRequired = 1;
[stp requireGestureRecognizerToFail:dtp];
[self.tableView addGestureRecognizer:stp];
}
Where dtp is a double-tap recognizer.
My handleSingleTap function looks like this:
- (void)handleSingleTap:(UITapGestureRecognizer *)gestureRecognizer {
CGPoint p = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if(indexPath != nil) {
//Add contents to cell and update height here
}
}
Basically how it works is that when a user taps a cell (and don't proceed to tap it again (no double-tap)), handleSingleTap runs. Here we get the indexPath of the cell tapped, and if that is anything else than nil (which means a cell was tapped, not the background), I wanna add more content to the cell and update the height.
How would I proceed to do this?
Is it possible to just add the content as multiple subtitles, and then call [self.tableView realoadData] or [self.tableView beginUpdates] and [self.tableView endUpdates], and iOS handles the resizing itself? If that's the case, how do I add the data? And how would I call the resize method? If not, how would I manually resize the cells after inserting the data?
My setup looks like this:
In the ViewController class:
#property NSMutableArray *toDoItems;
In the ToDoItem class (inherits from subitem):
#property NSMutableArray *toDoSubItems;
In the SubItem class:
#property NSString *itemName;
#property BOOL completed;
#property (readonly) NSDate *creationDate;
I hope someone is able to figure out how to solve this.
Thanks in advance,
Aleksander.
I personally really like using [tableView beginUpdates]; [tableView endUpdates];, it gives you a really nice animation. :-)
The problem with that code is that it doesn't actually update the cells themselves, it doesn't call cellForRowAtIndexPath. I would recommend using the previously mentioned code, to animate the height change, together with [tableView reloadData] or [tableView reloadRowsAtIndexPaths:NSArray* withRowAnimation:UITableViewRowAnimation]; to update the cells content.
Simply store which row was touched and when cellForRowAtIndexPath and heightForRowAtIndexPath gets called, check if it's the same row and act accordingly.
From a usability stand-point I would recommend putting all the sub-tasks in the cell from the beginning but putting them outside of the cells frame. That way, when the cell gets touched only the cells height needs to change to reveal the views outside of the cells frame. This way, you don't have to deal with the content changing either.
Cheers!
Related
I'm using a button inside a tableView in which I get the indexPath.row when is pressed. But it only works fine when the cells can be displayed in the screen without scroll.
Once the tableView can be scrolleable and I scrolls throught the tableview, the indexPath.row returned is a wrong value, I noticed that initially setting 20 objects, for example Check is just printed 9 times no 20.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
lBtnWithAction = [[UIButton alloc] initWithFrame:CGRectMake(liLight1Xcord + 23, 10, liLight1Width + 5, liLight1Height + 25)];
lBtnWithAction.tag = ROW_BUTTON_ACTION;
lBtnWithAction.titleLabel.font = luiFontCheckmark;
lBtnWithAction.tintColor = [UIColor blackColor];
lBtnWithAction.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[cell.contentView addSubview:lBtnWithAction];
}
else
{
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
}
//Set the tag
lBtnWithAction.tag = indexPath.row;
//Add the click event to the button inside a row
[lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
//This is printed just 9 times (the the number of cells that are initially displayed in the screen with no scroll), when scrolling the other ones are printed
NSLog(#"Check: %li", (long)indexPath.row);
return cell;
}
To do something with the clicked index:
-(void)rowButtonClicked:(UIButton*)sender
{
NSLog(#"Pressed: %li", (long)sender.tag);
}
Constants.h
#define ROW_BUTTON_ACTION 9
What is the correct way to get the indexPath.row inside rowButtonClicked or setting a tag when I have a lot of of cells in my tableView?
My solution to this kind of problem is not to use a tag in this way at all. It's a complete misuse of tags (in my opinion), and is likely to cause trouble down the road (as you've discovered), because cells are reused.
Typically, the problem being solved is this: A piece of interface in a cell is interacted with by the user (e.g. a button is tapped), and now we want to know what row that cell currently corresponds to so that we can respond with respect to the corresponding data model.
The way I solve this in my apps is, when the button is tapped or whatever and I receive a control event or delegate event from it, to walk up the view hierarchy from that piece of the interface (the button or whatever) until I come to the cell, and then call the table view's indexPath(for:), which takes a cell and returns the corresponding index path. The control event or delegate event always includes the interface object as a parameter, so it is easy to get from that to the cell and from there to the row.
Thus, for example:
UIView* v = // sender, the interface object
do {
v = v.superview;
} while (![v isKindOfClass: [UITableViewCell class]]);
UITableViewCell* cell = (UITableViewCell*)v;
NSIndexPath* ip = [self.tableView indexPathForCell:cell];
// and now we know the row (ip.row)
[NOTE A possible alternative would be to use a custom cell subclass in which you have a special property where you store the row in cellForRowAt. But this seems to me completely unnecessary, seeing as indexPath(for:) gives you exactly that same information! On the other hand, there is no indexPath(for:) for a header/footer, so in that case I do use a custom subclass that stores the section number, as in this example (see the implementation of viewForHeaderInSection).]
I agree with #matt that this is not a good use of tags, but disagree with him slightly about the solution. Instead of walking up the button's superviews until you find a cell, I prefer to get the button's origin, convert it to table view coordinates, and then ask the table view for the indexPath of the cell that contains those coordinates.
I wish Apple would add a function indexPathForView(_:) to UITableView. It's a common need, and easy to implement. To that end, here is a simple extension to UITableView that lets you ask a table view for the indexPath of any view that lies inside one of the tableView's cells.
Below is the key code for the extension, in both Objective-C and Swift. There is a working project on GitHub called TableViewExtension-Obj-C that illustrates the uses of the table view extension below.
EDIT
In Objective-C:
Header file UITableView_indexPathForView.h:
#import <UIKit/UIKit.h>
#interface UIView (indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view;
#end
UITableView_indexPathForView.m file:
#import "UITableView_indexPathForView.h"
#implementation UITableView (UITableView_indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view {
CGPoint origin = view.bounds.origin;
CGPoint viewOrigin = [self convertPoint: origin fromView: view];
return [self indexPathForRowAtPoint: viewOrigin];
}
And the IBAction on the button:
- (void) buttonTapped: (UIButton *) sender {
NSIndexPath *indexPath = [self.tableView indexPathForView: sender];
NSLog(#"Button tapped at indexpPath [%ld-%ld]",
(long)indexPath.section,
(long)indexPath.row);
}
In Swift:
import UIKit
public extension UITableView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let origin = view.bounds.origin
let viewOrigin = self.convert(origin, from: view)
let indexPath = self.indexPathForRow(at: viewOrigin)
return indexPath
}
}
I added this as a file "UITableView+indexPathForView" to a test project to make sure I got everything correct. Then in the IBAction for a button that is inside a cell:
func buttonTapped(_ button: UIButton) {
let indexPath = self.tableView.indexPathForView(button)
print("Button tapped at indexPath \(indexPath)")
}
I made the extension work on any UIView, not just buttons, so that it's more general-purpose.
The nice thing about this extension is that you can drop it into any project and it adds the new indexPathForView(_:) function to all your table views without having do change your other code at all.
You are running into the issue of cell-reuse.
When you create a button for the view you set a tag to it, but then you override this tag to set the row number to it.
When the cell get's reused, because the row number is longer ROW_BUTTON_ACTION, you don't reset the tag to the correct row number and things go wrong.
Using a tag to get information out of a view is almost always a bad idea and is quite brittle, as you can see here.
As Matt has already said, walking the hierarchy is a better idea.
Also, your method doesn't need to be written in this way. If you create your own custom cell, then the code you use to create and add buttons and tags isn't needed, you can do it in a xib, a storyboard, or even in code in the class. Furthermore, if you use the dequeue method that takes the index path, you will always get either a recycled cell, or a newly created cell, so there is no need to check that the cell returned is not nil.
I'm developing an iOS app with a UITableView that requires a swipe gesture to perform a certain action for any cell (row) in the table.
When I initially implemented the IBAction for the gesture in my view controller (the one with the UITableView in it), I wasn't even able to run the app, as Xcode informed me with an error that it doesn't allow attaching gestures to repeating interface elements (the cell being a repeating element, since a new one is generated each time one is needed via dequeueing a reusable cell).
So I then proceeded to place the IBAction for the swipe gesture inside my custom table cell class instead, at which point I no longer receive the error that prevents me from building/running, but am still receiving a warning in the debug console while running the app, which states that since iOS9, the system now enforces the prohibition of gestures attached to repeating elements (in this case, my table cell).
If anyone has insight into gesture recognizers, I'd appreciate it if you could help me figure out the following questions.
Should I take this warning at face value and assume that I'm not allowed to attach any gestures to table cells at all?
In either case, is there some other way to attach a gesture to repeating elements like prototype table cells and avoid any kind of warnings/prohibitions?
You should put the swipe in your viewcontroller and get a cell through Swipe IBAction with the code below. Once you've done that, you can do what you want: cell.do_something or cell.element.do_something
You get the cell with the position of swipe, like:
Objective C:
CGPoint location = [sender locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
Swift:
let location : CGPoint = sender.locationInView(self.tableView)
let indexPath : NSIndexPath = tableView.indexPathForRowAtPoint(location)
let cell : UITableViewCell = tableView(tableView, cellForRowAtIndexPath: indexPath)
I have a UITableView with searchBar and searchDisplayController. What I wanted to do was to show a button when no results were found. So user could search server's database. I have a NSMutableArrayto store searchResults. So my delegate method looks like this:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"titulo contains [cd] %#", searchText];
NSArray *filtroUsuario = [self.objects filteredArrayUsingPredicate:predicate];
self.searchResults = [[NSMutableArray alloc] initWithArray:filtroUsuario];
if (self.searchResults.count < 1) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
btn.frame = CGRectMake(0, 0, 320, 50);
btn.showsTouchWhenHighlighted = YES;
[btn setTitle:#"Procurar no Banco de Dados" forState:UIControlStateNormal];
btn.tag = 1;
[self.searchResults addObject:btn];
self.adicionar = YES;
}
}
Basically, when there are no results, I create and add a buttonto my results array. Then, in cellForRowAtIndexPath, I have the following:
if (tableView == self.searchDisplayController.searchResultsTableView) {
if (self.adicionar == YES) {
cell.textLabel.text = #"";
[cell.contentView addSubview:self.searchResults[indexPath.row]];
self.adicionar = NO;
}
This shows the buttonexactly the way I want, and when cancelButtonis pressed, or the buttonin question, I just remove it from searchResultsin case user searches again. [self.searchResults removeAllObjects].
The problem was that, since I'm reusing cells, the subview was still there when user searched again. I had a few options to deal with this, I could create a property for the cell and remove subview when buttonwas pressed. But I opted to include the line [[cell.contentView viewWithTag:1] removeFromSuperview];at the beginning of cellForRowAtIndexPath, so when it's called again, it deletes the subViewsbefore continue.
Everything works perfectly now. My question is if this is the best approach or if there's something more simple. Since my app is a complex app, I'm very concerned about memory and performance, besides, I would really like to learn the coolest techniques available.
Any thoughts?
take a look at -prepareForReuse on the UITableViewCell. Probably the best fit for the exact situation you describe. (sketched example below)
#interface CustomCellName : UITableViewCell
#property (strong, nonatomic) UIButton *someButton;
#end
#implementation
- (void)prepareForReuse
{
_someButton.hidden = YES;
//etc...
}
#end
Tags are also perfectly fine, although many people find that approach to be unsatisfying. (*citation needed)
An alternate approach is to subclass UITableViewCell with a UIButton as a property which you can tweak from the tableView by -cellForRowAtIndexPath.
Best is a flexible term - best in what regard...
What you have works, but it isn't a good allocation of responsibility. It would be better to create a custom cell subclass and have it provide an API where a custom button can be added. The cell subclass would handle cleanup of the button in prepareForReuse.
From a memory and performance point of view there is little difference. But using a cell subclass is more correct.
For performance, it's better not to create and destroy button instances. So, it would be better for your cell subclass to create a button but keep it hidden until it's needed, then show it. Now, prepareForReuse would simply hide the button. Generally this would use a little more memory on average - it's a trade off...
First thing, mixing controls with your app data is not a good practice. You can make it work as you already have, but in long term trie to separate UI from application data. Table view should do all the work of presenting data and controls that manipulate your data so it is probably better idea to add string "No Data" to your result array and then handle it in cellForRowAtIndexPath. Create a button on your cell and just hide/show it when you need it. Don't worry about memory consumption, the cells are reusable so you will have very small number of buttons (hidden ones).
Second thing if you really wont to create only one button then tag it, for example, when you decide you need it create it and tag it with say 1000. Then later when you reload and don't want to show it, just ask your cell if it has a view with tag == 1000 and remove it from superview. But again it is an overkill and your memory integrity wont be endangered if you create one button for every visible cell.
I'm trying to find the best way to save how long users of my app have looked at each UITableViewCell, for optimization and metrics purposes. I haven't found a tool (Parse, AppSee, etc) that can monitor that, so I'm doing it manually. Problem is, it's incredibly not efficient.
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView
{
UITableView *tableView = self.tableView; // Or however you get your table view
NSArray *paths = [tableView indexPathsForVisibleRows];
// For getting the cells themselves
NSMutableSet *visibleCells = [[NSMutableSet alloc] init];
for (NSIndexPath *path in paths) {
NSLog(#"visible: %i", path.row);
[visibleCells addObject:[tableView cellForRowAtIndexPath:path]];
}
}
I will then start a NSTimer as soon as is't visible and stop it when it's not visible anymore.
This has number of flaws:
When the tableview is presented, the user can look at the first cell without scrolling - thus this code isn't called.
This code is called tens of time a second - there must be a better way
This code is called when the user is looking at, for example, the second cell, but the first cell is still a few pixels visible. So there should be a condition where the call has to be at least 20% visible to make the timer active.
If the user taps another tab bar while looking at cells, this code isn't aware and keeps counting.
My solution is obviously not optimal. Have a better way? Your solution is welcome
Use the table view delegate methods and the view controller display methods. Specifically:
tableView:willDisplayCell:forRowAtIndexPath:
tableView:didEndDisplayingCell:forRowAtIndexPath:
viewDidDisappear:
I have in one ViewControl two TableViews implemented. Initially one has data and another doesn't.
So, I was wondering how would I implement the gesture event of touching one row in the fulfilled TableView and swipe it to the empty TableView, in order to move that datum.
add the swipe gesture to each cell of your table, this gesture call a Custom method where you pass the indexPath of the cell then in your Custom method write a code for add this object from the array of first table to the second and remove it from first array. Finally you refresh boh table with reloadData method
What I've done so far... (It's slightly different of what #Mirko told)
Added the UISwipeGesture to the table (inside ViewDidLoad method), and defined it to capture the "Swipe Up" event, calling my event handler.
swipeCell = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipeUpEvent:)];
swipeCell.direction = UISwipeGestureRecognizerDirectionUp;
[self.tableA addGestureRecognizer:swipeCell];
Implemented the event handler, that get the cell selected by the swipe movement when it starts.
-(IBAction)swipeUpEvent:(UIGestureRecognizer *) sender
{
CGPoint location = [sender locationInView:self.tableA];
NSIndexPath *swipedIndexPath = [self.tableA indexPathForRowAtPoint:location];
NSString *temp = [arrayA objectAtIndex:swipedIndexPath.row];
[arrayB addObject:temp];
[arrayA removeObjectAtIndex:swipedIndexPath.row];
[self.tableB reloadData];
[self.tableA reloadData];
}
It may not be the most elegant solution for the problem, but at least I avoided creating an ActionListener for each cell. I hope it saves me some memory.