Property Wrappers
SwiftUIのpropertyWrapperの前に、そもそもpropertyWrapperとは何かを説明させてください。
変数をラッピングして、アクセス(get/set)を制御するための仕組みです。
簡単な例を見てみましょう。
@propertyWrapper
struct HelloWorld {
private var text: String
init() {
text = ""
}
var wrappedValue: String {
get { return text }
set {
if newValue == "世界" {
text = "こんにちは, \(newValue)!"
return
}
text = "Hello, \(newValue)!"
}
}
}
クラスや構造体、enumに@peopertyWrapperをつけることで定義できます。
定義したら、それを以下のように変数に適用することができます。
@HelloWorld var name: String
nameに何らかの値をセットすると、"Hello, (name)!"という値に変換してセットされるようになります。日本語で"世界"とセットしたときだけ、"こんにちは, 世界!"となるようにしてみました。
wrappedValue
propertyWrapperはラッピングした値を返す変数であるwrappedValueを実装する必要があります。この値のget/setで、ラッピングした値を変更したりします。

projectedValue
追加でprojectedValueという変数を実装することもできます。ラッピングした値の状態を示したり、wrappedValueに関するなんらかの情報を返すことができます。外からこの値にアクセスするには$を頭につけなければいけません。例えば、
@propertyWrapper
struct HelloWorld {
var projectedValue: Bool
init() {
projectedValue = false
}
var wrappedValue {
// ... get ..
set {
if newValue == "世界" {
text = "こんにちは, \(newValue)!"
projectedValue = true
return
}
text = "Hello, \(newValue)!"
projectedValue = false
}
}
}
とすると、$nameで、Bool値が返ります。
class Hello {
@HelloWorld(text: "Hello, World!") var str: String
func sayHello() {
print(str) // Hello, World!
print("\($str)") // false
str = "世界"
print(str) // こんにちは, 世界!
print("\($str)") // true
}
}
「世界」という値をセットしたときだけ、projectedValueがtrueになるようにしています。
このようにprojectedValueを使ってwrappedValueの追加情報(投影値?)を返すことができます。
SwiftUI の propertyWrapper
以降はSwiftUIで定義されたpropertyWrapperについてみていこうと思います。
State
SwiftUIのviewは構造体(値)のため、変更不可です。ですが、変数に@Stateをつけると、その変数に変更が入ったら自動でviewを再構築してくれます。なのでviewで変更する必要があるものは@Stateをつけます。
struct GreetingView: View {
@State var message: String = ""
var body: some View {
VStack {
Text(message)
}
}
}
StateはprojecteValueとして、後述するBindingを返します。上記の例だと、 $message で、StringをBindingでラップした値を返してくれます。

Binding
双方向データバインディングというやつです。Bindingの値を変更すると、その情報源(変数)に、値の変更を反映してくれます。逆に情報源を更新すれば、Bindingから取得できる値も更新されています。
StateのprojectedValue($)は、自身の値と結びつくBindingを返してくれるので、自身の値を更新してほしいときに渡します。
struct GreetingView: View {
@State var message: String = "ボタンは押さないでね★"
var body: some View {
VStack {
Text(message)
// Bindingする例
GreetingButton(message: $message)
// Bindingしない例
StateGreetingButton(message: message)
}
}
}
struct GreetingButton: View {
@Binding var message: String
var body: some View {
Button(action: {
self.message = "ボタン...押しましたね..."
}) {
Text("絶対に押すな!")
}
}
}
struct StateGreetingButton: View {
@State var messageState: String
var body: some View {
Button(action: {
self.messageState = "Bindingではないです"
}) {
Text("押してもいいですよ")
}
}
}
GreetingButtonで、Bindingであるmessageを更新すると、情報源であるGreetingViewのmessageに変更が反映されます。

