Xcode
iOS
Swift
並列処理

[Swift] Swiftにおける並列処理のOperationとOperationQueueの情報をまとめました

並列処理に関するチュートリアル記事の翻訳

今回は並列処理のチュートリアルを翻訳しました。

Operation and OperationQueue Tutorial in Swift

を翻訳した記事です。

API通信やデータベースにアクセスする必要があるアプリを開発していると時々と言いますか、
割といろんなパターンでUIが固まることがあると思います。
初心者の頃で並列処理の知識が貧しいと割と簡単にハマって気づいたら日が暮れていたなんてこともあるのではないでしょうか。
(私はしょっちゅう経験していますが(笑))

ネットでも割と日本語で書かれた記事がヒットしてそれを頼りに実装していくのですが、
そんなあやふやな知識では今後もいろんなところでハマりそうだと思い知識をまとめたいと思いました。

こういう知識は英語圏のちゃんとしたページを参考にした方がかなりしっくりきますので
上記の英語圏のページを翻訳した感じです。

なんせ、共同開発してコードレビューをしていくと別の方が的を外した指摘をしてきて、
その回答に困るケースが多いですからね。並列処理に関わる案件になると特に開発者によって解釈が変わりますからね。。。
(プログラミングの設計って割と2,3のベストプラクティスに集約されるもんだと思ってました。
が、仕事をしていると集約されないためか宗教論争になってケンカに発展することが多かった)

なので、今後こういう問題でトラブった時に知識を共有したいこともあってこのページを作ってみました。
並列処理に困っている開発者には特に読んでもらいたいと思っての共有になります。

Operation and OperationQueue Tutorial in Swift (SwiftによるOperationとOperationQueueのチュートリアル)

(James GoodwillがXcode 10とSwift 4.2のチュートリアルを更新しました。 Soheil Azarpourが元の投稿を書き、Richard Turtonが以前の更新を完了しました。)

誰もがボタンをタップしたり、iOSやMacアプリでテキストを入力したりするという、イライラした経験をしています。ユーザーインターフェイスが応答を停止するといった感じですね。

MacではユーザーはUIとやり取りできるようになるまで、しばらくの間、回転しているカラフルなホイールを見続けます。 iOSアプリではユーザーはアプリがすぐに反応することを期待しています。 応答の遅いアプリは不器用で低速で、通常は悪いレビューを受けます。

あなたのアプリをレスポンシブに維持することは完了したことよりも簡単です。もしあなたのアプリケーションがいくつかのタスク以上を実行する必要があるならば、その処理はすぐに複雑になります。メインのランループで大量の作業を実行する時間はあまりなく、応答性の高いUIを提供します。

poorな開発者は何をするのか?解決策は並列処理を使用してメインスレッドから作業を移動することです。並列性とはアプリケーションが複数のストリーム(またはスレッド)を同時に実行することを意味します。このようにしてあなたが作業を実行している間、ユーザーインターフェイスは反応し続けることができます。

iOSで同時に操作を実行する方法の1つは、OperationおよびOperationQueueクラスです。 このチュートリアルではそれらを使用する方法を学習します。あなたは並列性をまったく使用しないアプリケーションから始めます。したがって、非常に遅く応答しないように見えます。次に並列操作を追加し、ユーザーにもっと反応の良いインターフェースを提供するためにアプリを再加工します!

Getting Started (始める前に)

このチュートリアルのサンプルプロジェクトの全体的な目標は、フィルタリングされたimageのTableViewを表示することです。 イメージはインターネットからダウンロードされ、フィルタが適用され、TableViewに表示されます。

アプリモデルの概略図は次のとおりです。

NSOperation_model_stalled.png

A First Try

このチュートリアルの上部または下部にある「Download Materials」ボタンを使用して、starter projectをダウンロードします。 このチュートリアルで作業するプロジェクトの最初のバージョンです。

ビルドしてプロジェクトを実行すると、(最終的に)写真のリストとともに実行されているアプリが表示されます。 リストをスクロールしてみてください。 痛いでしょう?

アクションはすべてListViewController.swiftで行われ、そのほとんどはtableView(_:cellForRowAtIndexPath :)内にあります。

その方法を見て、2つのことがかなり集中的に起こっていることを処理してください。

  1. ウェブから画像データを読み込む。 これが簡単な作業であっても、アプリ側はダウンロードが完了するまで待ってから続行する必要があります。
  2. Core Imageを使用して画像をフィルタリングする。 このメソッドはセピアのフィルターを画像に適用します。 Core Imageのフィルターについて詳しく知りたい場合は、Swiftの「Beginning Core Image」を参照してください。

さらに、最初にリクエストが走った時にウェブから写真のリストをロードしています:

Sample.swift
lazy var photos = NSDictionary(contentsOf:dataSourceURL)!

この作業はすべてアプリケーションのメインスレッドで行われています。メインスレッドはユーザーのやりとりも担当しているため、ウェブからの読み込みや画像のフィルタリングにメインスレッドを忙しくさせ続けるのはアプリケーションの応答性が損なわれます。Xcodeのゲージビュー(gauges view)を使用すると、このことの概要を簡単に知ることができます。デバッグナビゲータ(Command-7)を表示して、アプリケーションの実行中にCPUを選択すると、ゲージビューにアクセスできます。

gauges-700x360.png

あなたは、アプリケーションの主なスレッドであるスレッド1のすべてのスパイクを見ることができます。詳細については、Instrumentsでアプリを実行できますが、これは他のチュートリアル全体です。(クリックでリンク先に飛びます)

そのユーザーエクスペリエンスをどのように改善できるか考えてみましょう。

Tasks, Threads and Processes (タスク、スレッド、プロセス)

さらに進む前に、いくつか理解する必要のある技術的な概念があります。いくつかの重要な用語があります:

  • Task(タスク): 完了される必要のあるシンプルな簡単な作業
  • Thread(スレッド): 一つのアプリケーション内で複数の命令セットが同時に動作することを可能にする、オペレーティングシステムによって提供されるメカニズム。
  • Process(プロセス): 実行可能なコードのチャンク。複数のスレッドで構成されます

Note: iOSおよびmacOSではスレッド機能はPOSIXスレッドAPI(またはpthread)によって提供され、オペレーティングシステムの一部です。これはかなり低いレベルのもので、間違いを犯すのは簡単だと分かります。おそらくスレッドに関する最悪の事は、そのミスは見つけるのが非常に難しいことです!

FoundationのframeworkにはThreadというクラスが含まれています。これは扱いがはるかに簡単ですが、Threadで複数のスレッドを管理することは依然として難しいです。OperationとOperationQueueは、複数のスレッドを処理するプロセスを大幅に簡素化した上位レベルのクラスです。

この図では、プロセス、スレッド、およびタスクの関係を確認できます。

Process_Thread_Task.png

ご覧のようにプロセスには複数のスレッドが含まれ、各スレッドは一度に複数のタスクを実行できます。

この図ではスレッド2はファイルを読み込む作業を行い、スレッド1はユーザーインターフェイスに関連するコードを実行します。これはiOSでコードを構造化する方法と非常によく似ています。メインスレッドはユーザーインターフェイスに関連する作業を実行し、セカンダリスレッドは例えばファイルの読み取りやネットワークへのアクセスなどの低速または長時間の操作を実行します。

Operation vs. Grand Central Dispatch (GCD)

あなたはGrand Central Dispatch(GCD)について聞いたことがありますと思います。簡単に言えば、GCDは言語機能、ランタイムライブラリ、およびシステムの拡張機能で構成されておりiOSとmacOSのマルチコアハードウェアの並行性をサポートするためのシステム全体および包括的な改良を提供します。GCDの詳細についてはGrand Central Dispatch Tutorialをご覧ください。

OperationOperationQueueはGCDの上に構築されています。非常に一般的なルールとして、Appleは最高レベルのabstraction(抽象化)を使用することを推奨し、測定値が必要であることを示す場合は下位レベルに落とすことを推奨します。

