SwiftでHTTPリクエスト
04 Feb 2024今日はふとGoogle Domainsで管理していたドメインを移管してみることにした。 2件分で3,300円かかった。 加えてドメインの更新にSSL証明書の更新も加えて今日だけで2万円もかかってしまった。 なかなかの出費である。
閑話休題。今日はちょうど私がSwiftを学習してから1ヶ月経った日であった。 チュートリアルが現在Swift UIのあたりで止まっているのだけれども、個人的にはまだStoryboardのアプリケーションも作ってみたい気が残っているのでなんとなく箸休めをしていた。 このまま放置していたらいよいよ本格的に忘れてしまう気がしたので、とりあえず簡単なサーバーを作ってそこでHTTP通信をしてみることにした。
iOSの開発自体もライセンスの問題が解決していないので正直気が重いのだけれども、触る機会を増やすには正直これしかないと思っている。
Rails側は特になんの変哲もない感じだ:
class HelloController < ApplicationController
# GET /messages
def greet
render json: { greet: "Happy world!" }
end
end
レスポンスの文言も特に意味はないが、配列だと少々面倒なので単純な文字列にした。
私の開発環境はXcodeとこのRailsサーバーがそもそも違うので、http://localhost:3000
とかではなくわざわざHTTPS環境でデプロイしてアクセスできるようにしている。
SSL証明書が出てきたのもこれが理由である。
通常Xcodeで作成したアプリケーションでHTTP通信をする場合はInfo.plistを変更する必要があったはずだ。
import Foundation
struct Greeter: Decodable {
var greet: String
}
まずは上記のレスポンスにあわせてGreeterという構造体を用意する。
let url = URL(string: "https://example.com")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("error: \(error)")
return
}
guard let data = data else { return }
let greeter = try? JSONDecoder().decode(Greeter.self, from: data)
guard let greeter = greeter else { return }
print("greet: \(greeter.greet)")
}
task.resume()
おそらく簡単な書き方はこうだ。
これは仕方ないことであるけれども、SwiftはOptionalがよく出てくるのでインデントが深くなりがちなのでこのように直線っぽく書くようにした。
Xcode上のエミュレータで確認するとgreet: Happy world!
の出力を確認できる。
Rails側のログを確認してみると:
I, [2024-02-04T08:42:26.903094 #10] INFO -- : [3848cee9-9ba8-4208-8fc1-424991febf27] Started GET "/" for 128.66.0.0 at 2024-02-04 08:42:26 +0000
I, [2024-02-04T08:42:26.903460 #10] INFO -- : [3848cee9-9ba8-4208-8fc1-424991febf27] Processing by HelloController#greet as */*
I, [2024-02-04T08:42:26.903682 #10] INFO -- : [3848cee9-9ba8-4208-8fc1-424991febf27] Completed 200 OK in 0ms (Views: 0.1ms | ActiveRecord: 0.0ms | Allocations: 128)
本当に単純なHTTPリクエストだと思うので
もしUILabelなどのテキストを更新したい場合はこうする:
@IBAction func greetPressed(_ sender: UIButton) {
DispatchQueue.main.async {
self.greetLabel.text = greeter.greet
}
}
次はJSON形式のPOSTについて。 サーバー側はまず簡単なモデルを用意した。
class CreateMessages < ActiveRecord::Migration[7.1]
def change
create_table :messages do |t|
t.string :text
t.timestamps
end
end
end
class MessagesController < ApplicationController
# POST /messages
def create
@message = Message.new(message_params)
if @message.save
render json: @message, status: :created
else
render json: @message.errors, status: :unprocessable_entity
end
end
private
# Only allow a list of trusted parameters through.
def message_params
params.require(:message).permit(:text)
end
end
このあたりはscaffold
から流用しているのですぐに終わる。
POSTもSwiftのスニペットを貼っておしまいと思ったが、少し厄介である。
というのも、Railsとの通信では主に2つ気をつけておきたいことがある:
- Rubyは
snake_case
でSwiftはcamelCase
であること - RailsはStrong Parametersで通信すること
レスポンスはサーバー側をクライアントにあわせてcamelで返すべきか、あるいはクライアント側でcamelに変換すべきかは人によって意見はわかれるかもしれないが、私はクライアント側で対応するほうが好ましいと思っている。
NodeJSであればNPMでパッケージをインストールすれば済むのだけれども、Swiftの場合はJSONDecoderというクラスにあらかじめ用意されているのでそれを使えば問題ない。
import Foundation
struct MessageParams: Codable {
let text: String
}
struct MessageContainer: Codable {
let messageParams: MessageParams
}
struct Message: Codable {
let id: Int64
let text: String
let createdAt: String
let updatedAt: String
}
まずはそれぞれのリクエストとレスポンスで使うための構造体を用意しておく。 今回はDecodableではなくCodableを使っているが、これは特に深い意味はない。 厳密に言えばDecodeとEncodeでそれぞれに分けるべきかもしれないが、今回は単純にしている。
MessageContainerクラスを使うことでJSONのStrong Parametersを表現しているのだが、わざわざ2つ構造体を用意しなければいけないのは正直あまりいけていない。 このあたりはもう少しうまくやる方法があるかもしれない。
let messageParams = MessageParams(text: text)
let messageContainer = MessageContainer(messageParams: messageParams)
guard let uploadData = try? JSONEncoder().encode(messageContainer) else { return }
let url = URL(string: "https://example.com/messages")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in
if let error = error {
print("error: \(error)")
return
}
guard let data = data else { return }
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // rails
let message = try? decoder.decode(Message.self, from: data)
guard let message = message else { return }
print("ID: \(message.id)")
}
task.resume()
非常に長ったらしいコードになってしまったが、どのコードにも意味があると考えれば仕方がない。
HTTP POSTする場合はhttpMethod
を設定する必要があったり、JSONDecoderにkeyDecodingStrategy
を設定するなどいろいろと静的型付き言語の回りくどいところは出ているけれども、特にライブラリなどを使わずともサーバーにデータを届けることができた。
エラー処理も現状はprint
するだけなのでこのあたりも見直していきたいが、今回はこれくらいに留めておく。
I, [2024-02-04T08:42:35.565112 #10] INFO -- : [968276ea-6fb1-460d-a8d5-7407a7a0c4ff] Started POST "/messages" for 128.66.0.0 at 2024-02-04 08:42:35 +0000
I, [2024-02-04T08:42:35.565483 #10] INFO -- : [968276ea-6fb1-460d-a8d5-7407a7a0c4ff] Processing by MessagesController#create as */*
I, [2024-02-04T08:42:35.565503 #10] INFO -- : [968276ea-6fb1-460d-a8d5-7407a7a0c4ff] Parameters: {"message"=>{"text"=>"Hello"}}
I, [2024-02-04T08:42:35.566428 #10] INFO -- : [968276ea-6fb1-460d-a8d5-7407a7a0c4ff] Completed 201 Created in 1ms (Views: 0.1ms | ActiveRecord: 0.2ms | Allocations: 391)
ログも想定した通り。 非常によい。
おまけでPlaygroundではなくシミュレーター上で動かしてみたスクリーンショットを添付してみる:
——レイアウトは今後の課題である。