30333


swiftにおいて、非同期処理によって複数のURLを取得し、処理終了後に命令を与える方法について質問です。

以下のコードのように url_arr にいくつかのURLを入力しておき、for文によってhtmlを取得し、それぞれのhtml長さを length_arr
に保存します。全ての処理が終わったら length_arr を print したい、といった場合、どのような書き方が可能でしょうか？

ViewController.swift

[code]

    import UIKit
    
    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        var url_arr:[String] = ["http://yahoo.co.jp", "http://qiita.com", "http://apple.com"]
        var length_arr:[Int] = []
    
        @IBAction func button(sender: AnyObject) {
            length_arr=[]
            print("buttonpushed")
            for url in url_arr{
                self.url2length(url)
            }
            print(length_arr)
        }
    
        @IBAction func button2(sender: AnyObject) {
            print("button2pushed")
            print(length_arr)
        }
    
        func url2length(url: String) -> Void  {
            //print (url)
            let session = NSURLSession.sharedSession()
            let urll = NSURL(string:url)!
            let request = NSURLRequest(URL: urll)
    
            let task =
            session.dataTaskWithRequest(request) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
                if let data = data{
                    let nsvalue = NSString(data: data, encoding: NSUTF8StringEncoding)
                    let value = String(nsvalue)
                    print(value.characters.count)
                    self.length_arr.append(value.characters.count)
                }
            }
            task.resume()
        }
    
    }
    
[/code]

実行結果(信用度の関係でリンクを複数はれないようなのでhttp://は省略しました）  
buttonpushed  
yahoo.co.jp  
qiita.com  
apple.com  
[]  
36762  
24884  
16038  
button2pushed  
[36762, 24884, 16038]

dispatch groupを使用すると可能なようなので、該当部分を以下のように書き直しましたが、結果は変わりませんでした。

[code]

    let dispatchGroupe = dispatch_group_create()
    let queue = dispatch_queue_create("q1", DISPATCH_QUEUE_SERIAL)
    
    
    @IBAction func button(sender: AnyObject) {
        length_arr=[]
        print("buttonpushed")
        for url in url_arr{
            dispatch_group_async(dispatchGroupe, queue, { () -> Void in
                self.url2length(url)
            })
        }
        dispatch_group_notify(dispatchGroupe, queue) { () -> Void in
            print(self.length_arr)
        }
    }
    
[/code]

（この場合はURLをGETするという三つの命令が終了した段階でnotifyが実行されてしまっているのでしょうか。）  
理想としては最初の print length_arr の段階で三つの要素のあるarrayが表示されて欲しいのですが、どこを直せば良いのか、ご教授ください。

よろしくお願いします。


CC BY-SA 3.0
2016-11-11T13:33:23.323
30331
2016-11-11T15:54:44.417
19453
question
swift xcode
swiftにおけるURL取得の非同期処理の終了時の処理について


dispatch groupをうまく使うコツは、

  * すべての並列処理が走り出す前にその処理をグループに登録してしまうこと

と言うことになります。具体的に言うと、「並列処理の数だけ、並列処理を起動する前に`dispatch_group_enter(_:)`を呼んでおく」と言うことになります。

(私の経験から言うと`dispatch_group_async`はなかなか思うようには動いてくれません。`dispatch_group_enter(_:)`の呼び出しと並列処理の起動は別にして自前で行う方が確実です。)

そこら辺を踏まえてあなたの処理を書き換えるとこんな感じになります。

[code]

    class ViewController: UIViewController {
        //...
    
        var url_arr:[String] = ["http://yahoo.co.jp", "http://qiita.com", "http://apple.com"]
        var length_arr:[Int] = []
    
        var dispatchGroup: dispatch_group_t?
    
        @IBAction func button(sender: AnyObject) {
            dispatchGroup = dispatch_group_create()
            //バックグラウンドスレッドが走り出す前にすべての処理をdispatch_groupに登録しておく必要がある
            for _ in url_arr {
                dispatch_group_enter(dispatchGroup!)
            }
            length_arr=[]
            print("buttonpushed")
            //dispatch_group_enter()をすべて終えてから非同期処理を起動
            for url in url_arr{
                //NSURLSession自体が非同期処理なので、それをシリアルキューの中から起動する必要はない
                self.url2length(url)
            }
            print(length_arr) //->[] ここではまだ空
            //メインキューの指定は必須ではないが、普通はUIの更新を含むのでメインキューを指定している
            dispatch_group_notify(dispatchGroup!, dispatch_get_main_queue()) {
                //全てのバックグランドスレッド終了後の処理
                print("After dispatch_group_wait ", self.length_arr)
            }
        }
    
        @IBAction func button2(sender: AnyObject) {
            print("button2pushed")
            print(length_arr)
        }
    
        func url2length(url: String) -> Void  {
            print("sending request for", url)
            let session = NSURLSession.sharedSession()
            let urll = NSURL(string:url)!
            let request = NSURLRequest(URL: urll)
    
            let task = session.dataTaskWithRequest(request) { data, response, error in
                if let
                    data = data,
                    value = String(data: data, encoding: NSUTF8StringEncoding)
                {
                    print(value.characters.count)
                    //SwiftのArrayはスレッドセーフではないので、その操作はメインキューの中だけで行うのが安全
                    dispatch_async(dispatch_get_main_queue()) {
                        self.length_arr.append(value.characters.count)
                        print("finished processing response for", url)
                        //処理が完了したら、自身の所属するグループに伝える
                        dispatch_group_leave(self.dispatchGroup!)
                    }
                }
            }
            task.resume()
        }
    }
    
[/code]

* * *

余談ですが、Appleは「Swift2をサポートするのはXcode8.2が最後だよ」と言う宣言を出しています。早めにSwift
3への移行を検討された方が良いのではないでしょうか。


CC BY-SA 3.0
2016-11-11T15:54:44.417
30333
2016-11-11T15:54:44.417
13972
30331
answer