ここでは、GCDやOperationをいつどこで使用するかを決めるのに役立つ2つの簡単な比較を示します。

  • GCDは、並列処理される作業単位を表す軽量な方法です。あなたはこれらの作業単位をスケジュールしません。 システムがあなたのためにスケジューリングを行います。ブロック間に依存関係を追加することは悩みのタネになる可能性があります。ブロックをキャンセルまたは一時停止すると、開発者として余分な作業ができます!
  • OperationはGCDに比べて少し余分なオーバーヘッドを加えますが、さまざまな操作の間に依存関係を追加し、それらを再利用、キャンセル、または中断することができます。

このチュートリアルでは、テーブルビューを扱っているため、パフォーマンスとパワー消費の理由からユーザーが画面からそのイメージをスクロールした場合に特定のイメージの操作を取り消す機能が必要になるため、Operationが使用されます。Operationがバックグラウンドのスレッド上にあっても、queue上で数十個が待機している場合でも、パフォーマンスは低下します。

Refined App Model (洗練されたAppModel)

予備スレッドのないモデルを改良する時が来ました!予備モデルを深く見てみると、3つのスレッドボギー領域が改善できることがわかります。これら3つの領域を分離して別々のスレッドに配置することで、メインスレッドは解放されユーザーのやりとりに対応できます。

NSOperation_model_improved.png

アプリケーションのボトルネックを解消するには、あなたは特にユーザーのやりとりに対応するためのスレッド、データソースと画像のダウンロード専用のスレッド、画像フィルタリングを実行するためのスレッドが必要です。新しいモデルでは、アプリはメインスレッドで起動し、空のテーブルビューを読み込みます。 同時に、アプリはデータソースをダウンロードするために2番目のスレッドを起動します。

データソースがダウンロードされたら、テーブルビューにリロードするよう指示します。これはメインスレッドで行わなければなりません。なぜなら、それはユーザインタフェースを必要とするからです。この時点で、テーブルビューにはいくつのrowが必要か、そして表示する必要があるイメージのURLはわかっていますが、実際のイメージはまだありません。この時点ですぐに「すべての画像」をダウンロードし始めた場合は非常に非効率です!一度にすべての画像を必要としないからです。

これをより良くするためには何ができるでしょう。

より良いモデルは、それぞれの行が画面に表示される画像のダウンロードを開始することです。なので、コードは最初にテーブルビューにどの行が表示されているかを尋ね、ダウンロードタスクを開始させます。同様に、画像のフィルタリングタスクは、イメージが完全にダウンロードされるまで開始できません。したがって、処理待ちのフィルタリングされていない画像があるまで、アプリは画像のフィルタリングタスクを開始してはなりません。

アプリの応答性を高めるため、コードはダウンロード後すぐに画像を表示します。その後、画像のフィルタリングを開始され、フィルタリングされた画像を表示させるためにUIを更新します。

以下の図は、このための概略的な制御フローを示しています。

3_NSOperation_workflow.png

これらの目的を達成するには、画像がダウンロード中か、ダウンロードが終わったか、フィルタリングされているかのいずれかを追跡する必要があります。また各Operationのステータスとタイプを追跡する必要があるため、ユーザーがスクロールするたびにキャンセル、一時停止、または再開できるようになります。

はい! 今すぐあなたはコーディングを受ける準備が整いました!

Xcodeで、PhotoOperations.swiftと命名した新しいSwiftファイルをプロジェクトに追加します。 次のコードを追加します。

PhotoOperations.swift
import UIKit

// This enum contains all the possible states a photo record can be in
enum PhotoRecordState {
  case new, downloaded, filtered, failed
}

class PhotoRecord {
  let name: String
  let url: URL
  var state = PhotoRecordState.new
  var image = UIImage(named: "Placeholder")

  init(name:String, url:URL) {
    self.name = name
    self.url = url
  }
}

この単純なクラスは、現在の状態とともに、アプリに表示される各写真を表します。デフォルトの値は.newです。 イメージのデフォルトはプレースホルダです。

各Operationのステータスを追跡するには、別のクラスが必要です。 PhotoOperations.swiftの最後に次の定義を追加します。

