Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 488 - URLSessionを使ってテキストデータをダウンロード

2017年2月25日:Swift 3に向けて改訂中*1

「テキストデータをダウンロードする」が今日の課題ですが、まずURLSessionの勉強をします。

URLSession

iOS7以前にネットワークをコントロールするクラスはNSURLConnectionでした。しかし、iOS7の登場に併せて新しいネットワークAPIも発表されました。それがNSURLSessionクラスです。iOS8は移行期間だったのでNSURLConnectionの利用もできていたのですが、iOS9から廃止になりNSURLSessionの利用だけになったんです、そしてiOS10からNSURLSessionはURLSessionに様変わりしました。自分なりにURLSessionクラスの勉強に取り組んでまとめていきます。

なぜURLSessionを使うのか

  • データのアップロードやダウンロードをバックグラウンドで実行
  • ネットワーキングを中断したり再開できる
  • 設定変更可能なコンテナとしての働き
  • サブクラス化してプライベートなストレージを作れる
  • 承認システムの改善
  • 豊富なデリゲーションメソッド
  • データとメタデータの取り扱いを区別化

2パターンのダウンロード

カスタムメソッドを用意する方法とクロージャを使用する方法があります。Swift初心者はメソッドを用意する方法が理解しやすいでしょう。メソッドを用意しなければ関数を引数として与える「クロージャ」を使用しなければ鳴りません。でも知らなければ理解できないので、Swiftで遊ぼう! - 192 - アプリを作ろう UIAlertControllerとUIAlertAction - Swiftで遊ぼう! on Hatenaを先に読みましょう。

まずテキストデータのサンプルを著者*2の個人サイトにオープンしています。

http://www.ymori.com/itest/test.txt

ここのサイトをクリックすると「テストテキストデータ」という文字がブラウザに表示されます。これをアプリに取り込んでいきます。

このアドレスデータを扱うクラスがURLクラスです。

let url = URL(string: "http://www.ymori.com/itest/test.txt")

これは変更のしようがない所ですが、次のURLSessionクラスの起動(インスタンス化)で「あれ?」でした。

 let session = URLSession.shared

なんとSwiftではじめる iPhoneアプリ開発の教科書 【Swift 2&Xcode 7対応】 (教科書シリーズ)チュートリアルではシングルトンパターンを使ってURLSessionクラスのクラスプロパティを利用しています。URLSessionクラスの作成はURLSessionConfigurationクラスのインスタンスを使うのが標準的です。ここで全くURLSessionConfigurationクラスの話に言及していないのは「あれ?」ですね。どうしてかというとパフォーマンスに影響があるからです。文字列のようなデータ量が少ない場合はパフォーマンスに違いがありませんが、画像のような大きなデートを扱う場合パフォーマンスが落ちるため、必ずConfigurationを使ってURLSessionを起動させましょう。

このシングルトンモデルの問題点について後で説明します。

次にデータをダウンロードするタスク(作業)を実装します。

このクラスプロパティのメソッド「dataTaskL()」を使って処理をコードします。

dataTask(with: URL, 
    completionHandler: (Data?, URLResponse?, Error?) -> Void)

// クロージャで書くと下のようになります
dataTask(with: URL, completionHandler: {(data, response, error) in 
 //処理
})

このメソッドに与える引数は2つです。アドレスデータを含むURLクラス型のインスタンス、ダウンロード処理が完了した時に戻されるタップル(data, response, error)を処理する関数(competionHandler)、そして、この関数こそ「クロージャ」なんです。

このクロージャの処理が長くなるとコードが分かりにくくなるので、次のセクションで説明する「メソッドを利用する方法」を使用してコードします。

実際にプロジェクトに実装して試してみます。

このテストのために新しいプロジェクトを作るのが面倒くさいので、前回イメージをダウンロードした時に使ったプロジェクト「NSURLSessionTest」を使います。

ここに追加するオブジェクトは2つ。UILableとUIButtonです。

この2つのオブジェクトをMain.storyboardにオブジェクト・ライブラリからドラッグ&ドロップして適当に設置してから、「Ctrl + ドラッグ」でViewControllerに@IBOutletと@IBActionを作ります。

f:id:yataiblue:20170225211724j:plain

