Программирование
Обертки свойств в 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
}
Вот и всё. Одна строка на каждое свойство. Логика ограничения находится в одном месте. Если вы хотите изменить поведение, вы меняете обертку.
Давайте разберем, что происходит:
@propertyWrapperпомечает структуру как обертку свойстваwrappedValue— это фактическое значение, которое хранится и к которому осуществляется доступInitпринимает начальное значение и конфигурацию (диапазон)- Когда вы пишете
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 также стоит прочитать, если вы хотите понять проектные решения.
-
Новости2 недели назадВидео и подкасты о мобильной разработке 2026.20
-
Видео и подкасты для разработчиков2 недели назадОт личной продуктивности к командной: сила шаблонизации в IDE
-
Новости3 недели назадВидео и подкасты о мобильной разработке 2026.19
-
Разработка4 недели назадПодсветка синтаксиса на Android — интеграция движка Shiki в Compose