PhotoOperations.swift
class PendingOperations {
  lazy var downloadsInProgress: [IndexPath: Operation] = [:]
  lazy var downloadQueue: OperationQueue = {
    var queue = OperationQueue()
    queue.name = "Download queue"
    queue.maxConcurrentOperationCount = 1
    return queue
  }()

  lazy var filtrationsInProgress: [IndexPath: Operation] = [:]
  lazy var filtrationQueue: OperationQueue = {
    var queue = OperationQueue()
    queue.name = "Image Filtration queue"
    queue.maxConcurrentOperationCount = 1
    return queue
  }()
}

このクラスには、table内の各rowのアクティブおよび保留中のダウンロードやフィルタ操作を追跡するための2つのディクショナリと、各OperationタイプのOperationキューが含まれています。

すべての値はlazyで作成されます。最初にアクセスされるまで初期化されません。 これにより、アプリのパフォーマンスが向上します。

あなたが確認できるように、OperationQueueを作成することは非常に簡単です。 instrumentやデバッガに名前が表示されるため、キューの名前を付けるとデバッグに役立ちます。maxConcurrentOperationCountは、このチュートリアルのために1に設定されています。これにより、操作が1つずつ完了するのを見ることができます。この部分を残して、queueに一度に処理できるOperationの数をいくらにするかを決めさせることができるようになります。これによりパフォーマンスがさらに向上します。

どのようにキューが一度に実行できるOperationの数を決定するのでしょう。それは良い質問です! ハードウェアによって異なります。デフォルトでは、OperationQueueはバックグラウンドで何らかの計算を行い、実行中の特定のプラットフォームに最適なものを決定し、可能な限り多くのスレッドを起動します。

次の例を考えてみましょう。システムが休眠状態で、利用可能なリソースがたくさんあるとします。この場合、キューは同時に8つのスレッドを起動するかもしれません。次回プログラムを実行すると、システムはリソースを消費している他の無関係なOperationでビジー状態になる可能性があります。このときキューは2つの同時スレッドだけを起動するかもしれません。このアプリでは最大同時操作数を設定しているため、一度に1つの操作しか実行されません。

Note: なぜあなたは、アクティブで保留中のすべての操作を追跡しなければならないのか疑問に思うかもしれません。キューにはoperationの配列を返すOperationメソッドがあります。なぜそれを使用しないのですか?このプロジェクトでは、それほど効率的ではありません。どのOperationがどのテーブルビューの行に関連付けられているかを追跡する必要があります。これは、あなたが必要なたびに配列を繰り返し処理することを含みます。インデックスパスをキーとする辞書にそれらを格納することは、検索が迅速かつ効率的であることを意味します。

今度はダウンロードとフィルタリングのOperationに注意する時です。 PhotoOperations.swiftの最後に次のコードを追加します。

PhotoOperations.swift
class ImageDownloader: Operation {
  //1
  let photoRecord: PhotoRecord

  //2
  init(_ photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }

  //3
  override func main() {
    //4
    if isCancelled {
      return
    }

    //5
    guard let imageData = try? Data(contentsOf: photoRecord.url) else { return }

    //6
    if isCancelled {
      return
    }

    //7
    if !imageData.isEmpty {
      photoRecord.image = UIImage(data:imageData)
      photoRecord.state = .downloaded
    } else {
      photoRecord.state = .failed
      photoRecord.image = UIImage(named: "Failed")
    }
  }
}

Operationは、サブクラス化のために設計された抽象クラスです。各サブクラスは、前の図に示すような特定のタスクを表します。

上のコードの各番号付きコメントでは、次のようなことが起こります。

  1. Operationに関連するPhotoRecordオブジェクトへの定数参照を追加します。
  2. photoRecordを渡すための指定されたイニシャライザを作成します。
  3. main()は、実際に作業を実行するためにOperationのサブクラスでオーバーライドするメソッドです。
  4. 開始前にキャンセルを確認します。 Operationは、長時間または集中的な作業を試みる前に、それらがキャンセルされているかどうか定期的にチェックする必要があります。
  5. 画像データをダウンロードします。
  6. キャンセルを再度確認します。
  7. データがある場合は、イメージオブジェクトを作成してレコードに追加し、状態を移動します。 データがない場合は、レコードを失敗としてマークし、適切なイメージを設定します。

