-
Notifications
You must be signed in to change notification settings - Fork 473
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
dataSourceproperty must be set to an object that implements theUITableViewDataSourceprotocol. This object is responsible for the content of the table including providing the actualUITableViewCellsthat will be shown. -
The
delegateproperty must be set to an object that implements theUITableViewDelegateprotocol. 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;
}
@endProvided 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) -> Intis responsible for telling theUITableViewhow many rows are in each section of the table. Since we only have one section, we simply return the length of ourdataarray 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 thesection` that was passed in. -
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellis responsible for returning a preconfigured cell that will be used to render the row in the table specified by theindexPath. TheindexPathidentifies a specific row in a specific section of the table the via theindexPath.sectionandindexPath.row. Since we are only working with one section, we can ignoresectionfor 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;
}
@endIn 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
UITableViewbefore 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
cellForRowAtwe 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 ofUITableViewCelland 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-inUITableViewCellgives us access to itstextLabelso 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
UITableViewCelland 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;
@endNotice 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;
}
@endPutting 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];
}
@endThen 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;
}
@endDepending 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;
}
...
@endThere 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;
}
...
@endIf 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;
}
...
@endIn 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;
}
...
@endIn 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;
}
...
@endIf 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)];
}
@endNext, 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;
}
...
@endRows 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 registerUITableViewHeaderFooterViewsfor reuse -
dequeueReusableHeaderFooterViewWithIdentifier:is used to obtain aUITableViewHeaderFooterViewinstance 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;
}
@endThe 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;
}
@endNotice 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];
}
@endWhen 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;
}
@endHere 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
}
@endUnderstanding 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 ...
}
}
@endIdeally, 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;
// ...
@endIn 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


















