前書き
上司から画面(TableView)スクロール時のカクツキを兼ねてから言われていたので、その改善に着手することに。
そして先日、
AKIBA.swift × エウレカ コードレイアウト勉強会
に参加したのがきっかけで、脱AutoLayoutを行ってみた次第です。
(ためになる資料が公開されているので是非目を通して見てください)
改善
脱AutoLayout以外にも行ったものも合わせて掲載しておきます。
前提
以下の中にある事例が、いくつかが当てはまりそうだったのでやってみた。
結果、目に見える改善はあまり見られなかったのですが笑
(そもそもほとんど最初から適応していた)
対策①UILabel → UITextView に変更
UILabel → UITextView に変更した部分は若干滑らかになりました。
(メディアアプリなので、テキストが500~700文字を突っ込むこともざらにある)
こういう質問をしている外人もおられますし、
これのベスト回答によれば、アップル公式に書いてあるとのことで
以下引用
Although these classes actually can support the display of arbitrary amounts of text, labels and text fields are intended to be used for relatively small amounts of text, typically a single line. Text views, on the other hand, are meant to display large amounts of text.
つまり
- UILabel → 短いテキスト
- UITextField → 短いテキスト
- UITextView → 長いテキスト
にサポートしてるよってことで!
UILabel に長文を突っ込むのはやめましょう笑
対策②セルの高さを事前に計算する
もともと、画像を扱うセルのみ対応を行っていたが、パーツが単純で高さの計算がしやすいXIBのセルも計算するようにしました。
根本的にAutolayoutで描画が崩れる(高さが取られない問題)等を解決できる(ことが多い)ので、やっておくことをオススメします。
実装
自社アプリでは、以下の記事方針を参考にしました。
記事内では、高さの計算処理をheightForRow
で行っているのですが、heightForRowで重たい処理をすべきではないので(ただ値をセットするだけが理想なので)
記事を参考にしつつ以下の流れをとりました。
①APIでデータ取得
②ダミーのセルを生成(for文とかでモデルごとに回す)
③ダミーのセルで高さを計算
④計算したものをViewModelに保持(全てのモデルごとに)
⑤TableViewのreloadで描画
⑥heightForRowでViewModelから高さをセット
ポイント
TableViewのreloadが走る前にすべて計算しておくということです。
heightForRow
やcellForRow
内でのコストを極力抑えることが、高速化に近づきます。
コード例
具体的に簡単なコード例を出しておく。
以下は、ただテキストを表示するためだけのセル
/*
テキストの高さを計算するためのもの
他のセルでも使用するのでプロトコルとして切り出している
*/
protocol TextCalculatable {
func calculateTextHeight() -> CGFloat
}
// MARK: - Class
class TextTableViewCell: UITableViewCell {
// 実際に描画されるテキストの部分
@IBOutlet fileprivate weak var textView: UITextView!
/* いろいろ中略 */
}
// MARK: - TextCalculatable
extension TextTableViewCell: TextCalculatable {
// テキストの高さを横幅から計算する
func calculateTextHeight() -> CGFloat {
return textView.sizeThatFits(
CGSize(width: UIScreen.main.bounds.width-margin*2, // 左右のマージンを引く
height: .greatestFiniteMagnitude)
).height
}
}
実際は、セルによっては複数の要素の高さを計算する必要があるので、もっと気持ち悪いコードになるかと思います笑
対策③脱Xibのセルにする
Instrumentsを使用して、処理の重たい関数を探してみた。
ここから、重たいメソッドは1つ
_人人人人人人人人人人人人人人_
> cellForRowの処理が重い <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
どこが重い?
cellForRowで行っていることと言えば
- セルのXib読み込み
- セルの中身をセット
この二つのみ。
アプリの現状
配信しているアプリは、元々WEBメディアとして3年運営していたものをフルネイティブ化している
→アプリに最適な構造になっていない
特にAPIで返ってくるものをタイプ分けすると、セルのタイプが25個もあった。
→読み込むセルが(Autolayoutを使用したXIBが)最大25個ある(1画面でのはなしです!!!)
ここで1つの仮説がたった。
複数のXIB読み込みはコストがでかいのでは?
毎度cellForRowで判定しだし分けしていたため、いくら使い回してるとは言え重たそう、、。
ということで、重たいセルに絞って脱Xib化しました。
参考
対策④脱AutoLayoutのセルにする
やっとタイトルの本題です!
①〜③まで行ったが、まだ微妙にカクツキが残っていた。
ここで勉強会でのことを思い出しました。以下参考。
上記のブログ内にも出てきますが、
[引用](https://github.com/layoutBox/FlexLayout#performance)細々ありますが、
- Autolayoutはコストが高い
- StackViewはなお高い
のです。(注:他の線はAutoLayoutを使用しないライブラリです)
上記のことから
「AutoLayout」 = 「重い」
という結論に至りました。
ということで脱AutoLayoutに。
実装
先のグラフ及び、上記のブログ内で
**「AutoLayoutではないレイアウトエンジン」**のライブラリがいくつか記載されていますが、
- 1週間ほどで結果を残す必要があった(mustではないが一応リリースサイクルもそんな感じなので)
- 25個のセル全てを描き直す時間がない
ということで、ライブラリを使わず自前で実装しました。
また重たいセル(動的に高さが変わるもの)に絞って書き直しを行った。
How To ?
単純な話ですが、AutoLayoutが行っていた処理を自分で行うだけです。
CGRectを使って自分でframeを操作していきます。
①セルのインスタンス生成
②CGRectでフレームを操作
コード例
先ほどの「対策②セルの高さを事前に計算する」で出てきた、テキストを表示するためだけのセルです。
プロトコルに関しては、直接の趣旨には関係ないので、無視していただいて構いません。
/* セルのセットアッププロトコル */
protocol ItemBindable {
func setupCell(with viewModel: ItemViewModel)
func calculateHeight() -> CGFloat
}
/* 高さ計算のプロトコル */
protocol TextCalculatable {
func calculateTextHeight() -> CGFloat
}
// MARK: - Class
class TextTableViewCell: UITableViewCell {
// MARK: Variable
fileprivate let margin: CGFloat = 15.0
// 描画するテキストを先にセットアップしておく
fileprivate let textView: UITextView = {
let text = UITextView(frame: .zero)
text.textAlignment = .left
text.backgroundColor = .white
text.isEditable = false
text.layer.borderWidth = 0
text.dataDetectorTypes = [.link, .address]
text.isScrollEnabled = false
text.textContainer.lineFragmentPadding = 0
text.textContainerInset = .zero
return text
}()
// MARK: Override Mehtods
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// テキストの描画
contentView.backgroundColor = .white
contentView.addSubview(textView)
}
required init(coder aDecoder: NSCoder) {
fatalError("---- fatalError TextTableViewCell ----")
}
override func prepareForReuse() {
super.prepareForReuse()
textView.text = nil
}
}
// MARK: - ItemBindable
extension TextTableViewCell: ItemBindable {
// セルのセットアップメソッド。cellForRowで呼ぶ。
func setupCell(with viewModel: ItemViewModel) {
// テキストのセット
textView.attributedText = viewModel.attrData
textView.frame = CGRect(
origin: CGPoint(x: margin,
y: margin),
// totalCellHeightはあらかじめ計算しておいたセルの高さ
size: CGSize(width: UIScreen.main.bounds.width-margin*2,
height: viewModel.totalCellHeight-margin*2)
)
}
func calculateHeight() -> CGFloat {
return calculateTextHeight() + margin*2
}
}
// MARK: - TextCalculatable
extension TextTableViewCell: TextCalculatable {
func calculateTextHeight() -> CGFloat {
return textView.sizeThatFits(
CGSize(width: uiscreen.main.bounds.width-margin*2,
height: .greatestFiniteMagnitude)
).height
}
}
あとがき
まだすべてのセルを書き直したわけではないが、以前からあったカクツキが減少した。
- Xibの読み込み
- AutoLayout
はコストが高いと実感。
余談
AKIBA.swift × エウレカ コードレイアウト勉強会で教えてもらったのですが、
AutoLayoutの制約がバッティングした際に、Xcodeに出るエラーをわかりやすくしてくれるツールがあります。
制約(Constraints)は多いほど重くなるので、バッティングしている箇所があれば直すことをオススメします!
良いXcodeライフを!