Что такое Property Wrapper в Swift и как он работает?

«Что такое Property Wrapper в Swift и как он работает?» — вопрос из категории Swift Core, который задают на 23% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Property Wrapper — это механизм в Swift, который позволяет инкапсулировать стандартную логику работы с getter и setter свойства в переиспользуемый тип. Он «оборачивает» значение свойства, добавляя к нему дополнительное поведение при чтении и записи.

Синтаксис и компоненты:

  1. Атрибут @propertyWrapper: Помечает структуру или класс как обертку.
  2. Свойство wrappedValue: Обязательное свойство, которое содержит логику get/set для обернутого значения.
  3. Инициализатор: Может принимать начальное значение.
  4. Свойство projectedValue (опционально): Предоставляет дополнительный API для обернутого свойства (доступ через $).

Пример: Обертка для автоматического триммирования строки.

@propertyWrapper
struct Trimmed {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue // Используется setter
    }
}

// Использование
struct UserProfile {
    @Trimmed var username: String
    @Trimmed var email: String
}

var profile = UserProfile(username: "  john_doe  ", email: " email@example.com ")
print(profile.username) // "john_doe" (пробелы удалены)
print(profile.email)    // "email@example.com" (пробел удален)

Пример с projectedValue (логирование изменений):

@propertyWrapper
struct Logged<Value> {
    private var value: Value
    private let name: String

    var wrappedValue: Value {
        get { value }
        set {
            print("Свойство '(name)' изменено: (value) -> (newValue)")
            value = newValue
        }
    }

    var projectedValue: Self { self } // Проецируем саму обертку

    init(wrappedValue: Value, _ name: String = "") {
        self.value = wrappedValue
        self.name = name
    }
}

struct Settings {
    @Logged("volume") var volume: Double = 0.5
}

var settings = Settings()
settings.volume = 0.7 // В консоли: "Свойство 'volume' изменено: 0.5 -> 0.7"
let wrapper = settings.$volume // Доступ к самой обертке через projected value

Распространенные применения:

  • Валидация и нормализация (как в @Trimmed).
  • Синхронизация с UserDefaults (@UserDefault).
  • Ограничение значений (@Clamped(min: 0, max: 100)).
  • Ленивая инициализация (встроенный @Lazy).
  • Thread-safe доступ к свойствам.

Итог: Property Wrapper — мощный инструмент для соблюдения принципа DRY, позволяющий выносить стандартную логику работы со свойствами в отдельные, тестируемые компоненты.