次に、別のOperationを作成して画像のフィルタリングを行います。 PhotoOperations.swiftの最後に次のコードを追加します。

PhotoOperations.swift
class ImageFiltration: Operation {
  let photoRecord: PhotoRecord

  init(_ photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }

  override func main () {
    if isCancelled {
        return
    }

    guard self.photoRecord.state == .downloaded else {
      return
    }

    if let image = photoRecord.image, 
       let filteredImage = applySepiaFilter(image) {
      photoRecord.image = filteredImage
      photoRecord.state = .filtered
    }
  }
}

これはダウンロード操作に非常に似ていますが、ダウンロードする代わりにフィルタを画像に適用しています(まだ実装されていないメソッドを使用しているため、コンパイラエラーが発生します)。

不足している画像のフィルタメソッドをImageFiltrationクラスに追加します。

ImageFiltration.swift
func applySepiaFilter(_ image: UIImage) -> UIImage? {
  guard let data = UIImagePNGRepresentation(image) else { return nil }
  let inputImage = CIImage(data: data)

  if isCancelled {
    return nil
  }

  let context = CIContext(options: nil)

  guard let filter = CIFilter(name: "CISepiaTone") else { return nil }
  filter.setValue(inputImage, forKey: kCIInputImageKey)
  filter.setValue(0.8, forKey: "inputIntensity")

  if isCancelled {
    return nil
  }

  guard 
    let outputImage = filter.outputImage,
    let outImage = context.createCGImage(outputImage, from: outputImage.extent) 
  else {
    return nil
  }

  return UIImage(cgImage: outImage)
}

画像フィルタリングは、以前ListViewControllerで使用されていたものと同じです。バックグラウンドで別のOperationとして実行できるように、ここに移動しました。繰り返しますが、キャンセルを頻繁に確認する必要があります。高度なメソッド呼び出しの前後に行うことをお勧めします。フィルタリングが完了したら、pthotoRecordのインスタンスの値を設定します。

すばらしいですね! これでOperationをバックグラウンドタスクとして処理するために必要なすべてのツールと基盤ができました。今度はViewControllerに戻って、これらの新しいメリットをすべて活用できるように変更しましょう。

ListViewController.swiftに切り替え、lazy var photosプロパティ宣言を削除します。代わりに次の宣言を追加します。

ListViewController.swift
var photos: [PhotoRecord] = []
let pendingOperations = PendingOperations()

これらのプロパティは、PhotoRecordオブジェクトの配列とPendingOperationsオブジェクトを保持してOperationを管理します。

新しいプロパティをクラスに追加して、写真のプロパティリストをダウンロードします。

ListViewController.swift
func fetchPhotoDetails() {
  let request = URLRequest(url: dataSourceURL)
  UIApplication.shared.isNetworkActivityIndicatorVisible = true

  // 1
  let task = URLSession(configuration: .default).dataTask(with: request) { data, response, error in

    // 2
    let alertController = UIAlertController(title: "Oops!",
                                            message: "There was an error fetching photo details.",
                                            preferredStyle: .alert)
    let okAction = UIAlertAction(title: "OK", style: .default)
    alertController.addAction(okAction)

    if let data = data {
      do {
        // 3
        let datasourceDictionary =
          try PropertyListSerialization.propertyList(from: data,
                                                     options: [],
                                                     format: nil) as! [String: String]

        // 4
        for (name, value) in datasourceDictionary {
          let url = URL(string: value)
          if let url = url {
            let photoRecord = PhotoRecord(name: name, url: url)
            self.photos.append(photoRecord)
          }
        }

        // 5
        DispatchQueue.main.async {
          UIApplication.shared.isNetworkActivityIndicatorVisible = false
          self.tableView.reloadData()
        }
        // 6
      } catch {
        DispatchQueue.main.async {
          self.present(alertController, animated: true, completion: nil)
        }
      }
    }

    // 6
    if error != nil {
      DispatchQueue.main.async {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        self.present(alertController, animated: true, completion: nil)
      }
    }
  }
  // 7
  task.resume()
}

