LoginSignup
31
22

More than 3 years have passed since last update.

SwiftUIのProperty Wrappersまとめ

Last updated at Posted at 2020-03-23

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で、ラッピングした値を変更したりします。
SwiftUI PropertyWrappers1.png

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
    }
}

「世界」という値をセットしたときだけ、projectedValuetrueになるようにしています。
このように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)
        }
    }
}

StateprojecteValueとして、後述するBindingを返します。上記の例だと、 $message で、StringBindingでラップした値を返してくれます。
SwiftUI PropertyWrappers2.png

Binding

双方向データバインディングというやつです。Bindingの値を変更すると、その情報源(変数)に、値の変更を反映してくれます。逆に情報源を更新すれば、Bindingから取得できる値も更新されています。

StateprojectedValue($)は、自身の値と結びつく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を更新すると、情報源であるGreetingViewmessageに変更が反映されます。
SwiftUI PropertyWrappers3.png
しかし、StateGreetingButtonのボタンを押して、messageStateを変更しても、情報源であるGreetingView.messageには反映されません。

もう一つの例をあげると、Toggleの引数isOnBindingを渡しますよね。これは、Toggle自身がisOnに渡したBindingの情報源を変更するためです。

@State var onOff: Bool = true

var body: some View {
    Toggle(isOn: $onOff) {
        Text("情報源(var onOff)の値を変えますよ: \(onOff.description)")
    }
}

ObservedObject

ObservedObjectObservableObjectを実装したクラスを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
    }
}

上記の例だと、ボタンを押すたびに、PlayerViewPlayerViewModelの変更が通知され、viewが更新されます。
データフローは@Stateと変わらないのですが、@ObservedObjectの場合は、wrappedValueObservableObjectのオブジェクトでないといけない、という点が異なります。

ObservableObject

ObservableObjectCombineフレームワークに含まれるプロトコルです。オブジェクトの変更を通知できることを表します。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を更新してくれます。
SwiftUI PropertyWrappers4.png

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
}

上記の例だと、SubViewsettingは親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)
31
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
22