iOS
Swift
qolk

Cellなどの高さ計算の処理をViewのstructで行う

More than 3 years have passed since last update.

ちょっとしたtipというかメモ的な記事です。最近、これでうまくいっているので自分に刻み付ける目的もあって書きます。

セルに複数行の文字を出力したい場合、セルの高さ計算が必要です。iOS8からself-sizingが加わりましたが、表示時は良いのですが画面に表示するセルが多くなった時に、セルの高さ計算がうまくいかずスクロールがかくつくという問題に直面し、結局計算をしています。

その際の計算処理をどこにどう書くかということです。

僕はMVVMで開発していて、モデルの情報は一旦 ItemViewModel というようなViewModelにもたせます。その段階で表示時の見た目を用意したりできるのですが、そうすると複数の違う見せ方をしたい時に複数のビューの見た目を持つのはやっぱりおかしいです。ViewのことはViewにやらせるのが良いと思って書き換えました。

構造

具体的にいうと下記のような構造になります。まずはシンプルに高さ計算が必要ない場合の例です。ViewModelを渡していますが、これは別に表示情報であればなんでも良いです。

final class UserCell: UITableViewCell {
    struct Layout {
        static func height(vm: UserViewModel) -> CGFloat {
            return 68.0
        }
    }

    ...
}

利用時は、このようになります。

let vm = ...
return UserCell.Layout.height(vm)

高さ計算が必要な場合は下記のようになります。

final class ItemCell: UITableViewCell {
    struct Layout {
        static let LeftAreaMargin: CGFloat = 67.0
        static let RightAreaMargin: CGFloat = 14.0
        static let TopAreaHeight: CGFloat = 41.0

        static let NameFont = UIFont.boldSystemFontOfSize(12)

        static let TitleTopHeight: CGFloat = 8.0
        static var TitleMaximumSize = CGSize(width: UIScreen.mainScreen().bounds.width - Layout.LeftAreaMargin - Layout.RightAreaMargin, height: CGFloat.max)
        static let TitleFont = UIFont.boldSystemFontOfSize(16.0)
        static let TitleAttributes = [NSFontAttributeName: Layout.TitleFont]

        static let BottomHeight: CGFloat = 18.0

        static func height(vm: ItemViewModel) -> CGFloat {
            return height(NSAttributedString(string: vm.title.value, attributes: Layout.TitleAttributes))
        }

        static func height(attributedTitle: NSAttributedString) -> CGFloat {
            var height: CGFloat = 0.0
            height += Layout.TopAreaHeight
            height += Layout.TitleTopHeight
            height += Layout.TitleHeight(attributedTitle)
            height += Layout.BottomHeight
            return ceil(height)
        }

        static func TitleHeight(attributedTitle: NSAttributedString) -> CGFloat {
            let options : NSStringDrawingOptions = .UsesLineFragmentOrigin | .UsesFontLeading
            let rect: CGRect = attributedTitle.boundingRectWithSize(Layout.TitleMaximumSize, options: options, context: nil)
            return ceil(rect.size.height)
        }
    }

    ...
}

わかりやすいようにシンプルに必要な部分を抜き出して書きましたが、実際はもう少し計算処理があったりします。利用時はこのようになります。

let vm = ...
return ItemCell.Layout.height(vm)

シンプルな場合と同じです。

補足的な

高さ計算のメソッドをstaticなfuncにしていますが、これは引数を取るからです。引数を取らない場合もありますが、取る場合もあるのでfuncに統一しています。Layoutというstructをわざわざ作らなくても良い気がしますが、これを作っているのは高さ計算の処理を一つの場所にまとめやすいからです。計算済みのセルの高さをどこでキャッシュするかという問題ですが、これは利用側でやれば良いと思っています。

こんな感じです。誰かのヒントになれば。また、もっといい感じのやり方があるよ!というのがあれば是非、コメントください〜。