Connect with us

Программирование

Обертки свойств в Swift: сокращаем шаблонный код

Обертки свойств — одна из самых мощных функций Swift для уменьшения шаблонного кода. Это не магия, это просто шаблон, который компилятор понимает и преобразует для вас.

Опубликовано

/

     
     

Раньше я везде копировал и вставлял один и тот же код валидации.

Каждый раз, когда мне нужно было ограничить значение от 0 до 100, я писал один и тот же геттер и сеттер. Каждый раз, когда я хотел сохранить что-то в UserDefaults, — один и тот же шаблонный код. Каждый раз, когда мне нужен был потокобезопасный доступ, — одна и та же процедура блокировки/разблокировки.

Затем я открыл для себя обертки свойств и почувствовал себя по-настоящему глупым, что не изучил их раньше.

Обертки свойств позволяют вынести эту повторяющуюся логику свойств в многоразовый компонент. Напишите один раз, используйте везде. Синтаксис чистый, цель понятна, и ваш код значительно сокращается.

SwiftUI использует их повсюду. @State, @Binding, @Published, @Environment. Вы все это время использовали обертки свойств. Но вот в чем дело: вы можете создать свои собственные. И как только вы это научитесь это делать, вы будете удивляться, как вы вообще жили без них.

Проблема: повторяющаяся логика свойств

Допустим, у вас есть система рейтингов. Пользователи могут оценивать товары по шкале от 0 до 5 звезд. Все довольно просто.

class Review {
    private var _rating: Int = 0

    var rating: Int {
        get { _rating }
        set { _rating = min(max(newValue, 0), 5) }
    }
}

Теперь вам потребуется такое же ограничение для громкости (от 0 до 100):

class AudioPlayer {
    private var _volume: Int = 50

    var volume: Int {
        get { _volume }
        set { _volume = min(max(newValue, 0), 100) }
    }
}

И для яркости (от 0 до 255):

class Display {
    private var _brightness: Int = 128
    var brightness: Int {
        get { _brightness }
        set { _brightness = min(max(newValue, 0), 255) }
    }
}

Видите закономерность? Та же логика, разные ограничения. Копипаст повсюду. Если вы хотите изменить принцип работы ограничения, вам нужно найти каждый инстанс.

Именно это и решают обертки свойств.

Ваша первая обертка свойств

Вот обертка @Clamped, которая обрабатывает все эти случаи:

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

Посмотрите, насколько удобнее стало использование:

class Review {
    @Clamped(0...5) var rating: Int = 0
}

class AudioPlayer {
    @Clamped(0...100) var volume: Int = 50
}
class Display {
    @Clamped(0...255) var brightness: Int = 128
}

Вот и всё. Одна строка на каждое свойство. Логика ограничения находится в одном месте. Если вы хотите изменить поведение, вы меняете обертку.

Давайте разберем, что происходит:

  1. @propertyWrapper помечает структуру как обертку свойства
  2. wrappedValue — это фактическое значение, которое хранится и к которому осуществляется доступ
  3. Init принимает начальное значение и конфигурацию (диапазон)
  4. Когда вы пишете rating = 10, Swift вызывает сеттер для wrappedValue

Магия заключается в том, что Swift преобразует @Clamped(0...5) var rating: Int = 0 во что-то вроде:

private var _rating = Clamped(wrappedValue: 0, 0...5)
var rating: Int {
    get { _rating.wrappedValue }
    set { _rating.wrappedValue = newValue }
}

Вы не видите этого преобразования, но именно это происходит под капотом.

Сделаем его универсальным

Наш @Clamped работает только с Int. Давайте сделаем так, чтобы он работал с любым Comparable:

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

Теперь это работает с типами данных Double, Float и любыми аналогичными:

@Clamped(0.0...1.0) var opacity: Double = 1.0
@Clamped(0...100) var percentage: Int = 50

Проецируемые значения: синтаксис $

