MENU

Swiftのプロパティオブザーバー「didSet」を使いこなそう!

Swiftでアプリ開発をしていると、「プロパティの値が変わった時に何かしたい」という場面がよくあります。そんな時に活躍するのがdidSetです。今回は、didSetの基本から実践的な使い方まで、詳しく解説していきます!

目次

didSetとは?

didSetは、プロパティオブザーバーと呼ばれる機能の一つで、プロパティの値が変更された直後に自動的に実行されるコードブロックです。

基本的な書き方

var プロパティ名: 型 = 初期値 {
    didSet {
        // プロパティが変更された後に実行したい処理
    }
}

なぜdidSetが便利なの?

従来の方法とdidSetを使った方法を比較してみましょう。

didSetを使わない場合

class CounterViewController {
    private var _count: Int = 0
    
    var count: Int {
        get { return _count }
        set {
            _count = newValue
            updateCountLabel()  // 毎回手動で呼び出す必要がある
        }
    }
    
    private func updateCountLabel() {
        print("カウント: \(_count)")
    }
}

didSetを使った場合

class CounterViewController {
    var count: Int = 0 {
        didSet {
            updateCountLabel()  // 自動的に実行される
        }
    }
    
    private func updateCountLabel() {
        print("カウント: \(count)")
    }
}

結果: コードがシンプルになり、更新処理を忘れる心配がなくなりました!

didSetの重要な特徴

1. oldValueが使える

didSet内では、変更前の値にoldValueでアクセスできます。

class Temperature {
    var celsius: Double = 20.0 {
        didSet {
            let change = celsius - oldValue
            if change > 0 {
                print("温度が\(change)度上昇しました")
            } else if change < 0 {
                print("温度が\(abs(change))度下降しました")
            }
        }
    }
}

let temp = Temperature()
temp.celsius = 25.0  // "温度が5.0度上昇しました"
temp.celsius = 22.0  // "温度が3.0度下降しました"

2. 初期化時は実行されない

プロパティに初期値を設定する時や、初期化子(イニシャライザ)内での代入時には、didSetは呼ばれません。

class Example {
    var value: Int = 10 {  // この時点ではdidSetは呼ばれない
        didSet {
            print("値が変更されました: \(value)")
        }
    }
    
    init(initialValue: Int) {
        value = initialValue  // この代入でもdidSetは呼ばれない
    }
}

let example = Example(initialValue: 5)
example.value = 20  // この時だけ "値が変更されました: 20" が出力される

3. 自分自身の値も変更できる

didSet内で、そのプロパティ自体の値を変更することも可能です(ただし注意が必要)。

class BoundedValue {
    var value: Int = 0 {
        didSet {
            if value < 0 {
                print("負の値は許可されていません。0に設定します。")
                value = 0  // 再帰的にdidSetが呼ばれることはない
            } else if value > 100 {
                print("100を超える値は許可されていません。100に設定します。")
                value = 100
            }
        }
    }
}

let bounded = BoundedValue()
bounded.value = -5   // "負の値は許可されていません。0に設定します。"
bounded.value = 150  // "100を超える値は許可されていません。100に設定します。"

実践的な使用例

1. UIの自動更新

class ProgressView {
    var progress: Float = 0.0 {
        didSet {
            updateProgressBar()
            updateProgressLabel()
        }
    }
    
    private func updateProgressBar() {
        // プログレスバーのUI更新
        print("プログレスバー更新: \(Int(progress * 100))%")
    }
    
    private func updateProgressLabel() {
        // ラベルのテキスト更新
        print("進行状況: \(Int(progress * 100))%完了")
    }
}

let progressView = ProgressView()
progressView.progress = 0.3  // UI が自動的に更新される
progressView.progress = 0.7  // UI が自動的に更新される

2. データの保存とログ記録

class UserSettings {
    var username: String = "" {
        didSet {
            saveToUserDefaults()
            logChange()
        }
    }
    
    var theme: String = "light" {
        didSet {
            applyTheme()
            saveToUserDefaults()
        }
    }
    
    private func saveToUserDefaults() {
        // UserDefaultsへの保存処理
        print("設定を保存しました")
    }
    
    private func logChange() {
        print("ユーザー名が '\(oldValue)' から '\(username)' に変更されました")
    }
    
    private func applyTheme() {
        print("テーマを '\(theme)' に変更しました")
    }
}

3. バリデーションと通知

class PasswordField {
    var password: String = "" {
        didSet {
            validatePassword()
            notifyStrengthChange()
        }
    }
    
    private var isValid: Bool = false
    
    private func validatePassword() {
        isValid = password.count >= 8 && 
                 password.rangeOfCharacter(from: .decimalDigits) != nil &&
                 password.rangeOfCharacter(from: .uppercaseLetters) != nil
        
        print("パスワード有効性: \(isValid ? "有効" : "無効")")
    }
    
    private func notifyStrengthChange() {
        let strength = calculateStrength()
        print("パスワード強度: \(strength)")
    }
    
    private func calculateStrength() -> String {
        switch password.count {
        case 0..<4: return "弱い"
        case 4..<8: return "普通"
        default: return isValid ? "強い" : "普通"
        }
    }
}

willSetとの使い分け

SwiftにはdidSetの他にwillSetもあります。

class DataManager {
    var data: [String] = [] {
        willSet {
            print("データを変更予定: \(data.count)件 → \(newValue.count)件")
            // 変更前の準備処理
        }
        didSet {
            print("データ変更完了: \(oldValue.count)件 → \(data.count)件")
            // 変更後の処理
        }
    }
}

let manager = DataManager()
manager.data = ["A", "B", "C"]
// 出力:
// データを変更予定: 0件 → 3件
// データ変更完了: 0件 → 3件

使い分けのポイント:

  • willSet: 変更前の準備処理(バックアップ、バリデーションなど)
  • didSet: 変更後の処理(UI更新、保存、通知など)

注意すべきポイント

1. 無限ループを避ける

// 危険な例:無限ループになる可能性
var badExample: Int = 0 {
    didSet {
        if badExample < 0 {
            badExample = 0  // これ自体がdidSetを呼び出す
        }
    }
}

2. パフォーマンスを考慮する

// 頻繁に変更されるプロパティでは軽い処理を心がける
var position: CGPoint = .zero {
    didSet {
        // 重い処理は避ける
        updateUI()  // 軽い処理にする
    }
}

3. stored propertyでのみ使用可能

// OK: stored property
var storedProperty: Int = 0 {
    didSet {
        print("変更されました")
    }
}

// NG: computed propertyでは使えない
var computedProperty: Int {
    get { return storedProperty * 2 }
    set { storedProperty = newValue / 2 }
    // didSet { } // コンパイルエラー
}

まとめ

didSetは、プロパティの変更を自動的に監視して処理を実行できる便利な機能です。主なメリットは:

  • 自動実行: 値の変更を忘れずに処理できる
  • コードの簡潔性: 手動での処理呼び出しが不要
  • 保守性: 変更処理を一箇所に集約できる
  • oldValueアクセス: 変更前の値も利用できる

UIの更新、データの保存、バリデーションなど、様々な場面で活用できます。ただし、無限ループやパフォーマンスには注意が必要です。

ぜひプロジェクトでdidSetを活用して、より保守しやすいコードを書いてみてください!


参考リンク

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次