カルパス食べたい

日々の色々を書きます

UITableViewCellのprepareForReuseとNSLayoutConstraintのpriority

はじめに

iOSエンジニアとして4月から働きはじめた新卒です。
今後iOSエンジニアとしてやっていくに当たって、ちょっとしたアウトプットの場を作りたいなと思い、ブログに自分が困ったことや、今後忘れたくないことを備忘録的なものとして残していこうと思います。
Flutterもやってたり、バックエンドにも興味はあるので、その内色々アウトプットしていこうと思います。

今日はとりあえずiOS開発周りで最近実装した部分について書いていきます。

UITableViewCellのprepareForReuse

まずはUITableViewCellのprepareForReuseについてです。
大学の時にSwiftをやったことは少しだけあって、その時はよくあるリスト形式のお知らせが流れてくるようなアプリを作っていました。
そのため、UITableViewを使用したこともあったし、Cell周りも実装したりしていたので、prepareForReuseを使ったことはありました。
ただ、最近prepareForReuseを改めて使用するような場面になって、結果的にはprepareForReuseをその際に使用してはいけないということに気づくことができました。

そのことについて詳しく説明する前に、前提としてUITableViewのCellについて少しだけ書いていこうと思います。
基本的にTableViewでCellの内容を決めていく際には、以下のようなtableView(_:cellForRowAt: )を使用することになると思います。

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // dequeueReusableCell -> セルを再利用
   let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyle", for: indexPath)

   cell.textLabel!.text = "Title text"
   cell.imageView!.image = UIImage(named: "kalupas")
   return cell
}

Apple Developer Documentationの文章を抜粋します。

When you need a cell object at runtime, call the table view’s dequeueReusableCell(withIdentifier:for:) method, passing the reuse identifier for the cell you want. The table view maintains an internal queue of already-created cells. If the queue contains a cell of the requested type, the table view returns that cell. If not, it creates a new cell using the prototype cell in your storyboard. Reusing cells improves performance by minimizing memory allocations during critical times, such as during scrolling.

公式ドキュメントにも書かれている通りで、基本的にTableViewのCellは同じようなものが連続しているので、上記のコードのdequeueReusableCellでセルを生成することによって、再利用できるようになっています。
そして、再利用されることによって、パフォーマンスも良くなると書かれています(例えばスクロール時など)。
ただ、再利用できるばかりに自分のような初心者は、よくこの再利用Cellの罠にハマってしまいがちな気がしています。 具体的には、別々の画像が載せられたCellを実装したはずなのに、Cellが再利用されることによって、Cell内の画像までもが再利用されてしまい、同じ画像が載せられたCellがTableViewに並んでしまうような現象があります。
このような現象を回避するために、大学の時の自分はprepareForReuseを使用していました。
prepareForReuseが何なのかは、公式ドキュメントを参照します。

If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned from the UITableView method dequeueReusableCell(withIdentifier:). For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view's delegate in tableView(_:cellForRowAt:) should always reset all content when reusing a cell. If the cell object does not have an associated reuse identifier, this method is not called. If you override this method, you must be sure to invoke the superclass implementation.

prepareForReuseは基本的に、Cellを再利用しようとするその直前に呼ばれます。
つまり、prepareForReuseの中で、例えば画像をnilにしてあげたりすることによって、再利用前にCell内の画像が初期化され、Cell内には異なる画像が並ぶようになります。
しかし、これはprepareForReuseの(多分よくある)誤った利用方法になります。
上記公式ドキュメントにしっかりと書かれているのですが、prepareForReuseはパフォーマンス的な観点で、Cellの内容に関係のないもの(例えば、alpha・editing・selectionなど)の状態をリセットする時にしか使うべきではないとされています。
では、Cellごとに画像は再利用されて欲しくないなどの場合はどのように実装を行えば良いのでしょうか。
それはCellの内容を設定する時に利用しているtableView(_:cellForRowAt: )メソッドになります。
という基本的なことが理解できていなかったので、もし同じような問題にハマった人がいればtableView(_:cellForRowAt: )メソッドの中でCellの内容を設定するようにしてみると良いかと思います。

NSLayoutConstraintのpriority

こちらも、またView周りのことでハマったことです。
こちらのpriorityは例えば、AutoLayoutなどでViewに制約をつける時に利用します。普通に一つずつ制約を付けていく場合に利用することはないと思うのですが、場合によってViewの右側の間隔を32pxではなく、48pxにしたい...みたいな時に利用します。
ちょっとわかりにくいかもですが、イメージは↓みたいな感じです。

Cell double constraint image
Cellの同じ部分に二つ制約が付いた状態

では、場合によって、この二つの制約のどちらかを有効にしたい。そんな場合はどうすれば良いでしょうか?
答えは簡単で、制約にはpriority(優先度)というプロパティが存在しています。
そのpriority@property UILayoutPriority priority;という形で定義されています。
では、priorityの型であるUILayoutPriorityは何なのかというと、typedef float UILayoutPriority;のように定義されています。typedefObjective-Cの記法なので、あまり気にする必要はありませんが名前の通り型の定義みたいな感じです。
その定義の中では色々プロパティが定義されていて、UILayoutPriorityRequiredとかUILayoutPriorityDefaultHighUILayoutPriorityDefaultLowなどがあります。
これらそれぞれについて詳しく説明すると既に長いのに、さらに長くなってしまいそうな気がしているので、公式ドキュメントを参照していただければと思います。
では、戻って、このUILayoutPriority priorityの値の変更方法についてですが、floatなので数値で指定してあげることもできますし、独自にプロパティとして定義されている前述したUILayoutPriorityRequired等を指定して変更することができます。
しかし、値を変更する際に注意すべきポイントがあります。まずは例のごとく公式ドキュメントを参照します。

Priorities may not change from nonrequired to required, or from required to nonrequired. An exception will be thrown if a priority of NSLayoutPriorityRequired in macOS or UILayoutPriorityRequired in iOS is changed to a lower priority, or if a lower priority is changed to a required priority after the constraints is added to a view. Changing from one optional priority to another optional priority is allowed even after the constraint is installed on a view.

注意すべきポイントとは、このpriorityの値はnonrequiredからrequiredにすることもできないし、requiredからnonrequiredにすることもできないよというものです。
自分は、例で言うと32pxではなく48pxにしたい...みたいな時に、
変更前:32pxのpriority -> required, 48pxのpriority -> nonrequired
変更後:32pxのpriority -> nonrequired, 48pxのpriority -> required
のように変更することによって、動的な制約の変更を実現しようとしました。しかし無理でした。
原因は上に説明した通りです。なので、このように動的に制約を変更したいという場合は、最初から二つとものprioritynonrequiredな値(1000未満)に設定しておいて、その後で優先度を高くしたい制約のpriorityrequired(1000)以外の値で、もう一つの制約より高い数値にしてあげれば良いかと思います。

おわりに

以上View周りで最近困ったことについてアウトプットしてみました。
説明がわかりにくい上に、非常に長い文章になってしまったので、アウトプットしつつ文章の質も高めていくことができればと思います。
ちょっと疲れましたが、また次回以降も書けるように頑張っていきたいです。