Вы когда-нибудь задумывались, что такое $binding в SwiftUI? Это и есть проецируемое значение.

Обертки свойств могут предоставлять вторичное значение через projectedValue. Доступ к нему осуществляется с помощью $.

@propertyWrapper
struct Validated {
    private var value: String
    private let validator: (String) -> Bool

    var wrappedValue: String {
        get { value }
        set { value = newValue }
    }

    var projectedValue: Bool {
        validator(value)
    }

    init(wrappedValue: String, validator: @escaping (String) -> Bool) {
        self.value = wrappedValue
        self.validator = validator
    }
}

Использование:

struct User {
    @Validated(validator: { $0.contains("@") })
    var email: String = ""
}
let user = User()
user.email = "test@example.com"
print(user.email)   // "test@example.com"
print(user.$email)  // true (the projected value, validation result)
user.email = "invalid"
print(user.$email)  // false

Переменная $email возвращает результат проверки. Обертка хранит значение независимо от этого, но вы можете проверить валидность через спроецированное значение.

Это делает аннотация @State в SwiftUI. $name возвращает Binding<String>, а не саму строку. В этом и заключается работа спроецированного значения.

Пример из реальной жизни: @UserDefault

Это, вероятно, самая полезная пользовательская обертка, которую вы когда-либо напишете:

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    let container: UserDefaults

    var wrappedValue: Value {
        get {
            container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }

    init(wrappedValue: Value, _ key: String, container: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = wrappedValue
        self.container = container
    }
}

Теперь сохранение в UserDefaults стало элементарным делом:

struct Settings {
    @UserDefault("hasSeenOnboarding")
    var hasSeenOnboarding: Bool = false

    @UserDefault("username")
    var username: String = ""

    @UserDefault("volumeLevel")
    var volumeLevel: Int = 50
}

// Usage
var settings = Settings()
settings.hasSeenOnboarding = true  // Automatically saved to UserDefaults
print(settings.hasSeenOnboarding)  // Automatically read from UserDefaults

Больше никаких разбросанных повсюду вызовов UserDefaults.standard.set() и UserDefaults.standard.object(forKey:). Обёртка обрабатывает это автоматически.

Вы даже можете добавить проецируемое значение для сброса к значению по умолчанию:

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    let container: UserDefaults
    var wrappedValue: Value {
        get { container.object(forKey: key) as? Value ?? defaultValue }
        set { container.set(newValue, forKey: key) }
    }
    var projectedValue: Self { self }
    func reset() {
        container.removeObject(forKey: key)
    }
    init(wrappedValue: Value, _ key: String, container: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = wrappedValue
        self.container = container
    }
}
// Usage
settings.$username.reset()  // Removes from UserDefaults, returns to default

Пример из реальной жизни: @ThreadSafe

Потокобезопасность — ещё один прекрасный пример её оберток:

@propertyWrapper
struct ThreadSafe<Value> {
    private var value: Value
    private let lock = NSLock()

    var wrappedValue: Value {
        get {
            lock.lock()
            defer { lock.unlock() }
            return value
        }
        set {
            lock.lock()
            defer { lock.unlock() }
            value = newValue
        }
    }

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

Использование:

class Counter {
    @ThreadSafe var count: Int = 0
}

let counter = Counter()
// Safe to access from multiple threads
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
    counter.count += 1
}

Логика блокировки скрыта. Свойство просто работает безопасно.

Примечание: В современном Swift рекомендуется использовать акторы для обеспечения потокобезопасности. Но эта обертка по-прежнему полезна в контекстах, где акторы неуместны.

Пример из реальной жизни: @Trimmed

Для строк, которые никогда не должны содержать пробелов в начале/конце:

