UITableView — iOSで垂直リストを表示するためのUIKitの主要コンポーネントです。テーブルの各行は、定義済みスタイルを持つUITableViewCellです。UITableViewの基礎(DataSource、デリゲート、セルの再利用、iOSアプリでのテーブルパフォーマンス)について説明します。Apple(Human Interface Guidelines、2026)によると、UITableViewはiOSで最も使用されるUIコンポーネントであり続けており、App Storeトップ100の85%のアプリに少なくとも1つのテーブルが含まれています。複雑なレイアウトについては、UICollectionViewに関する記事をお読みください。
重要なポイント
UITableViewは、1列でスクロール可能な行のリストを表示するために設計されたUIKitフレームワークのクラスです。各行はUITableViewCellオブジェクトで表されます。UITableViewはiPhone OS 2(2008)で初めて登場し、それ以来iOSのリストの主要コンポーネントであり続けています。UITableViewのアーキテクチャはMVCパターンに基づいており、データはDataSource、外観と動作はDelegate、表示はテーブル自体が管理します。
2つのテーブルスタイル:.plain(オプションのセクションを持つ連続リスト)と.grouped(角丸とマージンを持つグループ化セクション)。iOS 13では.insetGroupedが追加されました — 端にマージンを持つグループ化スタイルで、設定とヘルスケアアプリで使用されています。スタイルの選択はデフォルトの外観に影響します。plainテーブルはスクロール時にセクションヘッダーを上部に固定します(スティッキーヘッダー)が、groupedテーブルは固定しません。
iOS 8以降、UITableViewはセルフサイジングセルをサポートしています。有効にするには、tableView.estimatedRowHeight(例:80)とtableView.rowHeight = UITableView.automaticDimensionを設定します。テーブルはセル内のAuto Layout制約に基づいて行の高さを自動計算します。セルフサイジングは複雑なセルではパフォーマンスが低下するため、500以上の要素があるテーブルでは固定高さ(rowHeight)を使用してください。
UITableViewCellは、カスタムセルを作成しなくても80%のシナリオをカバーする4つの組み込みスタイルを提供します。各スタイルは、textLabel、detailTextLabel、imageViewの組み合わせで構成されます。スタイルはinit(style:reuseIdentifier:)でセルを初期化する際に選択します。
| スタイル | textLabel | detailTextLabel | imageView | 例 |
|---|---|---|---|---|
| .default (.basic) | 左、太字 | なし | オプション | メニュー、項目リスト |
| .subtitle | 左、太字 | textLabelの下、グレー | オプション | 連絡先、プレイリスト |
| .value1 | 左、太字 | 右、グレー | なし | 値付き設定 |
| .value2 | 右、青 | 左、グレー | なし | 電話帳(iOS 6スタイル) |
// カスタムセルでテーブルを作成
class ContactListController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 60
view.addSubview(tableView)
// Auto Layout
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension ContactListController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
contacts.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let contact = contacts[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = contact.name
content.secondaryText = contact.phone
content.image = UIImage(systemName: "person.circle")
cell.contentConfiguration = content
return cell
}
}
UIContentConfiguration(iOS 14+) — textLabel/detailTextLabelに直接アクセスする代わりに、contentConfigurationを介してセルを設定するモダンな方法です。cell.defaultContentConfiguration()を使用するか、カスタムのUIContentConfigurationを作成します。利点:Dynamic Type、ダークモード、VoiceOverの組み込みサポート。Apple(WWDC 2024)は、新しいUITableViewすべてにcontentConfigurationを推奨しています。
UITableViewDataSource — テーブルにデータを提供するプロトコル。必須メソッド:tableView(_:numberOfRowsInSection:)とtableView(_:cellForRowAt:)。これらがないと、テーブルは1行も表示できません。UITableViewDelegate — 外観と動作を管理するプロトコル:行の高さ、選択、編集、コンテキストアクション。
// 複数のセクションと編集の例
extension ContactListController: UITableViewDelegate {
// 各行のカスタム高さ
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// 選択処理
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Swipe-to-delete
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "削除") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
デリゲートのパフォーマンス:サイズメソッド(heightForRowAt)は、スクロールのたびに表示されている各行に対して呼び出されます。均一な高さの場合は、直接rowHeightを設定してください — これはheightForRowAtよりも10倍高速です。セルフサイジングセルの場合は、estimatedRowHeightとautomaticDimensionを設定します — テーブルは表示されている行に対してのみheightForRowAtを呼び出し、残りにはestimatedRowHeightを使用します。
再利用キュー — 再利用可能なUITableViewセルのプールです。セルが画面外にスクロールすると、削除されずにキューに入れられます。新しいセルはゼロから作成されず、システムはdequeueReusableCell(withIdentifier:for:)を呼び出し、キューからセルを返すか、キューが空の場合は新しいセルを作成します。これがUITableViewの主要なパフォーマンスメカニズムです。
// キャッシュ付きカスタムセル
class ContactCell: UITableViewCell {
private let avatarView = UIImageView()
private let nameLabel = UILabel()
private let subtitleLabel = UILabel()
override init(style: UITableViewCell.Style, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(avatarView)
contentView.addSubview(nameLabel)
contentView.addSubview(subtitleLabel)
// Auto Layout制約の設定
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constraints ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// アバターの非同期読み込み
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// 登録と使用
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// cellForRowAt内:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — セルを再利用キューに入れる前に呼び出されるメソッドです。テキスト、画像、アニメーション、ローディング操作など、すべてのセルの状態をリセットしてください。prepareForReuseがないと、再利用されたセルが古いデータを表示する可能性があります:新しい画像がロードされるまで、前の画像が一瞬表示されます。Apple(WWDC 2023)は、prepareForReuseでURLSessionTask.cancel()を介して非同期操作をキャンセルすることを推奨しています。
セクションはUITableViewで論理的に関連する行をグループ化します。各セクションにはヘッダーとフッターを設定できます。セクション数はnumberOfSections(in:)メソッドで決定されます(デフォルトは1)。groupedおよびinsetGroupedスタイルでは、セクションはマージンと角丸で視覚的に区切られます。
| DataSourceメソッド | 説明 | 戻り値 |
|---|---|---|
| numberOfSections | セクション数(デフォルト1) | Int |
| numberOfRowsInSection | セクション内の行数 | Int |
| titleForHeaderInSection | セクションヘッダーのタイトルテキスト | String? |
| titleForFooterInSection | セクションフッターのタイトルテキスト | String? |
| viewForHeaderInSection | ヘッダー用のカスタムビュー | UIView? |
| heightForHeaderInSection | ヘッダーの高さ | CGFloat |
インデックス:セクション間のクイックナビゲーションのために、右側に表示される文字列の配列であるインデックスタイトルを追加します。sectionIndexTitles(for:)を実装すると、各セクションの最初の文字の配列が返されます。26以上のセクションがある場合、これによりUXが大幅に向上します。iOS 15+では、テーブルはsectionHeaderTopPaddingをサポートしています — ステータスバーと最初のヘッダーの間のパディングで、密集したレイアウトのために0にリセットできます。
パフォーマンスは、長いリストを持つアプリにとってUITableViewのパフォーマンスが重要です。一般的な問題:cellForRowAtでの重い処理による遅いスクロール(ジャンク)、頻繁なheightForRowAt呼び出し、prefetchingの欠如。Apple(WWDC 2025)は、1000以上の要素を持つテーブルのための5つの主要なプラクティスを挙げています。
// Prefetchingによる最適化
extension ContactListController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.prefetch(url: contact.avatarUrl)
}
}
func tableView(_ tableView: UITableView,
cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.cancelPrefetch(url: contact.avatarUrl)
}
}
}
// スムーズな更新のためのDiffableDataSourceの使用
class ModernTableController: UIViewController {
private var dataSource: UITableViewDiffableDataSource<Section, Contact>!
func applyContacts(_ contacts: [Contact]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
実用的な結果:これらの最適化を適用すると、iPhone 12以降のデバイスでスクロールFPSが30から60に向上します(Appleのテストデータ、2025年による)。IT Sectrのプロジェクトでは、すべての新しいテーブルにDiffableDataSourceを使用し、画像(アバター、プレビュー、アプリアイコン)を含むテーブルにはprefetchingを追加しています。
よくある質問
確認事項:(1)contentSize > frame.size — テーブルは高さよりも多くのコンテンツを持つ必要があります。(2)テーブルがジェスチャーをインターセプトする別のUIScrollView内にないこと。(3)isScrollEnabled = true。(4)numberOfRowsInSectionが正しい数を返すこと。典型的な問題は、estimatedRowHeightなしでheightForRowAt = UITableView.automaticDimensionを使用しているテーブルで、無限の高さ計算を引き起こします。
reloadData()の代わりにperformBatchUpdatesまたはDiffableDataSourceを使用してください。reloadData()はアニメーションなしで完全に再読み込みを行い、視覚的なちらつきを引き起こします。対象を絞った更新には、reloadRows(at:with:)、insertRows、deleteRowsを使用します。performBatchUpdates内のすべての変更は同時にアニメーション化されます。DiffableDataSourceは手動呼び出しなしでこれを自動的に行います。
UITableViewは、定義済みのセルスタイルと組み込みの編集機能を備えた単一カラムの垂直リストです。UICollectionViewは、柔軟なレイアウト(グリッド、リスト、カルーセル、ウォーターフォール)で要素を任意に配置できます。シンプルなリスト(設定、連絡先、メッセージ)にはUITableViewを選びます。ギャラリー、カタログ、ボード、および非線形レイアウトにはUICollectionViewを選びます。
tableView.tableFooterView = UIView(frame: .zero)を設定します。デフォルトでは、UITableViewは最後のアイテムの下から下端まで空の行(区切り線)を表示します。空のUIViewをtableFooterViewとして設定すると、これらが削除されます。groupedスタイルの場合は不要です — テーブルは最後のセクションの角丸のみで実際の行のみを表示します。
tableView.rowHeight = 80とtableView.estimatedRowHeight = 0を設定するか、estimatedRowHeightを省略します。固定高さの場合、UITableViewはheightForRowAtを呼び出さないため、100以上のアイテムがあるリストのレンダリングが5~10倍高速化されます。カスタム高さ(異なるコンテンツ)のセルには、セルフサイジング(estimatedRowHeight + UITableView.automaticDimension)を使用します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。