この記事では、Combineを使ってネットワークアクセスする方法を解説します。
Combineが初めての方やCombineの他の例については、次の記事もご覧ください。
[clink url=”https://www.rk-k.com/archives/3937″]
DataTaskを実行するPublisherを作る
Combineを使ってネットワークアクセスするには、DataTask用のPublisherを作ります。Publisherを作るためのメソッドは以下の2つです。
func dataTaskPublisher(for: URL) -> URLSession.DataTaskPublisher
func dataTaskPublisher(for: URLRequest) -> URLSession.DataTaskPublisher
引数が異なる2つのメソッドです。最初のメソッドはURL
を指定します。単純にGET
でURLに接続してダウンロードしたいときは、こちらを使います。
2番目のメソッドはURLRequest
を指定できます。URLRequest
なら、HTTPのメソッドや送信するボディデータ、HTTPのヘッダーなど色々な指定できます。
この2つのメソッドがあれば、全てのケースに対応できるでしょう。但し、ダウンロードするファイルが巨大な場合には、プログレス処理ができないのと、メモリ不足になるので、URLSessionのDownloadTaskを単体で使った方が望ましいと思います。
受信する
サーバーのデータやレスポンス、エラーなどの受信は、sink
を使います。
func sink(receiveCompletion: @escaping ((Subscrivers.Completion<URLError>) -> Void),
receiveValue: @escaping (((data: Data, response: URLResponse)) -> Void)) -> AnyCancellable
sink
の戻り値はどこかに保持しておきます。
sink
は次のような形で使用します。
cancellable = urlSession.dataTaskPublisher(for: url)
.sink(receiveCompletion: { (failure) in
switch failure {
case .finished:
// エラーなく完了できたときの処理
break
case .failure(let error: URLError):
// HTTPレベルではないエラーが起きたときの処理
break
}
}, receiveValue: { (data: Data, response: URLResponse) in
// サーバーから取得したデータの処理
})
実行されるスレッドに注意
sink
の引数で渡した完了処理やデータの取得処理はどちらもメインスレッドではなく、サブスレッドで実行されます。GUIの更新など、メインスレッドでしかできないことを、やらないように注意しましょう。
サンプルコード
“GET”ボタンをタップすると、テキストフィールドに入力したURLからダウンロードするサンプルコードを作りました。
SwiftUIで書いているので、ContentView.swiftにコピー&ペーストで動作します。試してみてください。
import SwiftUI
import Combine
struct ContentView: View {
class ContentViewContext {
public var cancellable: AnyCancellable?
init() {
self.cancellable = nil
}
}
@State private var urlString: String = "https://example.com/"
@State private var logText: String = ""
private var urlSession: URLSession = URLSession(configuration: .default)
private var context: ContentViewContext = ContentViewContext()
var body: some View {
VStack {
TextField("URL", text: $urlString)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.URL)
Button(action: {
self.getFromServer()
}) {
Text("GET")
}
ScrollView(.vertical, showsIndicators: true) {
Text("\(self.logText)")
.frame(width: 400)
}
.frame(width: 400, height: 400, alignment: .top)
}.padding()
}
func getFromServer() {
guard let url = URL(string: self.urlString) else {
return
}
self.logText = ""
self.context.cancellable = self.urlSession.dataTaskPublisher(for: url)
.sink(receiveCompletion: { (failure) in
DispatchQueue.main.async {
switch failure {
case .finished:
self.logText += "Finished\n"
case .failure(let error):
self.logText += "Failure: \(error.localizedDescription)\n"
}
}
}, receiveValue: { (data: Data, response: URLResponse) in
DispatchQueue.main.async {
if let httpURLResponse = response as? HTTPURLResponse {
self.logText += "HTTP STATUS: \(httpURLResponse.statusCode)\n\n"
}
if let text = String(data: data, encoding: .utf8) {
self.logText += "\(text)\n"
}
}
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}