Swiftで遊ぼう! - 1015 - URLSessionDataTaskPublisherの基礎
URLSessionの基本をまとめたのが5年前...
5年経過したので、NSURLは消えてしまったようですが、URLSessionはネットからデータを取り込むために今でも利用されている。しかし、新しいフレームワークであるCombineと組み合わせて利用するのが一般的になってきている。Combineは「関数型プログラミング」を実現するためのフレームワークであり、SwiftUIと組み合わせることで、最も新しいiOS開発環境ということになるだろう。そこで、今回、Combineに関して少々勉強できたので、5年前のデモを新しく書き直してみた。
当初は、上記のリンクの記事を更新するつもりでしたが、SwiftUIとCombineを使って開発をすると、Autolayoutは使わないし、非同期処理も違うので、古い記事はそのままにして、新しい記事を書いてみた。
新しいプロジェクトを作ります。「Simple View Application」を選び、「User Interface」を必ず「SwiftUI」にする。そして、プロジェクト名は適当につけますが、私は「URLSessionDataTaskPublisherDemo」にしました。
SwiftUIの基本的な扱いに関して説明はしません。分からない人はSwiftUIのチュートリアルをこなしてください。SwiftUIの特徴は、MVC開発からMVVM開発への変化でないかと思ってるんですが、まだ私も勉強中で分からないことだらけ。ContentViewでユーザーインターフェイスの開発に集中。そして、Modelで処理させて、Combineなどの通知機能を利用することで自動的にViewと連携させてやるという考えじゃないだろうか。今まで使用していたUIViewContorollerも使いません。私の嫌いなAutolayoutも必要無いので、処理をどうすればいいか開発に集中できるようになっているので、これからiOS環境を勉強し始める人が羨ましいですね。
さて、画面上のボタンを押して、Amazonのサイトから画像をダウンロードさせるだけですが、その処理をContentViewには加えません。新しいModelになるファイルを作りますメニューから「File」→「New」→「File...」を選択します。そして「Swift File」を選択してから名前を「ImageLoader」にしてプロジェクト内に加えます。「import Foundation」しかないので、次のように「SwiftUI」と「Combine」を加えてください
import Foundation import SwiftUI import Combine
さて次に、アプリを立ち上げた時に表示するイメージを用意します。アプリのテスト環境は「iPhone 11」にします。すると、プロジェクトで使用するイメージのサイズは2種類でいいんです*1。先ず、プロジェクト・ナビゲータでAssets Xcassetsを選択して、下部にある「+」ボタンを選び、「New Folder」を選択します。フォルダーの名前を「Placeholder」に変更してから「+」ボタンを再び選択して「New Image Set」を選ぶと「1X」「2X」「3X」の空のスロットができます。
次に何でもいいので「png」イメージを用意します。オブジェクトのサイズを「240×240ピクセル」にしているので、1Xはそのまま、2Xは「480×480ピクセル」、そして3Xは「720×720ピクセル」の画像を用意するのですが、ここでは2Xスロットだけセットします。そして、名前を「backgroundImage」に変更します。
これで画像は用意できたので、ImageLoaderに初期値として、先ほど用意したbackgroundImageを持つUIImageを持たせるためにコードを次のように書きます。
import Foundation import SwiftUI import Combine class ImageLoader: ObservableObject { @Published var image = UIImage(named: "backgroudImage") }
ここで、2つの重要なキーワードが出てきました。ObservableObjectと@Publishedキーワードです。実はこのObservableObjectですが、Combineに含まれるんです。でも、Combineフレームワークをimportしなくても、SwiftUIのみで使えるため、最初に私は混乱しました。SwiftUIを使って、システム標準の通知機能を使うだけならCombineフレームワークは必要ないのです。外部との通信や、通知機能を自作するためにCombineが用意されています。また、非同期処理もCombineでさせる方が処理させやすいということですが、まだまだ私の知識で使いこなせるとは思ってません。
ここで用意したimgaeをContentViewで表示させてみます。次のコードをContentViewに加えます。
struct ContentView: View { @ObservedObject var loader = ImageLoader() var body: some View { VStack { Image(uiImage: loader.image!) .resizable() .frame(width: 240, height: 240) .scaledToFit() } } }
そしてランしてみましょう。当然プレビューをみてもかまいません。次のようになっているでしょう。
次にAmazonから画像データをダウンロードさせる関数をModelに用意させるのです。ここでCombineフレームワークを使います。Combineフレームワークを利用するために、Publisher、Subscriber、そしてOperatorを利用しなけらばなりません。自分で作るクラスなら、それぞれのProtocolやExtensionに準拠させないといけないのですが、URLSessionにはCombineが利用できるようになっています。URLSessionで用意されているPublisherがDataTaskPublisherなんです。このDataTaskPublisherを使ってSubsriberを作り、情報処理をOperatorでするっていう流れです。従って、DataTaskPublisherを利用すると、必ずData、URLResponnse、URLErrorの処理をしなければならないので、利用するしないに関係なく対応をしなければなりません。ということで、ImageLoaderクラスに次の変数を持たせます。
class ImageLoader: ObservableObject { @Published var image = UIImage(named: "backgroundImage") var cancellable: Cancellable? = nil var data: Data? = nil var response: URLResponse? = nil var error: URLError? = nil } }
cancellableがSubscriberですが、Pubulisherの宣言がないですよね。Publisherはシングルトン利用するので、インスタンスは作らないことにします。
次の関数を加えます。
func fetchImage() { let url = URL(string: "https://s3.amazonaws.com/ CoverProject/album/album_david_bowie_lets_dance.png") cancellable = URLSession.shared.dataTaskPublisher(for: url!) .print("subscriber now") .eraseToAnyPublisher() .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): fatalError(error.localizedDescription) } },receiveValue: { let image = UIImage(data: $0.data)! self.image = image }) }
これで自分が持っているイメージがダウンロードされたイメージと変わります。では、ContentViewにボタンを加えましょう。
struct ContentView: View { @ObservedObject var loader = ImageLoader() var body: some View { VStack { Image(uiImage: loader.image!) .resizable() .frame(width: 240, height: 240) .scaledToFit() Button(action: loader.ftechImage, label: { Text("Get Image") }) } } }
じゃあランしてボタンを押してみるといいでしょう。次のようにデビッド・ボウイのジャケットが今も変わらずダウンロードできますよ。
実はImageLoaderクラスにUIResponseやUIErrorのプロパティを持たせましたが、$0のデータしか使っていないので、流れてきたデータを殆ど捨ててます。そして、このときのデータの流れがコンソールに出るのですが、色々な情報が紛れていて興味深かったです。まだまだCombineの勉強が必要なので、もう少し勉強します。
*1:理由がわからなければSwiftで遊ぼう! - 249 - iOSの座標システム: 2020年3月 - Swiftで遊ぼう! on Hatenaで説明しているScaling値を確かめましょう。