しかし、StateGreetingButtonのボタンを押して、messageStateを変更しても、情報源であるGreetingView.messageには反映されません。
もう一つの例をあげると、Toggleの引数isOnにBindingを渡しますよね。これは、Toggle自身がisOnに渡したBindingの情報源を変更するためです。
@State var onOff: Bool = true
var body: some View {
Toggle(isOn: $onOff) {
Text("情報源(var onOff)の値を変えますよ: \(onOff.description)")
}
}
ObservedObject
ObservedObjectはObservableObjectを実装したクラスをwrappedValueとして持ちます。
struct PlayerViewView: View {
@ObservedObject var user: PlayerViewModel = PlayerViewModel()
var body: some View {
VStack {
Text("あなたは \(user.age) 歳です")
Button(action: {
self.user.haveBirthday()
}) {
Text("何歳ですか?")
}
}
}
}
final class PlayerViewModel: ObservableObject {
private(set) var age: Int = 0 {
willSet {
objectWillChange.send()
}
}
func haveBirthday() {
age += 1
}
}
上記の例だと、ボタンを押すたびに、PlayerViewへPlayerViewModelの変更が通知され、viewが更新されます。
データフローは@Stateと変わらないのですが、@ObservedObjectの場合は、wrappedValueがObservableObjectのオブジェクトでないといけない、という点が異なります。
ObservableObject
ObservableObjectはCombineフレームワークに含まれるプロトコルです。オブジェクトの変更を通知できることを表します。objectWillChangeというPublisher(変更イベントの発行元)を持っており、これを購読していれば、ObservableObjectの変更を検知できます。
ObservableObjectが自身に変更が入った、ということを通知するには、以下のようにobjectWillChange.send()を実行します。
class SomeObservableObject: ObservableObject {
var hello: String {
willSet {
// ObservableObjectに適合するクラスは objectWillChangeを持っている
objectWillChange.send()
}
}
}
以下のように購読すれば、変更が入ったことを検知できます。
class Hoge {
var cancellable: AnyCancellable?
init() {
cancellable = someObservalbeObject.objectWillChange.sink { _ in
// objectWillChange.send() が呼ばれた!
}
}
}
@ObservedObjectのpropertyWrapperを使うと、SwiftUIがObservableObjectを購読し、変更が入るたびにviewを更新してくれます。

Published
SwiftUIではなく、Combineで定義されたpropertyWrapperですが、ObservableObjectでよく使われるので説明します。
変数に@Publishedを付与すると、その変数のprojectedValueは自身の変更を通知する Publisher を返してくれます。
class Dog {
@Published var name: String = "Pero"
private var cancellable: AnyCancellable?
init() {
self.cancellable = $name.sink { newName in
print("あなたの名前: \(newName)")
}
}
}
let dog = Dog() // あなたの名前: Pero
dog.name = "John" // あなたの名前: John
dog.name = "Taro" // あなたの名前: Taro
sink で購読し、値の変更を監視しています。変更が入るたびにログ出力しています。
ObservableObjectというプロトコルに適合したクラスで、@Publishedの変数を持っている場合、それに対して変更すると、ObservableObjectの変更を自動で通知してくれます。
class Dog: ObservableObject {
@Published var name: String = "Pero"
private var cancellable: AnyCancellable?
init() {
self.cancellable = objectWillChange.sink {
print("名前、変わりましたね...")
}
}
}
let dog = Dog()
dog.name = "John" // 名前、変わりましたね...
なので、@Publishedを使えば、自前でwillSetにてobjectWillChange.send()なんて書かずによいので楽です。
EnvironmentObject
ObservableObjectを子ビューでも使いまわしたい場合は、@ObservedObjectではなく、@EnvironmentObjectを使用します。
struct ContentView: View {
@EnvironmentObject var setting: PlayerSettings
var body: some View {
VStack {
Text("あなたの通知設定: \(setting.isNotificationEnable.description)")
SubView()
}
}
}
struct SubView: View {
@EnvironmentObject var setting: PlayerSettings
var body: some View {
VStack {
Button(action: {
self.setting.isNotificationEnable.toggle()
}) {
Text("通知設定を変更")
}
}
}
}
/// View間で共有するオブジェクト
final class PlayerSettings: ObservableObject {
@Published var isNotificationEnable: Bool = false
}
上記の例だと、SubViewのsettingは親View(ContentView)のsettingが自動でインジェクトされます。なので、子View側で、変更したら、親Viewにも反映されます。
ただし、最初に@EnvironmentObjectを使用しているViewの生成時に、オブジェクトを生成して渡す必要があります。渡さないとクラッシュします。
let contentView = ContentView().environmentObject(PlayerSettings())
もう一つ注意する点は、sheetなどで表示した親子関係でない別のViewには自動でインジェクトされないということです。共有したい場合は、明示的に渡す必要があります。
struct ContentView: View {
@EnvironmentObject var setting: PlayerSettings
@State var showModal: Bool = false
var body: some View {
VStack {
Text("あなたの通知設定: \(setting.isNotificationEnable.description)")
Button(action: {
self.showModal = true
}) {
Text("べつのがめん")
}.sheet(isPresented: $showModal) {
// 親子関係でないViewでも共有したい場合は、明示的に渡す必要がある
SubView().environmentObject(self.setting)
}
}
}
}
Environment
SwiftUIで定義されたViewの設定値を取得することができます。
struct SubView: View {
@Environment(\.isEnabled) var enable: Bool
var body: some View {
VStack {
Text("活性状態? \(enable.description)")
}
}
}
例えば上記画面の表示時に、以下のようにdisabled(true)とすれば、@Environment(\.isEnabled)はfalseになります。
SubView().disabled(true)