何をしているのかというと

  1. バックグラウンドスレッド上のimagesのプロパティリストをダウンロードするURLSessionデータタスクを作成します。
  2. エラーが発生した場合に使用するUIAlertControllerを設定します。
  3. requestが成功した場合は、プロパティリストからDictionaryを作成します。 Dictionaryは、image nameをキーとして使用し、そのURLを値として使用します。
  4. DictionaryからPhotoRecordオブジェクトの配列をビルドします。
  5. メインスレッドに戻り、テーブルビューをリロードしてイメージを表示します。
  6. エラーが発生した場合にアラートコントローラを表示します。バックグラウンドスレッドで実行されるURLSessionタスクと、画面上のメッセージの表示は、メインスレッドから行う必要がありますのを忘れないでください。
  7. ダウンロードタスクを実行します。

viewDidLoad()の最後に新しいメソッドを呼び出します。

ListViewController.swift
fetchPhotoDetails()

次に、tableView(_:cellForRowAtIndexPath :)を見つけ、(コンパイラがそれに不平を言っているので簡単に見つけられますが)次の実装に置き換えます:

ListViewController.swift
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath)

  //1
  if cell.accessoryView == nil {
    let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
    cell.accessoryView = indicator
  }
  let indicator = cell.accessoryView as! UIActivityIndicatorView

  //2
  let photoDetails = photos[indexPath.row]

  //3
  cell.textLabel?.text = photoDetails.name
  cell.imageView?.image = photoDetails.image

  //4
  switch (photoDetails.state) {
  case .filtered:
    indicator.stopAnimating()
  case .failed:
    indicator.stopAnimating()
    cell.textLabel?.text = "Failed to load"
  case .new, .downloaded:
    indicator.startAnimating()
    startOperations(for: photoDetails, at: indexPath)
  }

  return cell
}

何をしているかというと、

  1. ユーザーにフィードバックを提供するに、UIActivityIndicatorViewを作成し、それをセルの accessory viewとして設定します。
  2. データソースにPhotoRecordのインスタンスが含まれています。 現在のindexPathに基づいて正しいものを取得します。
  3. cellのtext labelは(ほとんど)常に同じで、イメージは処理されるときにPhotoRecordで適切に設定されるため、レコードの状態にかかわらずここで両方を設定できます。
  4. レコードを調べます。 activity indicatorとtextを適切に設定し、Operationを開始します(まだ実装されていません)。

クラスに次のメソッドを追加して、Operationを開始します。

ListViewController.swift
func startOperations(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
  switch (photoRecord.state) {
  case .new:
    startDownload(for: photoRecord, at: indexPath)
  case .downloaded:
    startFiltration(for: photoRecord, at: indexPath)
  default:
    NSLog("do nothing")
  }
}

ここでは、PhotoRecordのインスタンスとそのIndexPathを渡します。photoRecordの状態に応じて、ダウンロードまたはフィルタ操作のいずれかを開始します。

Note: イメージのダウンロードとフィルタリングの方法は、イメージがダウンロードされている間にユーザーがスクロールしてイメージフィルタをまだ適用していない可能性があるため、別々に実装されています。ユーザーが次に同じrowに来るときに、イメージを再ダウンロードする必要はありません。イメージフィルタを適用することだけ必要です!効率が悪いのですから!

これで、上記のメソッドで呼び出されたメソッドを実装する必要があります。operationを追跡するためにカスタムクラスPendingOperationsを作成したことを忘れないでください。今、あなたは実際にそれを使用するようになる! クラスに次のメソッドを追加します。

