iOSアプリケーションでのテーブルアニメーションの要約

画像

ユーザーは変更を見たい。



アニメーション化されたリストの更新は、iOSでは常に困難な作業でした。 これは不愉快なことですが、それはいつも日常的なことです。



Facebook、Twitter、Instagram、VKなどの大企業のアプリケーションは、テーブルを使用します。 さらに、ほとんどすべてのiOSアプリケーションはUITableViewまたはUICollectionViewを使用して記述されており、ユーザーは画面の変更を確認したいため、reloadDataは画面の更新には適していません。 このタスクのためにいくつかの既存のフレームワークを見て、アニメーションを計算することに加えて、それらがどれだけ一般化されているかに驚きました。 ただし、最初に1つの要素を挿入するときに、他のすべての要素の動きを喜んで報告した人もいました。



アニメーションの構築と起動の一般化の問題を解決し始めたとき、UIKitの荒野の落とし穴の量を理解できませんでした。 しかし、まず最初に。



計算の変更



テーブルをアニメーション化するには、最初に2つのリストの変更点を把握する必要があります。 セルの変更には、追加、削除、更新、移動の4種類があります。 追加と削除の計算は非常に簡単で、2つの減算リストを使用できます。 アイテムが元のリストにないが新しいリストにある場合は追加され、元のリストにあるが新しいリストにない場合は削除されました。



var initialList: Set<Int> = [1,2,3,5,6,7] var resultList: Set<Int> = [1,3,4,5,6] let insertList = resultList.subtracting(initialList) // {4} let deleteList = initialList.subtracting(resultList) // {7, 2}
      
      





セルの更新は少し複雑です。 セルを比較するだけでは、セルの更新を判断するのに十分ではなく、ユーザーにそれを知らせる必要があります。 これを行うには、ユーザーがセルの関連性の兆候を示すupdateFieldフィールドが入力されます。 日付 (タイムスタンプ)、ある種の整数または文字列ハッシュ(たとえば、変更されたメッセージの新しいテキスト)にすることができます。 一般的に、これは画面に描画されるフィールドの合計です。

状況は移動の場合と似ていますが、少し異なります。 理論的には、フィールドを比較して、一方が他方に対して移動したことがわかりますが、実際には次のことが起こります。



たとえば、2つの配列があります。



 let a = [1,2,3,4] let b = [4,1,2,3]
      
      





リストをすばやく見ると、「4」が位置を変えて左に移動したことがすぐにわかりますが、実際には「1」、「2」、「3」が右に移動し、「4」が所定の位置に残っていることがあります。 結果は同じですが、方法は完全に異なり、論理的に異なるだけでなく、視覚的にユーザーのアニメーションも異なります。 ただし、要素を比較する方法しか持っていないため、正確に移動したものを絶対に言うことは不可能です。

しかし、要素が上にのみ移動するというステートメントを導入するとどうなりますか? 次に、 何が正確に移動したが明らかになります。 したがって、変位を計算するための優先方向を選択することができます。 たとえば、メッセンジャーを作成する場合、新しいメッセージを追加すると、特定のチャットが下から上に移動する可能性が高くなります。 ただし、細胞の動きを示す機能を提供できます。 ビューモデルに、新しいメッセージで変更されるlastMessageDateフィールドがあり、それに応じて、このビューモデルの並べ替え順序が他と比較して変化するとします。

その結果、4種類すべての変更を計算しました。 ポイントは小さいです-それらを適用します。



テーブルに変更を適用する



テーブルの変更をトリガーするために、UITableViewとUICollectionViewには特別な変更メカニズムがあるため、標準機能を使用するだけです。



 tableView.beginUpdates() self.currentList = newList tableView.reloadRows(animations.toUpdate, with: .fade) tableView.insertRows(at: animations.toInsert, with: .fade) tableView.deleteRows(at: animations.toDelete, with: .fade) tableView.reloadRows(at: animations.toUpdate, with: .fade) for (from, to) in animations.cells.toMove { tableView.moveRow(at: from, to: to) } tableView.endUpdates()
      
      





開始、確認、すべてが正常で、すべてが機能しています。 しかし、とりあえず...



画像



セルを更新して移動しようとすると、次のエラーで失敗します。 同じインデックスパスを削除して再ロードしようとします



頭に浮かんだ最初の考え:「しかし、私は細胞を除去しようとはしていません!」。 実際、 移動更新delete + insertに過ぎず、テーブルは実際にそのようなアクションを好まず、エラーをスローします(なぜtry-catchを既に実行してはいけないのかいつも思っていました)。 それは単に扱われ、次のサイクルで更新を行います。



 tableView.beginUpdates() // insertions, deletions, moves… tableView.endUpdates() tableView.beginUpdates() tableView.reloadRows(animations.cells.toDeferredUpdate, with: .fade) tableView.endUpdates()
      
      





