
Привет! На связи Влад, iOS-разработчик из Ozon. Сегодня я поделюсь с вами, возможно, не самым очевидным способом использования propertyWrappers. Обёртки позволяют добавлять дополнительную логику свойствам. В одну из них мы спрятали описание безопасного декодинга массивов, и теперь нам достаточно пометить свойство как @SafeDecode — и всё начинает работает автоматически. О том, как они работают и как их завести у себя, читайте дальше.
Что такое безопасный декодинг
Для тех, кто сталкивается с безопасным декодингом впервые, поясню: безопасный декодинг массива — это декодинг, при котором в декодируемом массиве может содержаться элемент, не соответствующий ожидаемому формату; при этом в результате мы получим все элементы массива, которые смогли распарсить.
Например, у нас есть структура:
struct Article { let title: String // обязательное поле let subtitle: String? // не обязательное поле }
И мы пытаемся распарсить такой массив данных:
[ { "title": "Title1", "subtitle": "Subtitle1" }, { // В этом элементе нет: "title": "Title1", "subtitle": "Subtitle1" } ]
do { let articles = try JSONDecoder().decode([Article].self, from: jsonData) } catch { print(error) // Мы получим ошибку: "No value associated with key title (\"title\")." // Потому что во втором элементе нет title, из-за этого // весь массив не распарсится }
Чтобы всё-таки получить все остальные элементы, мы используем propertyWrapper. Он содержит внутри логику, которая фильтрует ошибки и возвращает полученные значения.
Для тех, кто ещё не работал с propertyWrapper
Если вы уже знаете, как работает обёртка свойств, смело переходите к следующему разделу. Или можете освежить знания.
PropertyWrapper — это обёртка, позволяющая добавлять дополнительную логику самому свойству. То есть, например, мы можем сделать так, чтобы все слова в строке начинались с заглавной буквы или чтобы числа в переменной были всегда меньше 12. И всё это — всего одной строкой.
Давайте попробуем.
Для начала сделаем основу propertyWrapper:
@propertyWrapper struct Example { public var wrappedValue: Any public init(wrappedValue: Any) { self.wrappedValue = wrappedValue } }
Она состоит из маркировки @propertyWrapper и обязательного свойства wrappedValue.
Эту обёртку уже можно использовать:
struct Numbers { @Example let value: Any }
Но она пока что ничего не делает.
Посмотрим, как выглядит propertyWrapper, который будет устанавливать в свойство только положительные числа с помощью abs():
@propertyWrapper struct Abs { private var number: Int = 0 var wrappedValue: Int { get { number } set { number = abs(newValue) } } }
Вся логика работы у нас спрятана в одной строке: number = abs(newValue). Чтобы сделать из этой обёртки что-то новое, достаточно поменять только эту строку.
Также у нас нет init(wrappedValue: Any), как в основе, потому что мы сразу задали значение для number. Если этого не сделать, придётся дописать init().
Пример использования:
struct Number { @Abs var nonNegativeNumber: Int } var number = Number() number.nonNegativeNumber = -1 print(number.nonNegativeNumber) // 1 number.nonNegativeNumber = -77 print(number.nonNegativeNumber) // 77
Теперь любое число, установленное в nonNegativeNumber, будет положительным благодаря обёртке @Abs.
Давайте посмотрим, как ещё можно сделать эту же обёртку. Мы можем вместо приватного number сделать всё в wrappedValue, для этого нам понадобится наблюдатель свойства didSet {}:
@propertyWrapper struct Abs { var wrappedValue: Int { didSet { wrappedValue = abs(wrappedValue) } } init(wrappedValue: Int) { self.wrappedValue = abs(wrappedValue) } }
Результат будет тот же:
struct Number { @Abs var nonNegativeNumber: Int } var number = Number() number.nonNegativeNumber = -15 print(number.nonNegativeNumber) // 15 number.nonNegativeNumber = -40 print(number.nonNegativeNumber) // 40
А теперь рассмотрим пример, в котором propertyWrapper будет удалять цифры из конца строки:
@propertyWrapper struct WithoutDecimalDigits { var wrappedValue: String { didSet { wrappedValue = wrappedValue.trimmingCharacters(in: .decimalDigits) } } init(wrappedValue: String) { self.wrappedValue = wrappedValue.trimmingCharacters(in: .decimalDigits) } }
Вся логика работы содержится в didSet{}. При таком подходе нам обязательно нужно установить значение wrappedValue через init(). Это связано с тем, что наблюдатели свойств начинают работать только после установки значения в объект. Проще говоря, блок didSet{} заработает только после установки значения wrappedValue в init().
Реализация:
struct Example { @WithoutDecimalDigits var value: String } let exampleString = Example(value: "Hello 123") print(exampleString.value) // "Hello "
Теперь наша обёртка удаляет все цифры из строки.
Зная эти основы, можно делать удобные propertyWrappers для своего проекта. Но использовать их нужно с осторожностью. Если скрыть внутри сложную логику, то, в будущем, можно случайно добавить неочевидное поведение.
Как безопасно декодировать массив с propertyWrapper
Обёртки очень легко использовать:
struct Example: Decodable { @SafeArray let articlesArray: [Article] }
Мы помечаем декодируемый массив как @SafeArray — и в нём будут все элементы, которые можно получить.
Чтобы propertyWrapper заработал, нужно сделать две вещи:
Подготовить новый тип
Throwable, который может содержать либо значение, либо ошибку.Написать расширение для SingleValueDecodingContainer.
Делаем тип, он будет очень простым:
enum Throwable<T: Decodable>: Decodable { case success(T) case failure(Error) init(from decoder: Decoder) throws { do { let decoded = try T(from: decoder) self = .success(decoded) } catch let error { self = .failure(error) } } }
А теперь сделаем расширение.
Шаг 1. Подготавливаем расширение:
extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { } }
Шаг 2. Добавляем декодинг массива:
extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] } }
Шаг 3. Фильтруем и возвращаем декодируемый массив:
extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] let filtredArray = decodedArray.compactMap { result -> T? in switch result { case let .success(value): return value case .failure(_): return nil } } return filtredArray } }
В результате декодинга safelyDecodeArray вернёт либо все полученные элементы, либо пустой массив.
Следующие два шага — для тех, кто хочет добавить обработку ошибок и проверку на пустой массив; если вы хотите сразу перейти к реализации propertyWrapper, их можно пропустить.
Шаг 4. Добавляем проверку и возвращаем ошибку, если после фильтрации получился пустой массив:
extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { ... if filtredArray.isEmpty { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed") } return filtredArray } }
Шаг 5. Добавляем возможность выводить все полученные ошибки через callback:
extension SingleValueDecodingContainer { // 1. Добавим callback для вывода описания ошибок onItemError: (([String: Any]) -> Void)? func safelyDecodeArray<T>(onItemError: (([String: Any]) -> Void)?) throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] // 2. Чтобы иметь доступ к индексу элемента, добавим enumerated() и index let filtredArray = decodedArray.enumerated().compactMap { index, result -> T? in switch result { case let .success(value): return value // 3. Добавим errorInfo и его передачу через callback case let .failure(error): var errorInfo = [String: Any]() errorInfo["error"] = error errorInfo["index"] = index onItemError?(errorInfo) return nil } } if filtredArray.isEmpty { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed") } return filtredArray } }
Теперь у нас есть возможность использовать вывод описания ошибок декодинга в нашем propertyWrapper.
Финальный шаг. Реализуем propertyWrapper:
@propertyWrapper public struct SafeArray<T: Decodable>: Decodable { public let wrappedValue: [T] public init(wrappedValue: [T]) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try container.safelyDecodeArray() } }
Это всё, что нужно сделать, чтобы использовать обёртку для безопасного декодинга массивов. Теперь можно помечать массивы как SafeArray — и всё заработает автоматически.
Код из статьи целиком вы найдёте в последнем разделе.
Дополнительные propertyWrappers
В примере мы парсили массив в константу. Если нам нужно менять массив после парсинга, достаточно заменить в обёртке let на var, потому что обёрнутое свойство должно быть таким же, как wrappedValue:
@propertyWrapper public struct SafeMutableArray<T: Decodable>: Decodable { public var wrappedValue: [T] ... }
Тогда свойство тоже можно будет сделать переменной:
struct Example: Decodable { @SafeMutableArray var articlesArray: [Article] }
Если нам нужно получить опциональный массив, необходимо добавить опциональность и для wrappedValue:
@propertyWrapper public struct SafeOptionalArray<T: Decodable>: Decodable { public let wrappedValue: [T]? public init(wrappedValue: [T]?) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try? container.safelyDecodeArray() } }
Чтобы после декодинга опциональный массив можно было изменить, достаточно снова заменить let wrappedValue на var wrappedValue.
Вместо вывода
Это был, на мой взгляд, не самый очевидный способ декодирования данных в Swift, однако это ещё не все возможности property Wrapper.
Так как обёртки используются в структурах и классах, то их можно попробовать использовать в любом месте приложения, добавляя любую нужную логику, которую можно уместить.
Но всегда помните о том, что большая сила влечёт за собой и большую ответственность: если оставить внутри обёртки сложную логику, то она может аукнуться неочевидным поведением обёрнутого свойства. Применяйте инструмент там, где это действительно необходимо и к месту.
Код из статьи
@propertyWrapper public struct SafeArray<T: Decodable>: Decodable { public let wrappedValue: [T] public init(wrappedValue: [T]) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try container.safelyDecodeArray(onItemError: nil) } } extension SingleValueDecodingContainer { func safelyDecodeArray<T>(onItemError: (([String: Any]) -> Void)?) throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] let filtredArray = decodedArray.enumerated().compactMap { index, result -> T? in switch result { case let .success(value): return value case let .failure(error): var errorInfo = [String: Any]() errorInfo["error"] = error errorInfo["index"] = index onItemError?(errorInfo) return nil } } if filtredArray.isEmpty { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed") } return filtredArray } } enum Throwable<T: Decodable>: Decodable { case success(T) case failure(Error) init(from decoder: Decoder) throws { do { let decoded = try T(from: decoder) self = .success(decoded) } catch let error { self = .failure(error) } } }
