-
Notifications
You must be signed in to change notification settings - Fork 472
Table View Guide
UITableViews
are one of the most commonly used views in
iOS programming. They are used to display grouped lists of cells.
Here are some examples of UITableViews
:
UITableViews
can be highly performant, displaying thousands of rows of
data. They also have built-in facilities for handling common behavior
such as scrolling, selecting rows, editing the table's contents, and
animating the addition or removal of rows.
This guide covers typical use cases and common issues that arise when
using UITableViews
. All our examples are provided in Swift, but they
should be easy to adapt for an Objective-C project. A more
comprehensive guide by Apple (written for Objective-C) can found
here.
In order to use a UITableView
, you must first add one to your view
controller's root view. When working with storyboards, this can be done
in Interface Builder simply by dragging a UITableView
from the Object
Library onto your view controller and then creating an @IBOutlet
so
that you have a reference to your UITableView
in your view
controller's code.
Of course, you can also programmatically instantiate a UITableView
and
add it as a subview to your view controller's root view. The remainder of
this guide assumes that you are able to properly instantiate and obtain
a reference to a UITableView
.
You'll notice that in the Object Library there are two objects: Table View
and Table View Controller
. You'll almost always want to use
Table View
object. A Table View Controller
or
UITableViewController
is a built-in view controller class that has its
root view set to be a UITableView
. This class does a small amount of
work for you (e.g. it already implements the UITableViewDataSource
and UITableViewDelegate
protocols), but the requirement that your view
controller's root view be a UITableView
ends up being too inflexible
if you need to layout other views in the same screen.
As with other views in the UIKit framework, in order to use a
UITableView
you provide it with delegates that are
able to answer questions about what to show in the table and how the
application should respond to user interactions with the table.
The UITableView
has two delegates that you must provide by setting the
corresponding properties on your UITableView
object.
-
The
dataSource
property must be set to an object that implements theUITableViewDataSource
protocol. This object is responsible for the content of the table including providing the actualUITableViewCells
that will be shown. -
The
delegate
property must be set to an object that implements theUITableViewDelegate
protocol. This object controls the basic visual appearance of and user interactions with the table. It is not technically mandatory for you provide your owndelegate
, but in practice you will almost always want to do something that requires implementing your ownUITableViewDelegate
.
The following is the most basic way to set up a UITableView.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = data[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
}
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
// ViewController.m
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
NSArray *data;
- (void)viewDidLoad {
[super viewDidLoad];
data = @[@"New York, NY", @"Los Angeles, CA", @"Chicago, IL", @"Houston, TX",
@"Philadelphia, PA", @"Phoenix, AZ", @"San Diego, CA", @"San Antonio, TX",
@"Dallas, TX", @"Detroit, MI", @"San Jose, CA", @"Indianapolis, IN",
@"Jacksonville, FL", @"San Francisco, CA", @"Columbus, OH", @"Austin, TX",
@"Memphis, TN", @"Baltimore, MD", @"Charlotte, ND", @"Fort Worth, TX"];
self.tableView.dataSource = self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
cell.textLabel.text = data[indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return data.count;
}
@end
Provided that the @IBOutlet
tableView
has been connected to a
UITableView
in your storyboard, you will see something like this when running
the above code:
Notice that we set self.tableView.dataSource = self
in the
viewDidLoad
method. A common error that will result in a blank or misbehaving
table is forgetting to set the dataSource
or delegate
property on your
UITableView
. If something is not behaving the way you expect with
your UITableView
, the first thing to check is that you have set your
dataSource and delegate properly.
In this case, since the only view managed by our ViewController
is the table, we
also have our ViewController
implement UITableViewDataSource
so that all the
code for this screen is in one place. This is a fairly common pattern when
using UIKit delegates, but you may want to create a separate class to
implement UITableViewDataSource
or UITableViewDelegate
in more complex
situations.
We implement the two required methods in the UITableViewDataSource
protocol:
-
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
is responsible for telling theUITableView
how many rows are in each section of the table. Since we only have one section, we simply return the length of ourdata
array which corresponds to the number of total cells we want. To create tables with multiple sections we would implement the [numberOfSections][numberofsections] method and possibly return different values in our [
numberOfRowsInSection][numberofrowsinsection] method depending the
section` that was passed in. -
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
is responsible for returning a preconfigured cell that will be used to render the row in the table specified by theindexPath
. TheindexPath
identifies a specific row in a specific section of the table the via theindexPath.section
andindexPath.row
. Since we are only working with one section, we can ignoresection
for now.
An implementation of the
[cellForRowAt
][cellforrowat] method must return an
instance of UITableViewCell
that is configured with
the data for the row specified by the indexPath
. In the above code, we
created a new instance of the UIKit-provided UITableViewCell
class for each call to cellForRowAt
. Since our table had
only a few simple cells you might not have noticed any appreciable
performance drop. However, in practice, you will almost never create
a new cell object for each row due to performance costs and memory
implications. This becomes especially important once you start creating
more complex cells or have tables with large numbers of rows.
In order to avoid the expensive costs of creating a new cell object for each
row, we can adopt a strategy of cell reuse. Notice that the table can only
display a small number of rows on the screen at once. This means we only have
to create at most as many UITableViewCell
objects as there are rows that
appear on the screen at once. Once a row disappears from view—say when
the user scrolls the table—we can reuse the same cell object to render
another row that comes into view.
To implement such a strategy from scratch we would need to know which rows are
currently being displayed and to be able to respond if the set of visible rows
is changed. Luckily UITableView
has built-in methods that make cell reuse
quite simple to implement. We can modify our code example above to read
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
let CellIdentifier = "com.codepath.MyFirstTableViewCell"
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// If there are no cells available for reuse, it will always return a cell so long as the identifier has previously been registered
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
}
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
// ViewController.m
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
NSArray *data;
NSString *CellIdentifier = @"com.codepath.MyFirstTableViewCell";
- (void)viewDidLoad {
[super viewDidLoad];
data = @[@"New York, NY", @"Los Angeles, CA", @"Chicago, IL", @"Houston, TX",
@"Philadelphia, PA", @"Phoenix, AZ", @"San Diego, CA", @"San Antonio, TX",
@"Dallas, TX", @"Detroit, MI", @"San Jose, CA", @"Indianapolis, IN",
@"Jacksonville, FL", @"San Francisco, CA", @"Columbus, OH", @"Austin, TX",
@"Memphis, TN", @"Baltimore, MD", @"Charlotte, ND", @"Fort Worth, TX"];
self.tableView.dataSource = self;
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// If there are no cells available for reuse, it will always return a cell so long as the identifier has previously been registered
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = data[indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return data.count;
}
@end
In viewDidLoad
we call our UITableView
's
register(AnyClass?, forCellReuseIdentifier:
to associate the
built-in class UITableViewCell
with the constant string identifier
CellIdentifier
. Notice that we do not explicitly create an instance here.
The UITableView
will handle the creation of all cell objects for us.
In cellForRowAt indexPath:
, we call
dequeueReusableCell(withIdentifier: for:)
to obtain a
pre-created instance of UITableViewCell
and then we proceed to populate this cell with the data for the given row before returning it.
-
Be sure to provide a unique reuse identifier for each type of cell that you in your table so that you don't end up accidentally getting an instance of the wrong type of cell. Also be sure to register a cell identifier with the
UITableView
before attempting to dequeue a cell using that identifier. Attempting to calldequeueReusableCell(withIdentifier: for:)
with an unregistered identifier will cause your app to crash. -
When we explicitly instantiated each cell object in
cellForRowAt
we were able to specify the cellstyle: .Default
. When we calleddequeueReusableCell(withIdentifier: for:)
there was no place to specify the style. When usingdequeueReusableCell(withIdentifier: for:)
you have no control over the initialization of your cell. In practice, you will want to create your own subclass ofUITableViewCell
and add initialization common to all cells in the class in the initializer. -
Any configuration of the cell on a per row basis should be done in
cellForRowAt
. When designing a custom cell class be sure to expose a way to configure properties you need to change on a per row basis. In this case, the built-inUITableViewCell
gives us access to itstextLabel
so that we are able to set different text for each row. With more complex cells, however, you may want to provide convenience methods that wrap the configuration logic within the custom cell class. -
There are no guarantees on the state of the cell that is returned by
dequeueReusableCell(withIdentifier: for:)
. The cell will not necessarily be in the newly initialized state. In general, it will have properties that were previously set when configuring it with the data of another row. Be sure reconfigure all properties to match the data of the current row!
UIKit provides a number of cell styles that can be
used with the built-in UITableViewCell
class. Depending on the cell
style you specify when initializing the UITableViewCell
you can use
the properties textLabel
, detailTextLabel
, and imageView
to
configure the contents of your cell. In practice, you'll almost never
use any of the built-in cell styles except maybe the default one that
contains a single textLabel
. However, you should be aware of these
properties when subclassing UITableViewCell
and avoid using these
names for properties that refer to subviews in your own custom cell
classes. Doing so may lead to strange bugs when manipulating the sizes
of elements in a cell.
You will rarely ever use the built-in standard UITableViewCell
class.
In almost all cases you will want to create your own types of cells that
have components and layout matching your needs. As with any other view
in UIKit, there are three ways you can design your custom cell type:
within a storyboard itself via prototype cells, creating a separate NIB
via Interface Builder, or programmatically laying out your cell.
All three methods can be broken down into the following steps:
- Design your cell's layout and populate it with UI elements that configurable. This creates a template that can then be configured later on a per-row basis with different data.
- Create a subclass of
UITableViewCell
and associate it with the user interface for the cell. This includes binding properties in your class to UI elements. You will also need to expose a way for users of the cell class to configure the appearance of the cell based on a given row's data. - Register your cell type and give it a reuse identifier.
- Dequeue a cell instance using the reuse identifier and configure it to match a row's data.
We'll continue our previous example by creating a custom cell that has two separate labels with different font sizes for the city name and state initials.
To use prototype cells you must be in the Interface Builder and have already
placed a table view in your view controller. In order to create a prototype
cell, you simply drag a Table View Cell
from the Object Library onto your table
view. You can now layout and add objects your prototype cell as you would with
any other view.
Once you are satisfied with the design of your cell, you must create a custom
class and associate it with your UI template. Select File -> New -> File... -> iOS -> Source -> Cocoa Touch Class
. Create your class as a subclass of
UITableViewCell
. You must now associate your class with your prototype cell.
In the storyboard editor, select your prototype cell and then select the
Identity Inspector. Set the Custom Class
property of the prototype cell to
the name of the class you just created.
You will now be able to select your custom cell class in the Assistant Editor (tuxedo mode) and connect IBOutlets from your prototype cell into your class as you would with any other view. Note that you must select the "content view" of your prototype cell in order for your custom cell class to show up under the Assistant Editor's automatic matching.
One you are satisfied with the design of your cell and the corresponding code in your custom class, you must register your cell for reuse by providing it with a reuse identifier. In the storyboard editor, select your prototype cell and then select the Attributes Inspector. Set the Identifier field (Reuse Identifier) to a unique string that can be used to identify this type of cell.
You can now use this identifier when calling dequeueReusableCell(withIdentifier: for:)
in your implementation of cellForRowAt indexPath:
.
import UIKit
class DemoPrototypeCell: UITableViewCell {
@IBOutlet weak var cityLabel: UILabel!
@IBOutlet weak var stateLabel: UILabel!
}
// DemoPrototypeCell.h
#import <UIKit/UIKit.h>
@interface DemoPrototypeCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *cityLabel;
@property (weak, nonatomic) IBOutlet UILabel *stateLabel;
@end
Notice that the compiler cannot infer the type of your custom cell class from the reuse identifier and you must explicitly cast the resulting object to the correct class.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "com.codepath.DemoPrototypeCell", for: indexPath) as! DemoPrototypeCell
let cityState = data[indexPath.row].components(separatedBy: ", ")
cell.cityLabel.text = cityState.first
cell.stateLabel.text = cityState.last
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
}
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
// ViewController.m
#import "ViewController.h"
#import "DemoPrototypeCell.h"
@interface ViewController ()
@end
@implementation ViewController
NSArray *data;
- (void)viewDidLoad {
[super viewDidLoad];
data = @[@"New York, NY", @"Los Angeles, CA", @"Chicago, IL", @"Houston, TX",
@"Philadelphia, PA", @"Phoenix, AZ", @"San Diego, CA", @"San Antonio, TX",
@"Dallas, TX", @"Detroit, MI", @"San Jose, CA", @"Indianapolis, IN",
@"Jacksonville, FL", @"San Francisco, CA", @"Columbus, OH", @"Austin, TX",
@"Memphis, TN", @"Baltimore, MD", @"Charlotte, ND", @"Fort Worth, TX"];
self.tableView.dataSource = self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
DemoPrototypeCell *cell = [tableView dequeueReusableCellWithIdentifier:@"com.codepath.DemoPrototypeCell" forIndexPath:indexPath];
NSArray *cityState = [data[indexPath.row] componentsSeparatedByString:@", "];
cell.cityLabel.text = cityState.firstObject;
cell.stateLabel.text = cityState.lastObject;
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return data.count;
}
@end
Putting everything together we get a table that looks like this:
There may be times when you do not want to use prototype cells, but still want to use Interface Builder to layout the design of your custom cell. For example, you may be working on a project without storyboards or you may want to isolate your custom cell's complexity from the rest of your storyboard. In these cases, you will create a separate Interface Builder file (NIB) to contain your custom cell's UI template.
NB: Technically NIB and XIB are different formats that both store
descriptions of UI templates created with Interface Builder. The NIB
format is largely deprecated except in the names of classes and methods
in UIKit. Most files you create with Interface Builder will have the
.xib
extension. We'll use the two names interchangeably throughout
this guide.
The procedure in for working with a separate NIB is almost the same as
working with prototype cells. You still design your cell in Interface
Builder and associate it with a custom cell class that inherits from
UITableViewCell
. The only difference is that you must now explicitly
load your NIB and register it for reuse.
You can create your NIB as you would any other view by going to File -> New -> File... -> iOS -> User Interface -> View
and then later create a
separate class and associate your Interface Builder view with your class
by setting the Custom Class
property as you did with the prototype
cell.
However, most of the time you will want to create your NIB and custom
class at once by selecting File -> New -> File... -> iOS -> Source -> Cocoa Touch Class
. You should then create your class as a subclass of
UITableViewCell
and tick the box marked Also create XIB file
. This
will create a .xib
and .swift
file and automatically sets the Custom Class
property of your table view cell to be the class you just
created.
You can now open the .xib
file in Interface Builder, edit your view
and connect IBOutlets to your custom class using the Assistant Editor
(tuxedo) as you would any other view.
You do not need to set the reuse identifier attribute in Interface Builder as we did for our prototype cell. This is because once you are ready to use your new cell you must explicitly load the NIB and register it for reuse in your view controller:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
let cellNib = UINib(nibName: "DemoNibTableViewCell", bundle: Bundle.main)
tableView.register(cellNib, forCellReuseIdentifier: "com.codepath.DemoNibTableViewCell")
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "com.codepath.DemoNibTableViewCell", for: indexPath) as! DemoNibTableViewCell
let cityState = data[indexPath.row].components(separatedBy: ", ")
cell.cityLabel.text = cityState.first
cell.stateLabel.text = cityState.last
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
}
// ViewController.m
#import "ViewController.h"
#import "DemoNibTableViewCell.h"
@implementation ViewController
NSArray *data;
- (void)viewDidLoad {
[super viewDidLoad];
data = @[@"New York, NY", @"Los Angeles, CA", @"Chicago, IL", @"Houston, TX",
@"Philadelphia, PA", @"Phoenix, AZ", @"San Diego, CA", @"San Antonio, TX",
@"Dallas, TX", @"Detroit, MI", @"San Jose, CA", @"Indianapolis, IN",
@"Jacksonville, FL", @"San Francisco, CA", @"Columbus, OH", @"Austin, TX",
@"Memphis, TN", @"Baltimore, MD", @"Charlotte, ND", @"Fort Worth, TX"];
self.tableView.dataSource = self;
UINib *cellNib = [UINib nibWithNibName:@"DemoNibTableViewCell" bundle: NSBundle.mainBundle];
[self.tableView registerNib:cellNib forCellReuseIdentifier:@"com.codepath.DemoNibTableViewCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
DemoNibTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"com.codepath.DemoNibTableViewCell" forIndexPath:indexPath];
NSArray *cityState = [data[indexPath.row] componentsSeparatedByString:@", "];
cell.cityLabel.text = cityState.firstObject;
cell.stateLabel.text = cityState.lastObject;
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return data.count;
}
@end
By default, your NIB will be in the main resource bundle, although you
may change this in larger projects by editing your build steps. The
code in viewDidLoad
loads your NIB by creating an instance of
UINib and registers it for reuse with the provided reuse
identifier.
Finally, you may work with projects that do not use Interface Builder at
all. In this case, you must lay out your custom cell programmatically.
Create a custom cell class that subclasses UITableViewCell
, but be
sure not to tick the Also create XIB file
checkbox.
In order to be able to register your custom cell for reuse, you must
implement the init(style:reuseIdentifier:)
method
since this is the one that will be called by the UITableView
when
instantiating cells. As with any other UIView
, you can also take
the advantage of other entry points in the view's lifecycle (e.g.
drawRect:
) when programming your custom cell.
Once you are ready to use the cell, you must then register your custom cell class for reuse in your view controller similarly to how we registered the NIB for reuse above:
import UIKit
class DemoProgrammaticTableViewCell: UITableViewCell {
let cityLabel = UILabel(), stateLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initViews()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
initViews()
}
func initViews() {
let (stateRect, cityRect) = frame.insetBy(dx: 10, dy: 10).divided(atDistance: 40, from:.maxXEdge)
cityLabel.frame = cityRect
stateLabel.frame = stateRect
addSubview(cityLabel)
addSubview(stateLabel)
}
}
// DemoProgrammaticTableViewCell.h
#import <UIKit/UIKit.h>
@interface DemoProgrammaticTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *cityLabel;
@property (nonatomic, strong) UILabel *stateLabel;
@end
// DemoProgrammaticTableViewCell.m
#import "DemoProgrammaticTableViewCell.h"
@implementation DemoProgrammaticTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if(self){
[self initViews];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if(self){
[self initViews];
}
return self;
}
- (void)initViews {
CGRect sourceRect = CGRectInset(self.frame, 10, 10);
CGRect cityRect;
CGRect stateRect;
CGRectDivide(sourceRect, &stateRect, &cityRect, 40, CGRectMaxXEdge);
self.cityLabel = [[UILabel alloc] initWithFrame:cityRect];
self.stateLabel = [[UILabel alloc] initWithFrame:stateRect];
[self addSubview:self.cityLabel];
[self addSubview:self.stateLabel];
}
@end
Then you would use this custom table view cell in your view controller:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(DemoProgrammaticTableViewCell.self, forCellReuseIdentifier: "com.codepath.DemoProgrammaticCell")
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "com.codepath.DemoProgrammaticCell", for: indexPath) as! DemoProgrammaticTableViewCell
let cityState = data[indexPath.row].components(separatedBy: ", ")
cell.cityLabel.text = cityState.first
cell.stateLabel.text = cityState.last
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
}
// ViewController.m
#import "ViewController.h"
#import "DemoProgrammaticTableViewCell.h"
@implementation ViewController
NSArray *data;
- (void)viewDidLoad {
[super viewDidLoad];
data = @[@"New York, NY", @"Los Angeles, CA", @"Chicago, IL", @"Houston, TX",
@"Philadelphia, PA", @"Phoenix, AZ", @"San Diego, CA", @"San Antonio, TX",
@"Dallas, TX", @"Detroit, MI", @"San Jose, CA", @"Indianapolis, IN",
@"Jacksonville, FL", @"San Francisco, CA", @"Columbus, OH", @"Austin, TX",
@"Memphis, TN", @"Baltimore, MD", @"Charlotte, ND", @"Fort Worth, TX"];
self.tableView.dataSource = self;
[self.tableView registerClass:[DemoProgrammaticTableViewCell class] forCellReuseIdentifier:@"com.codepath.DemoProgrammaticCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
DemoProgrammaticTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"com.codepath.DemoProgrammaticCell" forIndexPath:indexPath];
NSArray *cityState = [data[indexPath.row] componentsSeparatedByString:@", "];
cell.cityLabel.text = cityState.firstObject;
cell.stateLabel.text = cityState.lastObject;
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return data.count;
}
@end
Depending on design you may want the height of rows in your table to be fixed across all cells or to vary depending on the content of the cells. There are a few pitfalls to aware of when manipulating the height of rows in a table.
One of the implementation strategies that keeps UITableViews
performant is avoiding instantiating and laying out cells that are not
currently on the screen. However, in order to compute some geometries
(e.g. how long the scrollbar segment is and how quickly it scrolls down
your screen), iOS needs to have at least an estimate of the total size
of your table. Thus one of the goals when specifying the height of your
rows is to defer if possible performing the layout and configuration
logic for each cell until it needs to appear on the screen.
If you want all the cells in your table to the same height you should
set the rowHeight
property on your UITableView
. You
should not implement the heightForRowAtIndexPath:
method in your UITableViewDelegate
.
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.rowHeight = 100
}
...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.rowHeight = 100;
}
...
@end
There are two ways to have different row heights on a per cell basis.
If project is targeted only for iOS 8 and above, you can simply have
Auto Layout adjust your row heights as necessary. In other cases, you
will need to manually compute the height of each row in your
UITableViewDelegate
.
One way to help iOS defer computing the height of each row until the
user scrolls the table is to set the
estimatedRowHeight
property on your
UITableView
to the height you expect a typical cell to have. This is
especially useful if you have a large number of rows and are relying on
Auto Layout to resolve your row heights or if computing the height of
each row is a non-trivial operation.
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.estimatedRowHeight = 100
}
...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.estimatedRowHeight = 100;
}
...
@end
If your estimate is wildly incorrect or if you have extremely variable
row heights, you may find the behavior and sizing of the scroll bar
to be less than satisfactory. In this case, you may want to implement
the
estimatedHeightForRowAtIndexPath:
method in your UITableViewDelegate
. This is rare in practice and is
only useful if you have a way of estimating the individual row heights
that are significantly faster than computing the exact height.
If you are targeting exclusively iOS 8 and above you can take advantage
of a new feature that has Auto Layout compute the height of rows for
you. You should add Auto Layout constraints to your cell's content view
so that the total height of the content view is driven by the intrinsic
content size of your variable height elements (e.g. labels). You
simply need then to set your UITableView
's rowHeight
property to the
value UITableViewAutomaticDimension
and provide an estimated row
height.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
}
...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
...
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.estimatedRowHeight = 100;
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
...
@end
In other situations, you will need to manually compute the height of each row in your table and provide it to UITableView
by implementing the
heightForRowAtIndexPath:
method in your
UITableViewDelegate
.
If you are using Auto Layout, you may wish to still have Auto Layout
figure out the row height for you. One way you accomplish this is to
instantiate a reference cell that is not in your view hierarchy and use
it to compute the height of each row after configuring it with the data
for the row. You can call layoutSubviews
and
systemLayoutSizeFittingSize
to obtain the size
height that would be produced by Auto Layout. A more detailed
discussion of this technique can be found
here.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var referenceCell: DemoNibTableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.estimatedRowHeight = 50
let cellNib = UINib(nibName: "DemoNibTableViewCell", bundle: Bundle.main)
tableView.register(cellNib, forCellReuseIdentifier: "com.codepath.DemoNibTableViewCell")
referenceCell = cellNib.instantiateWithOwner(nil, options: nil).first as DemoNibTableViewCell
referenceCell.frame = tableView.frame // makes reference cell have the same width as the table
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cityState = data[indexPath.row].componentsSeparatedByString(", ")
referenceCell.cityLabel.text = cityState.first
referenceCell.stateLabel.text = cityState.last
referenceCell.layoutSubviews()
return referenceCell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
...
DemoNibTableViewCell *referenceCell;
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.estimatedRowHeight = 50;
UINib *cellNib = [UINib nibWithNibName:@"DemoNibTableViewCell" bundle: NSBundle.mainBundle];
[self.tableView registerNib:cellNib forCellReuseIdentifier:@"com.codepath.DemoNibTableViewCell"];
referenceCell = [cellNib instantiateWithOwner:nil options:nil].firstObject;
referenceCell.frame = self.tableView.frame;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSArray *cityState = [data[indexPath.row] componentsSeparatedByString:@", "];
referenceCell.cityLabel.text = cityState.firstObject;
referenceCell.stateLabel.text = cityState.lastObject;
[referenceCell layoutSubviews];
return [referenceCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}
...
@end
In some cases, the height of your cell may be entirely dominated by one
or more elements so that you are able to figure out the row height by
simply doing some arithmetic and without actually needing to lay out the
subviews. For example, if the height of you row is determined by an
image thumbnail with some padding around it, you can simply return the
value of the size of the image added to the appropriate padding. In
many cases, the height of your row will be determined by the height of
the text in one or more labels. In these cases, you can compute the the space a piece of text will occupy without actually rendering it by calling NSString
's boundingRectWithSize
method. A discussion of how to do this in Swift can be found
here
UITableViewCell
and every subclass of you create comes built-in
with an accessory view that can be useful for displaying a status
indicator or small control to the right of your cell's main content
view. If the accessory view is visible, the size content view will be
shrunk to accommodate it. If you plan on using accessory views, be sure
the elements in your content view are configured to properly resize when
the width available to them changes.
You can use either the built-in accessory views via the
accessoryType
property or use any UIView
by setting
the accessoryView
property. You should not set
both properties.
There are a few built-in accessory views that can be activated by
setting the accessoryType
property on your
UITableViewCell
. By default this value is .none
. Returning to
our prototype cell example, you can see what each accessory type looks
like below.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
...
let accessoryTypes: [UITableViewCellAccessoryType] = [.none, .DisclosureIndicator, .DetailDisclosureButton, .Checkmark, .DetailButton]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("com.codepath.DemoPrototypeCell", forIndexPath: indexPath) as DemoPrototypeCell
let cityState = data[indexPath.row].componentsSeparatedByString(", ")
cell.cityLabel.text = cityState.first
cell.stateLabel.text = cityState.last
cell.accessoryType = accessoryTypes[indexPath.row % accessoryTypes.count]
return cell
}
...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
...
NSArray *accessoryTypes;
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
...
accessoryTypes = @[@(UITableViewCellAccessoryNone), @(UITableViewCellAccessoryDisclosureIndicator),
@(UITableViewCellAccessoryDetailDisclosureButton), @(UITableViewCellAccessoryCheckmark),
@(UITableViewCellAccessoryDetailButton)];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
DemoPrototypeCell *cell = [tableView dequeueReusableCellWithIdentifier:@"com.codepath.DemoPrototypeCell" forIndexPath:indexPath];
NSArray *cityState = [data[indexPath.row] componentsSeparatedByString:@", "];
cell.cityLabel.text = cityState.firstObject;
cell.stateLabel.text = cityState.lastObject;
UITableViewCellAccessoryType accessoryType = [accessoryTypes[indexPath.row % accessoryTypes.count] intValue];
cell.accessoryType = accessoryType;
return cell;
}
...
@end
If you use the .DetailDisclosureButton
or .DetailButton
accessory
types you can handle the event of a tap on your button by implementing
the accessoryButtonTappedForRowWithIndexPath method
in your UITableViewDelegate
.
You can use any UIView
—including custom ones—as an
accessory view by setting the accessoryView
property on
your UITableViewCell
. You should be aware of the same performance
considerations regarding the creation of UIViews
per row when using
this feature. Also, note that if you want to handle any events from a
custom accessory view, you will have to implement your own event
handling logic (see how to propagate events below). For more complex
situations, you might opt to simply include this custom "accessory view"
as part of your main content view.
Setup your prototype cell as follows:
class DemoPrototypeCell: UITableViewCell {
@IBOutlet weak var cityLabel: UILabel!
@IBOutlet weak var stateLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
}
}
// DemoPrototypeCell.m
#import "DemoPrototypeCell.h"
@implementation DemoPrototypeCell
- (void)awakeFromNib {
[super awakeFromNib];
self.accessoryView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 30, 30)];
}
@end
Next, modify the accessory view's background color in the view controller:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("com.codepath.DemoPrototypeCell", forIndexPath: indexPath) as DemoPrototypeCell
let cityState = data[indexPath.row].componentsSeparatedByString(", ")
cell.cityLabel.text = cityState.first
cell.stateLabel.text = cityState.last
let greyLevel = CGFloat(indexPath.row % 5) / 5.0
cell.accessoryView?.backgroundColor = UIColor(white: greyLevel, alpha: 1)
return cell
}
...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
DemoPrototypeCell *cell = [tableView dequeueReusableCellWithIdentifier:@"com.codepath.DemoPrototypeCell" forIndexPath:indexPath];
NSArray *cityState = [data[indexPath.row] componentsSeparatedByString:@", "];
cell.cityLabel.text = cityState.firstObject;
cell.stateLabel.text = cityState.lastObject;
CGFloat greyLevel = (indexPath.row % 5)/5.0;
cell.accessoryView.backgroundColor = [UIColor colorWithWhite:greyLevel alpha:1];
return cell;
}
...
@end
Rows in a UITableView
can be grouped under section headers. You can
control how many sections are in the table and how many rows are
in each section by implementing the
numberOfSections
and the
numberOfRowsInSection
methods respectively in
our UITableViewDataSource
. You would then need your
cellForRowAtIndexPath
implementation to
support multiple sections and return the correct row under the correct
section specified by the indexPath
.
You can control the view displayed for a section header by implementing
viewForHeaderInSection
and returning a
UITableViewHeaderFooterView
configured with data
specific to the section. Although, you might opt to implement the simpler
titleForHeaderInSection:
if you are OK with the default
styling.
Each of the concepts and methods we discussed for using
UITableViewCells
has an analogue for UITableViewHeaderFooterViews
.
-
registerNib:forHeaderFooterViewReuseIdentifier:
andregisterClass:forHeaderFooterViewReuseIdentifier:
can be used to registerUITableViewHeaderFooterViews
for reuse -
dequeueReusableHeaderFooterViewWithIdentifier:
is used to obtain aUITableViewHeaderFooterView
instance from the reusable views pool - We can implement custom header views by creating a NIB and subclassing
UITableViewHeaderFooterViews
. - We can customize the height of our header views by implementing
heightForHeaderInSection:
NB: The above discussion regarding section headers applies equally to footers by replacing "header" with "footer" throughout.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
let data = [("Arizona", ["Phoenix"]),
("California", ["Los Angeles", "San Francisco", "San Jose", "San Diego"]),
("Florida", ["Miami", "Jacksonville"]),
("Illinois", ["Chicago"]),
("New York", ["Buffalo", "New York"]),
("Pennsylvania", ["Pittsburg", "Philadelphia"]),
("Texas", ["Houston", "San Antonio", "Dallas", "Austin", "Fort Worth"])]
let CellIdentifier = "TableViewCell", HeaderViewIdentifier = "TableViewHeaderView"
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier)
tableView.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: HeaderViewIdentifier)
}
func numberOfSections(in tableView: UITableView) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data[section].1.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as UITableViewCell
let citiesInSection = data[indexPath.section].1
cell.textLabel?.text = citiesInSection[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterViewWithIdentifier(HeaderViewIdentifier) as! UITableViewHeaderFooterView
header.textLabel.text = data[section].0
return header
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 30
}
}
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
// ViewController.m
#import "ViewController.h"
@implementation ViewController
NSArray *data;
NSString *CellIdentifier = @"TableViewCell";
NSString *HeaderViewIdentifier = @"TableViewHeaderView";
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.delegate = self;
data = @[@[@"Arizona", @[@"Phoenix"]], @[@"California", @[@"Los Angeles", @"San Francisco", @"San Jose", @"San Diego"]], @[@"Florida", @[@"Miami", @"Jacksonville"]], @[@"Illinois", @[@"Chicago"]], @[@"New York", @[@"Buffalo", @"New York"]], @[@"Pennsylvania", @[@"Pittsburg", @"Philadelphia"]], @[@"Texas", @[@"Houston", @"San Antonio", @"Dallas", @"Austin", @"Fort Worth"]]];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
[self.tableView registerClass:[UITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:HeaderViewIdentifier];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return data.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[data[section] lastObject] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSArray *citiesInSection = [data[indexPath.section] lastObject];
cell.textLabel.text = citiesInSection[indexPath.row];
return cell;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UITableViewHeaderFooterView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:HeaderViewIdentifier];
header.textLabel.text = [data[section] firstObject];
return header;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 30;
}
@end
The above code can produce two different behaviors depending on whether
our UITableView
is configured to have the Plain
style or the
Grouped
style. Plain
is on the left and Grouped
is on the right
below. Notice that the section header sticks at the top of the table
while we are still scrolling within the section.
The table view section style can be changed in Interface Builder under the Attributes Inspector or can be set when the table view is initialized if it is created programmatically.
UITableView
and UITableViewCell
have several built-in facilities for
responding to a cell being selected a cell and changing a cell's visual
appearance when it is selected.
To respond to a cell being selected you can implement
didSelectRowAtIndexPath:
in your
UITableViewDelegate
. Here is one way we can implement a simple
checklist:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
let CellIdentifier = "TableCellView"
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
var checked: [Bool]!
override func viewDidLoad() {
super.viewDidLoad()
checked = [Bool](count: data.count, repeatedValue: false)
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
checked[indexPath.row] = !checked[indexPath.row]
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath)
cell.textLabel?.text = data[indexPath.row]
if checked[indexPath.row] {
cell.accessoryType = .Checkmark
} else {
cell.accessoryType = .none
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
}
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
// ViewController.m
#import "ViewController.h"
@implementation ViewController
NSArray *data;
NSString *CellIdentifier = @"TableCellView";
bool checked[];
- (void)viewDidLoad {
[super viewDidLoad];
data = @[@"New York, NY", @"Los Angeles, CA", @"Chicago, IL", @"Houston, TX",
@"Philadelphia, PA", @"Phoenix, AZ", @"San Diego, CA", @"San Antonio, TX",
@"Dallas, TX", @"Detroit, MI", @"San Jose, CA", @"Indianapolis, IN",
@"Jacksonville, FL", @"San Francisco, CA", @"Columbus, OH", @"Austin, TX",
@"Memphis, TN", @"Baltimore, MD", @"Charlotte, ND", @"Fort Worth, TX"];
checked[data.count] = false;
self.tableView.dataSource = self;
self.tableView.delegate = self;
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:true];
checked[indexPath.row] = !checked[indexPath.row];
[tableView reloadRowsAtIndexPaths:[[NSArray alloc] initWithObjects: indexPath, nil] withRowAnimation:UITableViewRowAnimationAutomatic];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = data[indexPath.row];
if (checked[indexPath.row]){
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else{
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return data.count;
}
@end
Notice that we deselect the cell immediately after selection event.
Selection is not a good way to store or indicate
state. Also, notice that we reload the row once
we've modified our checked
data model. This necessary so that the
table knows to reconfigure and rerender the corresponding cell to have a
checkmark. More info on handling updates to your
data can be found below.
There are several ways the UITableViewCell
itself can respond to a
selection event. The most basic is setting the
selectionStyle
. In particular, the value
.none
can be useful here—though you should set the flag
allowsSelection on your UITableView
if you wish to
disable selection globally.
You can have a cell change its background when selected by setting the
selectedBackgroundView property. You can also
respond programmatically to the selection event by overriding the
setSelected
method in your custom cell class.
import UIKit
class DemoProgrammaticTableViewCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initViews()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initViews()
}
func initViews() {
selectedBackgroundView = UIView(frame: frame)
selectedBackgroundView.backgroundColor = UIColor(red: 0.5, green: 0.7, blue: 0.9, alpha: 0.8)
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
let fontSize: CGFloat = selected ? 34.0 : 17.0
self.textLabel?.font = self.textLabel?.font.fontWithSize(fontSize)
}
}
// DemoProgrammaticTableViewCell.h
#import <UIKit/UIKit.h>
@interface DemoProgrammaticTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *cityLabel;
@property (nonatomic, strong) UILabel *stateLabel;
@end
// DemoProgrammaticTableViewCell.m
#import "DemoProgrammaticTableViewCell.h"
@implementation DemoProgrammaticTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if(self){
[self initViews];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if(self){
[self initViews];
}
return self;
}
- (void)initViews {
self.selectedBackgroundView = [[UIView alloc] initWithFrame:self.frame];
self.selectedBackgroundView.backgroundColor = [UIColor colorWithRed:0.5 green:0.7 blue:0.9 alpha:0.8];
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated{
[super setSelected:selected animated:animated];
CGFloat fontSize = selected ? 34.0 : 17.0;
self.textLabel.font = [self.textLabel.font fontWithSize:fontSize];
}
@end
When a user taps on one of the cells in your tableView, there are a couple of events that fire:
- Highlighted => This happens when the user first touches down on the cell (touch down).
- Selected => This happens when the user releases his or her finger from the cell (touch up).
In iOS, you can customize what happens for both of these events.
To customize what happens for the Selected event, you can use one of the following options:
-
Set the cell selectionStyle property. This allows you to set the color to gray or have no color at all.
// No color when the user selects cell cell.selectionStyle = .none
cell.selectionStyle = UITableViewCellSelectionStyleNone;
-
Set a custom selectedBackgroundView. This gives you full control to create a view and set it as the cell's
selectedBackgroundView
.// Use a red color when the user selects the cell let backgroundView = UIView() backgroundView.backgroundColor = UIColor.red cell.selectedBackgroundView = backgroundView
UIView *backgroundView = [[UIView alloc] init]; backgroundView.backgroundColor = UIColor.redColor; cell.selectedBackgroundView = backgroundView;
In order to discuss some topics relating to working with tables that load data from a network resource we present an example application that fetches the top stories from the New York Times' news feed and presents them to the user in a table view.
Our setup is almost the same as in the custom prototype
cell example above. We've created a prototype
cell and an associated custom class StoryCell
that can display a
single headline and possibly an associated image. We've also added a
model class Story
that also handles our network request and response
parsing logic. More on making network requests can be found in the
basic network programming guide.
Here is the view controller:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
var stories: [Story] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
Story.fetchStories({ (stories: [Story]) -> Void in
dispatch_async(dispatch_get_main_queue(), {
self.stories = stories
self.tableView.reloadData()
})
}, error: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("StoryCell") as StoryCell
cell.story = stories[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stories.count
}
}
// ViewController.m
#import "ViewController.h"
#import "Story.h"
#import "StoryCell.h"
@implementation ViewController
NSArray *stories;
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
[Story fetchStories:^(NSArray *storiesArray) {
stories = storiesArray;
[self.tableView reloadData];
} error:^(NSError *error) {
}];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
StoryCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StoryCell" forIndexPath:indexPath];
cell.story = stories[indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return stories.count;
}
@end
Here is the table view cell defined:
import UIKit
class StoryCell: UITableViewCell {
@IBOutlet weak var thumbnailView: UIImageView!
@IBOutlet weak var headlineLabel: UILabel!
var story: Story? {
didSet {
headlineLabel?.text = story?.headline
headlineLabel?.sizeToFit()
}
}
}
// StoryCell.h
#import <UIKit/UIKit.h>
#import "Story.h"
@interface StoryCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UIImageView *thumbnailView;
@property (weak, nonatomic) IBOutlet UILabel *headlineLabel;
@property (strong, nonatomic) Story *story;
@end
// StoryCell.m
#import "StoryCell.h"
@implementation StoryCell
@synthesize story = _story;
-(void)setStory:(Story *)story{
_story = story;
self.headlineLabel.text = story.headline;
[self.headlineLabel sizeToFit];
}
Here is the model for parsing from the network:
import UIKit
private let apiKey = "53eb9541b4374660d6f3c0001d6249ca:19:70900879"
private let resourceUrl = URL(string: "http://api.nytimes.com/svc/topstories/v1/home.json?api-key=\(apiKey)")!
class Story {
var headline: String?
var thumbnailUrl: String?
init(jsonResult: NSDictionary) {
if let title = jsonResult["title"] as? String {
headline = title
}
if let multimedia = jsonResult["multimedia"] as? NSArray {
// 4th element is will contain the image of the right size
if multimedia.count >= 4 {
if let mediaItem = multimedia[3] as? NSDictionary {
if let type = mediaItem["type"] as? String {
if type == "image" {
if let url = mediaItem["url"] as? String{
thumbnailUrl = url
}
}
}
}
}
}
}
class func fetchStories(successCallback: ([Story]) -> Void, error: ((NSError?) -> Void)?) {
NSURLSession.sharedSession().dataTaskWithURL(resourceUrl, completionHandler: {(data, response, requestError) -> Void in
if let requestError = requestError? {
error?(requestError)
} else {
if let data = data? {
let json = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as NSDictionary
if let results = json["results"] as? NSArray {
var stories: [Story] = []
for result in results as [NSDictionary] {
stories.append(Story(jsonResult: result))
}
successCallback(stories)
}
} else {
// unexepected error happened
error?(nil)
}
}
}).resume()
}
}
// Story.h
#import <Foundation/Foundation.h>
@interface Story : NSObject
@property (nonatomic, strong) NSString *headline;
@property (nonatomic, strong) NSString *thumbnailUrl;
+ (void)fetchStories:(void(^)(NSArray *))successCallback error:(void(^)(NSError *))error;
@end
// Story.m
#import "Story.h"
@implementation Story
NSString *apiKey = @"53eb9541b4374660d6f3c0001d6249ca:19:70900879";
- (void)initWithDictionary:(NSDictionary *)dictionary {
self.headline = dictionary[@"title"];
NSArray *multimedia = dictionary[@"multimedia"];
if([multimedia isEqual:@""] != YES && multimedia.count >= 4)
{
NSDictionary *mediaItem = multimedia[3];
NSString *type = mediaItem[@"type"];
if([type isEqualToString:@"image"])
{
NSString *url = mediaItem[@"url"];
self.thumbnailUrl = url;
}
}
}
+ (void)fetchStories:(void(^)(NSArray *))successCallback error:(void(^)(NSError *))error{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *resourceUrl = [NSString stringWithFormat: @"http://api.nytimes.com/svc/topstories/v1/home.json?api-key=%@",apiKey];
NSURL *URL = [NSURL URLWithString:resourceUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *requestError) {
if (requestError != nil) {
error(requestError);
}
else
{
NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSArray *results = responseObject[@"results"];
if (results) {
NSMutableArray *stories = [[NSMutableArray alloc] init];
for (NSDictionary *result in results) {
Story *story = [[Story alloc] init];
[story initWithDictionary:result];
[stories addObject:story];
}
successCallback(stories);
}
}
}];
[dataTask resume];
}
@end
to be completed...
to be completed...
Pull-to-refresh is a ubiquitous method to update your list with the latest information. Apple provides developers with UIRefreshControl
, as a standard way to implement this behavior. As UIRefreshControl
is for lists, it can be used with UITableView
, UICollectionView
, and UIScrollView
.
###Using a Basic UIRefreshControl
Using a basic UIRefreshControl
requires the following steps:
- Initialize an instance of UIRefreshControl
- Implement an action to handle updating your table view
- Bind the action to the instance of UIRefreshControl
- Insert the UIRefreshControl as a subview of your table view
####Initializing a UIRefreshControl
In order to implement a UIRefreshControl
, we need an instance of UIRefreshControl
. Update the viewDidLoad
function to include the following code:
override func viewDidLoad() {
super.viewDidLoad()
// Initialize a UIRefreshControl
let refreshControl = UIRefreshControl()
}
- (void)viewDidLoad {
[super viewDidLoad];
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
}
We need to implement an action to update our list. It's common to fire a network request to get updated data, so that is shown in the example below. Note: Make sure to call tableView.reloadData
and refreshControl.endRefreshing
after updating the data source.
// Makes a network request to get updated data
// Updates the tableView with the new data
// Hides the RefreshControl
func refreshControlAction(_ refreshControl: UIRefreshControl) {
// ... Create the URLRequest `myRequest` ...
// Configure session so that completion handler is executed on main UI thread
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)
let task: URLSessionDataTask = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
// ... Use the new data to update the data source ...
// Reload the tableView now that there is new data
myTableView.reloadData()
// Tell the refreshControl to stop spinning
refreshControl.endRefreshing()
}
task.resume()
}
// Makes a network request to get updated data
// Updates the tableView with the new data
// Hides the RefreshControl
- (void)beginRefresh:(UIRefreshControl *)refreshControl {
// Create NSURL and NSURLRequest
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
session.configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// ... Use the new data to update the data source ...
// Reload the tableView now that there is new data
[self.tableView reloadData];
// Tell the refreshControl to stop spinning
[refreshControl endRefreshing];
}];
[task resume];
}
With the action implemented, it now needs to be bound to the UIRefreshControl
so that something will happen when you pull-to-refresh.
override func viewDidLoad() {
super.viewDidLoad()
// Initialize a UIRefreshControl
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshControlAction(_:)), for: UIControlEvents.valueChanged)
}
- (void)viewDidLoad {
[super viewDidLoad];
// Initialize a UIRefreshControl
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:@selector(beginRefresh:) forControlEvents:UIControlEventValueChanged];
}
####Insert the refresh control into the list
The UIRefreshControl
now needs to be added to the table view.
override func viewDidLoad() {
super.viewDidLoad()
// Initialize a UIRefreshControl
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshControlAction(_:)), for: UIControlEvents.valueChanged)
// add refresh control to table view
tableView.insertSubview(refreshControl, at: 0)
}
- (void)viewDidLoad {
[super viewDidLoad];
// Initialize a UIRefreshControl
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:@selector(beginRefresh:) forControlEvents:UIControlEventValueChanged];
[self.tableView insertSubview:refreshControl atIndex:0];
}
###Understanding a UIRefreshControl
The UIRefreshControl
is a subclass of UIView
, and consequently a view, so it can be added as a subview to another view. It's behavior and interaction is already built in. UIRefreshControl
is a subclass of UIControl
, and pulling down on the parent view will initiate a UIControlEventValueChanged
event. When the event is triggered, a bound action will be fired. In this action, we update the data source and reset the UIRefreshControl
.
###Creating a custom pull-to-refresh For now, see the following [reference][customRefreshControl] [customRefreshControl]: http://www.jackrabbitmobile.com/design/ios-custom-pull-to-refresh-control/
###Gotchas The following are common mistakes:
- After pulling to refresh, nothing changes? Did you forget
tableView.reloadData
?
- Is the UI changing in a strange sequence or the wrong order? Did you update the UI on the main thread (
dispatch_async(dispatch_get_main_queue())
)?
###Further notes
Apple documentation does specify that UIRefreshControl
is only meant to be used with a UITableViewController
where it is included, however, we have not seen any problems in practice before.
Because the refresh control is specifically designed for use in a table view that's managed by a table view controller, using it in a different context can result in undefined behavior.
to be completed...
As a user is going through the table view, they will eventually reach the end. When they reach the end, the service may still have more data for them. In order to provide the user with a smooth experience, and avoid pagination, we implement infinite scrolling to a table view. There's two ways we can accomplish this:
##1. Using the willDisplayCell
method
Inorder to implement Infinite Scrolling, you’ll need to have a method that detects when you’re at the bottom cell in the tableview, UITableView has a method that we can use to achieve this. Add the following method and its contents to your view controller:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath:IndexPath) {
if indexPath.row + 1 == self.data.count {
loadMoreTweets()
}
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if(indexPath.row + 1 == [self.data count]){
[self loadMoreData:[self.data count] + 20];
}
}
Don’t forget to reload your tableview.
##2. Using a UIScrollViewDelegate
Implementing infinite scroll behavior involves the following:
- Detect when to request more data
- Request more data and update UI accordingly
- Create a progress indicator for user to know when data is being requested
- Update progress to indicate with UI when data is being requested and loaded
Here is a sample image of what we're going to make:
###Detecting when to request more data
In order to detect when to request more data, we need to use the UIScrollViewDelegate
. UITableView
is a subclass of UIScrollView
and therefore can be treated as a UIScrollView
. For UIScrollView
, we use the UIScrollViewDelegate
to perform actions when scrolling events occur. We want to detect when a UIScrollView
has scrolled, so we use the method scrollViewDidScroll
.
Add UIScrollViewDelegate
to the protocol list and add the scrollViewDidScroll
function:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {
// ...
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Handle scroll behavior here
}
}
// ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
// ViewController.m
#import "ViewController.h"
@implementation ViewController
// ...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// Handle scroll behavior here
}
@end
Understanding scrollViewDidScroll
When a user scrolls down, the UIScrollView
continuously fires scrollViewDidScroll
after the UIScrollView
has changed. This means that whatever code is inside scrollViewDidScroll
will repeatedly fire. In order to avoid 10s or 100s of requests to the server, we need to indicate when the app has already made a request to the server.
Create a flag called isMoreDataLoading
, and add a check in scrollViewDidScroll
:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {
var isMoreDataLoading = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (!isMoreDataLoading) {
isMoreDataLoading = true
// ... Code to load more results ...
}
}
}
// ViewController.m
#import "ViewController.h"
@interface ViewController()
@property (assign, nonatomic) BOOL isMoreDataLoading;
@end
@implementation ViewController
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if(!self.isMoreDataLoading){
self.isMoreDataLoading = true;
// ... Code to load more results ...
}
}
@end
Ideally, for infinite scrolling, we want to load more results before the user reaches the end of the existing content, and therefore would start loading at some point past halfway. However, for this specific tutorial, we will load more results when the user reaches near the end of the existing content.
We need to know how far down a user has scrolled in the UITableView
. The property contentOffset
of UIScrollView
tells us how far down or to the right the user has scrolled in a UIScrollView
. The property contentSize
tells us how much total content there is in a UIScrollView
. We will define a variable scrollOffsetThreshold
for when we want to trigger requesting more data - one screen length before the end of the results.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (!isMoreDataLoading) {
// Calculate the position of one screen length before the bottom of the results
let scrollViewContentHeight = tableView.contentSize.height
let scrollOffsetThreshold = scrollViewContentHeight - tableView.bounds.size.height
// When the user has scrolled past the threshold, start requesting
if(scrollView.contentOffset.y > scrollOffsetThreshold && tableView.isDragging) {
isMoreDataLoading = true
// ... Code to load more results ...
}
}
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if(!self.isMoreDataLoading){
// Calculate the position of one screen length before the bottom of the results
int scrollViewContentHeight = self.tableView.contentSize.height;
int scrollOffsetThreshold = scrollViewContentHeight - self.tableView.bounds.size.height;
// When the user has scrolled past the threshold, start requesting
if(scrollView.contentOffset.y > scrollOffsetThreshold && self.tableView.isDragging) {
self.isMoreDataLoading = true;
// ... Code to load more results ...
}
}
}
@end
Define a separate function to request more data, and call this function in scrollViewDidScroll
:
func loadMoreData() {
// ... Create the NSURLRequest (myRequest) ...
// Configure session so that completion handler is executed on main UI thread
let session = URLSession(configuration: URLSessionConfiguration.default,
delegate:nil,
delegateQueue:OperationQueue.main
)
let task : URLSessionDataTask = session.dataTask(with: myRequest, completionHandler: { (data, response, error) in
// Update flag
self.isMoreDataLoading = false
// ... Use the new data to update the data source ...
// Reload the tableView now that there is new data
self.myTableView.reloadData()
})
task.resume()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (!isMoreDataLoading) {
// Calculate the position of one screen length before the bottom of the results
let scrollViewContentHeight = tableView.contentSize.height
let scrollOffsetThreshold = scrollViewContentHeight - tableView.bounds.size.height
// When the user has scrolled past the threshold, start requesting
if(scrollView.contentOffset.y > scrollOffsetThreshold && tableView.isDragging) {
isMoreDataLoading = true
// Code to load more results
loadMoreData()
}
}
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
-(void)loadMoreData{
// ... Create the NSURLRequest (myRequest) ...
// Configure session so that completion handler is executed on main UI thread
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *requestError) {
if (requestError != nil) {
}
else
{
// Update flag
self.isMoreDataLoading = false;
// ... Use the new data to update the data source ...
// Reload the tableView now that there is new data
[self.tableView reloadData];
}
}];
[task resume];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if(!self.isMoreDataLoading){
// Calculate the position of one screen length before the bottom of the results
int scrollViewContentHeight = self.tableView.contentSize.height;
int scrollOffsetThreshold = scrollViewContentHeight - self.tableView.bounds.size.height;
// When the user has scrolled past the threshold, start requesting
if(scrollView.contentOffset.y > scrollOffsetThreshold && self.tableView.isDragging) {
isMoreDataLoading = true;
[self loadMoreData];
}
}
}
@end
When you run the app, you should see new data load into the table view, each time you reach the bottom. But there's a problem: there's no indicator to the user that anything is happening.
To indicate to the user that something is happening, we need to create a progress indicator.
There are a number of different ways a progress indicator can be added. Many cocoapods are available to provide this. However, for this guide, we'll create a custom view class and add it to the tableView's view hierarchy. Below is the code to create a custom view class that can be used as a progress indicator.
We make use of Apple's UIActivityIndicatorView
to show a loading spinner, that can be switched on and off.
class InfiniteScrollActivityView: UIView {
var activityIndicatorView: UIActivityIndicatorView = UIActivityIndicatorView()
static let defaultHeight:CGFloat = 60.0
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupActivityIndicator()
}
override init(frame aRect: CGRect) {
super.init(frame: aRect)
setupActivityIndicator()
}
override func layoutSubviews() {
super.layoutSubviews()
activityIndicatorView.center = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2)
}
func setupActivityIndicator() {
activityIndicatorView.activityIndicatorViewStyle = .gray
activityIndicatorView.hidesWhenStopped = true
self.addSubview(activityIndicatorView)
}
func stopAnimating() {
self.activityIndicatorView.stopAnimating()
self.isHidden = true
}
func startAnimating() {
self.isHidden = false
self.activityIndicatorView.startAnimating()
}
}
// InfiniteScrollActivityView.h
#import <UIKit/UIKit.h>
@interface InfiniteScrollActivityView : UIView
@property (class, nonatomic, readonly) CGFloat defaultHeight;
- (void)startAnimating;
- (void)stopAnimating;
@end
// InfiniteScrollActivityView.m
#import "InfiniteScrollActivityView.h"
@implementation InfiniteScrollActivityView
UIActivityIndicatorView* activityIndicatorView;
static CGFloat _defaultHeight = 60.0;
+ (CGFloat)defaultHeight{
return _defaultHeight;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if(self){
[self setupActivityIndicator];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(self){
[self setupActivityIndicator];
}
return self;
}
- (void)layoutSubviews{
[super layoutSubviews];
activityIndicatorView.center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
}
- (void)setupActivityIndicator{
activityIndicatorView = [[UIActivityIndicatorView alloc] init];
activityIndicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
activityIndicatorView.hidesWhenStopped = true;
[self addSubview:activityIndicatorView];
}
-(void)stopAnimating{
[activityIndicatorView stopAnimating];
self.hidden = true;
}
-(void)startAnimating{
self.hidden = false;
[activityIndicatorView startAnimating];
}
@end
Now that we have a progress indicator, we need to load it when more data is being requested from the server.
For this example, use the class InfiniteScrollActivityView
, and add it to your view controller to use for later.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {
var isMoreDataLoading = false
var loadingMoreView:InfiniteScrollActivityView?
// ...
}
// ViewController.m
#import "ViewController.h"
@implementation ViewController
bool isMoreDataLoading = false;
InfiniteScrollActivityView* loadingMoreView;
// ...
@end
In viewDidLoad
, initialize and add the loading indicator to the tableView's view hierarchy. Then add new insets to allow room for seeing the loading indicator at the bottom of the tableView.
override func viewDidLoad() {
super.viewDidLoad()
// Set up Infinite Scroll loading indicator
let frame = CGRect(x: 0, y: tableView.contentSize.height, width: tableView.bounds.size.width, height: InfiniteScrollActivityView.defaultHeight)
loadingMoreView = InfiniteScrollActivityView(frame: frame)
loadingMoreView!.isHidden = true
tableView.addSubview(loadingMoreView!)
var insets = tableView.contentInset
insets.bottom += InfiniteScrollActivityView.defaultHeight
tableView.contentInset = insets
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set up Infinite Scroll loading indicator
CGRect frame = CGRectMake(0, self.tableView.contentSize.height, self.tableView.bounds.size.width, InfiniteScrollActivityView.defaultHeight);
loadingMoreView = [[InfiniteScrollActivityView alloc] initWithFrame:frame];
loadingMoreView.hidden = true;
[self.tableView addSubview:loadingMoreView];
UIEdgeInsets insets = self.tableView.contentInset;
insets.bottom += InfiniteScrollActivityView.defaultHeight;
self.tableView.contentInset = insets;
}
Update the scrollViewDidScroll
function to load the indicator:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (!isMoreDataLoading) {
// Calculate the position of one screen length before the bottom of the results
let scrollViewContentHeight = tableView.contentSize.height
let scrollOffsetThreshold = scrollViewContentHeight - tableView.bounds.size.height
// When the user has scrolled past the threshold, start requesting
if(scrollView.contentOffset.y > scrollOffsetThreshold && tableView.isDragging) {
isMoreDataLoading = true
// Update position of loadingMoreView, and start loading indicator
let frame = CGRect(x: 0, y: tableView.contentSize.height, width: tableView.bounds.size.width, height: InfiniteScrollActivityView.defaultHeight)
loadingMoreView?.frame = frame
loadingMoreView!.startAnimating()
// Code to load more results
loadMoreData()
}
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if(!isMoreDataLoading){
// Calculate the position of one screen length before the bottom of the results
int scrollViewContentHeight = self.tableView.contentSize.height;
int scrollOffsetThreshold = scrollViewContentHeight - self.tableView.bounds.size.height;
// When the user has scrolled past the threshold, start requesting
if(scrollView.contentOffset.y > scrollOffsetThreshold && self.tableView.isDragging) {
isMoreDataLoading = true;
// Update position of loadingMoreView, and start loading indicator
CGRect frame = CGRectMake(0, self.tableView.contentSize.height, self.tableView.bounds.size.width, InfiniteScrollActivityView.defaultHeight);
loadingMoreView.frame = frame;
[loadingMoreView startAnimating];
// Code to load more results
[self loadMoreData];
}
}
}
Finally, update the loadMoreData
function to stop the indicator, when the request has finished downloading:
func loadMoreData() {
// ... Create the NSURLRequest (myRequest) ...
// Configure session so that completion handler is executed on main UI thread
let session = URLSession(configuration: URLSessionConfiguration.default,
delegate:nil,
delegateQueue:OperationQueue.main
)
let task : URLSessionDataTask = session.dataTask(with: myRequest, completionHandler: { (data, response, error) in
// Update flag
self.isMoreDataLoading = false
// Stop the loading indicator
self.loadingMoreView!.stopAnimating()
// ... Use the new data to update the data source ...
// Reload the tableView now that there is new data
self.myTableView.reloadData()
})
task.resume()
}
-(void)loadMoreData{
// ... Create the NSURLRequest (myRequest) ...
// Configure session so that completion handler is executed on main UI thread
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *requestError) {
if (requestError != nil) {
}
else
{
// Update flag
self.isMoreDataLoading = false;
// Stop the loading indicator
[loadingMoreView stopAnimating];
// ... Use the new data to update the data source ...
// Reload the tableView now that there is new data
[self.tableView reloadData];
}
}];
[task resume];
}
Thanks to SVPullToRefresh for providing source code and a general flow to follow for this basic version of an infinite scroll.
Infinite Scroll can also be implemented with the following cocoapod: SVPullToRefresh
To be added...
to be completed...
First, set the separator inset to zero on the tableview.
class MyTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.separatorInset = UIEdgeInsetsZero
}
}
@implementation MyTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.separatorInset = UIEdgeInsetsZero;
}
@end
Then, in your cell, disable the margins, and the margins it may inherit from parent views.
class MyTableViewCell: UITableViewCell
override func awakeFromNib() {
super.awakeFromNib()
self.layoutMargins = UIEdgeInsetsZero
self.preservesSuperviewLayoutMargins = false
}
}
@implementation MyTableViewCell
- (void)awakeFromNib {
[super awakeFromNib];
self.layoutMargins = UIEdgeInsetsZero;
self.preservesSuperviewLayoutMargins = false;
}
@end