20
21

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-10

まず始めに

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

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

以下のスクリーンショットのような画面を実装するとします。
ProfileView.png

  • セクション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)
    }
}

LayoutTypeenumInitializer内でfollowingCountfollowerCountの状態に合ったレイアウトを自身に割り当てます。
SectionTypeenumSectionの要素を示すためにしようします。
LayoutManagerStaticな定数としてLayoutTypeに合わせたListを保持させ、それらのListLayoutTypeをキーにしたDictionaryに保持させています。
LayoutManagerではsubscriptを使ってindexにあったSectionTypeを返す実装にします。

UITableViewDataSourceとUITableViewDelegateの実装

まずviewDidLoadなどでLayoutManagersetupを行った状態にしておきます。

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

本来、sectionindexPath.sectionで分岐を書かなければならない部分がLayoutManagersubscriptにそれらを渡すことで、状態に合ったenumが返されるのでswitch-case文で要素にあった処理をするだけでよくなります。

まとめ

  • sectionを意識した実装ではなく、enumを使って必要な要素に対するレイアウトという見え方の実装にする
  • sectionによる分岐などはLayoutManagerに内包する

最後に

近日、この記事のサンプルプロジェクトをGithubで公開予定です。

追記(2015/12/13)

下記のLayoutManagerは以前の実装になります。
SectionTypeInitializer内でindexlayoutTypeから自身を割り当てるという形にしていたのですが、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
    }
}
20
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21