ListViewController.swift
func startDownload(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
  //1
  guard pendingOperations.downloadsInProgress[indexPath] == nil else {
    return
  }

  //2
  let downloader = ImageDownloader(photoRecord)

  //3
  downloader.completionBlock = {
    if downloader.isCancelled {
      return
    }

    DispatchQueue.main.async {
      self.pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
      self.tableView.reloadRows(at: [indexPath], with: .fade)
    }
  }

  //4
  pendingOperations.downloadsInProgress[indexPath] = downloader

  //5
  pendingOperations.downloadQueue.addOperation(downloader)
}

func startFiltration(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
  guard pendingOperations.filtrationsInProgress[indexPath] == nil else {
      return
  }

  let filterer = ImageFiltration(photoRecord)
  filterer.completionBlock = {
    if filterer.isCancelled {
      return
    }

    DispatchQueue.main.async {
      self.pendingOperations.filtrationsInProgress.removeValue(forKey: indexPath)
      self.tableView.reloadRows(at: [indexPath], with: .fade)
    }
  }

  pendingOperations.filtrationsInProgress[indexPath] = filterer
  pendingOperations.filtrationQueue.addOperation(filterer)
}

上のコードで何が起きているのかを理解するための簡単なリストです:

  1. 最初に、特定のindexPathを調べて、それに対するdownloadsInProgressのOperationが既に存在するかどうかを確認します。その場合は、このrequestを無視します。
  2. そうでない場合は、指定されたイニシャライザを使用してImageDownloaderのインスタンスを作成します。
  3. Operationが完了したときに実行されるcompletion blockを追加します。これはアプリケーションの残りの部分にOperationが完了したことを知らせるのに最適な場所です。completion blockはOperationがキャンセルされても実行されるため、何かする前にこのプロパティをチェックする必要があります。completion blockが呼び出されたスレッドが保証されていないので、GCDを使用してメインスレッドのテーブルビューを再読み込みする必要があります。
  4. 流れを追跡するために、OperationをdownloadsInProgressに追加します。
  5. Operationをダウンロードキューに追加します。このようにして実際にこれらのOperationにrunをスタートさせます。いったんOperationを追加すると、キューはスケジューリングを処理します。

ImageFiltrationfiltrationsInProgressを使用してOperationを追跡することを除くと、画像をフィルタリングするメソッドは同じパターンに従います。演習として、このコードセクションで繰り返しを取り除くことができます。

やりましたね! あなたのプロジェクトは完了です。ビルドして実行して、あなたが改善したものを見てください!テーブルビューをスクロールすると、アプリは停止しなくなり画像のダウンロードが開始され、表示されるようにフィルタリングされます。

4_iOS-Simulator-Screen-Shot-8-Jul-2014-20.57.53-333x500.png

クールではないですか?いかにして少しの努力があなたのアプリケーションをもっとレスポンシブに向かわせる道を行くことができるかを見ることができます。ユーザーにとってははるかに楽しいです!

Fine Tuning (細かい調整)

あなたはこのチュートリアルで長い道のりを歩んできました!あなたの小さなプロジェクトは反応があり、元のバージョンよりも多くの改善を示しています。しかし、まだ注意が必要な細かい部分があります。

あなたはテーブルビューでスクロールすると、それらのオフスクリーンセルがまだダウンロードされ、フィルタリングされていることに気づいているかもしれません。すばやくスクロールすると、表示されていないにもかかわらず、リスト内のセルの画像をダウンロードしたりフィルタリングしたりするためにアプリがビジー状態になります。理想的なのは、アプリがオフスクリーンセルのフィルタリングをキャンセルし、現在表示されているセルに優先順位を付ける必要があります。

あなたのコードにキャンセルの規約を入れていませんでしたか?はい、入れていましたね。今はおそらくそれらを使用する必要があります!

ListViewController.swiftを開きます。tableView(_:cellForRowAtIndexPath :)の実装に進み、次のようにif文でstartOperationsForPhotoRecordへの呼び出しをラップします。

ListViewController.swift
if !tableView.isDragging && !tableView.isDecelerating {
  startOperations(for: photoDetails, at: indexPath)
}