@propertyWrapper
struct Trimmed {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

Использование:

struct RegistrationForm {
    @Trimmed var username: String = ""
    @Trimmed var email: String = ""
}
var form = RegistrationForm()
form.username = "  john_doe  "
print(form.username)  // "john_doe" - automatically trimmed

Пример из реальной жизни: @Capitalized

Аналогичный шаблон для автоматического написания заглавных букв:

@propertyWrapper
struct Capitalized {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}
struct Person {
    @Capitalized var firstName: String = ""
    @Capitalized var lastName: String = ""
}
var person = Person()
person.firstName = "john"
person.lastName = "DOE"
print("\(person.firstName) \(person.lastName)")  // "John Doe"

Объединение оберток

К сожалению, в Swift нельзя использовать несколько оберток свойств одновременно для одного свойства. Это не работает:

// This won't compile
@Trimmed @Capitalized var name: String = ""

В качестве обходного пути можно создать комбинированную обертку:

@propertyWrapper
struct TrimmedCapitalized {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set {
            value = newValue
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .capitalized
        }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

Не так элегантно, как объединение, но работает.

Понимание оберток свойств в SwiftUI

Теперь, когда вы понимаете, как работают обертки, SwiftUI становится более понятным.

Вот что дает вам каждая обертка SwiftUI:

@State             → wrappedValue: the value    → $: Binding<Value>
@Binding           → wrappedValue: the value    → $: Binding<Value>
@Published         → wrappedValue: the value    → $: Publisher
@ObservedObject    → wrappedValue: the object   → $: wrapper itself
@EnvironmentObject → wrappedValue: the object   → $: wrapper itself
@Environment       → wrappedValue: the value    → $: wrapper itself

Когда вы пишете $name в SwiftUI, вы обращаетесь к projectedValue. Для @State это привязка Binding. Для @Published это издатель Combine.

struct ContentView: View {
    @State private var name: String = ""
    var body: some View {
        // name is the String (wrappedValue)
        // $name is Binding<String> (projectedValue)
        TextField("Enter name", text: $name)
    }
}

Тот $name, который вы передаете в TextField? Это биндинг, который позволяет текстовому полю читать И записывать значение. Без проецируемых значений двусторонняя привязка SwiftUI не работала бы.

Обертки свойств с зависимостями

Иногда оберткам требуются внешние зависимости. Вот @Injected для внедрения зависимостей:

@propertyWrapper
struct Injected<Service> {
    private var service: Service?

    var wrappedValue: Service {
        mutating get {
            if service == nil {
                service = DependencyContainer.shared.resolve(Service.self)
            }
            return service!
        }
    }

    init() {}
}

// Simple dependency container
class DependencyContainer {
    static let shared = DependencyContainer()
    private var services: [String: Any] = [:]

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        let key = String(describing: type)
        services[key] = factory()
    }

    func resolve<T>(_ type: T.Type) -> T {
        let key = String(describing: type)
        return services[key] as! T
    }
}
// Usage
protocol NetworkService {
    func fetch() async throws -> Data
}
class ViewModel {
    @Injected var networkService: NetworkService

