Swiftプロパティのラッパーにアプローチする方法

公開: 2022-03-11

簡単に言うと、プロパティラッパーは、プロパティへの読み取りおよび書き込みアクセスをカプセル化し、プロパティに追加の動作を追加する一般的な構造です。 使用可能なプロパティ値を制限する必要がある場合、読み取り/書き込みアクセスにロジックを追加する必要がある場合(データベースやユーザーのデフォルトを使用する場合など)、またはいくつかのメソッドを追加する必要がある場合に使用します。

Swift5.1のプロパティラッパー

この記事では、プロパティをラップするための新しいSwift 5.1アプローチについて説明します。これにより、新しい、よりクリーンな構文が導入されます。

古いアプローチ

アプリケーションを開発していて、ユーザープロファイルデータを含むオブジェクトがあるとします。

 struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)

電子メール検証を追加する必要があります。ユーザーの電子メールアドレスが有効でない場合、 emailプロパティはnilである必要があります。 これは、プロパティラッパーを使用してこのロジックをカプセル化するのに適したケースです。

 struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }

アカウント構造でこのラッパーを使用できます。

 struct Account { var firstName: String var lastName: String var email: Email<String> }

これで、emailプロパティには有効な電子メールアドレスのみを含めることができると確信しています。

構文を除いて、すべてが良さそうです。

 let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)

プロパティラッパーを使用すると、このようなプロパティの初期化、読み取り、および書き込みの構文がより複雑になります。 それで、この複雑さを回避し、構文を変更せずにプロパティラッパーを使用することは可能ですか? Swift 5.1では、答えはイエスです。

新しい方法:@propertyWrapperアノテーション

Swift 5.1は、プロパティラッパーを作成するためのより洗練されたソリューションを提供し、 @propertyWrapperアノテーションでプロパティラッパーをマークすることができます。 このようなラッパーは、従来のラッパーに比べて構文がコンパクトであるため、コードがよりコンパクトで理解しやすくなります。 @propertyWrapperアノテーションの要件は1つだけです。ラッパーオブジェクトには、 wrappedValueと呼ばれる非静的プロパティが含まれている必要があります。

 @propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }

このようなラップされたプロパティをコードで定義するには、新しい構文を使用する必要があります。

 @Email var email: String?

そのため、プロパティに注釈@を付けました。 。 プロパティタイプは、ラッパーの`wrappedValue`タイプと一致する必要があります。 これで、通常のプロパティと同じようにこのプロパティを操作できます。

 email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil

素晴らしいです。古いアプローチよりも見栄えが良くなりました。 ただし、ラッパーの実装には1つの欠点があります。それは、ラップされた値の初期値を許可しないことです。

 @Email var email: String? = "[email protected]" //compilation error.

これを解決するには、次の初期化子をラッパーに追加する必要があります。

 init(wrappedValue value: Value?) { self.value = value }

以上です。

 @Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil

ラッパーの最終的なコードは次のとおりです。

 @propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }

構成可能なラッパー

別の例を見てみましょう。 あなたはゲームを書いていて、ユーザーのスコアが保存されているプロパティがあります。 要件は、この値が0以上100以下である必要があることです。これは、プロパティラッパーを使用して実現できます。

 @propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0

このコードは機能しますが、一般的ではないようです。 異なる制約(0と100ではない)で再利用することはできません。 さらに、整数値のみを制約できます。 Comparableプロトコルに準拠する任意のタイプを制約できる構成可能なラッパーを1つ持つ方がよいでしょう。 ラッパーを構成可能にするには、初期化子を介してすべての構成パラメーターを追加する必要があります。 イニシャライザにwrappedValue属性(プロパティの初期値)が含まれている場合は、それが最初のパラメータである必要があります。

 @propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }

ラップされたプロパティを初期化するために、アノテーションの後に括弧で囲まれたすべての構成属性を定義します。

 @Constrained(0...100) var scores: Int = 0

構成属性の数は無制限です。 イニシャライザと同じ順序で括弧内に定義する必要があります。

ラッパー自体へのアクセスの取得

(ラップされた値ではなく)ラッパー自体にアクセスする必要がある場合は、プロパティ名の前にアンダースコアを追加する必要があります。 たとえば、アカウント構造を見てみましょう。

 struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)

追加した追加機能を使用するには、ラッパー自体にアクセスする必要があります。 たとえば、アカウント構造をEquatableプロトコルに準拠させる必要があります。 電子メールアドレスが等しい場合、2つのアカウントは等しく、電子メールアドレスでは大文字と小文字が区別されない必要があります。

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }

これは機能しますが、電子メールを比較する場合は常に小文字の()メソッドを追加する必要があるため、最善の解決策ではありません。 より良い方法は、Eメール構造を同等にすることです。

 extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }

ラップされた値の代わりにラッパーを比較します。

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }

予測値

@propertyWrapperアノテーションは、もう1つのシンタックスシュガー(投影値)を提供します。 このプロパティには、任意のタイプを指定できます。 このプロパティにアクセスするには、プロパティ名に$プレフィックスを追加する必要があります。 それがどのように機能するかを説明するために、Combineフレームワークの例を使用します。

@Publishedプロパティラッパーは、プロパティのパブリッシャーを作成し、それを予測値として返します。

 @Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher

ご覧のとおり、ラップされたプロパティにアクセスするためにメッセージを使用し、パブリッシャーにアクセスするために$messageを使用します。 ラッパーに投影値を追加するにはどうすればよいですか? 特別なことは何もありません、ただそれを宣言してください。

 @propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }

前述のように、 projectedValueプロパティは、ニーズに基づいて任意のタイプにすることができます。

制限事項

新しいプロパティラッパーの構文は見栄えがしますが、いくつかの制限も含まれています。主な制限は次のとおりです。

  1. エラー処理には参加できません。 ラップされた値はプロパティ(メソッドではありません)であり、getterまたはsetterをthrowsとしてマークすることはできません。 たとえば、 Emailの例では、ユーザーが無効なEメールを設定しようとしてもエラーをスローすることはできません。 nilを返すか、 fatalError()呼び出しでアプリをクラッシュさせることができますが、これは場合によっては受け入れられない可能性があります。
  2. プロパティに複数のラッパーを適用することは許可されていません。 たとえば、 @Emailラッパーの大文字と小文字を区別しないようにするのではなく、別の@Emailラッパーを用意し、それを@CaseInsensitiveラッパーと組み合わせる方がよいでしょう。 ただし、このような構造は禁止されており、コンパイルエラーが発生します。
 @CaseInsensitive @Email var email: String?

この特定のケースの回避策として、 CaseInsensitiveラッパーからEmailラッパーを継承できます。 ただし、継承にも制限があります。継承をサポートするのはクラスのみであり、許可される基本クラスは1つだけです。

結論

@propertyWrapperアノテーションは、プロパティラッパーの構文を単純化し、通常のプロパティと同じようにラップされたプロパティを操作できます。 これにより、Swift開発者としてのコードがよりコンパクトで理解しやすくなります。 同時に、考慮しなければならないいくつかの制限があります。 それらのいくつかが将来のSwiftバージョンで修正されることを願っています。

Swiftのプロパティについて詳しく知りたい場合は、公式ドキュメントをご覧ください。