こんにちは!Popoです。
こんな方へのお勧めの記事です。
- UITableViewでアコーディオンのように開閉する画面を作成してみたい
- 3階層以上のアコーディオン画面を作成してみたい
- UITableViewの応用機能を学習したい
前回記事で、UITableView応用編という事でサイドメニューを作成してみました。
今回もさらに応用編として、3階層のアコーディオン画面を作成してみたいと思います。
アコーディオン画面については他の方も色々記事にされていました。
3階層以上のアコーディオン画面を紹介されている方がほとんどいなかったので記事にしてみました。
アプリの動作環境
今回のアプリ動作環境です。
項目 | バージョン |
Xcode | Version 14.3.1 (14E300c) |
Swift | Swift version 5.8.1 |
MacOS | macOS Ventura バージョン13.4(22F66) |
アプリの設計
はじめにアプリ開発の方針について説明をしてみたいと思います。
アコーディオンの考え方
- ①:1階層目はUITableViewのヘッダーを利用します。
- ②:ヘッダーを選択すると、2階層目が表示されます。これはセルが表示される事になります。
- ③:さらにセルを選択する事で、3階層目を表示させます。
- 3階層目以降のアコーディオンはセルの高さを変更して、あたかもセル数が増えているように見せていきます。
表示用配列の考え方
構造体を利用して配列を作成します。
- 「oneArray」という配列に「oneHeader」という構造体を格納します。
- 「oneHeader」構造体の配下に「cellDetaile」という配列の構造体を設定します。
実際の配列の中身です。1配列目だけ詳細を表示しています。
「headerName」「cellName」「subName」の名称で3階層を構成しています。
アプリのソースコード全体
では、ソースコードを見ていきましょう!!
UIViewController (OneViewController)
主な機能
- ナビゲーションバーカスタマイズ
- UITableView生成
- 表示用配列作成
- UITableViewDataSourceメソッド
- UITableViewヘッダー選択時の制御
import UIKit
class OneViewController: UIViewController {
//header構造体
struct oneHeader {
var isShown: Bool
var headerName: String
var detaileArray:[cellDetaile]
}
//cell構造体
struct cellDetaile {
var isShown: Bool
var cellName: String
var subName: String
}
//表示用配列
fileprivate var oneArray:[oneHeader] = []
let SCREEN_WIDTH = UIScreen.main.bounds.width
let SCREEN_HEIGHT = UIScreen.main.bounds.height
var navigationBarHeight: CGFloat = 44
var statusHieght:CGFloat = 0
var cellCounter:Int = 0
//UITableView
fileprivate var oneTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Popo"
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont(name: "HiraginoSans-W6",size:17)!,
.baselineOffset:1
]
// iOS15以降の場合
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .brown
appearance.titleTextAttributes = attrs
self.navigationController?.navigationBar.scrollEdgeAppearance = appearance
//UITableView
self.oneTableView = UITableView.init(frame: CGRect.zero, style: .grouped)
let dummyView:UIView = UIView()
dummyView.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.oneTableView.tableFooterView = dummyView
self.oneTableView.tableHeaderView = dummyView
self.oneTableView.sectionFooterHeight = 0.0
//境界線を全て消す
self.oneTableView.separatorStyle = .none
//背景色
self.oneTableView.backgroundColor = RGBAlpa(238, 245, 243, 1)
self.oneTableView.frame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: SCREEN_HEIGHT)
// DataSourceの設定をする.
self.oneTableView.dataSource = self
// Delegateを設定する.
self.oneTableView.delegate = self
UITableView.appearance().separatorInset = UIEdgeInsets.zero
UITableViewCell.appearance().separatorInset = UIEdgeInsets.zero
self.view.addSubview(self.oneTableView)
//表示用配列生成
for index1 in 0 ..< 10
{
let index1String:String = String(index1)
let saveHeaderName:String = "headerName " + index1String
let saveCellName:String = "cellName " + index1String
var saveDetaileArray:[cellDetaile] = []
for index2 in 0 ..< 5
{
let index2String:String = String(index2)
let saveSubName:String = "subName " + index2String
//let saveOneArray:oneHeader = oneHeader(isShown: false, headerName: saveHeaderName, detaileArray: [cellDetaile(isShown: false, cellName: saveCellName,subName: saveSubName)])
saveDetaileArray.append(cellDetaile(isShown: false, cellName: saveCellName,subName: saveSubName))
}
let saveOneArray:oneHeader = oneHeader(isShown: false, headerName: saveHeaderName, detaileArray: saveDetaileArray)
self.oneArray.append(saveOneArray)
}
}
}
//MARK: UITableView
extension OneViewController: UITableViewDelegate, UITableViewDataSource
{
/*
セクション数を返すデータソースメソッド.
(実装必須)
*/
func numberOfSections(in tableView: UITableView) -> Int
{
return self.oneArray.count
}
/*
Cellの総数を返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if self.oneArray[section].isShown
{
return self.oneArray[section].detaileArray.count
} else {
return 0
}
}
/*
ヘッダーの高さを返すデータソースメソッド.
*/
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
return 30
}
/*
ヘッダーのViewを返すデータソースメソッド.
*/
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
//header用View
let headerView:UIView = UIView()
headerView.frame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: 30)
headerView.backgroundColor = .white
//headerLabel
let headerLabel:UILabel = UILabel()
headerLabel.frame = CGRect(x: 35, y: 0, width: SCREEN_WIDTH - 80, height:30)
headerLabel.textAlignment = .left//整列
headerLabel.font = UIFont(name: "HiraKakuProN-W6",size:17)!//文字サイズ
headerLabel.textColor = RGBAlpa(138,138,138,1)//文字色
headerLabel.text = self.oneArray[section].headerName
headerLabel.numberOfLines = 0
headerLabel.backgroundColor = .white
headerView.addSubview(headerLabel)
let gesture = UITapGestureRecognizer(target: self,action: #selector(headertapped(sender:)))
headerView.addGestureRecognizer(gesture)
headerView.tag = section
return headerView
}
/*
フッターのViewを返すデータソースメソッド.
*/
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
return CGFloat.leastNormalMagnitude
}
/*
Cellの高さを返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
let saveIsShow:Bool = self.oneArray[indexPath.section].detaileArray[indexPath.row].isShown
if saveIsShow
{
return 60
} else {
return 30
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let workCellName : String! = NSString(format:"MyCell%d_%d",self.cellCounter,self.cellCounter) as String
self.cellCounter = self.cellCounter + 1
self.oneTableView.register(UITableViewCell.self, forCellReuseIdentifier: workCellName)
let cell = self.oneTableView.dequeueReusableCell(withIdentifier: workCellName, for: indexPath)
//これでセルをタップ時、色は変化しなくなる
cell.selectionStyle = UITableViewCell.SelectionStyle.none
//二重に表示されるのを防ぐ
for subview in cell.contentView.subviews{
subview.removeFromSuperview()
}
let saveSection:Int = indexPath.section
let saveIndex:Int = indexPath.row
if self.oneArray[saveSection].detaileArray[saveIndex].isShown
{
//header用View
let cellView:UIView = UIView()
cellView.frame = CGRect(x: 70, y: 0, width: SCREEN_WIDTH - 70, height: 60)
cellView.backgroundColor = .white
//cellLabel
let cellLabel:UILabel = UILabel()
cellLabel.frame = CGRect(x: 0, y: 0, width: cellView.frame.width, height:30)
cellLabel.textAlignment = .left//整列
cellLabel.font = UIFont(name: "HiraKakuProN-W6",size:17)!//文字サイズ
cellLabel.textColor = RGBAlpa(138,138,138,1)//文字色
cellLabel.text = self.oneArray[saveSection].detaileArray[saveIndex].cellName
cellLabel.numberOfLines = 0
cellLabel.backgroundColor = .white
cellView.addSubview(cellLabel)
//subLabel
let subLabel:UILabel = UILabel()
subLabel.frame = CGRect(x: 35, y: cellLabel.frame.height, width: cellView.frame.width - 35, height:30)
subLabel.textAlignment = .left//整列
subLabel.font = UIFont(name: "HiraKakuProN-W6",size:17)!//文字サイズ
subLabel.textColor = RGBAlpa(138,138,138,1)//文字色
subLabel.text = self.oneArray[saveSection].detaileArray[saveIndex].subName
subLabel.numberOfLines = 0
subLabel.backgroundColor = .white
cellView.addSubview(subLabel)
cell.contentView.addSubview(cellView)
} else {
//header用View
let cellView:UIView = UIView()
if self.oneArray[saveSection].isShown
{
cellView.frame = CGRect(x: 70, y: 0, width: SCREEN_WIDTH - 70, height: 30)
cellView.backgroundColor = .white
//cellLabel
let cellLabel:UILabel = UILabel()
cellLabel.frame = CGRect(x: 0, y: 0, width: cellView.frame.width, height:30)
cellLabel.textAlignment = .left//整列
cellLabel.font = UIFont(name: "HiraKakuProN-W6",size:17)!//文字サイズ
cellLabel.textColor = RGBAlpa(138,138,138,1)//文字色
cellLabel.text = self.oneArray[saveSection].detaileArray[saveIndex].cellName
cellLabel.numberOfLines = 0
cellLabel.backgroundColor = .white
cellView.addSubview(cellLabel)
} else {
cellView.frame = CGRect(x: 70, y: 0, width: SCREEN_WIDTH - 70, height: 0)
}
cell.contentView.addSubview(cellView)
}
return cell
}
/*
Cellが選択された際に呼び出されるデリゲートメソッド.
*/
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.oneTableView?.deselectRow(at: indexPath, animated: true)
let saveSection:Int = indexPath.section
let saveIndex:Int = indexPath.row
//Boolを反転
self.oneArray[saveSection].detaileArray[saveIndex].isShown.toggle()
//選択セルだけ更新
self.oneTableView.beginUpdates()
self.oneTableView.reloadSections([saveSection], with: .automatic)
self.oneTableView.endUpdates()
}
}
//MARK: UITableView Header タップ
extension OneViewController
{
//Header Click
@objc func headertapped(sender: UITapGestureRecognizer) {
guard let section = sender.view?.tag else {
return
}
//Boolを反転
self.oneArray[section].isShown.toggle()
self.oneTableView.beginUpdates()
self.oneTableView.reloadSections([section], with: .automatic)
self.oneTableView.endUpdates()
}
}
画面動作
アコーディオンの動作はこんな感じになります。
各ロジック解説
各処理の解説を行なっていきます。
viewDidLoad
- ナビゲーションバーカスタマイズ
今までのカスタマイズと同じです。
self.navigationItem.title = “Popo”
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont(name: “HiraginoSans-W6”,size:17)!,
.baselineOffset:1
]
// iOS15以降の場合
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .brown
appearance.titleTextAttributes = attrs
self.navigationController?.navigationBar.scrollEdgeAppearance = appearance
- UITableView生成
UITableViewも今までの通りですが、スタイルを「.grouped」の指定にしています。
//UITableView
self.oneTableView = UITableView.init(frame: CGRect.zero, style: .grouped)
let dummyView:UIView = UIView()
dummyView.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.oneTableView.tableFooterView = dummyView
self.oneTableView.tableHeaderView = dummyView
self.oneTableView.sectionFooterHeight = 0.0
//境界線を全て消す
self.oneTableView.separatorStyle = .none
//背景色
self.oneTableView.backgroundColor = RGBAlpa(238, 245, 243, 1)
self.oneTableView.frame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: SCREEN_HEIGHT)
// DataSourceの設定をする.
self.oneTableView.dataSource = self
// Delegateを設定する.
self.oneTableView.delegate = self
UITableView.appearance().separatorInset = UIEdgeInsets.zero
UITableViewCell.appearance().separatorInset = UIEdgeInsets.zero
self.view.addSubview(self.oneTableView)
- 表示用配列作成
「isShown」という項目で、各階層の表示・非表示を制御します。
- 「isShown」がtrueで表示
- 「isShown」がfalse非表示
//header構造体
struct oneHeader {
var isShown: Bool
var headerName: String
var detaileArray:[cellDetaile]
}
//cell構造体
struct cellDetaile {
var isShown: Bool
var cellName: String
var subName: String
}
ループのカウンタを利用して「headerName」「cellName」「subName」名称を設定します。
//表示用配列生成
for index1 in 0 ..< 10
{
let index1String:String = String(index1)
let saveHeaderName:String = “headerName ” + index1String
let saveCellName:String = “cellName ” + index1String
var saveDetaileArray:[cellDetaile] = []
for index2 in 0 ..< 5
{
let index2String:String = String(index2)
let saveSubName:String = “subName ” + index2String
//let saveOneArray:oneHeader = oneHeader(isShown: false, headerName: saveHeaderName, detaileArray: [cellDetaile(isShown: false, cellName: saveCellName,subName: saveSubName)])
saveDetaileArray.append(cellDetaile(isShown: false, cellName: saveCellName,subName: saveSubName))
}
let saveOneArray:oneHeader = oneHeader(isShown: false, headerName: saveHeaderName, detaileArray: saveDetaileArray)
self.oneArray.append(saveOneArray)
}
UITableViewDataSourceメソッド
UITableViewDataSourceメソッドについては下記記事を参考にしてください。
- セクション数、セル数
- 「func numberOfSections(in tableView: UITableView) -> Int」
セクション数はoneArrayの配列数になります。 - 「func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int」
セル選択時の開閉をisShownで制御しています。
oneArrayのdetaileArray配列数がセル数になり、「isShown」がfalse非表示時は、高さをZEROで返却してやります。
/*
セクション数を返すデータソースメソッド.
(実装必須)
*/
func numberOfSections(in tableView: UITableView) -> Int
{
return self.oneArray.count
}
/*
Cellの総数を返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if self.oneArray[section].isShown
{
return self.oneArray[section].detaileArray.count
} else {
return 0
}
}
- ヘッダーの高さ、ヘッダーView返却
- 「func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat」
ヘッダーの高さは「30」にしています。(お好きな高さに) - 「func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?」
oneArrayの「headerName」をUILabelに設定し、UIViewに貼り付けて返却しています。
/*
ヘッダーの高さを返すデータソースメソッド.
*/
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
return 30
}
/*
ヘッダーのViewを返すデータソースメソッド.
*/
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
//header用View
let headerView:UIView = UIView()
headerView.frame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: 30)
headerView.backgroundColor = .white
//headerLabel
let headerLabel:UILabel = UILabel()
headerLabel.frame = CGRect(x: 35, y: 0, width: SCREEN_WIDTH – 80, height:30)
headerLabel.textAlignment = .left//整列
headerLabel.font = UIFont(name: “HiraKakuProN-W6”,size:17)!//文字サイズ
headerLabel.textColor = RGBAlpa(138,138,138,1)//文字色
headerLabel.text = self.oneArray[section].headerName
headerLabel.numberOfLines = 0
headerLabel.backgroundColor = .white
headerView.addSubview(headerLabel)
let gesture = UITapGestureRecognizer(target: self,action: #selector(headertapped(sender:)))
headerView.addGestureRecognizer(gesture)
headerView.tag = section
return headerView
}
- フッターView返却
フッターは表示させたくないので、「CGFloat.leastNormalMagnitude」で指定してやります。
/*
フッターのViewを返すデータソースメソッド.
*/
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
return CGFloat.leastNormalMagnitude
}
下記を参考に!
- セルの高さ
detaileArrayのisShowmで高さを変更しています。
- 開いた場合は「60」
- 閉じた場合は「30」
セルの高さで3階層目の表示制御を行います。
/*
Cellの高さを返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
let saveIsShow:Bool = self.oneArray[indexPath.section].detaileArray[indexPath.row].isShown
if saveIsShow
{
return 60
} else {
return 30
}
}
- セル内容表示
今回は、メソッド内で直接UIView、UILabelを生成しました。
下記の制御を行なっています。
if self.oneArray[saveSection].detaileArray[saveIndex].isShown
{
2階層目、3階層目とも表示。
} else {
if self.oneArray[saveSection].isShown
{
3階層目は閉じて、2階層目は表示。
} else {
2階層目、3階層目とも閉じる
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let workCellName : String! = NSString(format:”MyCell%d_%d”,self.cellCounter,self.cellCounter) as String
self.cellCounter = self.cellCounter + 1
self.oneTableView.register(UITableViewCell.self, forCellReuseIdentifier: workCellName)
let cell = self.oneTableView.dequeueReusableCell(withIdentifier: workCellName, for: indexPath)
//これでセルをタップ時、色は変化しなくなる
cell.selectionStyle = UITableViewCell.SelectionStyle.none
//二重に表示されるのを防ぐ
for subview in cell.contentView.subviews{
subview.removeFromSuperview()
}
let saveSection:Int = indexPath.section
let saveIndex:Int = indexPath.row
if self.oneArray[saveSection].detaileArray[saveIndex].isShown
{
//header用View
let cellView:UIView = UIView()
cellView.frame = CGRect(x: 70, y: 0, width: SCREEN_WIDTH – 70, height: 60)
cellView.backgroundColor = .white
//cellLabel
let cellLabel:UILabel = UILabel()
cellLabel.frame = CGRect(x: 0, y: 0, width: cellView.frame.width, height:30)
cellLabel.textAlignment = .left//整列
cellLabel.font = UIFont(name: “HiraKakuProN-W6”,size:17)!//文字サイズ
cellLabel.textColor = RGBAlpa(138,138,138,1)//文字色
cellLabel.text = self.oneArray[saveSection].detaileArray[saveIndex].cellName
cellLabel.numberOfLines = 0
cellLabel.backgroundColor = .white
cellView.addSubview(cellLabel)
//subLabel
let subLabel:UILabel = UILabel()
subLabel.frame = CGRect(x: 35, y: cellLabel.frame.height, width: cellView.frame.width – 35, height:30)
subLabel.textAlignment = .left//整列
subLabel.font = UIFont(name: “HiraKakuProN-W6”,size:17)!//文字サイズ
subLabel.textColor = RGBAlpa(138,138,138,1)//文字色
subLabel.text = self.oneArray[saveSection].detaileArray[saveIndex].subName
subLabel.numberOfLines = 0
subLabel.backgroundColor = .white
cellView.addSubview(subLabel)
cell.contentView.addSubview(cellView)
} else {
//header用View
let cellView:UIView = UIView()
if self.oneArray[saveSection].isShown
{
cellView.frame = CGRect(x: 70, y: 0, width: SCREEN_WIDTH – 70, height: 30)
cellView.backgroundColor = .white
//cellLabel
let cellLabel:UILabel = UILabel()
cellLabel.frame = CGRect(x: 0, y: 0, width: cellView.frame.width, height:30)
cellLabel.textAlignment = .left//整列
cellLabel.font = UIFont(name: “HiraKakuProN-W6”,size:17)!//文字サイズ
cellLabel.textColor = RGBAlpa(138,138,138,1)//文字色
cellLabel.text = self.oneArray[saveSection].detaileArray[saveIndex].cellName
cellLabel.numberOfLines = 0
cellLabel.backgroundColor = .white
cellView.addSubview(cellLabel)
} else {
cellView.frame = CGRect(x: 70, y: 0, width: SCREEN_WIDTH – 70, height: 0)
}
cell.contentView.addSubview(cellView)
}
return cell
}
- セル選択
- detaileArrayのisShownを反転させます。
(3階層目の開閉を制御) - 選択したセクションを更新します。
/*
Cellが選択された際に呼び出されるデリゲートメソッド.
*/
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.oneTableView?.deselectRow(at: indexPath, animated: true)
let saveSection:Int = indexPath.section
let saveIndex:Int = indexPath.row
//Boolを反転
self.oneArray[saveSection].detaileArray[saveIndex].isShown.toggle()
//選択セルだけ更新
self.oneTableView.beginUpdates()
self.oneTableView.reloadSections([saveSection], with: .automatic)
self.oneTableView.endUpdates()
}
HeaderViewタップイベント
- oneArrayのisShownを反転させます。
(2階層目の開閉を制御) - 選択したセクションを更新します。
//Header Click
@objc func headertapped(sender: UITapGestureRecognizer) {
guard let section = sender.view?.tag else {
return
}
//Boolを反転
self.oneArray[section].isShown.toggle()
self.oneTableView.beginUpdates()
self.oneTableView.reloadSections([section], with: .automatic)
self.oneTableView.endUpdates()
}
まとめ
UITableViewの応用編として、3階層のアコーディオン画面を作成してみました。
4階層以上を作成したい場合は、下記のロジックを追加する必要があると思います。
- 「subName」以降を配列にする
- セルの高さの制御if文を追加する
- セルの内容表示の制御if文を追加する。
- セル選択時の制御を追加する。
また、お気づきかもしれませんが今回のロジックでは、ヘッダーを押下すればセクション配下の開閉が制御できます。
UITableViewを利用して色々な画面が作成できます。
こんな画面を作ってみたいと思っている画面があれば、一度チャレンジしてみてはいかがでしょうか。
それではまた!