BambooHero

#Programming #iOS #Investment #LifeHacks

[SwiftUI] PreferenceKeyの基本的な仕組みを知る

子ビューから親ビューにデータを渡す仕組みとして頻繁に使用するPreferenceKeyですが、

  • reduce()メソッドの実装ってこれで合ってるんだっけ?
  • onPreferenceChangeにはどういう値が入ってくる?

といった点について、毎回デバッグして調べてたので、改めて整理しました。



コード例

ビューとPreferenceKeyを以下の通り実装します。

struct ContentView: View {
    var body: some View {
        VStack {
            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle1")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle2")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle3")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle4")
        }
        .onPreferenceChange(StringPreferenceKey.self) { value in
            print("onPreferenceChange() value: \(value)")
        }
    }
}

struct StringPreferenceKey: PreferenceKey {
    typealias Value = String
    static var defaultValue: Value = "Default"

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value = next
    }
}


このビューを画面に表示したとき、コンソールには以下のように出力されます。

reduce() value: Rectangle1, nextValue: Rectangle2
reduce() value: Rectangle2, nextValue: Rectangle3
reduce() value: Rectangle3, nextValue: Rectangle4
onPreferenceChange() value: Rectangle4


PreferenceKeyのポイントを整理

ポイントを整理します。

  1. ビューに配置されたpreferenceモディファイアは4つ
  2. reduce()メソッドは3回呼ばれた
  3. reduce()メソッドが3回呼ばれた後、onPreferenceChange()には最後の処理結果が渡される


reduce()メソッドはビューに配置された同じPreferenceKeyの値をどう処理するかを決める役割を持ちます。 今回は以下のようにvaluenextValue()の値を代入する実装にしました(print文を消してわかりやすくしています)。

static func reduce(value: inout Value, nextValue: () -> Value) {
    value = nextValue()
}


すると、reduce()メソッドが呼ばれるたびにvalueの値は上書きされていき、最後の値であるRectangle4がonPreferenceChange()に渡されます。


次はPreferenceKeyを以下のように実装してみます。

struct StringPreferenceKey: PreferenceKey {
    typealias Value = [String]
    static var defaultValue: Value = []

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value.append(contentsOf: next)
    }
}


すると、コンソールには以下のように出力されます。

reduce() value: ["Rectangle1"], nextValue: ["Rectangle2"]
reduce() value: ["Rectangle1", "Rectangle2"], nextValue: ["Rectangle3"]
reduce() value: ["Rectangle1", "Rectangle2", "Rectangle3"], nextValue: ["Rectangle4"]
onPreferenceChange() value: ["Rectangle1", "Rectangle2", "Rectangle3", "Rectangle4"]


valueに値をappendしていくので、最終的には4つの要素を持つ配列がonPreferenceChange()に渡されます。 複数の子ビューのデータを一度に受け取って何か処理したいときはこのように実装すると良さそうです。

ちなみに、以下の記事ではPreferenceKeyの値を配列で渡すパターンを使用した例を紹介してますので、是非ご参照ください。

bamboo-hero.com


下層に同じPreferenceKeyが1つしかない場合どうなるのか?

ちなみに、下層に同じPreferenceKeyが1つしかない場合はreduce()メソッドは呼ばれません。

struct ContentView: View {
    var body: some View {
        VStack {
            Color.clear
                .preference(key: StringPreferenceKey.self, value: ["Rectangle1"])
        }
        .onPreferenceChange(StringPreferenceKey.self) { value in
            print("onPreferenceChange() value: \(value)")
        }
    }
}

struct StringPreferenceKey: PreferenceKey {
    typealias Value = [String]
    static var defaultValue: Value = []

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value.append(contentsOf: next)
    }
}


コンソールの出力。

onPreferenceChange() value: ["Rectangle1"]


defaultValueはどこで使われる?

defaultValueがどこで使われるのかわかってません。

どこで使われるのでしょう...?知ってる方教えてほしいです。