端的にいうと
Combine Frameworkのシンプルなテストベッドコード。
1) Combine Frameworkのテストベッドが欲しい
Combine Frameworkを使ったコードを書く場合、記述したコードの結果を表示しながら作成したいことがある。本来モデルクラスを作成したうえでSwiftUI上のコードに記述すべきだが目的がはっきりしない状態で手探りでコードを記述する場合はクラス名もはっきりせずに作業を進めなくてはならないことがある。
SwiftUI上でCombine Frameworkを使ったコードを書く上で障害となるのはcancellables(Set<AnyCancellable>)をView上に直接配置することができない点である。これはView上の変数は変更不可(immutable)扱いとなってしまうためである。
解決方法としては何らかのオブジェクトを定義した上でcancellables を配置すれば済むのだがSwiftUIを使い始めて最初の時期だったり、しばらくSwiftUIから離れているとビルドエラーから問題解決の糸口を掴めないことがある。
2) シンプルなテストベッド
async/await の投稿記事で見かけた変数をラップするシンプルなコードを見つけたのでこちらを使用する。
Swift ConcurrencyのwithTaskCancellationHandlerとSendable - cockscomblog?
Blog記事中のWrapper を使用する。
class Wrapper<Wrapped> {
var value: Wrapped
init(_ value: Wrapped) { self.value = value }
}
上記クラスはテンプレートWrapped型変数valueを持つクラスを定義できる。Blog中ではTaskを保持しているものをサンプルコードではSet<AnyCancellable> を保持するために使用している。
import SwiftUI | |
import Combine | |
// original code for CancellerWrapper was posted on @Cockscomb cockscomb's blog post. https://cockscomb.hatenablog.com/entry/2021/09/26/103128 | |
class CancellerWrapper<Wrapped> { | |
var value: Wrapped | |
init(_ value: Wrapped) { self.value = value } | |
} | |
class SimpleModel: UIResponder, ObservableObject { | |
struct State { | |
var items: [Int] | |
} | |
@Published var state = State(items: []) | |
} | |
struct ContentView: View { | |
@StateObject var simpleModel: SimpleModel | |
let cancellables: CancellerWrapper<Set<AnyCancellable>> = .init(Set<AnyCancellable>()) | |
var body: some View { | |
NavigationView { | |
Text("Simple Combine Framework SimpleCombineFrameworkTestbed") | |
.padding() | |
.onAppear { | |
simpleModel.$state.map(\.items).sink(receiveValue: { items in | |
print("items.count=\(items.count)") | |
}).store(in: &self.cancellables.value) | |
} | |
.navigationTitle(Text("Testbed")) | |
.navigationBarItems(trailing: | |
Button(action: { | |
simpleModel.state.items = [1,2,3,4,5] | |
}, label: { Text("Execute") }) | |
) | |
} | |
} | |
} | |
@main | |
struct SimpleCombineFrameworkTestbedApp: App { | |
var simpleModel = SimpleModel() | |
var body: some Scene { | |
WindowGroup { | |
ContentView(simpleModel: simpleModel) | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var simpleModel = SimpleModel() | |
static var previews: some View { | |
ContentView(simpleModel: Self.simpleModel) | |
} | |
} |
まとめ
UIKit上でテストコードを書くのに慣れているとSwiftUIの制約(View上でmutableな変数を置けない) に戸惑ってしまうことがある。本来ならModel(ないし仮オブジェクト)を作成したうえでmutableな変数を置けば済むのだがちょっとしたテストやメカニズムを確認するのであれば今回紹介したコードで十分だろう。