⛸️

Combine使ってみた!

2023/11/11に公開

Combineとは?

https://developer.apple.com/documentation/combine

翻訳してみた
Combine フレームワークは、時間の経過とともに値を処理するための宣言型 Swift API を提供します。これらの値は、さまざまな種類の非同期イベントを表すことができます。Combine は、パブリッシャーが時間の経過とともに変化する可能性のある値を公開し、サブスクライバーがパブリッシャーからそれらの値を受け取ることを宣言します。

このPublisherプロトコルは、時間の経過とともに一連の値を配信できる型を宣言します。パブリッシャーには、上流のパブリッシャーから受け取った値に基づいて動作し、それらを再パブリッシュするオペレーターがいます。

パブリッシャーのチェーンの最後で、 a はSubscriber要素を受け取るときに要素に作用します。パブリッシャーは、サブスクライバーによって明示的に要求された場合にのみ値を発行します。これにより、サブスクライバ コードは、接続しているパブリッシャーからイベントを受信する速度を制御できるようになります。

Timer、など、いくつかの Foundation タイプは、パブリッシャーを通じてその機能を公開しています。Combine は、Key-Value Observing に準拠するプロパティの組み込みパブリッシャーも提供します。NotificationCenterURLSession

複数のパブリッシャーの出力を組み合わせて、それらのインタラクションを調整できます。たとえば、テキスト フィールドの発行者からの更新を購読し、テキストを使用して URL リクエストを実行できます。その後、別の発行者を使用して応答を処理し、それを使用してアプリを更新できます。

Combine を採用すると、イベント処理コードを一元化し、ネストされたクロージャや規則ベースのコールバックなどの面倒な手法が排除されるため、コードが読みやすく、保守しやすくなります。

パッケージじゃなくて最初から入ってる

どうやら、標準機能らしい?
この機能を使って、こちらのAPIからデータを取得してUIに表示してみようと思います。Storyboardよりは簡単かも...

https://jsonplaceholder.typicode.com/posts

ドキュメントがなんだか親切じゃなかったので、ChatGPTに教えてもらいました😓

📦Combineを使っている箇所

fetchPostsメソッドの中で使用しています。

URLSession.shared.dataTaskPublisher(for: url)
    .map(\.data)
    .decode(type: [Post].self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        if case .failure(let err) = completion {
            print("Error: \(err)")
        }
    }, receiveValue: { [weak self] posts in
        self?.posts = posts
    })
    .store(in: &cancellables)

このコードは以下のように機能します:

URLSession.shared.dataTaskPublisher(for: url): この行は、指定されたURLからデータを取得するための非同期タスクを作成します。このタスクは、データとレスポンス、またはエラーを出力するPublisherを返します。

.map(.data): 受け取ったレスポンスからデータ部分だけを抽出します。.mapオペレータは、入力された値(この場合はデータとレスポンス)を変換し、新しい値(この場合はデータのみ)を返します。

.decode(type: [Post].self, decoder: JSONDecoder()): ここで、JSONデコーダを使用してJSONデータをPost型の配列にデコードします。これにより、JSON形式のデータがSwiftの構造体に変換されます。

.receive(on: DispatchQueue.main): このオペレータは、以降の処理をメインディスパッチキュー(つまりメインスレッド)で実行するように指定します。これは、UIを更新する場合はメインスレッドで行う必要があるため重要です。

.sink(receiveCompletion:receiveValue:): ここで、Publisherからの出力を処理します。receiveCompletionはタスクが完了したとき(成功または失敗)に呼び出され、receiveValueはデータを受け取ったときに呼び出されます。

.store(in: &cancellables): このオペレータは、購読をキャンセル可能なオブジェクトに格納し、不要になったときに購読を自動的にキャンセルするために使用されます。これにより、メモリリークを防ぐことができます。

結論

このコードは、指定したURLからデータを非同期に取得し、そのデータをデコードしてUIに表示するために使用されます。Combineは複雑に見えるかもしれませんが、非同期処理とデータの流れを効果的に扱う強力なツールです。

感想

未経験な技術なのと、SwiftはKotlinと比べると、ドキュメントがわかりずらいので、ChatGPTに頼りながらキャッチアップしました😓
ネイティブはそもそも学習コストが高いので、難しいですね。過去にUIkitでアプリを2個リリースしたことあるのですが、今後リリースするなら、SwiftUIでないとレガシーなものを作ってしまうことになりますし、新しい機能はSwiftUIで開発されているそうなので、保守するアプリでなければ、SwiftUIを今後は学習すべきですね。

全体のコード

import SwiftUI
import Combine

struct Post: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}

class PostViewModel: ObservableObject {
    @Published var posts: [Post] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        fetchPosts()
    }
    
    func fetchPosts() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            print("Invalid URL")
            return
        }
        
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [Post].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                if case .failure(let err) = completion {
                    print("Error: \(err)")
                }
            }, receiveValue: { [weak self] posts in
                self?.posts = posts
            })
            .store(in: &cancellables)
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = PostViewModel()
    
    var body: some View {
        NavigationView {
            List(viewModel.posts) { post in
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .font(.subheadline)
                }
            }
            .navigationTitle("Posts")
        }
    }
}

ビルドしたUI

Jboy王国メディア

Discussion