この設置の仕方は、ここの手順と全く同じなんで慣れる必要があります。UILabelは、@IBOutletの「testLabel」、UIButtonは、@IBActionの「downloadText」にします。名前はそれぞれMain.storyboard上でダブルクリックしてUILabelを「default message」、UIButtonを「ダウンロードテキスト」にします。

まずテキストデータをダウンロードするメソッド「getText」をコードします。この教科書でコードしている内容が次の通りです。

func getText() {
    guard let url = 
       URL(string: "http://www.ymori.com/itest/test.txt") else {
            print("no such file")
            return
    }
    let session = URLSession.shared
    let task = 
        session.dataTask(with: url,
            completionHandler: {(data, response, error) in
        let str = String(data: data!, encoding: String.Encoding.utf8)
        self.testLabel.text = str
    })
    task.resume()
}

このコードは分かります。URLのデータを用意して、セッションを起動させて、dataTask()を使ってcompletionHandler関数が起動されるように、タップル情報が渡されます。このタップル情報は、セッションが起動されてタスクが呼ばれると戻ってくるので、ここで記述するクロージャーはタップル情報を処理する必要があります。当然ですが、この情報の中で欲しいのはdata情報です。これはDataクラスになります。

このデータを使ってString型のクラスを生成させます*3

もう一つ重要なステップがあります。インセキュアなサイトからデータをダウンロードするためATSを避ける必要がありInfo.plistを編集します

そしてこのメソッドを@IBActionの「downloadText」内に加えます。

@IBAction func downloadText(sender: UIButton) {
  getText()
}

アプリをランしてボタンを押します... しばらくするとラベルにテキストが表示されます。

f:id:yataiblue:20170225212808j:plain

URLSessionをインスタンス化する時にURLSessionConfigurationをなぜ使わないのか?

試しに以下のように getTextAsync()にしてみます。

func getTextAsync() {
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)
    let url = URL(string: "http://www.ymori.com/itest/test.txt")
    let task = 
        session.dataTask(with: url!) {(data, response, error) in
        guard let getData = data else {
            print("no such data")
            return
        }
        DispatchQueue(label: "someTask").async {
            let str = String(data: getData, 
                          encoding: String.Encoding.utf8)
            DispatchQueue.main.async {
                self.testLabel.text = str
            }
        }
    }
    session.invalidateAndCancel()
    task.resume()
}

このメソッドは、URLSessionクラス利用の時の一般的なコーディングです。内容を完全に理解していると言えば嘘になりますが、スタンフォード大のポール先生の講義で取りあげられています*4

プログラミング初心者にとってこのマルチスレッドの扱いは難易度が高いのかもしれません*5
。そのためこのチュートリアルで触れていないのかもしれません。URLSessionConfigurationも使わない実装法なら説明がシンプルになるからかもしれません。しかし、URLSessionConfigurationを使わないと明らかにパフォーマンスが悪化してしまうんです。

試しに上記のメソッドを@IBActionの「downloadTextt」内のgetText()と書き換えます。

@IBAction func downloadText(sender: UIButton) {
 getTextAsync()
}

すると、ボタンを押したら一瞬で「テストテキストデータ」がラベルに表示されます!*6 教科書で書かれているコーディングではひと呼吸、ふた呼吸待たされるのが嘘のようです。というかこの位レスポンスがよくないと使えませんね。

このように明らかな実行レスポンスに差が出てしまいます。URLSessionConfigurationを使うと、ネットワーク通信の最適化が行われるということでしょうか?

*1:オリジナル記事は、Swiftではじめる iPhoneアプリ開発の教科書 【Swift 2&Xcode 7対応】 (教科書シリーズ)のNSURSessionの解説をしていました

*2:Swiftではじめる iPhoneアプリ開発の教科書 【Swift 2&Xcode 7対応】 (教科書シリーズ)の著者です。

*3:Swift 2までは、NSData型を使って直接String型を生成せきなかったんで、NSString型をワンクッション入れていました。しかし、Swift 3ではData型からダイレクトにString型を生成することができるので便利になりました。

*4:マルチスレッドの内容をすっかり忘れています(^^;)、そろそろ復習をしないといけない時期ですね

*5:マルチスレッドの解説が知りたければSwiftで遊ぼう! - 302 - マルチスレッド(まとめ) - Swiftで遊ぼう! on Hatenaを読みましょう。

*6:相変わらずOptionalの表示ですが...