はじめに
【Combine】Operatorsを使ってPublishersが出力する値を変換する - Swift・iOS
の続きです。Appleのドキュメント(https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine)の"Customize Publishers with Operators"にあたる内容です。SwiftUI版のサンプルを作って動きを確かめてみたので、記事に残します。
開発環境
- macOS Big Sur 11.5.1
- Xcode 12.5.1
- Swift 5.4.2
サンプルコード
ContentView.swift
import SwiftUI import Combine struct ContentView: View { @ObservedObject private var viewModel: ContentViewModel init(viewModel: ContentViewModel) { self.viewModel = viewModel } var body: some View { VStack { TextField("文字を入力", text: $viewModel.text) .textFieldStyle(RoundedBorderTextFieldStyle()) .frame(width: 300) .padding() Text("抽出条件に合致した文字列を出力\n\(viewModel.filteredText)") .padding() Button("サンプルをリセット") { viewModel.text = "" viewModel.filteredText = "" } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(viewModel: ContentViewModel()) } }
ContentViewModel.swift
import Foundation import Combine final class ContentViewModel: ObservableObject { @Published var text = "" @Published var filteredText = "" private var cancellable: AnyCancellable? init() { cancellable = $text .filter( { $0.unicodeScalars.allSatisfy( { CharacterSet.alphanumerics.contains($0) } ) } ) .debounce(for: .milliseconds(500), scheduler: RunLoop.main) // ユーザーの入力が停止するのを待つ .receive(on: RunLoop.main) .assign(to: \.filteredText, on: self) } }
※補足
・Appleのドキュメントの以下実装箇所に関して
NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField)
TextFieldに入力された文字に変更があった際に通知を受け取る処理ですが、SwiftUIの場合は@PublishedなどのProperty Wrappersを使って入力文字の状態を監視することができるため、以下に変更しています。
$text
・"debounce(for:scheduler:options:)"によって、指定された時間が経過した後に値を公開しています。使い方のイメージですが、以下チュートリアル用のアプリが参考になりました。
※参考:GitHub - manchan/MVVM-with-Combine-Tutorial-for-iOS: MVVM with Combine Tutorial for iOS
入力した文字に合わせてAPIから天気情報を取得して表示しているようなのですが、1文字入力するごとに即時APIにリクエストするのは仕様上よくありません。そのため、debounceを使ってユーザーの入力を一定時間待った上でAPIにリクエストしています。
(該当の処理はWeeklyWeatherViewModel.swiftにあります。)
・処理の結果をfilteredTextに代入してUIを更新するため、"receive(on: RunLoop.main)"を実装しています。
SampleApp.swift(ライフサイクルが"SwiftUI App"の場合)
import SwiftUI @main struct SampleApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: ContentViewModel()) } } }
おわりに
前回の内容(【Combine】Operatorsを使ってPublishersが出力する値を変換する - Swift・iOS)とテーマが同じではありますが、値を変換するだけでなく、タイミングなども制御できることを動かしながら理解することができました。サンプルを作りながら新たに理解できたこともあるので、別途記事に残そうと思います。
参考
- https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine
-
https://developer.apple.com/documentation/combine/publisher/filter(_:)
-
https://developer.apple.com/documentation/combine/publisher/debounce(for:scheduler:options:)
-
https://developer.apple.com/documentation/combine/publisher/receive(on:options:)
-
https://developer.apple.com/documentation/foundation/runloop
- https://developer.apple.com/documentation/foundation/runloop/1418388-main
-
GitHub - manchan/MVVM-with-Combine-Tutorial-for-iOS: MVVM with Combine Tutorial for iOS