Swiftでのサーバ通信はすでに多くの記事が書かれていて、今更僕が付け加えることは何もないので、
この記事はとにかく初学者にわかりやすいことをテーマに書こうと思います。
前提
初学者向けといっても、Swiftの初等文法は習得済という想定です。
クロージャ、非同期処理、エラー処理あたりは理解してなくても読めるように書きます(たぶん……)。
あとAlamofireは使いません。
(いずれAlamofire使ったバージョンの記事も書きたいですが)
Foundationだけでやります。
サーバ通信のコードがわかりづらい理由
サーバ通信のコードってわかりづらいですよね。
コードがわかりづらくなる原因を僕なりに考えたんですが、
サーバ通信はコードの中で制御できない要素だからわかりづらいという結論に落ち着きました。
あるURLにアクセスしたとき、サーバからどういうデータが返されるかは、アプリサイドは正直もらってみるまでわかりません。
アクセスしたときに、サーバが落ちていて、欲しかったデータが返ってこない可能性もあります。
また、ネットワーク状況によっては、タイムアウトで切れてしまう場合もあります。
ただモバイルアプリを作る際に、サーバ上のデータを使わないで完結するケースはほぼないと思うので、
アプリ作りたいなら何らかのAPI叩く手段を知っとくのは必須だと思います。
サーバ通信でやること
サーバと通信するためには、いくつかのプロトコルがあります。
http、https、ftpなどです。
昨今APIで通信する際は、基本的にはhttp(s)でやります。
もっというと、httpsをWebの標準にしようと各社動いています。
- HTTPリクエストを送る
- HTTPレスポンスが返ってくる
これだけです。
コーディングに入るといろんな情報が出てきて混乱するかもしれませんが、シンプルに考えてください。
同期と非同期
コーディングに入る前に、もう一つ大事な概念がありました。
サーバ通信には、同期通信と非同期通信という2パターンにわかれます。
同期通信は、サーバのレスポンスが返ってくるまで、後続処理を待ちます。
非同期通信だと、待たないで後続処理を行い、レスポンスが返ってきた段階で割込処理でさばきます。
後述しますが、URLSessionというFoundationのクラスを使ってHTTP通信を行うのですが、
基本的にはこのクラスを使った場合非同期処理になります。
今回の目標
さて、コーディングに入りましょう。
みんな大好きGithubのAPIを使って、ユーザー検索した結果を返すメソッドを作りましょう。
このURLの、q=のあとに、なにか文字を入れて、ブラウザで叩いてみてください。
たとえば「tanaka」と入れると、
{
"total_count": 2326,
"incomplete_results": false,
"items": [
{
"login": "tanakaedu",
"id": 7675810,
"node_id": "MDQ6VXNlcjc2NzU4MTA=",
"avatar_url": "https://avatars0.githubusercontent.com/u/7675810?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/tanakaedu",
"html_url": "https://github.com/tanakaedu",
"followers_url": "https://api.github.com/users/tanakaedu/followers",
"following_url": "https://api.github.com/users/tanakaedu/following{/other_user}",
"gists_url": "https://api.github.com/users/tanakaedu/gists{/gist_id}",
"starred_url": "https://api.github.com/users/tanakaedu/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/tanakaedu/subscriptions",
"organizations_url": "https://api.github.com/users/tanakaedu/orgs",
"repos_url": "https://api.github.com/users/tanakaedu/repos",
"events_url": "https://api.github.com/users/tanakaedu/events{/privacy}",
"received_events_url": "https://api.github.com/users/tanakaedu/received_events",
"type": "User",
"site_admin": false,
"score": 98.2799
},
(以下略)
こんな結果が返ってくると思います。
これをSwiftで書いて、受け取ってみましょう。
func searchGithubUser(query: String) {
//Githubのユーザー名検索結果を表示する
}
まずこんなメソッドを作ります。
queryで検索したい文字列を指定するわけですね。
FoundationのHTTPリクエストまわりの歴史
iOS9でHTTP通信のクラスに変更があったみたいで、NSURLConnectionから(NS)URLSessionというクラスに変わっています。
NSURLConnection時代の解説は、下記がわかりやすかったです。
(途中まで廃止されたことに気づかずに読んでしまいました)
NSURLConnection時代は、sendSynchronousRequest:returningResponse:errorというメソッドを使うと、同期処理が書けました。
非同期処理にしたかったら、sendAsynchronousRequest:queue:completionHandlerを使う、でした。
これはこれでわかりやすかったと思うのですが、同期処理がなくなったのは、そもそも非同期で全部やれや、というメッセージなんでしょうか。
HTTPリクエストをつくる
URLRequestというクラスを使って、HTTPリクエストをつくってあげましょう。
リクエストの実体は、下記のようなデータです。
GET / HTTP/1.1
Host: qiita.com
User-Agent: curl/7.54.0
Accept: */*
「HTTPリクエスト/レスポンスの構成要素を初心者にも分かるように解説してみた」より
このヘッダに関しては、HTTPというプロトコルで決められたフォーマットなので、このまま作る必要があります。
もちろん1から記述してもすべて正しければ多分リクエスト生成できると思いますが、
上記のようなシンプルな形だったらいいですが、実際にやりとりするのはもう少し複雑だったりするので、生成するためにクラスが用意されています。
URLRequestというクラスは、urlオブジェクトを渡すと、上記のHTTPリクエストを生成してくれます。
func searchGithubUser(query: String) {
let url = URL(string: "https://api.github.com/search/users?q=" + query)!
let request = URLRequest(url: url)
}
※URL変換でフォースアンラップしているため、queryに全角文字指定されると落ちます
はいこれでHTTPリクエストつくれましたー。
URLRequestはデフォルトではGETメソッドになっています。
POSTにしたいときは、
request.httpMethod = "POST"
など指定してください。
ヘッダの付与、ボディの付与もできます。
今回は何も設定を変えないので、letで定義しましたが、もし設定いじるならばvarで定義してください。
このように、複雑なデータをプログラマが扱いやすいようにラップしてくれるクラスをラッパークラスと呼ぶそうです。
もしラッパークラスがなければ、全力でHTTPリクエストを生成するロジックを書かなきゃいけなくて、しんどかったですね。
HTTPリクエストを投げる、その結果を受け取る
リクエストができたなら投げましょう。
リクエストを投げるためには、サーバと通信する必要がありますね。
URLSessionというクラスを使いましょう。
func searchGithubUser(query: String) {
let url = URL(string: "https://api.github.com/search/users?q=" + query)!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
//ここにデータ受信後の処理を書く
}
task.resume()
}
リクエストに比べると、コード的な難易度は高いので、一つずつ説明していきます。
URLSession.sharedとは
URLSessionのインスタンスを作る際に、設定情報を渡す必要があります。
URLSession.sharedを使うと、設定情報なしで、セッションが使えます。
しかもインスタンス化しなくても使えます。
(これはよく仕組みがわからないですが)
シングルトンオブジェクトにアクセスしています。
シングルトンというのは、アプリ全体でたった1つのオブジェクトを使い回すデザインパターンです。
よくわかんなくても、ここでは問題ないです。
詳しく知りたければシングルトンでググってください。
キャッシュを使うだとか、タイムアウト時間を変更したいだとか、特に必要がなければ、これを使えば簡単です。
設定は全部デフォルトになります。
「URLSession.shared」までで一つのインスタンスだと思ってください。
やってること的には下記と一緒です。
var config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session(with: request) { (data, response, error) in
(以下略)
詳しく知りたければ、下記のApple Documentを参照してください。
dataTask(with: request)とは
URLSessionが持っているメソッドです。
正確には
dataTask(with: request) { (data, response, error) in
//ここにデータ受信後の処理を書く
}
までで1メソッドです。
定義は下記です。
func dataTask(with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
dataTask(with:completionHandler:)
敢えて日本語で説明すると、
1つめの引数にHTTPリクエストオブジェクトを、
2つめの引数に「completionHandler」というクロージャをとって、
戻り値にURLSessionDataTaskオブジェクトを返すメソッドです。
completionHandlerはめちゃくちゃ簡単に言うと、通信が終了した後の完了処理をするためのクロージャです。
2つめの引数が、「,」を省略されているのは、トレイリングクロージャというやつで、省略されています。
詳しくは別記事に書きましたので、もし興味があれば。
Swiftっぽい配列の処理をするためのクロージャの使い方を学んだ(map, sort, filter)
completionHandlerは3つの引数をとって、戻り値を返さないクロージャです。
このクロージャが非同期処理のキモになります。
URLSessionは、サーバ通信が終わると、このcompletionHandlerを実行します。
引数3つの役割を説明すると、こうです。
Data?: サーバから返されたデータが入る(オプショナル)。
URLResponse?: HTTPレスポンスが入る(オプショナル)。
Error?: エラーが発生していた場合、エラーコードが入る(オプショナル)。
あまりHTTPレスポンスを直接触りたいときはアプリではないと思うので、
基本的にはDataとErrorをアンラップする処理を書きます。
ErrorはHTTPのエラーコードとは違うので、そこだけ注意してください。
@escapingは「関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性」で、
キャプチャしてくれるんですが、よくわからなければここでは一旦大丈夫です。
戻り値のURLSessionDataTaskというのは、URLSessionが作ったタスクです。
思想的には、1セッションが複数の具体的なタスクを持って、
あるデータをとって来ては何らかの処理を加える、という考えみたいです。
URLSessionDataTaskはインスタンス化しただけだと実行されないので、resume()メソッドを使って実行します。
task.resume()
completionHandlerの中身
長々と説明してきましたが、あとはデータ受け取った後の処理を書くだけです。
GithubのAPIはJSONデータを返してくるので、これをアンラップしてprintするコードを書きます。
実際のコードではprintでは終わらず、画面に表示したいというのがあるので、UIの再描画がからんできて、
そこの処理でまた辛いことになるのですが、それはまた後の話ということで。。。
//JSON Decodeのための構造体
struct User: Codable {
let total_count: Int
let incomplete_results: Bool
let items: [Item]
struct Item: Codable {
let login: String
let id: Int
let node_id: String
let avatar_url: URL
let gravatar_id: String?
let url: URL
let html_url: URL
let followers_url: URL
let subscriptions_url: URL
let organizations_url: URL
let repos_url: URL
let received_events_url: URL
let type: String
let score: Double
}
}
func searchGithubUser(query: String) {
let url = URL(string: "https://api.github.com/search/users?q=" + query)!
let request = URLRequest(url: url)
let decoder: JSONDecoder = JSONDecoder()
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
do {
let user: User = try decoder.decode(User.self, from: data)
print(user)
} catch let e {
print("JSON Decode Error :\(e)")
fatalError()
}
}
task.resume()
}
JSONデータの扱いについては別記事で書いているので、ご参照ください。
APIから受け取ったJSONデータをCodableプロトコルで構造体にマッピングする
try-catch構文が出てきて 更に難易度があがりますが、これはJSONDecoderを使ったためです。
キャッチしてるエラーは、サーバ通信時のErrorとは別モノです。
サーバ通信時のErrorの検査はサボっています。
もし純粋にサーバ通信だけ勉強したいのであれば、
{ (data, response, error) in
guard let data = data else { return }
let user: User = try decoder.decode(User.self, from: data)
print(user)
}
というコードにすると読みやすいと思います。
おわりに
壮絶に長くなってしまったので、終わります。
Qiitaで1番わかりやすく、という心づもりでしたが、あんまりわかりやすくならなかったですね。。。
何かの役に立てば幸いです!