テーブルビューがスクロールしていない場合にのみ、Operationを開始するようにテーブルビューに指示します。これらは実際はUIScrollViewのプロパティであり、UITableViewUIScrollViewのサブクラスなので、テーブルビューはこれらのプロパティを自動的に継承します。

次に、以下のUIScrollViewデリゲートメソッドの実装をクラスに追加します。

ListViewController.swift
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  //1
  suspendAllOperations()
}

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  // 2
  if !decelerate {
    loadImagesForOnscreenCells()
    resumeAllOperations()
  }
}

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  // 3
  loadImagesForOnscreenCells()
  resumeAllOperations()
}

上のコードを簡単に見てみましょう。

  1. ユーザーがスクロールを開始するとすぐに、すべてのOperationを中断しユーザーが見たいものを見たいと思うでしょう。あなたはすぐにsuspendAllOperationsを実装します。
  2. decelerateの値がfalseの場合は、ユーザーがテーブルビューのドラッグを停止したことを意味します。したがって、一時停止のOperationを再開したり、画面外のcellのOperationをキャンセルしたり、画面上のcellのOperationを開始したりする必要があります。同様に少しの間でもloadImagesForOnscreenCellsresumeAllOperationsを実装します。
  3. このデリゲートメソッドは、テーブルビューがスクロールを停止したことを示しているので、#2と同じ操作を行います。

これらの欠落したメソッドの実装をListViewController.swiftに追加します。

ListViewController.swift
func suspendAllOperations() {
  pendingOperations.downloadQueue.isSuspended = true
  pendingOperations.filtrationQueue.isSuspended = true
}

func resumeAllOperations() {
  pendingOperations.downloadQueue.isSuspended = false
  pendingOperations.filtrationQueue.isSuspended = false
}

func loadImagesForOnscreenCells() {
  //1
  if let pathsArray = tableView.indexPathsForVisibleRows {
    //2
    var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys)
    allPendingOperations.formUnion(pendingOperations.filtrationsInProgress.keys)

    //3
    var toBeCancelled = allPendingOperations
    let visiblePaths = Set(pathsArray)
    toBeCancelled.subtract(visiblePaths)

    //4
    var toBeStarted = visiblePaths
    toBeStarted.subtract(allPendingOperations)

    // 5
    for indexPath in toBeCancelled {
      if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
        pendingDownload.cancel()
      }
      pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
      if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
        pendingFiltration.cancel()
      }
      pendingOperations.filtrationsInProgress.removeValue(forKey: indexPath)
    }

    // 6
    for indexPath in toBeStarted {
      let recordToProcess = photos[indexPath.row]
      startOperations(for: recordToProcess, at: indexPath)
    }
  }
}

suspendAllOperations()resumeAllOperations()は単純な実装を持っています。suspendプロパティをtrueに設定すると、OperationQueuesを中断できます。 これは、キュー内のすべてのOperationを一時停止させます。個別にOperationを中断することはできません。

loadImagesForOnscreenCells()はもう少し複雑です。 ここでは何が起こっているのかというと、

  1. テーブルビューに現在表示されているすべてのrowのindexPathを含む配列から始めます。
  2. 進行中のすべてのダウンロードと進行中のすべてのフィルタを組み合わせて、保留中のすべての一連のOperationを作ります。
  3. キャンセルされるOperationを持つ一連のすべてのindexPathを作ります。すべてのOperationを開始し、表示されているrowのindexPathを削除します。これにより画面外のrowを含む一連のOperationが残ります。
  4. Operationを開始させるのに必要のある一連のindexPathを構築します。表示されているすべてのrowをindexPathで開始し、Operationがすでに保留中のrowを削除します。
  5. 取り消すべきものをループさせ、それらをキャンセルし、PendingOperationsから参照を削除します。
  6. 開始するべきものをループさせ、startOperations(for:at :)をそれぞれ呼び出します。

ビルドして実行すると、レスポンシブで優れたリソース管理アプリケーションが必要になります。自分自身に拍手を送りましょう。

5_improved-700x350.png

テーブルビューのスクロールが終了すると、表示されているrowのイメージがすぐに処理を開始することに注意してください。