Property Wrappersを理解する

Published by @SoNiceInfo at 6/24/2020


SwiftUIで変数の状態を監視するにはProperty Wrappers(プロパティラッパー)を使います。
Property Wrappersを使うことで、複数のViewで変数を共有したり、変数が更新されるとViewを再描画してくれます。
SwiftUIでは以下の種類のProperty Wrappersが用意されています。

Property Wrappers役割使い所(例)
@StateViewの状態と変数の値を監視するSheet(モーダル)の状態監視
@Binding他のViewの@State変数を参照・変更するSheet(モーダル)へ変数を渡す
@ObservedObject構造体・クラスの変更を監視する。ユーザ情報など複数のプロパティを持つクラスを扱う。親Viewからインスタンスを受け取る。
@Environment予め定義されたViewの環境情報を取得する画面のサイズを扱う
@EnvironmentObject構造体・クラスの変更を監視する複数のプロパティを持つクラスを扱う

@State

@Stateではひとつの値を保持することができます。
また@StateはひとつのView内でのみ使用することが推奨されています。
ここではView内でSheet(モーダル)遷移状態を保持するために使っています。
sheetにisPresentedのBindingを渡すために$(ドルマーク)をつけています。

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    @State private var isPresented: Bool = false
    @State private var catName: String = "Tora"
    
    var body: some View {
        VStack {
            Text(catName)
            Button("Show Result") {
                self.isPresented.toggle()
            }
        }
        .sheet(isPresented: $isPresented) {
            VStack {
                TextField("My cat's name is...", text: self.$catName)
                    .multilineTextAlignment(.center)
                Button("Close Result") {
                    self.isPresented.toggle()
                }
            }
        }
    }
}
    
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@Binding

@Bindingを利用すると他のViewの@State変数を受け継ぐことができます。
@Bindingした変数は変更すると他のViewにも変更が通知されます。
@Bindingする変数を持つViewには$(ドルマーク)をつけてBinding型にします。
このコードはSheet内のViewを分けていますが上のコードと同じ動きをします。

//
//  ContentView.swift
//

import SwiftUI

struct SheetView: View {
    @Binding var isPresented: Bool
    @Binding var catName: String
    
    var body: some View {
        VStack {
            TextField("My cat's name is...", text: $catName)
                .multilineTextAlignment(.center)
            Button("Close Result") {
                self.isPresented.toggle()
            }
        }
    }
}

struct ContentView: View {
    @State private var isPresented: Bool = false
    @State private var catName: String = "Tora"
    
    var body: some View {
        VStack {
            Text(catName)
            Button("Show Result") {
                self.isPresented.toggle()
            }
        }
        .sheet(isPresented: $isPresented) {
            SheetView(isPresented: self.$isPresented, catName: self.$catName)
        }
    }
}
    
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@ObservedObject

@ObservedObjectは構造体やクラスの値を監視するときに有効です。
ObservableObjectプロトコルに準拠したクラスといっしょに使うことで、複数のインスタンスを生成できます
ObservableObject内では@Publishedを使うことで変更が複数のViewに通知されます。

//
//  ContentView.swift
//

import SwiftUI

class CatModel: ObservableObject {
    @Published var name : String
    @Published var age : String
    
    init (name: String, age: String) {
        self.name = name
        self.age = age
    }
}

struct ResultView: View {
    @Binding var isPresented: Bool
    @ObservedObject var cat: CatModel

    var body: some View {
        VStack {
            Text("My cat is: \(cat.name), \(cat.age)")
            
            Button("Close Result") {
                self.isPresented.toggle()
            }
        }
    }
}

struct ContentView: View {
    @State private var isPresented: Bool = false
    @ObservedObject var cat = CatModel(name: "Mike", age: "3")
    
    var body: some View {
        VStack {
            HStack {
                Text("Name: ")
                TextField("Input name", text: $cat.name)
            }
            HStack {
                Text("Age: ")
                TextField("Input age", text: $cat.age)
            }
            Button("Show Result") {
                self.isPresented.toggle()
            }
        }
        .sheet(isPresented: $isPresented) {
            ResultView(isPresented: self.$isPresented, cat: self.cat)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
@ObservedObjectを使った例

@Environment

@Environmentはフォントやダークモード・ライトモード等の環境情報を取得することができます。
取得できる値はEnvironmentValues - SwiftUI | Apple Developer Documentation

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    @Environment(\.font) var font
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        VStack {
            Text("Text")
                .font(font)
        }
    }
}
    
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        HStack {
            ContentView()
                .environment(\.font, .body)
            ContentView()
                .environment(\.font, .largeTitle)
        }
    }
}
@Environmentを使う画像

@EnvironmentObject

@EnvironmentObjectは複数の値を保持することができます。
すべてのViewで使いたいときには@EnvironmentObjectが有効です。
@ObservedObjectが複数のインスタンスを作成できるのに対し、@EnvironmentObjectで作成できるインスタンスはひとつです。
さらにインスタンスは外部から渡されてすべてのViewで共有されるという特徴があります。
@EnvironmentObjectのインスタンスはenvironmentObjectでインスタンスを渡します。

iOS 13対応

//
//  SceneDelegate.swift
//

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Create the SwiftUI view that provides the window contents.
        // 大事!!
        let contentView = ContentView().environmentObject(CatModel())

        ...
    }
}

iOS 14対応

import SwiftUI

@main
struct iOS14App: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(CatModel())
        }
    }
}

共通

//
//  ContentView.swift
//

import SwiftUI

class CatModel: ObservableObject {
    @Published var name = ""
    @Published var age = ""
}

struct ContentView: View {
    @State var isPresented: Bool = false
    @EnvironmentObject var cat : CatModel
    
    var body: some View {
        VStack {
            HStack {
                Text("Name: ")
                TextField("Input name", text: $cat.name)
            }
            HStack {
                Text("Age: ")
                TextField("Input age", text: $cat.age)
            }
            Button("Show Result") {
                self.isPresented.toggle()
            }
        }
        .sheet(isPresented: $isPresented) {
            ResultView(isPresented: self.$isPresented)
                .environmentObject(self.cat)
        }
    }
}

struct ResultView: View {
    @Binding var isPresented: Bool
    @EnvironmentObject var cat : CatModel

    var body: some View {
        VStack {
            Text("My cat is: \(cat.name), \(cat.age)")
            
            Button("Close Result") {
                self.isPresented.toggle()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
@EnvironmentObjectを使う画像

参考

What’s the difference between @ObservedObject, @State, and @EnvironmentObject? - a free SwiftUI by Example tutorial

    アプリをリリースしました!

    ToDoアプリ

    iCloudを利用したデバイス間データ共有、ダークモードに対応しています。

    リリースしたToDoアプリのスクリーンショット

    IPアドレス履歴保存アプリ

    取得したIPアドレスを確認、位置情報とともに保存できます。

    リリースしたIPアドレス保存アプリのスクリーンショット