アプリ開発において、Viewとその値のデータバインディングは基本的で重要な実装です。Observationフレームワークは、Swift 5.9から導入された新しいフレームワークです。オブザーバーパターンを用いて、データバインディングを堅牢で型安全に、パフォーマンス高く実現します。オブザーバーパターンとは、プログラム内のオブジェクトに関するイベントを他のオブジェクトへ通知する処理で使われるデザインパターンの一種です。1特に、SwiftUIとの連携は強力で、アプリ開発をより簡単にすることでしょう。iOS 17.0、iPadOS 17.0、masOS 14以上から利用可能です。すぐにとはいきませんが、将来デファクトスタンダードになりうるフレームワークです。ソースコードはOSSとして公開され、Appleプラットフォーム以外でも利用が可能です。アプリ開発を超えて、サーバーサイドなどの他の分野での開発でも役立つことでしょう。
本書は、Observationフレームワークについて解説する入門書です。Observationフレームワークが登場した経緯、使い方、特にSwiftUIとの連携方法や従来のデータバインディングで用いられたObservableObjectプロトコルとの比較を解説します。Observationフレームワークは、Swiftマクロを利用します。ビルド時にマクロが必要なコードを追加するので、開発者が書くコードは最小限で済みます。本書では、Swiftマクロの生成コードも含めて、仕組みをしっかり解説し、アプリ開発でどのように利用できるかを丁寧に説明する予定です。
本書がアプリ開発の手助けになることを願います。
・第1章「Observationフレームワーク概要」
─Observationフレームワークが登場した背景や仕組みを解説します。Swiftマクロがコードをどのように変換しているかを深掘りします。
・第2章「計算プロパティーを初期化する新しい方法」
─Observableマクロの登場で、格納プロパティーが初期化メソッド内で初期化できなくなりました。SE-0400のプロポーザルはそれを解消します。Observationフレームワークの関連プロポーザルとしてSE-0400がどのような変更なのかを見ていきます。
・第3章「ObservableObjectとの比較」
─Observationフレームワークの特徴や従来のObservableObjectプロトコルと何が異なるのかを探ります。Observationフレームワークのメリットがよくわかる章です。
・第4章「SwiftUIとのデータバインディング」
─SwiftUIとの連携を中心に解説します。@Stateや@Environment、@Bindableといったデータを扱うProperty Wrapperの利用方法を解説します。
本書は次の読者を対象としています。
・Swift言語の基本文法を学んだ方
・UIKit/SwiftUIの基本動作を理解している方
ObservationフレームワークはiOS、iPadOS、またmacOSなどOS問わず利用できるフレームワークですが、本書のサンプルコードは主にiOS開発に向けたものを用意しています。また、Swift言語自体の解説やUIKit/SwiftUI自体の解説は行いません。あらかじめご了承ください。
本書は次の環境で検証しています。
・iOS 17.2
・Xcode 15.2
・Swift 5.9.2
・macOS Sonoma 14.3
・MacBook Pro(14インチ、2021)
─チップ: Apple M1 Max
次のリポジトリーに、本書のサンプルコードが掲載されています。
・https://github.com/SatoTakeshiX/Swift-Observation-HandsOn
各章のサンプルプロジェクトはディレクトリーでまとめられています。どのプロジェクトをサンプルとして利用するのかは各章でお伝えします。各章のサンプルコードはアプリを起動する、またはPreviewを使って確認ができます。本文で解説するコードには、ファイル名をコメントに記載しています。サンプルプロジェクトで前後のコードを確認する際の目安にしてください。また、記載がなければ対応するコードはないことを意味します。あらかじめご了承ください。
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
・サンプルコード
・https://github.com/SatoTakeshiX/Swift-Observation-HandsOn/tree/main/Chapter1
Observationフレームワークは、Swift 5.9から導入された新しいフレームワークです。データの値を監視するオブザーバーパターンを、堅牢で型安全に、パフォーマンス高く実現します。Swift Evolutionの提案はSE-03951で行われました。この章ではObservationフレームワークがどのような経緯で登場して、その仕組みや特徴は何なのかを解説します。
Observationフレームワークの使い方や仕組みの解説をする前に、どうしてこのフレームワークが登場したか、背景をお話ししましょう。Swiftのアプリ開発者が値を監視する仕組みはこれまでにもいくつかありますが、主に以下ふたつの仕組みを振り返ってみましょう。
・KVO(Key-Value Observing)
・ObservableObject
それぞれどのような仕組みかを見てみましょう。
KVO(Key-Value Observing)(以下KVO)は、iOSやmacOSで古くから導入されている値監視の方法です。あるオブジェクトのプロパティー更新を他のオブジェクトに通知します。
KVOで値の変更を通知するには、NSObjectを継承したクラスを定義する必要があります。図書館である本の貸出状況を確認できるアプリを念頭に、KVOの使い方を次のコードで見ていきましょう。
// BookObjectObserve.swift
final class BookObjectObserve: NSObject {
@objc dynamic var isBorrowed: Bool
init(isBorrowed: Bool) {
self.isBorrowed = isBorrowed
}
func switchBorrow() {
isBorrowed.toggle()
}
}
値を通知するオブジェクトをNSObjectを継承して定義します。ここではBookObjectObserveです。本の貸出状態を表すisBorrowedプロパティーを保持しています。このプロパティーは、他のオブジェクトに値変更を通知します。KVOを利用するにはObjective-CのRuntimeが必要なので、@objc属性やdynamic修飾子も必要です。isBorrowedプロパティーを変更して、本の貸出状態を変更するswitchBorrowメソッドも定義しています。
値を監視するViewController側では、observe(_:options:changeHandler:)メソッドを呼び出します。
// KVOViewController.swift
final class KVOViewController: UIViewController {
@objc let bookModel = BookObjectObserve(isBorrowed: false)
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
...
observation = observe(
\.bookModel.isBorrowed,
options: [.old, .new]
) { object, change in
Task { @MainActor in
if change.newValue == true {
object.label.text = "貸出中"
object.button.setTitle(
"この本を返す",
for: .normal
)
} else {
object.label.text = "貸出可能"
object.button.setTitle(
"この本を借りる",
for: .normal
)
}
}
}
button.addTarget(
self,
action: #selector(changeBorrowed),
for: .touchUpInside
)
}
@objc func changeBorrowed() {
bookModel.switchBorrow()
}
第一引数に値を監視するプロパティーをKeyPathで指定します。optionsパラメータにNSKeyValueObservedChangeを指定すると、値が新旧どのように変更したかを把握できます。changeHandlerのクロージャーの第一引数には監視しているオブジェクト、ここではKVOViewControllerが渡されます。第二引数からは監視対象の値、ここではbookModel.isBorrowedの値を取得できます。change.newValueに変更後の値が入っています。
BookObjectObserveのswitchBorrowメソッドを呼び出すと、observeメソッドのchangeHandlerのクロージャーが呼ばれるので、この中でテキストやボタンのタイトルを変更する処理を実装します。アプリを実行すると、ボタンタップでラベルとボタンタイトルの文字が変化するのがわかるので、値監視が実現できています。
これがKVOでの値監視の実装例です。
KVOは、古くから導入されているレガシーな仕組みです。導入にはNSObjectの継承が必要で、実行時にはObjective-C Runtimeが必要です。また、監視する値の指定は、現状KeyPath指定ができるとはいえ、内部では文字列指定となっているため、完全な型安全とは言い難い状況になっています。また、値の監視はプロパティーをひとつずつ監視しなければならず、プロパティーの数が多くなるとコードも煩雑になります。SwiftUIとの連携もできません。SwiftUIでのアプリ開発が主流になりつつある現在、KVOの仕組みを積極的に入れる場面は非常に限られています。
iOS 13からSwiftUIとCombineフレームワークが導入され、ObservableObjectプロトコルによる値監視ができるようになりました。Combineフレームワークは、時間とともに変化する値を宣言的に記述できる非同期処理のフレームワークです。値を送信するPublisherと、その値を変更して値を下流に送信するOperator、そして値送信のイベントを購読/受信するSubscriberという3つの役割で構成されています。Combineフレームワークでは値の変更を検知することを、値送信のイベントを「購読する」、または「受信する」と表現するので、本書でもそれに従います。ObservableObjectプロトコルを利用すると、値の変更がイベントとして送信され、そのイベントをViewが購読することでViewの更新ができます。
使い方は、値変更のイベントを送信したいクラスをObservableObjectプロトコルに準拠させ、値変更を送信したいプロパティーに対して、@PublishedのProperty Wrapperを付与します。SwiftUIのView側では、@StateObjectなどのデータ管理用のProperty Wrapperで値変更を受信したいクラスを保持すると、プロパティーの値が更新されたら自動でViewが再描画されるようになります。
図書館である本の貸出状況を確認できる画面をObservableObjectを使って実装しましょう。
// BookModel.swift
final class BookModel: ObservableObject {
@Published var isBorrowed: Bool = false
func switchBorrow() {
isBorrowed.toggle()
}
}
値変更のイベントを送信するクラスとして、BookModelを作り、ObservableObjectに準拠しています。isBorrowedプロパティーに@PublishedのProperty Wrapperを付与することで、イベントを送信できるようにします。
BookModelの値変更のイベントを購読するView、BookViewを実装しましょう。
// ObservableObjectView.swift
struct BookView: View {
@StateObject private var model = BookModel()
var body: some View {
VStack {
Spacer().frame(height: 16)
Image(systemName: "book")
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.frame(width: 88, height: 88)
Spacer().frame(height: 8)
Text(model.isBorrowed ? "貸出中" : "貸出可能")
Spacer().frame(height: 16)
Button {
model.switchBorrow()
} label: {
Text(model.isBorrowed ? "この本を返す" : "この本を借りる")
}
Spacer()
}
...
}
}
BookViewは、BookModelをプロパティーとして保持し、@StateObjectのProperty Wrapperを追加しています。その後、model.isBorrowedのようにmodelのプロパティーの値をViewに渡します。こうすることで、プロパティーの値が更新されると自動的にViewも再描画されます。ボタンがタップされたらmodel.switchBorrow()メソッドを呼び出し、isBorrowedプロパティーを更新します。アプリを実行すると、ボタンタップする毎にラベルとボタンのタイトルが変更されるのがわかるでしょう。
SwiftUIが登場して、ObservableObjectプロトコルは多く利用されてきましたが、いくつか制限があります。このプロトコルはCombineフレームワークに属するプロトコルですが、CombineフレームワークはAppleのプラットフォームでしか使えませんし、ソースコードも公開されていません。Swiftという言語自体はLinuxやWindowsでも動作するように進化を遂げていますが、この制限はマルチプラットフォームのサポートを阻害します。また、監視対象の値には@Publishedをつけなければいけませんが、プロパティーの数が多くなると手間がかかります。そして、詳しくは第3章の「3.4 特徴が異なるところ」で後述しますが、@Bindingによる子Viewの差分更新がされなかったり、値を入れ子にした場合に値変更のイベントが飛ばないことがあるという落とし穴もあります。
Appleプラットフォームしか利用できず、利用に関して細かな制限があるのがObservableObjectです。