この記事ではSwift5.1で導入された Property Wrapper について解説していきます。
Property Wrapper を使用すると、プロパティの「振る舞いを変更・追加し、簡単に再利用」できるようになります
この記事では「Property Wrapper 導入の背景」や「Property Wrapper の概要」、実際に Property Wrapper を使用した実装例について解説していきます。
- Property Wrapper とは
- Property Wrapper 導入の背景
- Property Wrapper の概要
- Property Wrapper を使用して UserDefaults に値を同期
- Property Wrapper を使用してバリデーションを実装
- Property Wrapper のその他 Tips
この記事は以下のドキュメント・プロポーザルをもとにしています
Property Wrapper とは
Property Wrapper 導入の背景
Property Wrapper とは「プロパティの振る舞いを変更・追加し、簡単に再利用」するための仕組みであると言えます。
Swiftで既に提供されている「プロパティの振る舞いを変更」するための機能としては lazy があります。
lazy を使用することで、プロパティを非 Optional として公開しつつ、遅延初期化を実現できます。
struct Foo {
lazy var foo = 1738
}
lazy を使用しないと以下のように多くのボイラープレートの記述が必要になります。
struct Foo {
private var _foo: Int?
var foo: Int {
get {
if let value = _foo { return value }
let initialValue = 1738
_foo = initialValue
return initialValue
}
set {
_foo = newValue
}
}
}
このように lazy が提供されているおかげで開発者は少ない記述量で遅延初期化を実現できます。
しかし、遅延初期化以外にも便利なパターンはいくつかあります。
例えば、
- Copy-on-write
- 数値の範囲制限
- 文字列のトリミング
などが考えられます。
しかし、これらをすべて lazy と同じように Swift 自体に組み込むのは現実的ではありません。
そこで、開発者が lazy と同じような「プロパティの振る舞いを変更」する機能を実装できるように Property Wrapper が導入されました。
また、 Property Wrapper は一度実装すれば複数箇所で使い回すことができます。
ここからは実際に Property Wrapper の概要を見ていきましょう。
Property Wrapper の概要
ここからは以下の2つの Prperty Wrapper の使用例を確認して、Property Wrapper の具体的なイメージを掴んでいきましょう。
- UserDefaults に値を同期する User Default Property Wrapper
- プロパティのバリデーションを行う Validated Property Wrapper
UserDefault Property Wrapper
まずは、以下の仕様を満たす UserDefault Property Wrapper の例を見てみましょう。
- プロパティの取得時に UserDefaults から取得する
- プロパティへの保存時に UserDefaults に保存する
コードは以下のようになります。
class SettingsViewModel {
@UserDefault(\.theme, defaultValue: "light") var theme: String
@UserDefault(\.nickname, defaultValue: "ゲスト") var nickname: String
}
let settingsViewModel = SettingsViewModel()
settingsViewModel.theme // light
settingsViewModel.theme = "dark" // darkをUserDefaultsに保存
settingsViewModel.theme // dark
settingsViewModel.nickname // ゲスト
settingsViewModel.nickname = "swift" // swiftをUserDefaultsに保存
settingsViewModel.nickname // swift
※UserDefault の実装は、後にこの記事内で解説します。
この例では、独自に実装した UserDefault Property Wrapper を使用してプロパティの振る舞いを変更しています。
この UserDefault Property Wrapper では、以下のようなプロパティの取得時に UserDefaults から値を取得するように振る舞いを変更しています。
settingsViewModel.theme
また、以下のようなプロパティの設定時には UserDefaults に値を保存するように振る舞いを変更しています。
settingsViewModel.theme = "dark"
この例では SettingsViewModel の theme と nickname に対して UserDefault Property Wrapper を宣言していますが、もちろん SettingsViewModel の他のプロパティや、SettingsViewModel 以外のクラスに対しても使い回すことができます。
もう1つ例を見てみましょう。
Validated Property Wrapper
もう1つ、バリデーションを行うValidated Property Wrapper の例を見てみましょう。
コードは以下のようになります。
struct RegistrationForm {
@Validated({
$0.size(min: 4, max: 8)
})
var name = ""
@Validated({
$0.lessThanOrEqualTo(5)
})
var rank = 0
}
let form = RegistrationForm()
form.name = "hoge"
form.$name.isValid // true
form.rank = 6
form.$rank.isValid // false
※Validated の実装は、後にこの記事内で解説します。
この例ではプロパティ自体の振る舞いを変更していませんが、以下のようにプロパティに振る舞いを追加しています。
form.$name.isValid
isValidはプロパティにバリデーションを適用して有効であればtrue、有効でなければfalseを返します。
バリデーションは以下を行なっていて、Validated Property Wrapper の宣言時に引数で渡しています。
- 名前は4文字以上8文字以下であること
- ランクは5以下であること
Validated Property Wrapper も、RegistrationForm の他のプロパティや RegistrationForm 以外のクラスでも使い回すことができます。
ここからは実際に Property Wrapper の実装を見ていきましょう。
Property Wrapper を実装してみる
Property Wrapper は最低限、以下の手順で実装することができます。
- @propertyWrapperアノテーションを付与した「struct or class or enum」を宣言
- wrappedValueプロパティを宣言
簡単な例として
- 数値が必ず12以下になる
を実現する LessThanOrEqualTo Property Wrapper を見てみましょう。
LessThanOrEqualTo Property Wrapper の実装例
@propertyWrapper
struct LessThanOrEqualTo {
var wrappedValue: Int {
get {
value
}
set {
value = min(12, newValue)
}
}
private var value: Int
init(wrappedValue: Int) {
value = min(12, wrappedValue)
}
}
先ほども見たように Property Wrapper は @propertyWrapper アノテーションを付与した
- struct
- class
- enum
として宣言します。
※enum の場合は static プロパティにのみ適用可能です。
今回の例では struct で LessThanOrEqualTo Property Wrapper を宣言しています。
また、Property Wrapper は wrappedValue というプロパティを宣言する必要があります。
今回の例では LessThanOrEqualTo Property Wrapper を付与するプロパティは数値であることを想定しているので、wrappedValue は Int として宣言しています。
var wrappedValue: Int {
get {
value
}
set {
value = min(12, newValue)
}
}
Property Wrapper とはで見たような「プロパティの振る舞いの変更」はこの wrappedValue で行います。
例えば今回の例では LessThanOrEqualTo Property Wrapper を付与したプロパティの取得時には、LessThanOrEqualTo Property Wrapper で保持している value をそのまま返しています。
また設定時には、min を使用して、LessThanOrEqualTo Property Wrapper が保持している value に12以下の値を設定しています。
実際にこのLessThanOrEqualTo Property Wrapper の使用例を見てみましょう。
struct Counter {
@LessThanOrEqualTo var count = 0
}
var counter = Counter()
counter.count = 6
counter.count // 6
counter.count = 16
counter.count // 12
このように LessThanOrEqualTo Property Wrapper を付与したプロパティは値が12以下になるように調整されます。
Property Wrapper を使用する場合は、 Property Wrapper に「@」を付けてプロパティに付与します。
@LessThanOrEqualTo var count = 0
このように Property Wrapper を付与したプロパティへのアクセスは、 Property Wrapper の wrappedValue を介して行われます。
まずは取得時を見てみましょう。
counter.count
このコードでは、LessThanOrEqualTo Property Wrapper の wrappedValue の get が呼び出されます。
LessThanOrEqualTo Property Wrapper の wrappedValue は get 時には値をそのまま返していたため、現在の count の値がそのまま取得できます。
次に設定時を見てみましょう。
counter.count = 6
このコードでは LessThanOrEqualTo Property Wrapper の wrappedValue の set が呼び出されます。
LessThanOrEqualTo Property Wrapper の wrappedValue の set では値を12以下に制限していたため、
counter.count = 16
のように12より大きい値を設定すると、12に調整されます。
counter.count = 16
counter.count // 12
Property Wrapper の初期値
先ほどのLessThanOrEqualTo Property Wrapper を使用していた Counter では以下のように LessThanOrEqualTo Property Wrapper を付与したプロパティに初期値を設定していました。
struct Counter {
@LessThanOrEqualTo var count = 0
}
このように Property Wrapper を付与したプロパティに初期値を設定すると Property Wrapper の以下の条件を満たしたイニシャライザが呼ばれます。
- 第1引数の外部引数名が wrappedValue
- 引数の型が wrappedValue と同じ
コードでは以下の部分になります。
init(wrappedValue: Int) {
value = min(12, wrappedValue)
}
また、以下のようにプロパティの初期値を省略した場合には
struct Counter {
@LessThanOrEqualTo var count: Int
}
以下のような引数を持たないイニシャライザが呼ばれるため、初期値のデフォルト値設定などに使用できます。
init() {
value = 0
}
Property Wrapper に引数を渡す
次は Property Wrapper に引数を渡す方法を見ていきます。
先ほど見たLessThanOrEqualTo Property Wrapper では「値が12以下になる」という仕様でしたが、この「12」をプロパティごとに設定できるように修正してみます。
@propertyWrapper
struct LessThanOrEqualTo {
var wrappedValue: Int {
get {
value
}
set {
//*******************************
// 12をmaxValueに変更
//*******************************
value = min(maxValue, newValue)
}
}
private var value: Int
//*******************************
// maxValueプロパティを追加
//*******************************
private var maxValue: Int
//*******************************
// 引数に
// maxValue: Int
// を追加
//*******************************
init(wrappedValue: Int, maxValue: Int) {
value = min(maxValue, wrappedValue)
self.maxValue = maxValue
}
}
修正版のLessThanOrEqualTo Property Wrapper には以下の変更を加えています。
- min 関数の第一引数を maxValue に変更
- maxValue プロパティを追加
- イニシャライザに maxValue: Int を追加
修正版の LessThanOrEqualTo Property Wrapper の使用例は以下のようになります。
struct Counter {
@LessThanOrEqualTo(maxValue: 16) var count = 0
}
var counter = Counter()
counter.count = 16
counter.count // 16
counter.count = 22
counter.count // 16
1つ1つ見ていきましょう。
Property Wrapper への引数はプロパティ宣言時に以下のように渡すことができます。
@LessThanOrEqualTo(maxValue: 16) var count = 0
このように渡した引数は Property Wrapper の対応するイニシャライザで受け取ることができます。
LessThanOrEqualTo Property Wrapper の場合は以下の部分です。
init(wrappedValue: Int, maxValue: Int) {
value = min(maxValue, wrappedValue)
self.maxValue = maxValue
}
ここでは引き続き初期値を wrappedValue で渡せていますが、初期値を宣言している場合には自動的にイニシャライザの wrappedValue という外部引数名の引数に割り当ててくれます。
これで LessThanOrEqualTo Property Wrapper に引数で maxValue として16を渡すことができました。
この maxValue は以下のように wrappedValue の set で使用しているため、
var wrappedValue: Int {
get {
value
}
set {
value = min(maxValue, newValue)
}
}
以下のように16より大きい値が16に調整されるようになります。
var counter = Counter()
counter.count = 16
counter.count // 16
counter.count = 22
counter.count // 16
ここまではプロパティの振る舞いを変更する方法を見てきましたが、ここからはプロパティに振る舞いを追加する方法を見ていきます。
プロパティに振る舞いを追加する
Property Wrapper は projectedValue という名前のプロパティを宣言することで対象のプロパティに振る舞いを追加することができます。
プロパティに振る舞いを追加するとValidated Property Wrapper で見たように追加の情報をエクスポートすることができます。
実際に先ほどまで見てきた LessThanOrEqualTo Property Wrapper に projectedValue を実装する例を見てみましょう。
以下の内容を実装していきます。
- projectedValue の型は LessThanOrEqualTo 自身にする
- 値が min によって変更されたかどうかを保持する isAdjusted を追加する
コードは以下のようになります。
@propertyWrapper
struct LessThanOrEqualTo {
var wrappedValue: Int {
get {
value
}
set {
//******************************
// isAdjustedを設定
//******************************
isAdjusted = maxValue < newValue
value = min(maxValue, newValue)
}
}
//******************************
// projectedValueを追加
//******************************
var projectedValue: LessThanOrEqualTo {
self
}
//******************************
// isAdjustedを追加
//******************************
private(set) var isAdjusted: Bool
private var value: Int
private var maxValue: Int
init(wrappedValue: Int, maxValue: Int) {
//******************************
// isAdjustedを設定
//******************************
isAdjusted = maxValue < wrappedValue
value = min(maxValue, wrappedValue)
self.maxValue = maxValue
}
}
この例では以下のように projectedValue を宣言しています。
var projectedValue: LessThanOrEqualTo {
self
}
また以下のように、minによって値が変更されたかどうかを isAdjusted に保持しています。
isAdjusted = maxValue < newValue
projectedValue は変数名の最初に$を付けることでアクセスできます。
var counter = Counter()
counter.count = 16
counter.count
counter.$count.isAdjusted // false
counter.count = 22
counter.count
counter.$count.isAdjusted // true
先ほど projectedValue からは self を return していたので、以下のように
counter.$count
とすると、LessThanOrEqualTo Property Wrapper のインスタンスを取得できます。
あとは LessThanOrEqualTo Property Wrapper のプロパティやメソッドに自由にアクセスできるので、以下のように値が変更されたかどうかを isAdjusted から判断できます。
counter.$count.isAdjusted
もし他に追加でエクスポートしたい情報があれば、isAdjusted と同じようにプロパティやメソッドを実装すれば追加することができるため、かなり自由度が高くなっています。
また、今回は projectedValue の型を LessThanOrEqualTo 自身にして、self を return していますが、projectedValue には任意の型を指定できます。
例えば、以下のように projectedValue の型を Bool にして isAdjusted を直接エクスポートすることもできます。
var projectedValue: Bool {
isAdjusted
}
ただしこの場合は、以下のように projectedValue を使用する側で何を取得しているのか分かりづらくなるため、基本的にはプリミティブではなく struct や class などを指定することをお勧めします。
counter.$count // Boolかどうか分かりづらい
ここまでは簡単な例を通して Property Wrapper の基本的な使い方を見てきました。
ここからはもう少し実践的な例として UserDefault Property Wrapper と Validated Property Wrapper の実装例を見ていきましょう。
UserDefault Property Wrapper の実装例
UserDefault Property Wrapper は UserDefault Property Wrapper でも見たように以下の仕様を満たす Property Wrapper です。
- プロパティの取得時に UserDefaults から取得する
- プロパティへの保存時に UserDefaults に保存する
コードは以下のようになっています。
@propertyWrapper
struct UserDefault<Value> {
var wrappedValue: Value {
get {
userDefaults.object(forKey: UserDefaultKey[key]) as? Value ?? defaultValue
}
set {
userDefaults.set(newValue, forKey: UserDefaultKey[key])
}
}
private let key: KeyPath<UserDefaultKey, String>
private let defaultValue: Value
private let userDefaults: UserDefaults
init(_ key: KeyPath<UserDefaultKey, String>, defaultValue: Value, userDefaults: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
}
}
※UserDefaultKey については後述します。
UserDefault Property Wrapper は以下を引数で受け取れるようにしています。
- UserDefaults のキー
- デフォルト値
- UserDefaults のインスタンス
コードは以下のようになっています。
init(_ key: KeyPath<UserDefaultKey, String>, defaultValue: Value, userDefaults: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
}
UserDefault Property Wrapper の wrappedValue は以下のように動作します。
- 取得時には指定された UserDefaults から指定されたキーの値を返し、なければデフォルト値を返す
- 設定時には指定された UserDefaults の指定されたキーに値を保存する
コードは以下のようになっています。
var wrappedValue: Value {
get {
userDefaults.object(forKey: UserDefaultKey[key]) as? Value ?? defaultValue
}
set {
userDefaults.set(newValue, forKey: UserDefaultKey[key])
}
}
ここまでで、UserDefaultKey という独自に実装した構造体が使われていますが、この構造体を使用することで UserDefaults のキーを文字列で指定する必要をなくし、typo を防いでいます。
今回の記事の主題とはずれるため、実装のみの紹介になりますが、UserDefaults のキーを文字列ではなく\.themeのように指定することができ、コンパイラによるチェックができるようになるためお勧めです。
struct UserDefaultKey {
static subscript(keyPath: KeyPath<UserDefaultKey, String>) -> String {
shared[keyPath: keyPath]
}
private static let shared = UserDefaultKey()
var theme = "theme"
var nickname = "nickname"
}
UserDefault Property Wrapper の具体的な使用例は UserDefault Property Wrapper をご覧ください。
Validated Property Wrapper の実装例
Validated Property Wrapper はValidated Property Wrapper でも見たようにバリデーションを行う Property Wrapper です。
コードは以下のようになっています。
@propertyWrapper
class Validated<Value> {
var wrappedValue: Value {
get {
value
}
set {
value = newValue
}
}
var projectedValue: Validated {
self
}
var isValid: Bool {
validators.allSatisfy { $0(value) }
}
private var value: Value
private var validators = [(Value) -> Bool]()
init(wrappedValue: Value, _ validate: (Validated) -> Void) {
self.value = wrappedValue
validate(self)
}
}
extension Validated where Value: Comparable {
@discardableResult
func lessThanOrEqualTo(_ rhs: Value) -> Validated {
validators.append { $0 <= rhs }
return self
}
}
extension Validated where Value: Collection {
@discardableResult
func size(min: Int, max: Int) -> Validated {
validators.append { $0.count <= max }
return self
}
}
Validated Property Wrapper ではプロパティの振る舞い自体は変更していませんが、projectedValue を使用して、振る舞いを追加しています。
wrappedValue は取得時は値をそのまま return し、設定時は値をそのままセットしています。
var wrappedValue: Value {
get {
value
}
set {
value = newValue
}
}
projectedValue は Validated Property Wrapper 自体をreturnしています。
var projectedValue: Validated {
self
}
Validated Property Wrapper はこの他に isValid 計算プロパティを定義しています。
Validated Property Wrapper を使用する側はこの isValid を通してバリデーション結果を取得します。
var isValid: Bool {
validators.allSatisfy { $0(value) }
}
isValid は validators に allSatisfy を適用して、全てのバリデーションが true を返すことを検証しています。
validators は Validated Property Wrapper のイニシャライザで初期化しているため、引数で受け取ることができます。
イニシャライザは以下のようになっています。
init(wrappedValue: Value, _ validate: (Validated) -> Void) {
self.value = wrappedValue
validate(self)
}
validate にバリデーション用のクロージャを受け取ります。
Validated Property Wrapper を使用する側は以下のように引数でバリデーションを渡すことができます。
struct RegistrationForm {
@Validated({
$0.size(min: 4, max: 8)
})
var name = ""
@Validated({
$0.lessThanOrEqualTo(5)
})
var rank = 0
}
バリデーション用のメソッドは、プロパティの型によって必要なものを extension で Validated Property Wrapper に追加しています。
extension Validated where Value: Comparable {
@discardableResult
func lessThanOrEqualTo(_ rhs: Value) -> Validated {
validators.append { $0 <= rhs }
return self
}
}
extension Validated where Value: Collection {
@discardableResult
func size(min: Int, max: Int) -> Validated {
validators.append { $0.count <= max }
return self
}
}
この例ではプロパティの型が Comparable なら lessThanOrEqualTo を、Collection なら size を追加しています。
Validated Property Wrapper の具体的な使用例は Validated Property Wrapper をご覧ください。
Property Wrapper のその他Tips
Property Wrapper を直接使用する
Property Wrapper は宣言したクラスや構造体内からであれば直接使用することができます。
例えば以下のように Property Wrapper を付与したプロパティを宣言した場合、
class SomeClass {
@SomeWrapper var someProperty = ""
}
実際にはコンパイラによって以下のように展開されます。
class SomeClass {
// 「_」が付いたプロパティが生成される
private var _someProperty = SomeWrapper()
var someProperty: String {
get {
_someProperty.wrappedValue
}
set {
_someProperty.wrappedValue = newValue
}
}
}
このように「_」が付いたプロパティが生成されるので、以下のようにして Property Wrapper を直接使用することができます。
class SomeClass {
@SomeWrapper var someProperty: String
init() {
_someProperty.someFunction()
}
}
ただし、アクセスレベルが private になっているのでクラスや構造体の外からアクセスすることはできません。
let someClass = SomeClass()
someClass._someProperty.someFunction() // '_someProperty' is inaccessible due to 'private' protection level
Property Wrapper を複数適用する
今回の記事では主に Property Wrapper が1つの場合を見てきましたが、 Property Wrapper は1つだけではなく複数適用することができます。
例えば、以下のように Pretix Property Wrapper と Suffix Proeprty Wrapper を1つのプロパティに対して適用する場合を考えます。
struct Cat {
@Prefix("PREFIX_") @Suffix("_SUFFIX") var name = "tama"
}
この場合、実際のコードは以下のように展開されます。
private var _name: Prefix<Suffix<String>> = .init()
var name: String {
get { return _name.wrappedValue.wrappedValue }
set { _name.wrappedValue.wrappedValue = newValue }
}
このように複数の Property Wrapper を入れ子にした場合は最初に付与した Property Wrapper から順に入れ子になっていきます。
Property Wrapper が入れ子になるため、前の Property Wrapper の wrappedValue が次の Property Wrapper を受け取れる必要があります。
例えば
- A の wrappedValue は B
- B の wrappedValue は Int
の場合、
@A @B var value = 0
は宣言可能ですが、
@B @A var value = 0
はコンパイルエラーになります。
このように Property Wrapper を複数適用するのはなかなか使いづらいです。
例えば冒頭の Prefix と Suffix の例でも以下のようにコードは煩雑になります。
protocol StringAppendable {
static func +(_ lhs: Self, _ rhs: String) -> Self
static func +(_ rhs: String, _ lhs: Self) -> Self
}
@propertyWrapper
struct Prefix<Value: StringAppendable>: StringAppendable {
static func +(_ lhs: Prefix, _ rhs: String) -> Prefix {
Prefix(wrappedValue: lhs.value + rhs, lhs.prefix)
}
static func +(_ lhs: String, _ rhs: Prefix) -> Prefix {
Prefix(wrappedValue: lhs + rhs.value, rhs.prefix)
}
var wrappedValue: Value {
get {
value
}
set {
value = prefix + newValue
}
}
private var value: Value
private var prefix: String
init(wrappedValue: Value, _ prefix: String) {
self.value = prefix + wrappedValue
self.prefix = prefix
}
}
@propertyWrapper
struct Suffix<Value: StringAppendable>: StringAppendable {
static func +(_ lhs: Suffix, _ rhs: String) -> Suffix {
Suffix(wrappedValue: lhs.value + rhs, lhs.suffix)
}
static func +(_ lhs: String, _ rhs: Suffix) -> Suffix {
Suffix(wrappedValue: lhs + rhs.value, rhs.suffix)
}
var wrappedValue: Value {
get {
value
}
set {
value = newValue + suffix
}
}
private var value: Value
private var suffix: String
init(wrappedValue: Value, _ suffix: String) {
self.value = wrappedValue + suffix
self.suffix = suffix
}
}
extension String: StringAppendable {
}
そのため、Property Wrapper は可能な場合は複数に分けずに1つにまとめた方が扱いやすくなります。
例えば Prefix と Suffix は Affix にまとめるとより扱いやすくなります。
@propertyWrapper
struct Affix {
var wrappedValue: String {
get {
value
}
set {
value = prefix + newValue + suffix
}
}
private var value: String
private var prefix: String
private var suffix: String
init(wrappedValue: String, prefix: String, suffix: String) {
self.value = prefix + wrappedValue + suffix
self.prefix = prefix
self.suffix = suffix
}
}
struct Cat {
@Affix(prefix: "PREFIX_", suffix: "_SUFFIX") var name = "tama"
}
Property Wrapper の制限
Property Wrapper には以下の制限があります。
- Protocol 内での宣言
- extension 内での宣言
- enum 内での宣言
- サブクラスでのoverride
- lazy・@NSCopying・@NSManaged・weak・unownedとの併用
- 複数プロパティへの Property Wrapper の適用
- getter と setter の宣言
- wrappedValue のアクセスレベル
- projectedValue のアクセスレベル
- イニシャライザのアクセスレベル
1つ1つ見ていきましょう。
Protocol 内での宣言
Property Wrapper を Protocol 内で宣言することはできません。
protocol SomeProtocol {
@SomeWrapper var someProperty: String { get set } // property 'someProperty' declared inside a protocol cannot have a wrapper
}
extension 内での宣言
Property Wrapper を extension 内のインスタンスプロパティに対して宣言することはできません。
extension SomeClass {
@SomeWrapper var someProperty: String // Non-static property 'someProperty' declared inside an extension cannot have a wrapper
}
enum 内での宣言
Property Wrapper を enum 内のインスタンスプロパティに対して宣言することはできません。
enum SomeEnum {
@SomeWrapper var someProperty: String // Non-static property 'someProperty' declared inside an enum cannot have a wrapper
}
サブクラスでのoverride
Property Wrapper が付与されたプロパティをサブクラスで override することはできません。
class SomeChild: SomeParent {
@SomeWrapper override var someProperty: String // Property 'someProperty' with attached wrapper cannot override another property
}
lazy・@NSCopying・@NSManaged・weak・unownedとの併用
Property Wrapper を lazy・@NSCopying・@NSManaged・weak・unownedと併用することはできません。
class SomeClass {
@SomeWrapper weak var someProperty: OtherClass? // Property 'someProperty' with a wrapper cannot also be weak
}
複数プロパティへの Property Wrapper の適用
以下のように複数プロパティに Property Wrapper を適用することはできません。
class SomeClass {
@SomeWrapper var (x, y) = (1, 2)
}
getter と setter の宣言
Property Wrapper を付与したプロパティに getter と setter を付与することはできません。
class SomeClass {
@SomeWrapper var someProperty: String { // Property wrapper cannot be applied to a computed property
get {
value
}
set {
value = newValue
}
}
private var value = ""
}
wrappedValue のアクセスレベル
wrappedValue に Property Wrapper よりも制限されたアクセスレベルを指定することはできません。
@propertyWrapper
struct SomeWrapper {
private var wrappedValue: String { // Private property 'wrappedValue' cannot have more restrictive access than its enclosing property wrapper type 'SomeWrapper' (which is internal)
""
}
}
projectedValue のアクセスレベル
projectedValue に Property Wrapper よりも制限されたアクセスレベルを指定することはできません。
@propertyWrapper
struct SomeWrapper {
var wrappedValue: String {
""
}
private var projectedValue: String { // Private property 'projectedValue' cannot have more restrictive access than its enclosing property wrapper type 'SomeWrapper' (which is internal)
""
}
}
イニシャライザ のアクセスレベル
イニシャライザに Property Wrapper よりも制限されたアクセスレベルを指定することはできません。
@propertyWrapper
struct SomeWrapper {
var wrappedValue: String {
""
}
private init() { // Private initializer 'init()' cannot have more restrictive access than its enclosing property wrapper type 'SomeWrapper' (which is internal)
}
}
関数の引数に Property Wrapper を使用する
Property Wrapper は関数の引数にも使用できます。
以下の例では関数の引数に渡された Collection が空ではないことを Property Wrapper を使用して検証しています。
※コードはWWDC2021「What‘s new in Swift」からの抜粋です。
@propertyWrapper
struct NonEmpty<Value: Collection> {
var wrappedValue: Value {
willSet {
precondition(!newValue.isEmpty)
}
}
init(wrappedValue: Value) {
precondition(!wrappedValue.isEmpty)
self.wrappedValue = wrappedValue
}
}
func logIn(@NonEmpty _ username: String) {
print("Logging in: \(username)")
}
logIn("") // error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
この例では Collection が空ではない事を検証する NonEmpty を作成し、logIn 関数の username に適用しています。
logIn 関数の username に空文字が渡された場合、NonEmpty 内の precondition による検証が失敗してクラッシュします。
まとめ
この記事では Property Wrapper について解説してきました。
ここまで見てきたように Property Wrapper を使用すると lazy のようなプロパティの振る舞いの変更を実装することができます。
今回は UserDefaults とバリデーションの例を紹介しましたが、この他にも様々な便利な Property Wrapper を実装することができます。
例えば Property Wrapper を使用してDIを実装することもできます。
こちらはまた別の記事で紹介します。
それでは、ここまでご覧いただきありがとうございました。