【Swift】URLSessionで非同期通信を実装する方法

こんにちは、のっくんです。

今日はSwiftのURLSessionの使い方について述べたいと思います。

URLSessionを使うと複数のファイルを非同期で同時にダウンロードすることができます。

ダウンロードするファイルが複数になると同期してダウンロードするのには時間がかかりますが、非同期処理を実装するとサクサク動くアプリを作れます。

非同期でXMLファイルを同時にダウンロードしてXMLパースするやり方を見ていきましょう。

Swiftは5.2、iOSは13.4です。

URLSessionを使わない場合

以下のように1つずつURLを取り出してXMLパースをかけていきます。

let dataSource = [
                  "https://scan.netsecurity.ne.jp/rss/index.rdf",
                   "https://www.lac.co.jp/lacwatch/feed.xml",
                   "https://jp.techcrunch.com/news/security/feed/",
                   "https://gigazine.net/news/rss_2.0/"]

for url in self.dataSource {
        if let url = URL(string: url){
            if let parser = XMLParser(contentsOf: url){
                self.parser = parser
                self.parser.delegate = self
                self.parser.parse()
            }
        }
    }

この実装だと処理速度が遅いです。

4つのURLがありますが、1つずつ順番にダウンロードしてパースしています。

順番に処理するのでURLの数が増えるにつれて、線形に時間が増えていきます。

URLSessionを使う方法(非同期処理)

URLSessionを使うと同時に処理できるので高速になります。

for文の中身は順番に実行されるのではなく、同時に実行されます。

URLが4つあるので4つのスレッドが割り当てられて同時に処理されます。

for urlString in self.dataSource{

    let req_url = URL(string: urlString)
    let req = URLRequest(url: req_url!)
    let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)

    let task = session.dataTask(with: req, completionHandler: {
        (data, response, error) in
        session.finishTasksAndInvalidate()
        
        //XMLをパースする
        self.parser = XMLParser(data: data!)
        self.parser.delegate = self
        self.parser.parse()
       
        })

    task.resume()
}

大まかなコードの流れは以下の通り。

  1. URLSessionを作成します。デフォルトのセッション構成にします。今回取得したデータはクロージャで処理するため、delegateはnilにしています。OperationQueue.mainを指定し、メインスレッドのキューを使うようにしています。
  2. タスクを作成します。completionHandlerはクロージャと呼ばれ、データのダウンロードが完了したときに呼ばれます。
  3. ダウンロードしたデータは変数dataに入っているので、XMLパーサに入れてパースします。

少しコードが多くなりましたが、これで複数のファイルのダウンロードが同時に実行できます。

処理結果を返すようにする

上記のコードではXMLパースして終了になっています。

パースした結果を何かの入れ物に入れて利用するにはどうすれば良いでしょうか。

func downloadPararrel(completion: @escaping ([Item]?) -> ()){
    
    for urlString in self.dataSource{

        let req_url = URL(string: urlString)
        let req = URLRequest(url: req_url!)
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)

        let task = session.dataTask(with: req, completionHandler: {
            (data, response, error) in
            session.finishTasksAndInvalidate()
            
            self.parser = XMLParser(data: data!)
            self.parser.delegate = self
            self.parser.parse()
           
            completion(self.items)
               })

        task.resume()
    }
}

1行目と17行目を追加しました。

パース結果をItemクラスに保存し、配列itemsに追加していくとします。

スレッドのタスクが終了したら配列itemsをcompletionに指定します。

こうすることで関数内の処理が終わったらitemsを関数の返り値としてリターンすることができます。

呼び出す側は以下の通り。

let handler = XMLHandler()
handler.downloadPararrel(completion: { returnData in
  if let returnData = returnData {
      // returnDataが使える。
 }
})

downloadPararrelではURLの数だけスレッドが実行されますが、スレッドの実行順序はランダムです。

各スレッドの処理が終わるとXMLパースの結果がreturnDataとして戻ってきます。

4つのスレッド処理が終わったタイミングでcompletionが呼ばれるので、completionは合計4回呼ばれることになってしまいます。

全てのスレッドの処理が終わってからデータを返すにはどうすれば良いのでしょうか。

DispatchGroupで非同期を管理する

上記の例だとスレッドの処理が全て終わらないままデータを返しているので、呼ぶ側としては使いづらいです。

全スレッドの非同期処理が完了するのを待ってからデータを返す方がベターです。

そういう場合にはDispatchGroupを使います。

func downloadPararrel(completion: @escaping ([Item]?) -> ()){
    
    let dispatchGroup = DispatchGroup()
    
    for urlString in self.dataSource{

        print("start fetching",urlString)
        
        dispatchGroup.enter()
        
        let req_url = URL(string: urlString)
        let req = URLRequest(url: req_url!)
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)

        let task = session.dataTask(with: req, completionHandler: {
            (data, response, error) in
            session.finishTasksAndInvalidate()
            print("finish",data!.count)
            
            self.parser = XMLParser(data: data!)
            self.parser.delegate = self
            self.parser.parse()
           
            dispatchGroup.leave()
               })
        
        task.resume()
        }
    dispatchGroup.notify(queue: .main){
        completion(self.items)
    }
}

DispatchGroupを使ってグループの動作を管理します。

グループを使用すると、一連のタスクを集約し、グループの動作を同期できます。 複数の作業項目をグループに関連付け、それらを同じキューまたは異なるキューで非同期実行するようにスケジュールします。 すべての作業項目の実行が完了すると、グループはその完了ハンドラーを実行します。 グループ内のすべてのタスクの実行が完了するのを同期的に待つこともできます。

https://developer.apple.com/documentation/dispatch/dispatchgroup

.enter()で各タスク(スレッド)の開始点を、.leave()で各タスク(スレッド)の終了点を決めます。

全てのタスクの非同期処理が終わった後に呼び出されるのが.notifyです。

この中で取得した結果を返すようにすれば、一度にまとめて返せます。

参考

https://fluffy.es/download-files-sequentially/

https://developer.apple.com/documentation/dispatch/dispatchgroup