SwiftUI のための Swift5

SwiftUI では、Swift5.1で導入された新機能がたくさん使われているので、ここでよく使われるものをピックアップします。

Opaque Return Type

戻り値の型を抽象的に記述する方法。戻り値の前に「some」をつけます。以下はSwiftUIでよく見られる記述です。

var body: some View {
    ...
}

bodyは1つのViewを返すプロパティですが、そのViewの種類はTextだったりVStackだったりさまざまなので、someが使われています。

Implicit Return

関数や計算型プロパティで、式が1つしかない場合は return キーワードが省略可能になりました。SwiftUI の body プロパティも return が省略されています。

Property Wrapper

構造体のプロパティをラップして、独自の setter/ getter 処理を定義できます。

@propertyWrapper
struct 構造体名 {
    var wrappedValue: 型 {
        get { ... }
        set { ... }
    }
}

上記のような構造体を定義して、ラップするプロパティに @構造体名をつけます。構造体なので、イニシャライザで引数を渡すことも可能です。以下は UserDefault の構造体をラップした場合の例です。

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

このラッパーを使用するときには @UserDefaultをつけます。

struct UserDefaultsConfig {
    @UserDefault(key: "name", defaultValue: "")
    static var name: String

    @UserDefault(key: "age", defaultValue: 20)
    static var age: Int
}

print(UserDefaultsConfig.name) => ""
print(UserDefaultsConfig.age) => 20

SwiftUIではこの仕組みを使っていくつかプロパティラッパーが用意されています。

State

@State をつけて宣言された変数の値が変更されると、自動的にViewの更新が走ります。

struct ContentView: View {
    @State var text: String = "1"

    var body: some View {
        VStack {
            Text(text)
            Button(action: {
                self.text = "2"
            }) {
                Text("2にする")
            }
        }
    }
}

上記でボタンを押すと自動的に"2"が表示されます。

Binding

@State で定義されたプロパティを他のViewで共有します。たとえば親子関係のView同士で共有する場合、親で@Stateプロパティを宣言して、子に渡すときに$つきの変数(「射影プロパティ」といいます)で値を渡し、さらに子で@Bindingをつけて変数を定義します。

struct ParentView: View {
    @State var text: String = "1"
    var body: some View {
        VStack {
            Text(text)
            Button(action: {
                self.text = "2"
            }) {
                Text("2にする")
            }
            ChildView(text: $text)
        }
    }
}

struct ChildView: View {
    @Binding var text: String

    var body: some View {
        VStack {
            Text(text)
            Button(action: {
                self.text = "3"
            }) {
                Text("3にする")
            }
        }
    }
}

親のViewでボタンの押しても、子のViewでボタンを押しても両方のViewが同時に更新されるようになります。

ObservedObject

任意のクラスのプロパティが変更されたときにビューを更新します。@Stateでは1つのプロパティのみ監視可能でしたが、これで複数のプロパティを監視できるので、監視したいプロパティをクラスにまとめたりすることができます。

任意のクラスにはObservableObjectプロトコルを実装し、監視対象のプロパティには@Publishedをつけます。

Viewの生成時に任意のクラスのインスタンスを渡してあげましょう。各 View でボタンを押すと @published をつけたプロパティの値が変更され、他のViewで自動的に更新が走ります。

struct ParentView: View {
    @ObservedObject var data: UserData

    var body: some View {
        VStack {
            Text(data.name)
            ChildView(data: data)
            ChildView2(data: data)
        }
    }
}

struct ChildView: View {
    @ObservedObject var data: UserData

    var body: some View {
        VStack {
            Text(data.name)
            Button(action: {
                self.data.name = "ChildView"
            }) {
                Text("更新する")
            }
        }
    }
}

struct ChildView2: View {
    @ObservedObject var data: UserData

    var body: some View {
        VStack {
            Text(data.name)
            Button(action: {
                self.data.name = "ChildView2"
            }) {
                Text("更新する")
            }
        }

    }
}

class UserData: ObservableObject {
    @Published var name: String = "View"
}

struct ParentView_Previews: PreviewProvider {
    static var previews: some View {
        return ParentView(data: UserData())
    }
}

EnvironmentObject

複数のViewでプロパティの状態を共有する際に、階層構造に従って親から子に順にデータを渡さなくても、任意の階層でデータを参照することができます。

まず親のインスタンスを生成するときに environmentObjectモディファイアで共有するデータを渡します。こうすると、そのViewと階層構造にあるViewでデータを共有することができます。

そして、そのデータを参照したいViewで @EnvironmentObject をつけてそのクラスを宣言します。

ObservedObject の時のようにViewの生成時にデータクラスのインスタンスを渡す手間が省けます。

struct ParentView: View {
    var body: some View {
        ChildView()
    }
}

struct ChildView: View {
    var body: some View {
        GrandChildView()
    }
}

struct GrandChildView: View {
    @EnvironmentObject var data: UserData

    var body: some View {
        Text(data.name)
    }
}

class UserData: ObservableObject {
    @Published var name: String = "View"
}

struct ParentView_Previews: PreviewProvider {
    static var previews: some View {
        let data = UserData()

        return ParentView().environmentObject(data)
    }
}

Environment

Viewの設定値を参照・操作するのに使用します。

SwiftUI では、フォントやロケールなど、Viewを生成するのに使用するさまざまな環境値(EnvironmentValues)があります。

EnvironmentValues はViewの生成時に enviormentメソッドで値を設定することができ、参照するには以下のように @Environment(\.key) をつけます。

struct ParentView: View {
    @Environment(\.locale) var locale: Locale

    var body: some View {
        Text(locale.description)
    }
}

struct ParentView_Previews: PreviewProvider {
    static var previews: some View {
        return ParentView().environment(\.locale, Locale(identifier: "ja_JP"))
    }
}

Function Builder

改行で区切られたコードを、あるメソッドの引数と渡して処理できる機能です。VStack {} や HStack {} のブロック内では、Viewを改行区切りで定義していきますが、これらは Function Builder 機能で実際には VStack で定義された別のメソッドの引数として渡され処理されています。

Function Builder を使うには、@_functionBuilder をつけて構造体を定義し、static な buildBlock メソッドで引数と処理を書きます。

使用する際は @構造体の名前 をつけてメソッドを定義します。

@_functionBuilder
struct StringBuilder {
    public static func buildBlock(_ a: String, _ b: String) -> String {
        a + b
    }
}

@StringBuilder func combineStrings() -> String {
    "田中"
    "太郎"
}

print(combineStrings()) // "田中太郎"

この combineStrings メソッドは、コンパイル時には以下のように変換されています。

func combineStrings() {
    let a = "田中"
    let b = "太郎"
    return StringBuilder.bildBlock(a,b)
}

ただ、結構特殊な機能なので、Function Builder を独自に定義する機会はあまりないと思われます。



当社で開発した予定とタスクを同時に管理できるiOSアプリ「My Schedule - マイスケジュール -」をリリースしました。ダウンロード&評価のほどよろしくお願いいたします。