Привет! На связи Влад, 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)
}
}
}
Speakus
Тут точно всё так как вы задумали? На бесконечную рекурсию похоже.
vmityuklyaev Автор
Спасибо, что подметили. Тут так и задумано, но бесконечной рекурсии не получится, потому что наблюдатели свойств (didSet{}) начинают работать только после установки значения через init().
Speakus
Пометил что? Да в ините не вызовется. Но как-только вызовется - тут же попадёт в рекурсию.
vmityuklyaev Автор
Подметили, что тут может быть бесконечная рекурсия.
На самом деле блок didSet{} не вызывает повторного вызова самого себя. Можете проверить это в проекте или в playground.
Speakus
да. действительно. хотя и не всегда: https://stackoverflow.com/q/68004198/751932