コンテンツへスキップ

Binding: Equatable vs EquatableBinding

Bindingを直接Equatableに準拠させるのではなく、Property Wrapperを使うことで、SwiftUIのPickerの微妙なバグを修正した方法を紹介します。

Binding: Equatable vs EquatableBinding

直感に反するかもしれませんが、自分のアプリRemafoXでは、SwiftUIに付属するBinding型(ビューの変更をデータにバインドするために使用)をEquatableプロトコルに準拠させる必要がありました。Bindingオブジェクトをアプリのデータレイヤーで受け渡すためです。そのエクステンションの実装は以下のようなものでした:

extension Binding: Equatable where Value: Equatable {
   static func == (left: Binding<Value>, right: Binding<Value>) -> Bool {
      left.wrappedValue == right.wrappedValue
   }
}

この解決策に100%満足していたわけではありませんが、最初に開発した時点では問題なく動作していたのでそのまま出荷しました。しかし、あるmacOSのアップデート以降、アプリ内のすべてのPickerビューに微妙なバグが忍び寄ってきました。Pickerはほとんどの場合正常に動作していましたが、時折おかしな挙動を示すようになりました。常に再現できた唯一の動作は、プログラム的にバインディングに選択値を設定し、その後ユーザーが別の値に変更すると、Pickerのドロップダウンに両方の値にチェックマークが表示されるというものでした。おそらくSwiftUIのどこかにあるバグです:

コードをコメントアウトしながら何時間もかけて根本原因を突き止めたところ、先ほど述べたBindingのエクステンションが原因だと判明しました。どうやら、アプリ内でこれを定義していることがSwiftUIのPickerビューの内部実装に影響を与えていたようです。そうなると、他にどんな副作用を引き起こしていたか、あるいは将来のシステムアップデートで引き起こす可能性があるかもわかりません。SwiftUIのビューの動作に影響を与えない、より良い解決策を見つける必要がありました。

BindingEquatableに準拠させたかった理由は、データレイヤーに要件があったからです。具体的には、TCAアーキテクチャの一部のState型で、保存するすべてのデータがEquatableに準拠している必要がありました。以下のような形です:

struct AppState: Equatable {
   // other properties

   var configFile: Binding<ConfigFile>
}

Want to see your ad here? Contact me at [email protected] to get in touch.


見つけた解決策はとてもシンプルでした。ただ、Property Wrapperの書き方を「学ぶ」必要がありました。自作するのは初めてだったので。しかし実際にはとても簡単で、書いたことがなくても理解できると思います:

@propertyWrapper
public struct EquatableBinding<Wrapped: Equatable>: Equatable {
   public var wrappedValue: Binding<Wrapped>

   public init(wrappedValue: Binding<Wrapped>) {
      self.wrappedValue = wrappedValue
   }

   public static func == (left: EquatableBinding<Wrapped>, right: EquatableBinding<Wrapped>) -> Bool {
      left.wrappedValue.wrappedValue == right.wrappedValue.wrappedValue
   }
}

@propertyWrapperの唯一の要件はwrappedValueプロパティです。モジュール化されたアプリケーション全体でこのラッパーを共有したかったため、publicイニシャライザも書く必要がありましたが、すべて直感的です。==関数はEquatableプロトコルの唯一の要件で、これも分かりやすいですね。

これで、すべてのBindingプロパティに@EquatableBindingを付けられるようになりました:

struct AppState: Equatable {
   // other properties

   @EquatableBinding<ConfigFile>
   var configFile: Binding<ConfigFile>
}

これだけです。AppState型は完全にEquatableになり、奇妙なドロップダウンの問題は解決され、副作用のリスクもありません。他のフレームワークの既存の型を新しいプロトコルに準拠させるのではなく、他のフレームワークが存在すら知らない新しい型を導入しただけなので、影響を受ける可能性がないのです。

ここから学べる教訓は、自分が所有していない型を自分が所有していないプロトコルに準拠させるべきではないということです。これをコンパイラ警告にするSwiftのプロポーザルもあります。なお、解決策が常にProperty Wrapperとは限りません。プロトコルに準拠する際に独自の型を含める方法はたくさんありますが、それを忘れないようにすることが大切です。

RemafoXとは? Xcodeと連携してアプリの翻訳を支援するネイティブMacアプリです。 開発時間を節約してローカライゼーションを簡単にするために、今すぐ入手してください。

この記事が参考になりましたか?BlueskyMastodonでフォローして、Swiftのヒントやインディー開発の最新情報をチェックしてください。