UIStackViewを使った詳細ページ実装のすすめ

こんにちは。 iOSエンジニアの遠藤です。 最近ユーザー詳細ページのリニューアルをすることになり、UIStackViewで実装しました。 UIStackViewを使ってとてもシンプルに実装できたので、UIStackViewで詳細ページを実装するメリットと実装について紹介します。

はじめに


f:id:vasilyjp:20170227190533p:plain

このような表示コンテンツの多い詳細ページを実装する際に、みなさんは何を使用していますか?

UIStackViewはiOS 9から追加されたクラスですが、 まだUITableViewやUICollectionViewで詳細ページを実装されている人も多いのではないでしょうか?

IQONも詳細ページをUITableViewやUICollectionViewを使って実装してきました。 しかし、UITableView、UICollectionViewは同じモジュールの繰り返しを表示するのには適していますが、詳細ページなどの違うモジュールを表示するのには、ビジネスロジックのコードなどでDataSourceがとても複雑になってしまいました。

UIStackViewを使うことで、複雑になりがちな詳細ページがとてもシンプルに実装することができました。 詳細ページを実装する際に、少しでも参考になれば幸いです。

UIStakcViewを使って詳細ページを実装する際のメリット・デメリット

UIStackViewの基本的な使い方は下記の記事が分かりやすくておすすめです。

遅ればせながら UIStackView 入門 - Qiita

メリット

  • データの出し分けが簡単
  • モジュールのサイズ計算をしなくていい
  • モジュール間のスペースを気にしなくていい

UITableViewやUICollectionViewを使用して実装していたときに、一番大変なのはDataSourceの実装ではないでしょうか?

class DetailViewController: UICollectionViewController {

    private func cellIdentifier(_ indexPath: IndexPath) -> String {
        switch indexPath.section {
        case 0:
            switch indexPath.item {
            case 0:
                return UserDetailHeaderCell.cellIdentifier()
            case 1:
            ・・・
        }
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return (viewModel.user != nil) ? 10 : 0
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        guard let section = UserDetailSection(rawValue: section) else {
            return 0
        }

        switch section {
        case .userName:
            return 2
        case .userDescription:
            return viewModel.shouldShowDescription ? 1 : 0
        case .userSets:
            return viewModel.sets.count
        ・・・
        }
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cellIdentifier = self.cellIdentifier(indexPath)
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        if let cachedCellSize = cachedCellSize[indexPath.item] {
            return cachedCellSize
        }

        // cellのサイズ計算
        let size: CGFloat = ・・・
        let key = "\(indexPath.section)-\(indexPath.item)"
        cachedCellSize[key] = size

        return size
    }
}

上記のコードのように、データがある場合とない場合とで表示するモジュール数を変えたりモジュールごとにCellを出し分けたりとコード量が多くなりがちです。 また、Cellのサイズ計算も楽ではありません。 レイアウトを元にCellのサイズを計算したり、スクロールパフォーマンスを悪くしないためにCellのサイズをキャッシュしたりするので複雑になってしまいます。

しかし、UIStackViewを使えばDataSourceもCellのサイズ計算も必要ありません。 UIStackViewはサブビューをhiddenにすることで、非表示にできるのでデータの有り無しの制御が簡単です。 また、UIStackViewはサブビューのintrinsicContentSizeをもとにしたサイズで描画されるのでAutoLayoutをきちんと設定すれば、サイズ計算のためのコードを書く必要がありません。

デメリット

  • iOS 9以降でしか使用できない
  • 各モジュール間ごとのスペースを自由に設定することはできない

iOS 9以降の機能なので、iOS 8をサポートしたい場合には、バージョンで出し分けの処理を書く必要があることが一番のデメリットだと思います。 また、UIStackViewではモジュール間のスペースを自由に設定することはできないため、デザインでモジュール間のスペースが異なる場合は実装が大変になると思います。 また、デザイナーもモジュールの表示、非表示すべてのパターンを網羅することは大変です。 そこはデザイナーに相談してすべてのモジュール間を等間隔にしてもらうことをおすすめします。

実装について

今回、詳細ページを実装する上で考慮したのはモジュールの出し分けをViewController側に書かないようにすることです。 ViewControllerにモジュールの出し分けを書かないようにすることで、ViewControllerの肥大化を防ぐことができます。また、ViewControllerはViewModelから受け取ったModelをViewに渡すだけにすることでコードがシンプルになります。


f:id:vasilyjp:20170227190717p:plain

実装は以下のとおりです。

f:id:vasilyjp:20170227190750p:plain

// ViewModel.swift

class ViewModel {

    var modelA: Model?
    var modelB: Model?

    func request() {
        // APIリクエストして返ってきたレスポンスをもとに各Modelオブジェクトを更新
    }
}


// ViewController.swift

class ViewController: UIViewController {

    private var viewModel: ViewModel = ViewModel()

    @IBOutlet private weak var viewA: CustomView!
    @IBOutlet private weak var viewB: CustomView!

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.request { [weak self] in
            self?.setupLayout()
        }
    }

    private func setupLayout() {
        viewA.model = viewModel.modelA
        viewB.model = viewModel.modelB    
    }
}


// ContentView.swift

class CustomView: UIView {

    var model: Model? {
        didSet {
            setupLayout()
        }
    }

    private func setupLayout() {
        guard let model = model else { return }

        isHidden = (model == nil)
    }
}

UITableViewやUICollectionViewで実装するよりもはるかにシンプルになったと思います。 この実装方法だと、表示するモジュールが多くなった場合でもViewControllerが肥大化しなくて済みます。

また、表示しているモジュールの並び替えがとても簡単で、UIStackViewのサブビューの並びを変えるだけで良いのです!


f:id:vasilyjp:20170227190846p:plain

まとめ

UIStackViewを使った詳細ページの実装について紹介しました。

表示要素が多い詳細ページはUITableViewやUICollectionViewを使用して実装するとViewControllerが肥大化してしまいがちでです。 UIStackViewを使用することでViewControllerの実装が複雑にならず肥大化せずに実装でき、メンテナンスもしやすくなるので詳細ページをUIStackViewで実装するのはおすすめです! ぜひ、詳細ページを実装する際に試してみてください。

さいごに

VASILYではデザインの実装にこだわるエンジニアを募集しています! 少しでもご興味のあるかたは以下のリンク先をご確認ください。