I have a static UITableView built from a Storyboard that works well. I want to fill the first category programmatically, though, from a user-defined file
Simply put, I want to go through all the strings in an array and add them as cells for the rows of the first category. For the second category, I have a series of mildly complex cells (containing a number of labels, textfields, buttons and other controls), defined in the storyboard, that I don't feel like recreating in code.
As far as I understand, the default behaviour for a UITableView built from a storyboard is to use the nib file as an implicit datasource. If I use a custom class as datasource, my second section doesn't work. I have thought of two possible ways to fix this:
Fill my first category from the datasource and delegate the rest to the nib file. Is this possible? Is there some method to programmatically ask the nib to fill my UITableView?
Export my storyboard-built cells into code and paste this code into my datasource. This method has the disadvantage of making my second category harder to modify.
Is one of those two options feasible? Is there another option?
I would use dynamic prototype cells. Then, I would set up the ViewController as the delegate and the dataSource. I would then create a custom subclass of UITableViewCell and connect the elements of the second section to IBOutlets in the custom UITableViewCell.
If the first section wasn't something that could be done with one of the generic cell types, I would also create a custom subclass of UITableViewCell for that section as well.
I would then use the cellForRowAtIndexPath: method to set up the cells with the information that I want in them. So if my first section used FirstSectionCell and my second section used SecondSectionCell as custom subclasses of UITableViewCell my cellForRowAtIndexPath: would look like this:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.section==0)
{
FirstSectionCell *firstCell = [tableView dequeueReusableCellWithIdentifier:#"First Cell Prototype"];
//Set up the first cell.
return firstCell;
}
else if(indexPath.section ==1)
{
SecondSectionCell *secondCell = [tableView dequeueReusableCellWithIdentifier:#"Second Cell Ptototype"];
//Set up second cell.
secondCell.someLabel.text = #"whatever";
//etc.
return secondCell;
}
else
{
//if you have another section handle it here.
}
}
There are two kinds of table views when you use Storyboards:
Static
Dynamic
You're currently using the former. You define everything in the Storyboard and have very little code.
But you need to change to the latter.
You can still keep your UITableViewCells in the Storyboard; there's no need to do that in code (though you can if it makes things easier). You can refer to the template cells using the "reuse identifer."
Otherwise you've pretty much got it. You'll need to write code to implement the data source and (possibly) more methods of the table view delegate.
It's kind of fiddly switching from static to dynamic. I keep meaning to raise a Radar because I'm sure Xcode could be making it easier to do...
Related
I have a static UITableView as shown in the first image, but now I need to add a row with a Button that when I touch it adds a dynamic cell to the table as shown in the second image, the issue is that I can't add new cells to a static UITableview.
What would be the best practice to accomplish this?
Basically static TableView is not supposed to be changed at runtime (except cell content). This is clearly mentioned in docs:
Use static cells when a table does not change its layout, regardless of the specific information it displays.
The best practice in this case is to create a dynamic TV and populate it with appropriate amount of cells. You'll need to use DataSource delegate to do so. DataSource itself is typically done through dictionaries or arrays.
E.g. you have a dict 'phoneNumbers' and a button that is supposed to add a new one.
First, you add a selector to the button in cellForRowAtIndexPath: via tag for example. Then button action is going to look like:
-(void)yourButtonClicked:(UIButton*)sender
{
[self.phoneNumbers setObject:phoneNumber forKey:numberKey];
[self.tableView reloadData];
}
//Swift
func yourButtonClicked(sender: UIButton) {
self.phoneNumbers["numberKey"] = phoneNumber
self.tableView.reloadData()
}
(sorry it's Obj-C but I'm quite sure swift isn't much different at this point)
reloadData is needed to refresh TableView layout after changes to DataSource objects are made. It's quite close to 'redraw' in this case.
On the image from Contacts App you showed object is '(555)555-5555' NSString and key is probably 'other'. You can use and store these any way you like
So after all you only need to setup numberOfRowsInSection: so that for section where you want to add cells it returns the count of objects in dictionary phoneNumbers
My setup
I have a UITableViewCell that is in my main storyboard in a UITableViewController. It gets populated with some JSON data pulled from a REST API that will cause each cell to be a variable height. There are UIImageViews, UILabels all of different heights and styles, think Instagram-esque.
My problem
When I scroll to maybe the 5th or 6th cell, then go back up, they start redrawing and overlapping, so text gets mixed, lines get redrawn, etc.
What I've tried
This seems like a common problem on SO, so I've tried several posted solutions. It seems like my issue is probably the same problem as others face, which is, I am calling addSubview on my cell every time it dequeues, but I've tried checking to see if the cell already exists. I came across another post somewhere (sorry, I can't remember where), that suggests that because I am creating this in the storyboard, it is already initialized and if ( !cell ) will already return false, so I don't know how to prevent it from redrawing.
When I try removing the cell from the storyboard, and creating it programmatically, I get an error saying it can't find a cell with my identifier #"Cell".
I've also tried someone's solution of removing all subviews when I dequeue, so I used:
for ( UIView *view in cell.contentView.subviews ) {
if ([view isKindOfClass:[UIView class]]) {
[view removeFromSuperview];
}
}
and it doesn't find anything.
#rdelmar's comment is correct. You shouldn't do what you're doing. Might work, but it's bad form and you don't want to get into bad habits.
First, take advantage of object oriented programming. A cell should be able to configure itself based on the data you ask it to display. The table view shouldn't be designing the cell.
UITableViewCells need to be optimized for speed. Creating and adding subviews is a slow process. It's OK to do it once, but the cell will be reused (a system optimization) and you should just reuse the existing views that were added the first time the cell was created.
For example, you can hide subviews if they're not needed. You might want to do this in -prepareForReuse. You can move them around in -layoutSubviews. Or change the position of subviews in -updateConstraints.
Typically you just want to pass the data to display to the table view cell subclass from the data source (often the view controller). Let the cell do the display work.
When you add your subview after dequeueing uour cell, give a tag to your subview. This way, when you dequeue a cell, you can first check for the presence of a subview with your tag, and if it exists, remove it before adding your new view:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
// try to dequeue a cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<yourCellIdentifier>];
if( !cell )
{
// create a new cell if necessary
}
static int _myViewTag = 1000987 // give it a high int : low value are used by the system in cells
UIView *v = cell.contentView viewWithTag:_myViewTag];
if( v ) // subview with such tag already exists, so remove it.
[v removeFromSuperview];
// now add your new subview
[cell.contentView addSubview:<yourView>];
// adjust height of cell to your view.
...
}
Try to add a new method in your cell class to reset cell to its default style and call this method after dequeueCell.
The most efficient way to manage this is to subclass UITableViewCell and adding all your required Views as properties. So now when a cell comes up for "recycling", you know where to find the old views, like :
[cell.myTextLabel setText:#""];
aaaand you're done.
UPDATE creating a subclass makes sense if you have only a small number of "TYPES" of cells. create a subclass for each. How much complicated it gets depends on your specific scenario. But i've done it and found it to be the most effective method.
UPDATE 2 or you could make multiple cells in the storyboard, and dequeue the appropriate one based on the data source, save all the coding.
I am somewhat new to iOS, but am experienced in Android.
I have an app I am working on and it needs to populate a page with your "history" of past people you've interacted with, and it shows their picture, name, rating, and some other information.
This needs to populate in a vertical list, maybe a table? See the image below...
Now, in android, I would create a custom class with a layout that houses the picture, name, information, rating, and what not in one xml file, and in the activity I would call that class in a for loop, grabbing all the users and then programmatically it would add each view one after another, with their own unique user information until there is no more users to populate with.
How exactly can I do this in iOS and xcode? Do I need to make an XIB and add the picture, name, rating, and info place holders in that, and create a custom class for it that I would use to run in a for loop as well? I am a little stuck on how to do this with iOS.
Any help is much appreciated, and I can provide any additional information! Thanks :)
In iOS, you probably want to use a UITableView, with each row being a custom subclass of UITableViewCell. You can either create the layout for those cells in a separate XIB, or put the whole lot, tableView and "prototype" cells in a storyboard. You can achieve a lot without even subclassing, so fire up a dummy project in XCode and play (using one of Apple's templates gives you a good start). Enjoy.
What you probably want is to use a UITableView.
You don’t do the for-loop yourself. What you do is implement a set of delegate methods that the table view calls back to.
You can create your prototype cell in your XIB or Storyboard. When you add a Table View to the layout, you can then add a cell to that table view, and that cell will be your prototype. It looks like you only need one prototype cell, but you can create as many as you need. In Interface Builder you give the prototype cell a “reuse identifier”, which is just an arbitrary tag you use to refer to the prototype in your code. Your prototype cell can be your own subclass of UITableViewCell, or if you don’t need any custom code in it, you can just use UITableViewCell.
Then you implement several delegate methods. One is where you set the number of sections in the table view; it looks like you will only have on section.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv
{
return 1;
}
Then you tell it how many items are in the table view. Assuming you have the objects you want to display in an array, you just return the length of the array.
- (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)section
{
return self.objects.count;
}
Then, for each item in the array, cellForRowAtIndexPath will be called. Make that method return the actual cell. You call dequeueReusableCellWithIdentifier to retrieve your prototype cell, using the reuse identifier you assigned in Interface Builder. Then use the corresponding object to set up the UI elements in your cell.
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)i
{
UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:i];
Thingy *item = self.objects[i.row];
cell.textLabel.text = item.name;
return cell;
}
That should be enough to get you started with the documentation, now that you have the overview of what you need to implement.
The first thing you have to do in switching from Android to iOS is to learn the terminology. Then you'll know what to search for on Google, SO, etc.
What's you're looking to do is create a UITableView.
Here is a link to a super basic 'how-to' to get you started with tableviews.
http://www.appcoda.com/uitableview-tutorial-storyboard-xcode5/
Once you've got the basics down, you'll want to take that a step further with learning how to customize the UITableViewCell within your tableview, so you can accomplish the look you've detailed in the question.
http://www.appcoda.com/customize-table-view-cells-for-uitableview/
I'm not sure I can help anymore than that at the moment. Jump in, learn tableviews, and start searching on OS to answer the million other questions you'll have a long the way.
Good luck!
Before I get crucified for this, let me state... Yes, I have read every "relevant" answer on this topic and have not found a workable solution. Most "correct" answers are pre-ARC and discuss "releasing" a cell, which just isn't done anymore. Secondly, my problem is not "global", meaning some views have no problems, while others do. So here is my question...
I have sub-classed uitableviewcell and setup some uilabels & custom uiviews. From there I wired everything up in ib (Xcode 5.x iOS 7.x). Once I put in the appropriate code and create the tableview & dynamic cells from a nsarray "not mutable" everything works exactly as expected with no issues.
This is the fun part. I am making changes to allow the data source of the tableview, which is an nsarray to be mutable to allow adding and removing of items / cells. This is where things get hairy. When I start to add more objects to the array and when the reuse cell is being put on screen visual data from old cells is being reused on new cells. I say "visual" because once the cell is selected the view updates to display the correct information. The part that is interesting is that as I stated I have some uilabels which never have any problems being redisplayed, my custom views however are now the piece of the puzzle that is displaying info from past cells, and when scrolling back up, the original cells no longer display the correct information. Once the cell is tapped, then the cell updates and displays the correct information.
the most confusing bit of this is that before my array was mutable and had a static amount of objects this worked fine. Even if a cell went off screen and came back, it was still the correct information being displayed. Now I know that shouldn't have anything to do with it, but it is strange that it worked using the same tableview & cell code that I am using now.
I have tried adding in
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if (!cell) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"]; // note: obviously as stated, tacking on "autorelease" here as mention in other suggestions is not going to work.
}
Which doesn't fix the issue.
I tried overriding the "prepareForReuse" method on my custom cell subclass and that does not resolve the issue either. I have made the views, "strong" & "weak", and all that and still every 3rd or so cell gets repeated with garbage data until it is refreshed. Again, the uilabels which are setup the same way as the views have no problems and data is never reused. I would say there is a problem with my custom views, but setting up the table from a static source of identical information there is no problem.
I would like to post some code, but it's all pretty generic code for tableviews & delegates. any suggestions would be greatly appreciated.
As i said the code is all pretty generic, but apparently it needs posing anyway so here it is..
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyThing *thing = self.stuffArray[indexPath.row];
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.thisLabel.text = thing.someText;
cell.thatLabel.text = thing.otherText;
cell.view1.someProptery = thing.object1.property
cell.view2.someProptery = thing.object2.property
cell.view3.someProptery = thing.object3.property
//"someProperty" on "view..." is an NSInt that is used to determine custom drawing in the view.
return cell;
}
I think the key to the solution lies in your comment about the custom views in the cells. If cellForRowAtIndexPath is altering the states of those views, they need to know that they must be redrawn, so you'll need to augment the synthesized setter in your custom view.m that has someProperty.
If the someProperty determines how this view get's drawn, then it's incumbent upon the setter to indicate that the view is out of date....
- (void)setSomeProperty:(NSInteger)someInt {
_someProperty = someInt;
[self setNeedsDisplay];
}
I'm trying to figure out how to have a few static cells in addition to dynamic cells (I think) but when I start a new Master-Detail app and switch the default Dynamic Prototypes to Static Cells, it crashes.
I think, since I'm still new at this, that I don't understand how all of the components (table views, cells, delegates) are wired. After I switched to Static Cells I made three cells but they don't show up. The app just crashes.
I can include code but basically, I create a new Master-Detail, switch to Static Cells, change the name of three cells and it crashes (in the main loop).
Thanks ahead for everything.
If I understand your question, this is the explanation you are looking for:
Prototype (reusability):
When you use Dynamic cells, you use Prototype cells. That means either in IB or in your code, you create an instance of UITableViewCell, and give it an identifier (reuseIdentifier). The UITableView uses this prototype to generate as many cells (rows) as you need. UITableView create (and use memory) for as many cells as it needs to fill the screen. Once some of these cells go off the screen, UITableView reuses them -- recycles them.
You can define more than one prototype cell in a UITableView. The idea is that each prototype serves a different purpose. For example, you define a prototype cell that has only one big UILabel and its purpose is to use static text. You define another prototype that has only one UIImageView. It depends on your design and how you want to display your data.
To use prototype cells, in IB, you use Dynamic Prototypes cells, and set an identifier. Then you have to implement UITableViewDataSource methods, such as:
– tableView:numberOfRowsInSection:
– tableView:cellForRowAtIndexPath:
These methods are delegate methods of UITableView.
If not using IB, you would create cells in – tableView:cellForRowAtIndexPath: method:
UITableViewCell *cell = [[UITableViewCell alloc]
initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString
*)reuseIdentifier];
Static:
The idea of static cells is just simplicity. You can use the prototype cells (e.g. only one prototype) and feed in a static NSArray, for example, as data source. To take the task of implementing UITableViewDataSource methods, Apple introduced static cells, where you would only use the storyboard and IB. In IB, you select the table view, and choose Static Cells instead, and type in your text, or set the image.
You don't want to make any dynamic changes to static cells later when the app is running because it is not meant to, and you would have to implement many more methods that it is not worth it. Although it not much of a work to get the indexPath of the selected row, but the idea is if you need to push a new view from selection of a static cell, you would use segues, instead of implementing any code.
When using static cells, you should not implement UITableViewDataSource method, otherwise your app crashes, and vice versa, if you do not implement UITableViewDataSource methods (required ones) when using prototype cells, your app crashes.