29975


1.NCMBから画像のurlを取得し、  
2.配列に加えた後  
3.URLにアクセスして画像を取得  
4.CollectionViewのCellの上に載っているImageViewに表示する

という作業をしたいのですが、データストアからデータを取得するより先に戻り値を返してしまい、処理が終了してしまいます。

4の処理を遅延させて、データストアからデータを取得し配列に加えた後に、CollectionViewのCellの上に載っているImageViewに表示するにはどうしたらよいのでしょうか。

[code]

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell{
    
        // クラスのNCMBObjectを作成
        let obj = NCMBObject(className: "image")
        // objectIdプロパティを設定
        obj?.objectId = "xxxxxxxxxxxx"
        // 設定されたobjectIdを元にデータストアからデータを取得
        obj?.fetchInBackground { error in
            if error != nil {
                // 取得に失敗した場合の処理
            }else{
                // 取得に成功した場合の処理
                // (例)取得したデータの出力
                print(obj!.object(forKey: "imageUrl"))
                self.imageUrlArray.append(obj!.object(forKey: "imageUrl") as! String)
    
            }
        }
    
        // Cell はストーリーボードで設定したセルのID
        let testCell:UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    
        var url = URL(string: self.imageUrlArray[indexPath.row])
    
        do {
            self.imageData = try Data(contentsOf: url!,options: NSData.ReadingOptions.mappedIfSafe);
    
        } catch {
            print("Error: can't create image.")
        }
    
        // Tag番号を使ってImageViewのインスタンス生成
        let imageView = testCell.contentView.viewWithTag(1) as! UIImageView
        // 画像配列の番号で指定された要素の名前の画像をUIImageとする
        imageView.image = UIImage(data: self.imageData! as Data)
    
        return testCell
    }
    
[/code]


CC BY-SA 3.0
2016-10-27T21:56:29.207
29950
2016-10-29T06:07:23.103
2016-10-29T06:07:23.103
13972
18443
question
swift ios swift3
CollectionViewの非同期処理


簡単に言うと、簡単ではありません…。

  * 非同期処理をうまく使うコツは「完了を待つ」のではなく、「完了してから実行されるところで結果を使う」ように頭を切り替えることです。

あなたのご質問の文面から言うと、「4の処理を遅延させ」るのではなく、「3.URLにアクセスして画像を取得」が「完了してから実行されるところで結果(その画像)を使う」と言うことになります。

つまり頭を切り替えて、取得した画像は完了ハンドラー(あなたのコードの`{ error in ... }`)の中で使えば良いと言うことになります。

もちろん`collectionView(_:cellForItemAt:)`の結果は画像取得が完了するまで返さないなんてことはできませんから、仮の画像を入れたCellをすぐに返すと言う必要があります。

  * Cellは再利用されるのでその考慮も必要です

`UITableViewCell`や`UICollectionViewCell`のインスタンスは出来るだけ再利用するように動きますので、画像の取得が完了した時には、その画像を設定したかったCellはもう再利用されて別の用途に使われているかもしれません。

(たまに`UITableView`を使った画面を高速にスクロールすると、本来とは違う画像が表示されるアプリがあったりしますが、この辺の考慮が足りない実装になっている可能性が高いと思われます。)

何らかの方法で、画像を設定したかったCellが再利用されていないことを確認しないといけません。

* * *

以上のような内容を踏まえてあなたのコードを書き直すと、次のような感じになります。NCMBを簡単に試すことはできないので、コンパイルが通る、程度のチェックしかしていませんから、このような流れになる、という雰囲気を理解するためのものと思ってください。

[code]

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        // Cell はストーリーボードで設定したセルのID
        let testCell:UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        // Tag番号を使ってImageViewのインスタンス取得 <- ここで「生成」するのではない
        let imageView = testCell.contentView.viewWithTag(1) as! UIImageView
        // クラスのNCMBObjectを作成⇒guard letを使えば後の`?`や`!`が不要になる
        guard let obj = NCMBObject(className: "image") else {
            //NCMBが利用できない場合の画像もアプリ内に用意する
            imageView.image = UIImage(named: "Unavailable")
            //たとえ何かの異常があってもnon-nilのUICollectionViewCellを返さないとコンパイルできない
            return testCell
        }
        //Loading用の仮イメージをアプリ内に用意して、ロード中はそちらを表示させる
        imageView.image = UIImage(named: "Loading")
        // objectIdプロパティを設定
        obj.objectId = "xxxxxxxxxxxx"
        //画像ローディングの管理IDを作成する(ユニークな値であれば何でも良い、arc4random()は実用的に十分)
        let loadingId = Int(Int32(bitPattern: arc4random())) //<-32 bit環境でも動くようにIntに変換している
        //管理IDをCellにも設定、tagは他用途に使う可能性もあるのでできれば専用カスタムCellにloadingId属性を持たせた方が良い
        testCell.tag = loadingId
        // 設定されたobjectIdを元にデータストアからデータを取得
        obj.fetchInBackground { error in
            if let error = error {
                // 取得に失敗した場合の処理
                print(error)
            } else {
                // 取得に成功した場合の処理
                // (例)取得したデータの出力
                let imageUrlString = obj.object(forKey: "imageUrl") as! String
                print(imageUrlString)
                //Cellは再利用されるので、load完了した時にはもう別用途に使われているかもしれないからそのチェック
                guard testCell.tag == loadingId else {
                    print("Cell seems to be reused")
                    return
                }
                //外部から取得した文字列はURLとして不正かもしれないので、それもチェックしておく
                guard let url = URL(string: imageUrlString) else {
                    print("Invalid imageUrl")
                    return
                }
                //イメージデータのロードも完了ハンドラーの中でやってしまう(mappedIfSafeは外部URLからのloadには使用しない)
                let imageData: Data
                do {
                    imageData = try Data(contentsOf: url)
                } catch {
                    print("Error: can't create image.")
                    return
                }
                //取得したデータをUIImageに変換、これも失敗する可能性があるのでguard-letでチェック
                guard let image = UIImage(data: imageData) else {
                    print("Invalid imageData")
                    return
                }
                //ここまで無事に終了したら目的のセルに画像を設定する
                DispatchQueue.main.async {
                    imageView.image = image
                    //Swiftの配列はスレッドセーフではないので配列の更新は必ずメインスレッドで行うことにした方が安全
                    if self.imageUrlArray.count <= indexPath.row {
                        self.imageUrlArray.append(contentsOf: (self.imageUrlArray.count...indexPath.row).map{_ in ""})
                    }
                    //非同期処理は順不同で終了するので、結果を配列の最後に入れても正しい順序にはならない
                    self.imageUrlArray[indexPath.row] = imageUrlString
                }
            }
        }
        //Loadが完了したら画像を設定するよ、という準備が済んだら(Loading画像の設定された)Cellを返す
        return testCell
    }
    
[/code]

細かい部分あれこれ手直しが必要かもしれませんが、あなた自身の環境と目的に合わせて修正しながらお試しください。

画像の遅延ロードをサポートするようなサードパーティ製のライブラリもあるようです(残念ながら私は使ったことがないので詳しくはわかりません)ので、そう言ったものを調べてみるのも良いかもしれません。


CC BY-SA 3.0
2016-10-29T04:52:30.363
29975
2016-10-29T04:52:30.363
13972
29950
answer