まず始めに
UITableView
でsection
の数が可変なレイアウトを行う場合に、DataSource
の実装がif
文やswitch-case
文の分岐でコードが見にくくなりがちなうえに、section
の番号を意識した実装をしなければならなくなってしまいます。
その問題点を改善できる実装方法を紹介したいと思います。
プロフィール画面のレイアウト
- セクション0... ユーザーのプロフィールの
View
- セクション1... フォローしているユーザーの一覧
- セクション2... フォロワーの一覧
ただしフォローしているユーザーの人数が0人のとき、またはフォロワーの人数が0人のときにはそれぞれのセクションが表示されないものとします。
そのためにセクションの番号は数値によって可変となります。
LayoutManagerの実装
まず、レイアウトの要素をあげています。
- プロフィール
- フォローしているユーザーの一覧
- フォロワーの一覧
- Unknown
Unknown
は想定していないセクション番号がきたとき用に準備しておきます。
次に、想定されるセクションのレイアウトをあげていきます。
- プロフィールのみ
- プロフィールとフォローしているユーザーの一覧
- プロフィールとフォロワーの一覧
- すべて表示
上記をenum
を使って実装していきます。
class ProfileViewSectionLayoutManager {
//MARK: - Inner Enums
enum LayoutType {
case Profile
case ProfileFollowing
case ProfileFollower
case ProfileFollowingFollower
init(followingCount: Int, followerCount: Int) {
if followingCount > 0 && followerCount > 0 {
self = .ProfileFollowingFollower
} else if followingCount > 0 && followerCount < 1 {
self = .ProfileFollowing
} else if followerCount > 0 && followingCount < 1 {
self = .ProfileFollower
} else {
self = .Profile
}
}
}
enum SectionType {
case Profile
case Following
case Follower
case Unknown
}
//MARK: - Static constants
private static let ProfileLayout: [SectionType] = [
.Profile
]
private static let ProfileFollowingLayout: [SectionType] = [
.Profile, .Following
]
private static let ProfileFollowerLayout: [SectionType] = [
.Profile, .Follower
]
private static let ProfileFollowingFollowerLayout: [SectionType] = [
.Profile, .Following, .Follower
]
private static let SectionLayoutList: [LayoutType: [SectionType]] = [
.Profile : ProfileLayout,
.ProfileFollowing : ProfileFollowingLayout,
.ProfileFollower : ProfileFollowerLayout,
.ProfileFollowingFollower : ProfileFollowingFollowerLayout
]
//MARK: - Properties
private var layoutType: LayoutType = .Profile
var numberOfSections: Int {
return self.dynamicType.SectionLayoutList[layoutType]?[index].count ?? 0
}
subscript(index: Int) -> SectionType {
return self.dynamicType.SectionLayoutList[layoutType]?[index] ?? .Unknown
}
func setup(followingCount followingCount: Int, followerCount: Int) {
layoutType = LayoutType(followingCount: followingCount, followerCount: followerCount)
}
}
LayoutType
のenum
のInitializer
内でfollowingCount
とfollowerCount
の状態に合ったレイアウトを自身に割り当てます。
SectionType
のenum
はSection
の要素を示すためにしようします。
LayoutManager
にStatic
な定数としてLayoutType
に合わせたList
を保持させ、それらのList
をLayoutType
をキーにしたDictionary
に保持させています。
LayoutManager
ではsubscript
を使ってindex
にあったSectionType
を返す実装にします。
UITableViewDataSourceとUITableViewDelegateの実装
まずviewDidLoad
などでLayoutManager
のsetup
を行った状態にしておきます。
extension ProfileViewController: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return layoutManager.numberOfSections
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch layoutManager[section] {
case .Profile: return 1
case .Following: return followingCount
case .Follower: return followerCount
case .Unknown: return 0
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let reuseIdentifier: String
switch layoutManager[indexPath.section] {
case .Profile: reuseIdentifier = ProfileViewMainCell.className
case .Following,
.Follower: reuseIdentifier = ProfileViewUserCell.className
case .Unknown: reuseIdentifier = UITableViewCell.className
}
return tableView.dequeueReusableCellWithIdentifier(reuseIdentifier)!
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView: ProfileViewHeaderView
switch layoutManager[section] {
case .Profile,
.Unknown:
return nil
case .Following:
headerView = followingHeaderView
headerView.textLabel.text = "Following \(followingCount)"
case .Follower:
headerView = followerHeaderView
headerView.textLabel.text = "Follower \(followerCount)"
}
return headerView
}
}
extension ProfileViewController: UITableViewDelegate {
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
switch layoutManager[section] {
case .Profile,
.Unknown: return 0.01
case .Following,
.Follower: return 60
}
}
func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0.01
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch layoutManager[indexPath.section] {
case .Profile: return 248
case .Following,
.Follower: return 80
case .Unknown: return 0
}
}
}
本来、section
やindexPath.section
で分岐を書かなければならない部分がLayoutManager
のsubscript
にそれらを渡すことで、状態に合ったenum
が返されるのでswitch-case
文で要素にあった処理をするだけでよくなります。
まとめ
-
section
を意識した実装ではなく、enum
を使って必要な要素に対するレイアウトという見え方の実装にする -
section
による分岐などはLayoutManager
に内包する
最後に
近日、この記事のサンプルプロジェクトをGithubで公開予定です。
追記(2015/12/13)
下記のLayoutManager
は以前の実装になります。
SectionType
のInitializer
内でindex
とlayoutType
から自身を割り当てるという形にしていたのですが、LayoutManager
自身にStatic
な定数としてlayoutを持たせて、subscript
からはそのlayoutを返すという形に変更しました。
この実装にしたことによって、sectionTypes: [SectionType]
のproperty
は不要となりました。
class ProfileViewSectionLayoutManager {
//MARK: - Inner Enums
enum LayoutType {
case Profile
case ProfileFollowing
case ProfileFollower
case ProfileFollowingFollower
var numberOfSection: Int {
switch self {
case .Profile: return 1
case .ProfileFollowing,
.ProfileFollower: return 2
case .ProfileFollowingFollower: return 3
}
}
init(followingCount: Int, followerCount: Int) {
if followingCount > 0 && followerCount > 0 {
self = .ProfileFollowingFollower
} else if followingCount > 0 && followerCount < 1 {
self = .ProfileFollowing
} else if followerCount > 0 && followingCount < 1 {
self = .ProfileFollower
} else {
self = .Profile
}
}
}
enum SectionType {
case Profile
case Following
case Follower
case Unknown
init(index: Int, layoutType: LayoutType) {
switch layoutType {
case .Profile:
self = SectionType.profile(index)
case .ProfileFollowing:
self = SectionType.profileFollowing(index)
case .ProfileFollower:
self = SectionType.profileFollower(index)
case .ProfileFollowingFollower:
self = SectionType.profileFollowingFollower(index)
}
}
private static func profile(index: Int) -> SectionType {
switch index {
case 0: return .Profile
default: return .Unknown
}
}
private static func profileFollowing(index: Int) -> SectionType {
switch index {
case 0: return .Profile
case 1: return .Following
default: return .Unknown
}
}
private static func profileFollower(index: Int) -> SectionType {
switch index {
case 0: return .Profile
case 1: return .Follower
default: return .Unknown
}
}
private static func profileFollowingFollower(index: Int) -> SectionType {
switch index {
case 0: return .Profile
case 1: return .Following
case 2: return .Follower
default: return .Unknown
}
}
}
//MARK: - Properties
private var sectionTypes: [Int : SectionType] = [:]
private var layoutType: LayoutType = .Profile
var numberOfSections: Int {
return layoutType.numberOfSection
}
subscript(index: Int) -> SectionType {
guard let sectionType = sectionTypes[index] else {
let st = SectionType(index: index, layoutType: layoutType)
sectionTypes[index] = st
return st
}
return sectionType
}
func setup(followingCount followingCount: Int, followerCount: Int) {
layoutType = LayoutType(followingCount: followingCount, followerCount: followerCount)
}
func reset() {
sectionTypes.removeAll()
layoutType = .Profile
}
}