能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

CombineFramework用Testbedコード - SwiftUI100行チャレンジ(11)

端的にいうと

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な変数を置けば済むのだがちょっとしたテストやメカニズムを確認するのであれば今回紹介したコードで十分だろう。

参考