Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
SwiftUI: 更新検知と値の生存期間
Search
Iceman
June 27, 2019
Programming
1.2k
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
SwiftUI: 更新検知と値の生存期間
Iceman
June 27, 2019
More Decks by Iceman
See All by Iceman
わいわいswift#39 Swiftの型をTypeScriptで表す
sidepelican
0
350
わいわいswiftc#35夢が広がる!コード生成でどこでもSwift
sidepelican
0
480
元ゲーム開発者が贈る描画パフォーマンス改善 / Rendering performance improvement from a game developer
sidepelican
4
1.8k
わいわいswiftc#19Genericsの特殊化
sidepelican
0
480
わいわいswiftc#17Genericsの特殊化
sidepelican
0
110
クックパッドiOSアプリのパフォーマンス改善
sidepelican
0
810
DispatchQueue.syncが動作するスレッド
sidepelican
0
390
Other Decks in Programming
See All in Programming
今さら聞けない .NET CLI
htkym
0
150
Build-to-own AI: Agentic Development for Humans
inesmontani
PRO
0
120
Haskell/Servantを通してWebミドルウェアを捉え直す
pizzacat83
1
620
【やさしく解説 設計編・中級 #6】良いアーキテクチャとは ~ 一本の登り道の、行き先 ~
panda728
PRO
0
190
言語を使う側から、作る側へ。 自作 Lisp で得た新たな気づき。
andpad
0
140
SREの積み重ねがAI駆動開発のガードレールになった ― 7つの実践/SRE Guardrails The 7
tomoyakitaura
9
5.8k
AWS CDK を「作」ってみた 〜フルスクラッチで見えた CDK の裏側〜 / aws-cdk-from-scratch
gotok365
3
2.6k
Foundation Models frameworkで画像分析
ryodeveloper
1
160
Claude Team Plan導入・ガイド
tk3fftk
0
240
【やさしく解説 設計編・中級 #4】ルールの寿命と、システムの年輪
panda728
PRO
2
170
PHPだって関数型したい 〜できること、できないこと〜 / fp-in-php
jsoizo
1
250
【SRE NEXT 2026 Lunch Session】一人目専任SREの立ち上げを加速する ― AIと進めたオンボーディングで2分を0.04秒にした話
pkshadeck
PRO
0
3.2k
Featured
See All Featured
The browser strikes back
jonoalderson
0
1.4k
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
220
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
11k
Chasing Engaging Ingredients in Design
codingconduct
0
240
Designing Experiences People Love
moore
143
24k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
55k
How Software Deployment tools have changed in the past 20 years
geshan
1
34k
Scaling GitHub
holman
464
140k
Why You Should Never Use an ORM
jnunemaker
PRO
61
9.9k
Optimizing for Happiness
mojombo
378
71k
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1k
The innovator’s Mindset - Leading Through an Era of Exponential Change - McGill University 2025
jdejongh
PRO
1
230
Transcript
わいわいswiftc #12 SwiftUI: 更新検知と値の生存期間 @iceman5499
SwiftUIにはViewのプロパティの更新を検知するため の@propertyDelegateがたくさんある @State @Binding @ObjectBinding @EnvironmentObject @Environment (@GestureStateもあるけど今回は未調査)
任意の型を入れられるものと、 BindableObject を入れるものと2種 類がある 名前 中に入る型 @State T @Binding T
@ObjectBinding T: BindableObject @EnvironmentObject T: BindableObject @Environment T @Stateなどは任意の型を使用できるが、 struct・enumとclassとで挙動が異なる(後述) 順にみていく
@State
@State 値を保持できる。値が変化したとき値を使用したViewがリビルドされる struct StateBasic : View { @State var count:
Int = 0 var body: some View { VStack { Text("\(count)") Button(action: { // ボタンを押すと表示が更新 self.count += 1 }) { Text("inc") } } } }
@State 値は1つのNavigation単位で生存する Backで画面が消えると初期化される (Viewの構成によっては初期化されないパターンもある) Presentationされていた場合は画面が消えても値が残る (Viewの構成によっては初期化されるパターンもある)
@State View.body内で値が1度も取り出されなかった場合、値が更新され てもそのViewはリビルドされない
struct StateUnused : View { @State var okane: Int =
0 var body: some View { VStack { if AccountManager.shared.isPurchased { // 初回でここに入らなかった場合は // 以降okane が変化してもリビルドされない Text("\(okane)") } else { Text("not purchased") } Button(action: { AccountManager.shared.isPurchased = true self.okane += 1 }) { Text("purchase") } } } }
修正後 struct StateUnusedFix : View { @ObjectBinding var accountManager: AccountManager
@State var okane: Int = 0 var body: some View { VStack { if accountManager.isPurchased { Text("\(okane)") } else { Text("not purchased") } Button(action: { self.accountManager.isPurchased = true self.okane += 1 }) { Text("purchase") } } } } propertyDelegateで監視されていない値を使って分岐をしてはいけない
@State 中の値がclassのときは値の保持はされるが更新検知はされない が、 BindableObject に適合していれば検知される enumは更新検知される
@Binding
@Binding プロパティに対するエイリアスのようなもの 実体は持たないが値を取り出せるし値を変化させれば依存している各所 がリビルドされる
private struct IncButton: View { @Binding var count: Int var
body: some View { Button(action: { self.count += 1 }) { Text("inc") } } } struct BindingBasic : View { @State var count = 0 var body: some View { VStack { Text("\(count)") // ↓ 値への参照を渡して更新してもらう IncButton(count: $count) } } }
@Binding うまく使えばリビルド範囲を制御できるかもしれない? リビルド範囲を狭めることでパフォーマンスに恩恵があるかは不明 struct BindingSibling : View { @State var
count = 0 var body: some View { // count が変化してもこれ自体はリビルドされない VStack { CounterView(count: $count) // ← この中はリビルド IncButton(count: $count) } } }
@ObjectBinding
@ObjectBinding BindableObject に適合したオブジェクトは@ObjectBindingを使用す ることで更新検知できるようになる public protocol BindableObject : AnyObject, DynamicViewProperty,
Identifiable, _BindableObjectViewProperty { associatedtype PublisherType : Publisher where Self.PublisherType.Failure == Never var didChange: Self.PublisherType { get } }
使用例? それらしい使い方だが、これは罠にはまる class CountViewModel: BindableObject { let didChange = PassthroughSubject<CountViewModel,
Never>() var count: Int = 0 func inc() { count += 1 didChange.send(self) } } struct ObjectBindingBasic : View { @ObjectBinding var viewModel = CountViewModel() var body: some View { VStack { Text("\(viewModel.count)") Button(action: { self.viewModel.inc() }) { Text("inc") } } } }
@ObjectBinding 値の保持はされない Viewがリビルドされるたびに値が初期化される 前ページのコードの場合、ネストした画面で使用されて親 Viewがリビルドを発火したタイミングでcountが0に戻る それなのに更新検知対象は初回に生成されたオブジェクトのみ 初回生成されたやつはどこかに保持され一生触れない バグっぽい Navigation単位ではなぜか値が保持される 最も挙動が不安定
値が保持されない問題の回避策? viewModelを@Stateとして保持することでNavigation単位で生存 させられる struct ObjectBindingBasicFix : View { @State var
viewModel = CountViewModel() var body: some View { VStack { Text("\(viewModel.count)") Button(action: { self.viewModel.inc() }) { Text("inc") } } } } しかし@Stateにclassを保持させること自体は想定されていなさそ うなため、おとなしく親Viewが子ViewのviewModelを管理してあ げるか後述する@EnvironmentObjectを利用したほうがよさそう
@ObjectBinding Q. @Stateを使っても BindableObject の更新検知はできるので何に 使うのこれ? A. delegateValueとDMLのマジックで $viewState.count と書く
と Binding<Int> が取得できるためそこらへんで差別化できる
@EnvironmentObject
@EnvironmentObject 親や祖父やその先の祖先で宣言されたオブジェクトを一気にジャンプし て子Viewが取得できる仕組み Flutterでいう InheritedWidget に近い
@EnvironmentObject struct CounterPage: View { @EnvironmentObject var counter: CounterEnvironment var
body: some View { // counter が利用できる or 実行時クラッシュ ... } } struct EnvironmentObjectBasic : View { var body: some View { VStack { CounterPage() CounterPage() } .environmentObject(CounterEnvironment()) // ⭐ } } ⭐ の行がなければ実行時クラッシュする
画面単位でしか生存していないので、NavigationかPresentationを またぐとリセットされる struct EnvironmentObjectPush : View { var body: some
View { VStack { // どちらもボタンタップでクラッシュする NavigationButton(destination: CounterPage()) { Text("push") } PresentationButton(destination: CounterPage()) { Text("present") } } .environmentObject(CounterEnvironment()) } }
@Environment
@Environment EnvironmentObjectと似ていてこちらは値型を扱うといったイメージ。 EnvironmentObjectとは異なり .environment(..., ...) が宣言され ていなくてもデフォルト値が使用されるためクラッシュしない
@Environment まず EnvironmentValues に値を生やす extension EnvironmentValues { var counter: CounterEnvironment
{ get { self[CounterEnvironmentKey.self] } set { self[CounterEnvironmentKey.self] =newValue} } } struct CounterEnvironmentKey: EnvironmentKey { static var defaultValue: CounterEnvironment {.init()} } struct CounterEnvironment { var count: Int = 0 }
その後@Environmentと取り出したいプロパティのkeyPathをわた して取り出す struct CounterPage2: View { @Environment(\.counter) var counter: CounterEnvironment
var body: some View { VStack { Text("\(counter.count)") Button(action: { // Environment はget-only なため変更できない }) { Text("inc") } } } }
@Environment @EnvironmentObjectと同様に画面単位でしか生存していないの で、NavigationかPresentationをまたぐとデフォルト値が利用され る
生存期間まとめ 名前 生存期間 @State Navigation単位。Presentationでは値が残る @Binding 参照先のオブジェクトと同じ @ObjectBinding 挙動が不安定だが基本生存しないがち @EnvironmentObject
Navigation・Presentation単位 @Environment Navigation・Presentation単位 あくまで現時点。リリースされるころには変わってそう Viewの構成によって変わることがある
まとめ 値の更新でViewがリビルドされるよう暗黙の更新検知に気をつか って実装すること Viewに紐付くプロパティの生存期間は思った以上に複雑 MVVMとかでviewModelなどをくっつけるときは、viewModelの 生成・破棄が予期せぬタイミングで何度も起こることに注意する 特にsubscribeしただけで通信を発火するみたいな実装をして いると大変なことになるかもしれない・・・・ 環境 macOS
Catalina 10.15 Beta(19A487l) Xcode Version 11.0 beta 2 (11M337n)