SwiftUIを使っていると、@State
や@Binding
といった@
マークが付いたプロパティを頻繁に見かけます。これらは**プロパティラッパー(Property Wrapper)**と呼ばれるSwiftの強力な機能です。
この記事では、Swift初心者の方に向けて、プロパティラッパーの基本から自作する方法まで、わかりやすく解説します。
プロパティラッパーとは?
プロパティラッパーは、プロパティに対して共通の処理やロジックを簡単に追加できるSwift 5.1で導入された機能です。
簡単に言うと
プロパティに@
マークを付けるだけで、値の取得や設定時に自動的に特定の処理を実行させることができます。これにより、同じようなコードを何度も書く必要がなくなります。
対応環境
- Swift 5.1以降
- iOS 13.0以降
- macOS 10.15以降
既に使っているプロパティラッパー
実は、SwiftUIやCombineを使っている方は、既にプロパティラッパーを使っています。
import SwiftUI
struct CounterView: View {
@State private var count = 0 // プロパティラッパー
@Binding var isEnabled: Bool // プロパティラッパー
@Environment(\.colorScheme) var scheme // プロパティラッパー
var body: some View {
Text("Count: \(count)")
}
}
よく使われるプロパティラッパーの例:
プロパティラッパー | 用途 | フレームワーク |
---|---|---|
@State | ビューの状態管理 | SwiftUI |
@Binding | 親子ビュー間のデータ共有 | SwiftUI |
@Published | 値の変更を通知 | Combine |
@Query | データベースからデータ取得 | SwiftData |
@Environment | 環境値の取得 | SwiftUI |
これらはすべてAppleが提供しているプロパティラッパーです。
なぜプロパティラッパーが必要なのか?
プロパティラッパーがない場合、同じような処理を何度も書く必要があります。
プロパティラッパーを使わない場合
例えば、ゲームキャラクターの体力を0以上に制限したい場合:
class Player {
private var _health: Int = 100
var health: Int {
get { _health }
set {
// 値を0以上に制限する処理
if newValue < 0 {
_health = 0
} else {
_health = newValue
}
}
}
private var _mana: Int = 50
var mana: Int {
get { _mana }
set {
// 同じ処理をまた書く必要がある
if newValue < 0 {
_mana = 0
} else {
_mana = newValue
}
}
}
}
この方法には以下の問題があります:
- コードの重複 – 同じロジックを何度も書く
- 保守性の低下 – ロジックを変更する際、全ての箇所を修正する必要がある
- 可読性の低下 – コードが冗長になる
プロパティラッパーを使う場合
同じ処理をプロパティラッパーとして定義すれば、簡単に再利用できます。
@propertyWrapper
struct NonNegative {
private var value: Int
var wrappedValue: Int {
get { value }
set { value = max(0, newValue) }
}
init(wrappedValue: Int) {
self.value = max(0, wrappedValue)
}
}
// 使用例:@マークを付けるだけ
class Player {
@NonNegative var health: Int = 100
@NonNegative var mana: Int = 50
@NonNegative var stamina: Int = 80
}
var player = Player()
player.health = -10 // 自動的に0になる
print(player.health) // 出力: 0
player.mana = 30
print(player.mana) // 出力: 30
メリット:
- コードがシンプルで読みやすい
- ロジックが一箇所に集約されている
- 再利用が簡単
プロパティラッパーの基本的な作り方
プロパティラッパーは、以下の手順で作成します。
ステップ1: @propertyWrapper属性を付ける
@propertyWrapper
struct YourWrapper {
// ここに実装
}
ステップ2: wrappedValueプロパティを定義する
wrappedValue
は、実際にラップされる値を表します。
@propertyWrapper
struct Uppercase {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.uppercased() }
}
init(wrappedValue: String) {
self.text = wrappedValue.uppercased()
}
}
ステップ3: 使用する
struct User {
@Uppercase var name: String = "taro"
}
var user = User()
print(user.name) // 出力: TARO
user.name = "hanako"
print(user.name) // 出力: HANAKO
プロパティラッパーの実践的な例
例1: UserDefaultsへの自動保存
アプリの設定を自動的にUserDefaultsに保存するプロパティラッパーです。
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
// 使用例
class AppSettings {
@UserDefault(key: "username", defaultValue: "ゲスト")
var username: String
@UserDefault(key: "isDarkMode", defaultValue: false)
var isDarkMode: Bool
@UserDefault(key: "fontSize", defaultValue: 14)
var fontSize: Int
}
let settings = AppSettings()
settings.username = "太郎" // 自動的にUserDefaultsに保存される
print(settings.username) // 出力: 太郎
メリット:
- UserDefaultsへの保存・読み込みコードを書く必要がない
- 型安全にデータを扱える
- デフォルト値を簡単に設定できる
例2: 値の範囲制限(Clamped)
値を指定した範囲内に収めるプロパティラッパーです。
@propertyWrapper
struct Clamped<T: Comparable> {
private var value: T
let range: ClosedRange<T>
var wrappedValue: T {
get { value }
set {
value = min(max(newValue, range.lowerBound), range.upperBound)
}
}
init(wrappedValue: T, _ range: ClosedRange<T>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
// 使用例
struct GameSettings {
@Clamped(0...100) var volume: Int = 50
@Clamped(0...100) var brightness: Int = 80
@Clamped(0.5...2.0) var gameSpeed: Double = 1.0
}
var settings = GameSettings()
settings.volume = 150 // 自動的に100になる
print(settings.volume) // 出力: 100
settings.volume = -10 // 自動的に0になる
print(settings.volume) // 出力: 0
settings.gameSpeed = 3.0 // 自動的に2.0になる
print(settings.gameSpeed) // 出力: 2.0
例3: 値の検証(Validated)
メールアドレスやURLなど、特定の形式に従っているかを検証します。
@propertyWrapper
struct Validated<T> {
private var value: T
private let validator: (T) -> Bool
var wrappedValue: T {
get { value }
set {
if validator(newValue) {
value = newValue
} else {
print("警告: 無効な値が設定されようとしました")
}
}
}
init(wrappedValue: T, validator: @escaping (T) -> Bool) {
self.validator = validator
if validator(wrappedValue) {
self.value = wrappedValue
} else {
fatalError("初期値が無効です")
}
}
}
// 使用例
struct ContactForm {
@Validated(validator: { $0.contains("@") && $0.contains(".") })
var email: String = "user@example.com"
@Validated(validator: { $0.count >= 8 })
var password: String = "password123"
}
var form = ContactForm()
form.email = "invalid-email" // 警告が表示され、値は変更されない
print(form.email) // 出力: user@example.com(元の値のまま)
form.email = "new@example.com" // 有効な値なので変更される
print(form.email) // 出力: new@example.com
例4: 遅延初期化(Lazy)
値が必要になるまで初期化を遅らせるプロパティラッパーです。
@propertyWrapper
struct LazyProperty<T> {
private var value: T?
private let initializer: () -> T
var wrappedValue: T {
mutating get {
if value == nil {
value = initializer()
print("値を初期化しました")
}
return value!
}
set {
value = newValue
}
}
init(wrappedValue initializer: @escaping () -> T) {
self.initializer = initializer
}
}
// 使用例
struct DataProcessor {
@LazyProperty var heavyData: [Int] = {
print("重いデータを読み込んでいます...")
return Array(1...1_000_000)
}
}
var processor = DataProcessor()
print("プロセッサを作成しました")
// この時点では heavyData はまだ初期化されていない
print(processor.heavyData.count)
// 出力:
// 重いデータを読み込んでいます...
// 値を初期化しました
// 1000000
例5: トリミング(Trimmed)
文字列の前後の空白を自動的に削除します。
@propertyWrapper
struct Trimmed {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.value = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
// 使用例
struct UserInput {
@Trimmed var username: String = ""
@Trimmed var comment: String = ""
}
var input = UserInput()
input.username = " 太郎 "
print("[\(input.username)]") // 出力: [太郎]
input.comment = "\n こんにちは \n"
print("[\(input.comment)]") // 出力: [こんにちは]
プロパティラッパーの高度な機能
projectedValueを使う
projectedValue
を使うと、ラップされた値とは別の値を提供できます。$
記号でアクセスします。
@propertyWrapper
struct Capitalized {
private var value: String
var wrappedValue: String {
get { value.capitalized }
set { value = newValue }
}
// 元の値(変換前)にアクセスできる
var projectedValue: String {
return value
}
init(wrappedValue: String) {
self.value = wrappedValue
}
}
// 使用例
struct Article {
@Capitalized var title: String = "swift programming"
}
var article = Article()
print(article.title) // 出力: Swift Programming(変換後)
print(article.$title) // 出力: swift programming(元の値)
article.title = "ios development"
print(article.title) // 出力: Ios Development
print(article.$title) // 出力: ios development
SwiftUIでの実際の使用例
SwiftUIの@State
も、内部的にはprojectedValue
を使っています。
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
// wrappedValueを使用(通常のアクセス)
Text("入力: \(text)")
// projectedValueを使用(Bindingを取得)
TextField("入力してください", text: $text)
}
}
}
$text
とすることで、Binding<String>
型の値を取得できます。
プロパティラッパーの組み合わせ
複数のプロパティラッパーを組み合わせて使うこともできます。
@propertyWrapper
struct Uppercase {
var wrappedValue: String {
didSet { wrappedValue = wrappedValue.uppercased() }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.uppercased()
}
}
@propertyWrapper
struct Trimmed {
var wrappedValue: String {
didSet { wrappedValue = wrappedValue.trimmingCharacters(in: .whitespaces) }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.trimmingCharacters(in: .whitespaces)
}
}
// 複数のプロパティラッパーを適用
struct User {
@Trimmed @Uppercase var name: String = ""
}
var user = User()
user.name = " taro yamada "
print("[\(user.name)]") // 出力: [TARO YAMADA]
注意: プロパティラッパーは外側から内側に向かって適用されます。上記の例では、まず@Trimmed
が適用され、その後@Uppercase
が適用されます。
よくある質問とトラブルシューティング
Q1: プロパティラッパーはどこで定義すべき?
A: 再利用性を考えて、別ファイルに定義することをおすすめします。
Project/
├── PropertyWrappers/
│ ├── UserDefault.swift
│ ├── Clamped.swift
│ └── Validated.swift
└── Models/
└── User.swift
Q2: structとclassのどちらで定義すべき?
A: 通常はstruct
で定義します。プロパティラッパーは値型であることが一般的です。
// 推奨
@propertyWrapper
struct MyWrapper {
// ...
}
// 参照の共有が必要な特殊なケースのみ
@propertyWrapper
class SharedWrapper {
// ...
}
Q3: プロパティラッパーで計算プロパティは使える?
A: いいえ、プロパティラッパーは格納プロパティにのみ使用できます。
struct Example {
@Clamped(0...100) var value: Int = 50 // OK
// エラー: 計算プロパティには使えない
// @Clamped(0...100) var computed: Int {
// return value * 2
// }
}
Q4: プロパティラッパーのパフォーマンスは?
A: 適切に設計されていれば、パフォーマンスへの影響は最小限です。ただし、頻繁に呼ばれるgetterやsetterで重い処理を行うのは避けましょう。
// 悪い例:重い処理
@propertyWrapper
struct SlowWrapper {
var wrappedValue: Int {
get {
// 重い計算処理
Thread.sleep(forTimeInterval: 0.1)
return value
}
set { value = newValue }
}
private var value: Int
}
// 良い例:軽量な処理
@propertyWrapper
struct FastWrapper {
var wrappedValue: Int {
get { value }
set { value = max(0, newValue) } // シンプルな検証のみ
}
private var value: Int
}
ベストプラクティス
1. 単一責任の原則
一つのプロパティラッパーは一つの責任だけを持つようにしましょう。
// 良い例:責任が明確
@propertyWrapper
struct Trimmed {
// トリミングのみを行う
}
@propertyWrapper
struct Uppercase {
// 大文字変換のみを行う
}
// 悪い例:複数の責任を持つ
@propertyWrapper
struct TrimmedAndUppercased {
// トリミングと大文字変換の両方を行う
// → 組み合わせで対応すべき
}
2. わかりやすい命名
プロパティラッパーの名前は、その動作が明確にわかるようにしましょう。
// 良い例
@propertyWrapper struct Clamped { }
@propertyWrapper struct NonNegative { }
@propertyWrapper struct Trimmed { }
// 悪い例
@propertyWrapper struct PW1 { }
@propertyWrapper struct Helper { }
3. ドキュメントを書く
プロパティラッパーの動作を説明するコメントを追加しましょう。
/// 値を指定した範囲内に制限するプロパティラッパー
///
/// 設定された値が範囲外の場合、自動的に範囲内の最も近い値に調整されます。
///
/// 使用例:
/// ```
/// @Clamped(0...100) var percentage: Int = 50
/// ```
@propertyWrapper
struct Clamped<T: Comparable> {
// 実装
}
4. テストを書く
プロパティラッパーのロジックが正しく動作するか、テストを書きましょう。
import XCTest
class ClampedTests: XCTestCase {
func testClampedUpperBound() {
struct Test {
@Clamped(0...100) var value: Int = 50
}
var test = Test()
test.value = 150
XCTAssertEqual(test.value, 100)
}
func testClampedLowerBound() {
struct Test {
@Clamped(0...100) var value: Int = 50
}
var test = Test()
test.value = -10
XCTAssertEqual(test.value, 0)
}
}
プロパティラッパーを使う場面・使わない場面
使うべき場面
✅ 同じロジックを複数のプロパティで使う場合 ✅ 値の検証や変換が必要な場合 ✅ UserDefaultsやKeychainなど、外部ストレージへのアクセスをラップする場合 ✅ コードの可読性を向上させたい場合
使わないほうがよい場面
❌ 一度しか使わないロジックの場合(通常のgetterとsetterで十分) ❌ 非常に複雑なロジックの場合(別のクラスやメソッドで実装すべき) ❌ パフォーマンスがクリティカルな処理の場合
まとめ
プロパティラッパーは、Swiftのコードをより簡潔で保守しやすくする強力な機能です。
ポイントのおさらい:
- プロパティラッパーは
@
マークで使用する便利な機能 @State
や@Binding
など、既に多くの場面で使われている- 自作することで、共通のロジックを簡単に再利用できる
@propertyWrapper
属性とwrappedValue
で定義する- UserDefaults、値の検証、範囲制限など、実用的な用途が多い
初心者の方は、まずSwiftUIで提供されている@State
や@Binding
を使いこなすことから始めて、徐々に自作のプロパティラッパーにも挑戦してみてください。
プロパティラッパーをマスターすれば、より読みやすく保守しやすいSwiftコードが書けるようになります!
参考リンク
- Apple公式ドキュメント – Properties
- Swift Evolution – SE-0258 Property Wrappers
- WWDC19 – Data Flow Through SwiftUI