SwiftUIでよく利用するアノテーション(Property Wrapper)を簡単にまとめました
テーマ
- 各PropertyWrapperの関係が ひと目でわかる
- 1つ1つについて簡潔にまとまっている
- チートシートとしてアプリ作成時に使える
経緯
SwiftUIアプリ開発をする中でよく出てきたのが Property Wrapper でした
しかし、初学者には それぞれどうやって使うのかが分かりにくかったため、チームメンバーと一緒に勉強し、その内容をまとめることにしました
この投稿では、SwiftUIを学ぶときに、最初に欲しかった SwiftUI Property Wrapperについての説明をします
Property Wrapper: プロパティ ラッパー
まず Property Wrapper って何?
プロパティに付加的な機能を与えるためのSwiftUIの言語機能です
監視対象のデータが更新されるとViewを更新してくれます
この記事で解説する各 Property Wrapper の関係を図で表したものが以下になります
State
値の更新に応じてViewが自動更新されるようになります
プリミティブ型プロパティ(IntやStringなど)にのみ付加できます
struct ItemResisterView: View {
@State var newItem: String = "" // @Stateを付加
var body: some View {
VStack {
TextField("新規アイテム名", text: $newItem) // 入力による操作
Text(newItem) // 入力したものがリアルタイムで画面に表示される
Binding
親Viewが持っている@State
プロパティと状態を共有し、双方向で更新ができるようになります
-
親View側
- 子Viewを呼び出す際に
@State
プロパティを渡します - $を付けることで子Viewの持つ
@Binding
プロパティに紐づきます
- 子Viewを呼び出す際に
-
子View側
-
@Binding
プロパティを用意します - 親View側のプロパティを参照するので 初期値は不要です
-
// 親View
struct TabBarView: View {
@State var listName: String = "買うものリスト" // @Stateプロパティ(対象)
var body: some View {
ShoppingListView(listName: $listName) // 子Viewの呼び出し(@Stateプロパティを乗せる)
// 子View
struct ShoppingListView: View {
@Binding var listName: String // @Bindingを付加(親ViewのlistNameと状態を共有する)
var body: some View {
VStack {
TextField("リスト名", text: $listName) // 入力内容が親Viewにも反映される
Environment
環境値を利用することで、親Viewから孫Viewへ(経由される子Viewを意識せず)状態の共有ができるようになります
-
読み取りの形式
@Environment(\.keyPath)
-
書き込みの形式
.environment(Environment用のKeyPath, 書き込む値)
// CoreDataで必要なNSManagedObjectContextの状態を環境値に保存して利用する場合
@main // 起動時
struct ZeroApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
// 書き込み
.environment(\.managedObjectContext, persistenceController.container.viewContext)
// 値を利用したいView
struct ShoppingListView: View {
// 読み取り
@Environment(\.managedObjectContext) var viewContext
ObservedObject
オブジェクト型プロパティの変更を監視し、値の更新に応じてViewが自動更新されるようになります
-
:ObservableObject
- 監視対象のクラスは
ObservableObject
プロトコルに準拠する必要があります
- 監視対象のクラスは
-
@Published
- 監視対象のクラスの変更を検知するプロパティに付加します
-
@StateObject
- 監視対象のインスタンスを保持するView側で、インスタンス定義時に付加します
- ライフサイクルは「自身のViewが表示されてから非表示になるまで」です
- データの発生源は自身のViewであるため 監視対象のオブジェクトをインスタンス化して自身で保持します
-
@ObservedObject
- 親Viewから参照している監視対象のインスタンスに対して付加します
- ライフサイクルは「親ビューが表示されてから非表示になるまで」です
- 親Viewから渡されるデータを参照する際に用いるため 子View自身でインスタンス化して保持するのはNGです
- 親Viewと同じ監視対象のオブジェクトを参照するため 状態は共有されます
// データオブジェクトの準備
class User: ObservableObject { // ObservedObjectに準拠したクラスを用意する
@Published var name = "" // 監視対象のプロパティに@Publishedを付加する
}
// 親View
struct TabBarView: View {
@StateObject var user = User() // 状態オブジェクトとしてインスタンス化する
var body: some View {
Text(user.name)
ShoppingListView(user: user) // そのインスタンスを監視対象オブジェクトとして子ビューに渡す
// 子View
struct ShoppingListView: View {
@ObservedObject var user: User // @ObservedObject属性を付加(親Viewのuserを参照する)
var body: some View {
TextField("名前", text: $user.name) // 入力による操作
Text("\(user.name)さんのリストです") // 入力したものがリアルタイムで画面に表示される
FetchRequest
CoreDataに保存しているデータベースからソート条件を指定してデータを取得することができます
例えば、CoreDataに以下のようなEntityが存在する場合
@FetchRequest
を利用してデータを取得する実装は以下となります
-
sortDescriptors
: 取得した結果のソート順を定義します -
predicate
: 取得された結果をフィルタリングします -
animation
: 変更アニメーションを指定します
@FetchRequest (
// nameの昇順でソートする
sortDescriptors: [NSSortDescriptor(keyPath: \ZeroEntity.name, ascending: true)],
// stateがtrueの場合に絞る
predicate: NSPredicate(format: "state == \(true)") ,
animation: .default
)
private var shoppingList: FetchedResults<ZeroEntity> // ソートされたデータが入る
まとめ
各 Property Wrapper の使い時をざっくりまとめると以下になります
Property Wrapper | いつ使うか |
---|---|
State | プリミティブ型の値の変更をViewに自動反映させたいとき |
Binding | 親Viewと値を共有したいとき |
Environment | 親Viewから孫Viewへ値を反映させたいとき |
ObservedObject/StateObject/Published | オブジェクト型インスタンスの変更を監視したいとき |
FetchRequest | CoreDataからデータを取得したいとき |