UITableViewのsection数が可変の場合にsectionを意識せずにレイアウトできる実装方法

## まず始めに

`UITableView`で`section`の数が可変なレイアウトを行う場合に、`DataSource`の実装が`if`文や`switch-case`文の分岐でコードが見にくくなりがちなうえに、`section`の番号を意識した実装をしなければならなくなってしまいます。
その問題点を改善できる実装方法を紹介したいと思います。

## プロフィール画面のレイアウト

以下のスクリーンショットのような画面を実装するとします。
<img width="300" alt="ProfileView.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/6e5af86c-c565-e179-50f9-d78f5598f687.png">

- セクション0... ユーザーのプロフィールの`View`
- セクション1... フォローしているユーザーの一覧
- セクション2... フォロワーの一覧

ただしフォローしているユーザーの人数が0人のとき、またはフォロワーの人数が0人のときにはそれぞれのセクションが表示されないものとします。
そのためにセクションの番号は数値によって可変となります。

## LayoutManagerの実装

まず、レイアウトの要素をあげています。

- プロフィール
- フォローしているユーザーの一覧
- フォロワーの一覧
- Unknown

`Unknown`は想定していないセクション番号がきたとき用に準備しておきます。

次に、想定されるセクションのレイアウトをあげていきます。

- プロフィールのみ
- プロフィールとフォローしているユーザーの一覧
- プロフィールとフォロワーの一覧
- すべて表示

上記を`enum`を使って実装していきます。

```swift
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`を行った状態にしておきます。

```swift
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](https://github.com/szk-atmosphere)で公開予定です。


## 追記（2015/12/13）

下記の`LayoutManager`は以前の実装になります。
`SectionType`の`Initializer`内で`index`と`layoutType`から自身を割り当てるという形にしていたのですが、`LayoutManager`自身に`Static`な定数としてlayoutを持たせて、`subscript`からはそのlayoutを返すという形に変更しました。
この実装にしたことによって、`sectionTypes: [SectionType]`の`property`は不要となりました。

```swift
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
    }
}
```