遅いファイル出力処理でのプログレスバーの表示がうまくいかない

投稿者: Anonymous

ファイルを出力してUIActivityViewController経由で転送する、という処理があります。
ファイルの元データを加工する処理を追加したのですが、この処理が遅くプログレスバーを表示させようと、現在苦戦しています。お知恵をお貸しください。

想定している動作は、
ボタンをタップ
->プログレスバーを載せたUIViewの子クラスが表示され、ファイルの加工処理開始。
->加工が完了したらプログレスバーの表示が100%になる。
->ファイルを出力。
->UIActivityViewControllerが表示される。
というものなのですが、
現状ではボタンタップ後、数秒経ってからプログレスバーが表示され、ほぼ同時にUIActivityViewControllerが表示されてしまいます。><;

GCDを組み込んでみたりしたのですが(★の箇所)動作も理解もいまいちな状態です。
コードを以下に記します。
お手数ではありますが、どのように直したら良いかお教え頂けますと大変助かります。
どうぞよろしくお願いいたします。

class MyClass: UITableViewController {

    fileprivate var _viewProgress: MyProgressView!
    @IBOutlet fileprivate weak var btnAction: UIBarButtonItem!

    override func viewDidLoad() {
        super.viewDidLoad()

        //プレグレスバーを載せたビューを準備
        _viewProgress = MyProgressView() //(UIViewの子クラス)
        _viewProgress.isHidden = true
        self.view.addSubview(_viewProgress)
    }

    /// ボタン押下時処理
    @IBAction func btnAction_Tap(_ sender: UIBarButtonItem) {
        //プログレスバーを表示
        _viewProgress.setProgress(0.0)
        //_viewProgress.isHidden = false
        _viewProgress.show()
        /* 追記 show関数の中身↓
            func show() {
            if let oya = self.superview {
                self.center = CGPoint(x: oya.frame.width / 2, y: oya.frame.height / 2) //中央に配置
                oya.bringSubview(toFront: self) //最前面に配置
            }
            self.isHidden = false
        */

        //ファイル作成
        createFile() //---(1)へ
    }

    /// (1) ファイル作成処理
    func createFile() {
        //ファイル出力
        let rs = getData() //---(2)へ
        let filePath: String = outputFile(rs) //tmpフォルダに出力

        //(出力成功)
        if filePath != "" {
            let file = URL(fileURLWithPath: filePath)
            let handler: ((Bool) -> Void) = { [unowned self] (completed) in
                if completed == true {
                    print("処理完了")
                }
                self._viewProgress.dismiss()
            }
            //ダイアログ(UIActivityViewController)を表示
            showDialog(self, activityItems: [file as AnyObject], handler: handler)

        //(出力失敗)
        } else {
            //〜エラーメッセージ表示〜
        }
    }

    /// (2) データ加工処理
    func getData() -> [[String : String]] {
        //加工元のレコードセットを取得
        var rs: [[String : String]] = getOrgData()

        DispatchQueue.global(qos: .default).sync { //---★
            //ループしてレコードを1行ずつ処理
            for i in 0 ..< rs.count {
                //進捗表示
                DispatchQueue.main.async { [unowned self] in //---★
                    self._viewProgress.setProgress(Float(i + 1) / Float(rs.count))
                }

                var data = rs[i]
                //〜データの加工をゴリゴリ〜
                rs[i] = data
            }
        }
        return rs
    }

}

解決

あなたのコードでせっかく書いたプログレスバー更新処理がうまくいかないのはこの部分のせいです。

    DispatchQueue.global(qos: .default).sync { //---★

syncメソッドは呼び出し側のスレッド(今の場合メインスレッド)をブロックしてしまい、せっかく並行処理可能なglobalキューで実行しても、DispatchQueue.main.async {...}のところでメインキューに入れられたself._viewProgress.setProgress(Float(i + 1) / Float(rs.count))は、ずっとメインキューの中で順番待ち、あなたのコードで言うとbtnAction_Tap(_:)の処理が完了するまで実行されません。

メインキューで行わなければいけない処理をブロックしないようにするためには、非同期処理を使って、「この部分は後で」あるいは「この部分は裏で(非同期に並列して)」と言うことだけ指定したら、さっさとbtnAction_Tap(_:)を終了しないといけません。

言葉で言うと大変なことをさせられるように見えるかもしれませんが、あなたのコードの場合、典型的な定石通りの完了ハンドラーを使うパターンに簡単に書き換えられます。

(質問文中には説明されていないメソッド等があるので、若干修正しないと使えないかもしれません。)

/// ボタン押下時処理
@IBAction func btnAction_Tap(_ sender: UIBarButtonItem) {
    //プログレスバーを表示
    _viewProgress.setProgress(0.0)
    _viewProgress.isHidden = false

    //ファイル作成
    createFile() //---(1)へ
    //下に書いた理由と同じで、ここに何か処理を書くと、完了ハンドラーより先に実行されてしまう
}

/// (1) ファイル作成処理
func createFile() {
    //ファイル出力
    getData{ rs in //---(2)へ <-- 非同期処理の結果は戻り値で受け取ろうとせず、クロージャーで拾う
        let filePath: String = self.outputFile(rs) //tmpフォルダに出力

        //(出力成功)
        if filePath != "" {
            let file = URL(fileURLWithPath: filePath)
            let handler: ((Bool) -> Void) = { completed in
                if completed {
                    print("処理完了")
                }
                self._viewProgress.dismiss()
            }
            //ダイアログ(UIActivityViewController)を表示
            self.showDialog(self, activityItems: [file as AnyObject], handler: handler)

        //(出力失敗)
        } else {
            //〜エラーメッセージ表示〜
        }
    } //<- 「この部分(完了ハンドラー)は後で」(処理完了後に)実行するのを登録されるだけで`getData(completion:)`はすぐに終了する
    //従ってこの部分にコードを書いてしまうと完了ハンドラーより先に実行されてしまうので注意
}

/// (2) データ加工処理
func getData(completion: @escaping ([[String : String]])->Void) {
    //加工元のレコードセットを取得
    var rs: [[String : String]] = getOrgData()

    DispatchQueue.global().async { //<- ここで同期メソッドを呼んでは意味がない、必ず`async`を使う
        //ループしてレコードを1行ずつ処理
        for i in 0 ..< rs.count {
            //進捗表示
            DispatchQueue.main.async {//<- いくらここでメインキューに処理を入れてもメインキューが停止中だと意味がない
                self._viewProgress.setProgress(Float(i + 1) / Float(rs.count))
            }

            var data = rs[i]
            //〜データの加工をゴリゴリ〜
            rs[i] = data
        }
        completion(rs)  //<- 非同期処理の結果は戻り値にせずに完了ハンドラーに渡すというのが定石の一つ
    } //<- 「この部分は裏で」実行するよう登録されるだけで`DispatchQueue.global().async`自体はすぐに終了する
}

ちなみにあなたのコードの使い方では[unowned self]は不要(循環参照になる心配はない)なので、省かせてもらっています。どんな場合に「循環参照になる心配はない」のかがよくわからないのであれば[unowned self]ではなく[weak self]を使った方がいいでしょう。unownedは使い方を間違えるとアプリが簡単にクラッシュしてしまいます。

コメントには他にも色々書いていますが、とりあえずあなたのコードに当てはめて動きを確かめてもらってから意味を考えた方がいいかもしれません。

回答者: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *