Представленные в Swift 5.1, обертки свойств быстро стали одной из наиболее часто используемых функций во всем, что связано со Swift. Все известные сейчас фреймворки SwiftUI, вероятно, не были бы такими, какими они есть сейчас, без @State
, @Binding
, @StateObject
и других черных магических штук, которые вы используете каждый день. Или, может быть, вы слышали или даже использовали одну из самых популярных объектно-реляционных фреймворков отображения (ORM) для серверной части Swift Vapor под названием Fluent . Они не оставляют вам выбора, как и когда использовать знак доллара $
перед свойством, аккуратно обернутым в еще одну обертку.
Правда в том, что обертки свойств не так уж и сложно взломать, если вы нырнете глубоко и попытаетесь использовать их самостоятельно. На самом деле, у вас, вероятно, есть куча мест в вашем проекте, где вы можете облегчить себе задачу, прибегнув к помощи обертки. В этом руководстве я расскажу о 5 основных вариантах использования, подобных этому. Но сначала давайте освежим наши воспоминания о самой теме.
Обертка свойств — это специально аннотированный тип, который инкапсулирует часть логики, применяемой к любым свойствам в целом или свойствам, ограниченным определенным типом, который вы выбираете. Это может быть структура, перечисление или класс, объявленный с атрибутом @propertyWrapper
и имеющий по крайней мере wrappedValue
в качестве обязательного свойства внутри:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { // mandatory property get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } }
Внутри как геттера, так и сеттера обернутого свойства значения вы можете добавить любую необходимую вам логику и перехватывать, преобразовывать или использовать значения практически любым желаемым способом. Затем вы можете использовать обертку следующим образом:
class SomeService { @SomeWrapper fancyNumber: Int init(number: Int) { self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper } func doSmth() { print(fancyNumber) } }
Что хорошо в обертках свойств, так это то, что вы можете добавлять свои собственные инициализаторы, дополнительные методы, свойства и т. д., которые могут иметь любую необходимую вам функциональность. Одним из них является необязательное свойство projectedValue
, которое может возвращать даже саму обертку или любой другой тип. Теперь помните: когда вы хотите получить доступ к чему-то, что принадлежит самой обертке, но не ее значению, вам нужно использовать пресловутый знак доллара:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } func methodToDoSmth() { ... } // extra wrapper method } class SomeService { ... func doSmth() { $fancyNumber.methodToDoSmth() // here we access the logic of the wrapper itself } }
Конечно, все приведенные выше бессмысленные примеры не демонстрируют в полной мере истинную мощь оберток свойств, поэтому давайте рассмотрим некоторые из наиболее полезных способов заставить их засиять.
Вдохновленные всеми возможностями Fluent, мы можем применять оболочки свойств для работы с UserDefaults
вместо создания для этого отдельной службы следующим образом:
@propertyWrapper struct DefaultsStorage<T: Codable> { private let key: String private let defaultValue: T init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { // getting and decodind the data from UserDefaults guard let data = UserDefaults.standard.object(forKey: key) as? Data, let value = try? JSONDecoder().decode(T.self, from: data) else { return defaultValue } return value } set { // encodind the data and saving to UserDefaults guard let encoded = try? JSONEncoder().encode(newValue) else { return } let defaults = UserDefaults.standard defaults.set(encoded, forKey: key) } } }
Как вы видите, обертку выше можно использовать с чем угодно, Codable
@DefaultsStorage(key: "upload_date", defaultValue: Date()) var uploadDate: Date
Реализацию выше можно сильно подправить и изменить в зависимости от ваших потребностей. Например, вы можете сделать ее по-настоящему универсальной, приняв другие типы или разрешив использовать UserDefaults
на основе набора вместо standard
синглтона из коробки.
Другое дело — данные, которые вы сохраняете с его помощью. Если вы хотите что-то надежно сохранить, вы можете написать похожее решение для Keychain.
Кстати, у Apple уже есть похожая оболочка под названием @AppStorage
, но она была разработана для использования со SwiftUI, что вам может не понадобиться.
В случаях, когда вам необходимо отслеживать определенное значение, вам может пригодиться следующая оболочка свойств:
@propertyWrapper struct Logged<Value> { private var value: Value private let label: String // label that will be used in the log event init(wrappedValue: Value, label: String) { self.value = wrappedValue self.label = label print("\(label): Initial value set to \(value)") } var wrappedValue: Value { get { value } set { // prints the set event to a console print("\(label): Value changed from \(value) to \(newValue)") value = newValue } } }
@Logged(label: "Total Price") var price = Double(12.5)
Эта оболочка свойств может стать еще более полезной с дополнительным внедрением службы аналитики. Это может быть что-то локальное или даже удаленное. При использовании последнего не забудьте отфильтровать место вызова такой службы, чтобы избежать ненужных вызовов.
Нам часто нужно проверять вводимые пользователем данные. Существует множество возможных типов ввода, например, адрес электронной почты, номер телефона, дата рождения и т. д. Все они требуют применения определенной логики к исходным данным. Вы можете решить эту проблему, используя обертку свойств для каждого ввода:
@propertyWrapper struct ValidatedEmail<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } init(wrappedValue value: Value?) { self.value = value } private func validate(email: Value?) -> Bool { guard let email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
Или вы даже можете попробовать универсальное решение, например:
@propertyWrapper struct Validated<Value> { // define all the possible inputs enum InputType { case email case phoneNumber ... } let inputType: InputType var value: Value? var wrappedValue: Value? { get { // return value if validation succeeds or nil if not return validate(value) ? value : nil } set { value = newValue } } init(inputType: InputType, wrappedValue value: Value?) { self.inputType = inputType self.value = value } private func validate(_ value: Value?) -> Bool { // include validation logic for every input type switch inputType { case .email: ... case .phoneNumber: ... } } }
Подобно проверке данных, вы можете форматировать даты с помощью оболочек свойств, определив набор префиксных форматов, которые вы будете использовать внутри оболочки.
Иногда вам нужно ограничить значение в определенном диапазоне, например, чтобы процент охватывал диапазон от 0 до 100 или дробное значение от 0 до 1 и т. д. Вы также можете инкапсулировать эту логику в обертке свойства:
@propertyWrapper struct Clamped<Value: Comparable> { private var value: Value private let range: ClosedRange<Value> init(wrappedValue: Value, _ range: ClosedRange<Value>) { self.range = range self.value = min(max(wrappedValue, range.lowerBound), range.upperBound) } var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } }
Вы можете использовать его так:
struct ClampedExample { @Clamped(0...10) var number: Int init(number: Int) { self._number = Clamped(wrappedValue: number, 0...10) } func clampTest() { number = 20 print(example.number) // 10, because 20 is clamped to the upper bound 10 number = -5 print(example.number) // 0, because -5 is clamped to the lower bound 0 } }
Другой вариант использования оберток свойств — помочь нам со всем, что Codable
. Пользовательская реализация логики кодирования и декодирования может быть безопасно помещена внутрь обертки, если это необходимо. Вы также можете автоматически кодировать строку в base64 с помощью этого простого решения:
@propertyWrapper struct Base64Encoding { private var value = "" var wrappedValue: String { get { Data(value.utf8).base64EncodedString() } set { value = newValue } } }
Используя оболочку выше, всякий раз, когда вы обращаетесь к тексту, преобразованному ею, она всегда будет возвращать строку в кодировке base64.
Однако при работе с обертками свойств не забывайте, что это те же самые базовые типы Swift, которые мы знаем уже много лет, но с добавленной дополнительной функцией сверху. Одной из многих популярных идей о том, как использовать обертку свойств в прошлом, была попытка сделать свойства потокобезопасными с ее помощью. Вы можете найти несколько реализаций @Atomic
в сети, но оказалось, что большинство из них довольно проблематичны . Несмотря на то, что для этого также можно найти исправления, я больше не рекомендую использовать @Atomic
, особенно с учетом того, что теперь для этой цели доступны акторы.
Надеюсь, вы многому научились или, по крайней мере, освежили свои знания о свойствах-обертках в Swift. С этим обновлением взгляните на свои текущие проекты: возможно, у вас есть какие-то части, которые могут использовать мощь этой функции!