今、私たちは解決しなければならなかった最も困難な問題の一つに目を向けます。

すべて正常に動作しているように見えますが、セルが更新されると、アニメーションスタイルが.fadeまたは.noneであるかどうかにかかわらず、奇妙な「点滅」が見られますが、これは論理的ではありません。

些細なことのように思えますが、テーブルにかなりの量の更新があると、うんざりするほど「再点滅」し始めますが、これは本当に望ましくありません。 これをすべて回避するには、insert-delete-moveを同期し、アニメーションを互いに更新する必要があります。 つまり、最初の.endUpdates()が終了するまで、新しい.beginUpdates()を開始することはできません。 この一見取るに足りない問題のため、このすべてを処理するアニメーション同期クラスを作成する必要がありました。 唯一の欠点は、変更が同期的にではなく、遅延して適用されるようになったことです。つまり、変更は順次キューに入れられます。



DispatchSemaphoreを使用したアニメーション同期コード
 let operation = BlockOperation() // 1.  .      ,          operation.addExecutionBlock { // 2.   .    ,       //          guard let currentList = DispatchQueue.main.sync(execute: getCurrentListBlock) else { return } do { // 3.    let animations = try animator.buildAnimations(from: currentList, to: newList) var didSetNewList = false DispatchQueue.main.sync { // 4.   ,    mainPerform(self.semaphore, animations) } // 5.    _ = self.semaphore.wait() if !animations.cells.toDeferredUpdate.isEmpty { // 6.    ,    update  DispatchQueue.main.sync { deferredPerform(self.semaphore, animations.cells.toDeferredUpdate) } _ = self.semaphore.wait() } } catch { DispatchQueue.main.sync { onAnimationsError(error) } } } self.applyQueue.addOperation(operation)
      
      







mainPerformおよびdeferredPerformの内部では、次のことが起こります。



 table.performBatchUpdates({ // insert, delete, move... }, completion: { _ in semaphore.signal() })
      
      





最後に、アイデアを完成させて、変則的なバグに出くわすまで、いつも繰り返すのではなく、動きとともに更新を適用するときに特定の一連の変更について、異常についてのすべてを知っていると信じていました。 更新と移動がまったく交差しない場合でも、テーブルは致命的な例外をスローする可能性があり、アニメーションを適用する次のサイクルにリロードすることを除いて、これが何らかの方法で解決できないことを最終的に確認しました。 「しかし、テーブルからセルを要求し、そこからデータを強制的に更新することができます」とあなたは言います。 再計算は単純に呼び出せないため、セルの高さが静的な場合のみ可能です。



後で別の問題が発生しました。 AppStoreでは、テーブルの落下エラーが頻繁に発生し始めました。 ログのおかげで、問題を特定することは難しくありませんでした。 アニメーション計算関数に渡されたフォームの無効なリスト:



 let a = [1,2,3] let b = [1,2,3,3,4,5]
      
      





つまり、同一の要素が複製されました。 それは非常に簡単に扱われ、アニメーターは計算中にエラーをスローし始めました(上記のリストに注意してください。計算はちょうどこの理由でtry-catchブロックにラップされています)。 元の配列の要素数(Array)と要素のセット(Set)を比較して、不整合を判断します。 既に存在するSetに要素を追加すると、要素が置き換えられるため、Setの要素は配列よりも少なくなります。 このチェックはオフにすることができますが、そうすることは非常に推奨されません。 信じられますが、多くの場所で、渡された引数の正確さに対する開発者の自信にもかかわらず、これはエラーから救いました。



おわりに



iOSのアニメーションテーブルはそれほど単純ではありません。 複雑さのほとんどは、UIKitのクローズドソースコードによって追加されます。UIKitでは、どの場合にエラーがスローされるかを常に理解することは不可能です。 この問題に関するAppleのドキュメントは非常に少なく、変更を転送する必要があるのはどのリストインデックス(古いまたは新しい)に関してのみであると述べています。 テーブルセクションを操作する方法は、セルを操作する場合と変わりません。そのため、例はセルでのみ示しています。 この記事では、理解を容易にし、サイズを小さくするために、コードを単純化しています。



ソースコードはGitHubにありココアポッドまたはソースコードを使用して取得できます。 コードは多くのケースでテストされており、現在は一部のアプリケーションの生産で使用されています。

iOS 8以降およびSwift 4との互換性



使用素材



Appleドキュメント.endUpdates

iOS 11での変更の適用



All Articles