複数のカスタムしたUITableViewCellをUITableViewで表示させる
表と言ってもさまざまな形のものがありますよね。
今回はそんな表をiOSで扱った時のお話です。
目次
はじめに
こんにちは、自称SwiftUI愛好家、クラウドソリューショングループのtakinami.sです。
今回はSwiftUIではなく、UIKitに関する記事です。
iOSアプリ開発において、表と言えばUITableViewですよね。
iPhoneの設定画面のようないくつも項目が並んだ画面を簡単に作成することができる素晴らしいクラスです。
しかし、今回はどうしてもデフォルトのままでは実現できなさそうな表の作成が求められたので、カスタムしてみました。
動作確認環境
Xcode 14.3.1
Swift v5.8.1
どのようにしたか
表の元となるのはDictionary型のデータです。
valueとしてさらにDictionary型でデータが格納されているときにはタイトルとして指定した文言をセットし、valueがString型やInt型の時に指定した項目名とvalueを横並びで表示させます。
今回はサンプルとしてこのようなデータを表示させてみたいと思います。
let usersFavorite = [
[
"id": "1",
"name": "ブログ書き太郎",
"favorite": [
"fruits": "りんご",
"number": 3,
"subject": "国語"
]
],
[
"id": "2",
"name": "ブログ読み次郎",
"favorite": [
"fruits": "みかん",
"number": 100,
"subject": "数学"
]
]
]
そして、作成した結果の完成イメージがこちらになります。
元データにはない『飲み物』の項目がありますが、こちらは後ほど触れます。
画面レイアウトの準備
まず、StoryBoardで画面レイアウトを決めていきましょう。
ViewControllerにUITableViewを配置します。
ViewControllerとUITableViewを紐づけておくのも忘れないようにしましょう。
@IBOutlet weak var displayFavoriteTableView: UITableView!
さらに、今回はカスタムしたCellが必要なので、新しくxibファイルとUITableViewCellファイルをそれぞれ2つ追加します。
タイトル用のCellがこちらです。UILabelを1つ配置し、Cellの背景色を設定しました。
import UIKit
class TopicTableViewCell: UITableViewCell {
@IBOutlet weak var topicLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
value用のCellがこちらです。
項目名とvalueは並べて表示する必要があるため、UILabelを2つ横並びに配置しました。
import UIKit
class DataTableViewCell: UITableViewCell {
@IBOutlet weak var logicNameLabel: UILabel!
@IBOutlet weak var valueLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
データの準備
次に、ViewController元となるデータを配列に格納していきます。Dictionary型のままUITableVIewCellに表示させようとすると順番が保証されずバラバラで表示されてしまうため、また要件として指定した文言をタイトルや項目名として使用することが指定されているためになります。
またDictionary型からkeyを元にvalueを取り出すと、Optional型として取り出されます。
今回はvalueがnilになった時を想定して、nilの場合はハイフンを表示するようにしました。
改めて、上で示した完成イメージを見てみましょう。
元データにはなかった『飲み物』の項目がハイフンで入っているのがわかりますね。
var labelTextArray : [Any] = []
// 配列に格納するための型定義
struct Item {
var logicName : String
var value : String
}
func prepareLabelArray() {
for i in usersFavorite {
labelTextArray.append("ユーザー情報")
labelTextArray.append(Item(logicName: "id", value: "\(i["id"] ?? "-")"))
labelTextArray.append(Item(logicName: "名前", value: "\(i["name"] ?? "-")"))
if (i["favorite"] != nil) {
labelTextArray.append("お気に入り")
let favoriteInfoData = (i["favorite"] as? [String : Any])!
labelTextArray.append(Item(logicName: "フルーツ", value: "\(favoriteInfoData["fruits"] ?? "-")"))
labelTextArray.append(Item(logicName: "数字", value: "\(favoriteInfoData["number"] ?? "-")"))
labelTextArray.append(Item(logicName: "教科", value: "\(favoriteInfoData["subject"] ?? "-")"))
labelTextArray.append(Item(logicName: "飲み物", value: "\(favoriteInfoData["drink"] ?? "-")"))
}
}
}
余談ですが、順番が保証されたDictionary型を作成したいときは、Appleから提供されているswift-collectionsをインポートし、OrderedDictionaryを使用しましょう。
カスタムしたUITableViewCellの紐付け
ViewController上にあるUITableViewに先ほど作成した2つのCellを紐づけていきます。
override func viewDidLoad() {
super.viewDidLoad()
displayFavoriteTableView.delegate = self
displayFavoriteTableView.dataSource = self
displayFavoriteTableView.separatorStyle = .none // UITableViewの区切り線をなくすかは要件などによります
displayFavoriteTableView.register(UINib(nibName: "TopicTableViewCell", bundle: nil), forCellReuseIdentifier: "topic_cell") // タイトル用Cellのxibファイルでidentifierを設定
displayFavoriteTableView.register(UINib(nibName: "DataTableViewCell", bundle: nil), forCellReuseIdentifier: "data_cell") // value用Cellのxibファイルでidentifierを設定
}
データの表示
それでは、準備が整ったので最後にデータの表示ができるようにUITableView用の設定処理を行います。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return labelTextArray.count // 行数を定義
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let topicCell = displayFavoriteTableView.dequeueReusableCell(withIdentifier: "topic_cell") as! TopicTableViewCell
let dataCell = displayFavoriteTableView.dequeueReusableCell(withIdentifier: "data_cell") as! DataTableViewCell
// 配列に格納されている要素の型によって使用するCellを切り替える
switch type(of: labelTextArray[indexPath.row]) {
case is String.Type:
topicCell.topicLabel.text = labelTextArray[indexPath.row] as? String
return topicCell
case is Item.Type:
dataCell.logicNameLabel.text = (labelTextArray[indexPath.row] as? Item)?.logicName
dataCell.valueLabel.text = (labelTextArray[indexPath.row] as? Item)?.value
return dataCell
default:
return dataCell
}
}
これでシミュレータや実機で起動することで完成イメージのような画面が表示されるかと思います。
お疲れ様でした。
ViewControllerのコードを改めてまとめて掲載いたします。
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
struct Item {
var logicName : String
var value : String
}
@IBOutlet weak var displayFavoriteTableView: UITableView!
var labelTextArray : [Any] = []
let usersFavorite = [
[
"id": "1",
"name": "ブログ書き太郎",
"favorite": [
"fruits": "りんご",
"number": 3,
"subject": "国語"
]
],
[
"id": "2",
"name": "ブログ読み次郎",
"favorite": [
"fruits": "みかん",
"number": 100,
"subject": "数学"
]
]
]
override func viewDidLoad() {
super.viewDidLoad()
displayFavoriteTableView.delegate = self
displayFavoriteView.dataSource = self
displayFavoriteTableView.separatorStyle = .none
displayFavoriteTableView.register(UINib(nibName: "TopicTableViewCell", bundle: nil), forCellReuseIdentifier: "topic_cell")
displayFavoriteTableView.register(UINib(nibName: "DataTableViewCell", bundle: nil), forCellReuseIdentifier: "data_cell")
self.prepareLabelArray()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return labelTextArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let topicCell = displayFavoriteTableView.dequeueReusableCell(withIdentifier: "topic_cell") as! TopicTableViewCell
let dataCell = displayFavoriteTableView.dequeueReusableCell(withIdentifier: "data_cell") as! DataTableViewCell
switch type(of: labelTextArray[indexPath.row]) {
case is String.Type:
topicCell.topicLabel.text = labelTextArray[indexPath.row] as? String
return topicCell
case is Item.Type:
dataCell.logicNameLabel.text = (labelTextArray[indexPath.row] as? Item)?.logicName
dataCell.valueLabel.text = (labelTextArray[indexPath.row] as? Item)?.value
return dataCell
default:
return dataCell
}
func prepareLabelArray() {
for i in usersFavorite {
labelTextArray.append("ユーザー情報")
labelTextArray.append(Item(logicName: "id", value: "\(i["id"] ?? "-")"))
labelTextArray.append(Item(logicName: "名前", value: "\(i["name"] ?? "-")"))
if (i["favorite"] != nil) {
labelTextArray.append("お気に入り")
let favoriteInfoData = (i["favorite"] as? [String : Any])!
labelTextArray.append(Item(logicName: "フルーツ", value: "\(favoriteInfoData["fruits"] ?? "-")"))
labelTextArray.append(Item(logicName: "数字", value: "\(favoriteInfoData["number"] ?? "-")"))
labelTextArray.append(Item(logicName: "教科", value: "\(favoriteInfoData["subject"] ?? "-")"))
labelTextArray.append(Item(logicName: "飲み物", value: "\(favoriteInfoData["drink"] ?? "-")"))
}
}
}
}
最後に
まとめて振り返るとあまり複雑なことはしていないのですが、実装当時少し悩んだ部分であったため今回記事にしてみました。
これはiOSもAndroidもですが、HTMLのtableタグのような表を作ることをあまり考慮していないように思えます。
スマートフォンでは一度に画面上に表示できる量に制限があるので、項目名をCellに表示して詳細な内容は画面遷移をすることが求められているのだろうと私は考えています。
要件には従いつつ、できるだけネイティブで推奨されている方針に則って行けると良いですね。
この記事で作成した表をさらに良いものにしていくための記事を書きましたので、よろしければぜひ。
UITableViewを使ってCellの各辺にボーダーが設定された表を作ってみる