    func load() async {
        let data = try? await networkService.fetch()
    }
}

Зависимость разрешается лениво при первом обращении. Чистый синтаксис, гибкая архитектура.

Когда НЕ следует использовать обертки свойств

Обертки свойств не всегда являются решением. Избегайте их, когда:

1. Логика сложна и требует видимости

Если поведение сложное, сокрытие его в обертке может затруднить отладку. Иногда явный код лучше, чем магия.

2. Вам нужен тонкий контроль

Обертки абстрагируют операции получения/установки. Если вам нужно разное поведение в разных местах, обертка может быть слишком ограничительной.

3. Она используется только один раз

Не создавайте обертку для одного свойства. Это избыточное проектирование. Обертки проявляют себя наилучшим образом, когда вы используете их повторно.

4. Производительность имеет решающее значение

Обертки добавляют небольшой уровень косвенности. Обычно это незначительно, но в тесных циклах с миллионами итераций это может иметь значение.

5. Команда плохо знает Swift

Обертки могут сбивать с толку начинающих разработчиков. Если ваша команда испытывает с ними трудности, явный код может быть более удобным для сопровождения.

Распространенные ошибки

Ошибка 1: Забыли инициализировать резервное значение

// Wrong - value might be uninitialized
@propertyWrapper
struct BadWrapper {
    var wrappedValue: Int  // No initial value
}
// Right
@propertyWrapper
struct GoodWrapper {
    var wrappedValue: Int
    init(wrappedValue: Int) {
        self.wrappedValue = wrappedValue
    }
}

Ошибка 2: Проблемы с изменением значений в структурах

Если ваша обертка используется в структуре, помните, что получение значения, которое приводит к изменению состояния, должно быть помечено mutating:

@propertyWrapper
struct Lazy<Value> {
    private var value: Value?
    private let builder: () -> Value
    // This needs 'mutating' because it modifies self
    var wrappedValue: Value {
        mutating get {
            if value == nil {
                value = builder()
            }
            return value!
        }
    }
    init(wrappedValue: @autoclosure @escaping () -> Value) {
        self.builder = wrappedValue
    }
}

Ошибка 3: Выполнение тяжёлых операций в геттерах и сеттерах

Обертки свойств должны оставаться лёгкими и быстрыми. Не выполняйте сетевые запросы или ресурсоёмкие вычисления внутри wrappedValue:

// Bad - network call in getter
@propertyWrapper
struct RemoteValue {
    var wrappedValue: String {
        get {
            // DON'T do this
            return fetchFromNetwork()  // Blocks, unpredictable
        }
    }
}

Ошибка 4: Отсутствие поддержки Codable

Если тип, использующий обертку, должен соответствовать протоколу Codable, потребуется реализовать собственную логику кодирования и декодирования:

struct User: Codable {
    @Trimmed var name: String
    // Custom Codable because @Trimmed isn't Codable by default
    enum CodingKeys: String, CodingKey {
        case name
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        _name = Trimmed(wrappedValue: try container.decode(String.self, forKey: .name))
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
    }
}

Или же сделайте так, чтобы ваша оболочка соответствовала требованиям Codable:

@propertyWrapper
struct Trimmed: Codable {
    var wrappedValue: String
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawValue = try container.decode(String.self)
        wrappedValue = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }

Краткий справочник

Вот шпаргалка по созданию оберток свойств:

// Basic wrapper
@propertyWrapper
struct WrapperName {
    var wrappedValue: ValueType

    init(wrappedValue: ValueType) {
        self.wrappedValue = wrappedValue
    }
}

// With configuration
@propertyWrapper
struct ConfigurableWrapper {
    var wrappedValue: ValueType
    let config: ConfigType

    init(wrappedValue: ValueType, config: ConfigType) {
        self.wrappedValue = wrappedValue
        self.config = config
    }
}
// With projected value
@propertyWrapper
struct ProjectedWrapper {
    var wrappedValue: ValueType
    var projectedValue: ProjectedType { /* return something */ }
}
// Generic wrapper
@propertyWrapper
struct GenericWrapper<Value> {
    var wrappedValue: Value
}

Шаблон использования:

// Basic
@WrapperName var property: ValueType = initialValue

// With config
@ConfigurableWrapper(config: value) var property: ValueType = initialValue
// Accessing values
let value = object.property      // wrappedValue
let projected = object.$property // projectedValue
let wrapper = object._property   // the wrapper itself (if accessible)

Что дальше?

Обертки свойств — одна из самых мощных функций Swift для уменьшения шаблонного кода. Это не магия, это просто шаблон, который компилятор понимает и преобразует для вас.

Начните с простого. Создайте обертку @UserDefault для вашего приложения. Затем @Clamped для ограниченных значений. Со временем создавайте свою библиотеку многократно используемых оберток.

Ознакомьтесь с документацией Apple по оберткам свойств для получения полной спецификации. Предложение SE-0258 также стоит прочитать, если вы хотите понять проектные решения.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: