iOSでアコーディオンメニューを実装する
何の話?
最近MENTAというサービスでiOS開発初学者のメンターをしているのですが、数ある質問の中でも非常によく聞かれるものがあったのでリンクを渡して説明できるようにここに記す事にします。
こんな感じの、特定のセルをタップしたらその下にヌッと別のセルが出てくるようなUIを実装します。
今回は、fuga
というセルをタップした場合のみfugafuga
というセルが表示されるような実装です。
実行環境
Xcode12.2
Swift5
iOS14.2
対象読者
以下のどれかひとつに当てはまるかた
- XcodeでTableViewの実装はなんとかできる
- CompositionalLayoutを使ってUIcollectionViewでリストを実装したい
実装方法
UITableViewを使う
サポートするOSバージョン問わず使える方法です。
まずStoryboardを使ってViewController
にUITableView
を配置していきます。
全画面になるように配置しましょう。
今回はついでにここでDataSource
とDelegate
の設定もしてしまいます。
UITableViewCell
も一つ配置しておきましょう。
idはcell
とかで大丈夫です。
次に、ViewController.swift
と先ほどのUITableView
をIBOutlet
で繋ぎます。
それができたらまずTableView
にhoge
, fuga
, piyo
が表示されるようにしてみます。
大体こんな感じでしょうか。
import UIKit
class ViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
private var contents = ["hoge", "fuga", "piyo"]
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contents.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = contents[indexPath.row]
return cell
}
}
しっかり表示されていますね。
では次にfuga
をタップしたらfugafuga
が表示されるようにしていきます。
まずセルのタップを検知するdelegateメソッドを追加して、タップされたセルがfugaかどうか判定します。
import UIKit
class ViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
private var contents = ["hoge", "fuga", "piyo"]
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contents.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = contents[indexPath.row]
return cell
}
}
// ここを追加
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if contents[indexPath.row] == "fuga" {
}
}
}
次にfugafugaを表示するメソッドを追加して、先ほどのfugaがタップされた際に呼び出します。
import UIKit
class ViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
private var contents = ["hoge", "fuga", "piyo"]
override func viewDidLoad() {
super.viewDidLoad()
}
// ここを追加
private func showFugaFuga() {
guard contents.count < 4 else { return }
contents.insert("fugafuga", at: 2)
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .automatic)
tableView.endUpdates()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contents.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = contents[indexPath.row]
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if contents[indexPath.row] == "fuga" {
showFugaFuga()
return
}
}
}
fugafuga
を表示するために、まずfuga
がタップされたらTableView
に表示するデータのリストであるcontents
のfuga
のすぐ後にfugafuga
を差し込んでいます。
contents.insert("fugafuga", at: 2)
その後、TableView
にセルを追加して表示しています。
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .automatic)
tableView.endUpdates()
このままではfugafuga
が表示されっぱなしなので、ついでにfugafuga
をタップしたら消えるようにします。
こちらも手順はfugafuga
を削除するメソッドを定義して、タップされたセルがfugafuga
だったら呼び出すようにします。
import UIKit
class ViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
private var contents = ["hoge", "fuga", "piyo"]
override func viewDidLoad() {
super.viewDidLoad()
}
private func showFugaFuga() {
guard contents.count < 4 else { return }
contents.insert("fugafuga", at: 2)
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .automatic)
tableView.endUpdates()
}
// ここを追加
private func hideFugaFuga() {
guard contents.count > 3 else { return }
contents.remove(at: 2)
tableView.beginUpdates()
tableView.deleteRows(at: [IndexPath(row: 2, section: 0)], with: .automatic)
tableView.endUpdates()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contents.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = contents[indexPath.row]
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if contents[indexPath.row] == "fuga" {
showFugaFuga()
return
}
// ここも追加
if contents[indexPath.row] == "fugafuga" {
hideFugaFuga()
return
}
}
}
これで冒頭にあったような画面が実装できます!
UICollectionViewを使う
アプリがサポートするOSがiOS14以降であれば、UICollectionViewを使って同様の機能を実装することが可能です。
まずUICollectionViewを定義します。
今回はStoryboardは使いません。
import UIKit
class ViewController: UIViewController {
private lazy var collectionView: UICollectionView = {
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
}
}
上記のコードを少し解説いたします。
UICollectionLayoutListConfiguration(appearance: .plain)
UICollectionViewCompositionalLayout.list(using: config)
でグルーピングされないフラットなリスト表示をするように設定しています。
ここを.grouped
などに変更するとセクション毎にグルーピングされたリストになります。
collectionView.translatesAutoresizingMaskIntoConstraints = false
これは後ほどAutoLayoutの制約をコードで付与するために書いています。
では次に、CollectionViewの設定をするメソッドを追加していきます。
private func setUpCollectionView() {
view.addSubview(collectionView)
// AutoLayoutの制約を付与
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
// セルを設定
let registration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, string in
var config = cell.defaultContentConfiguration()
config.text = string
cell.contentConfiguration = config
}
}
続いて、CollectionViewに表示するデータを設定します。
import UIKit
class ViewController: UIViewController {
// これを追加
private var dataSource: UICollectionViewDiffableDataSource<Section, String>?
private lazy var collectionView: UICollectionView = {
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
setUpCollectionView()
}
private func setUpCollectionView() {
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
let registration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, string in
var config = cell.defaultContentConfiguration()
config.text = string
cell.contentConfiguration = config
}
// ここも追加
dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: collectionView) { cv, indexPath, string in
cv.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: string)
}
}
}
// これも追加
extension ViewController {
private enum Section {
case main
}
}
続いて、CollectionViewに表示するデータを動的に変更するためのメソッドを定義します。
private func update(list: [String]) {
var snapShot = NSDiffableDataSourceSnapshot<Section, String>()
snapShot.appendSections([.main])
snapShot.appendItems(list)
dataSource?.apply(snapShot)
}
これを最初にviewDidLoad
から呼ぶようにして一度実行してみます。
override func viewDidLoad() {
super.viewDidLoad()
setUpCollectionView()
update(list: ["hoge", "fuga", "piyo"])
}
しっかり表示されていますね!
では最後にセルをタップした際のdelegateメソッドを実装して、タップしたセルによって動作を分岐させます。
import UIKit
class ViewController: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, String>?
private lazy var collectionView: UICollectionView = {
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
setUpCollectionView()
update(list: ["hoge", "fuga", "piyo"])
}
private func setUpCollectionView() {
//ここを追加
collectionView.delegate = self
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
let registration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, string in
var config = cell.defaultContentConfiguration()
config.text = string
cell.contentConfiguration = config
}
dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: collectionView) { cv, indexPath, string in
cv.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: string)
}
}
private func update(list: [String]) {
var snapShot = NSDiffableDataSourceSnapshot<Section, String>()
snapShot.appendSections([.main])
snapShot.appendItems(list)
dataSource?.apply(snapShot)
}
}
// ここも追加
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let id = dataSource?.itemIdentifier(for: indexPath) else { return }
collectionView.deselectItem(at: indexPath, animated: true)
if id == "fuga" {
update(list: ["hoge", "fuga", "fugafuga", "piyo"])
return
}
if id == "fugafuga" {
update(list: ["hoge", "fuga", "piyo"])
return
}
}
}
extension ViewController {
private enum Section {
case main
}
}
では実行してみます。
バッチリですね!!
最後までお読みいただきありがとうございました!
Discussion