Property Wrappers у Swift: приклади в коді
Дана стаття є перекладом статті від Paul Hudson. Я раджу по можливості читати статті про програмування в оригіналі. Для тих, хто тільки починає вивчати англійську, я публікую переклад. Enjoy!
Property Wrappers в Swift дозволяють вам виділити загальну логіку в окремий об’єкт-обгортку. Ця нова техніка з’явилася на WWDC 2019 та вперше стала доступною в Swift 5. Це чудове доповнення до бібліотеки Swift, яке дозволяє видалити багато шаблонного коду, який ми, напевно, всі писали у наших проектах.
Ви можете знайти історію створення property wrappers на форумах Swift за SE-0258. Хоча в основному говориться про те, що property wrappers є рішенням для властивостей @NSCopying, існує типовий патерн, який він вирішує, і ви, можливо, його впізнаєте.
Що таке Property Wrapper?
Ви можете розглядати property wrapper як додатковий шар, який визначає, як властивість зберігається або обчислюється при читанні. Це дуже корисно для заміни повторюваного коду, який знаходиться в гетерах та сетерах властивостей.
Типовим прикладом є властивості користувацьких налаштувань, визначені користувачем, в яких спеціальний гетер та сетер перетворюють значення відповідно. Приклад реалізації може виглядати так, деталі реалізації я поділюся пізніше:
Вираз @UserDefault — це виклик property wrapper. Як ви можете бачити, ми можемо надати йому декілька параметрів, які використовуються для налаштування property wrapper. Існує декілька способів взаємодії з property wrapper, наприклад, використання обгорнутого значення та проектованого значення. Ви також можете налаштувати property wrapper з введеними властивостями, про що ми розповімо пізніше. Давайте спочатку розглянемо приклад property wrapper для UserDefaults.
Property wrappers та UserDefaults
Наступний код показує шаблон, який ви, можливо, впізнаєте. Він створює обгортку навколо об’єкта UserDefaults, щоб зробити властивості доступними без необхідності вставляти рядкові ключі всюди у вашому проекті.
Це дозволяє вам встановлювати та отримувати значення з user defaults з будь-якого місця наступним чином:
Тепер, хоча це здається чудовим рішенням, розширення може швидко перетворитися на великий файл з багатьма визначеними ключами та властивостями. Код є повторюваним і є спосіб спростити це. Спеціальний property wrapper, що використовує ключове слово @propertyWrapper, може допомогти нам вирішити цю проблему.
Як створити property wrapper
Взявши вищенаведений приклад, ми можемо переписати код і видалити багато зайвого. Для цього нам потрібно створити новий property wrapper, який ми назвемо UserDefault. Це в kінцевому підсумку дозволить нам визначити властивість як властивість користувацьких налаштувань.
Якщо ви використовуєте SwiftUI, вам може бути цікаво використовувати property wrapper AppStorage. Розглядайте це як приклад заміни повторюваного коду.
Ви можете створити Property Wrapper, визначивши структуру та позначивши її атрибутом @propertyWrapper. Атрибут вимагатиме від вас додати властивість wrappedValue, щоб надати повернене значення на рівні реалізації.
Як показано у вищенаведеному прикладі, обгортка користувацьких налаштувань дозволяє передавати значення за замовчуванням, якщо ще немає зареєстрованого значення. Ми можемо передавати будь-яке значення, оскільки обгортка використовує загальний тип Value.
Тепер ми можемо змінити нашу попередню реалізацію коду та створити наступне розширення для типу UserDefaults:
Як ви можете бачити, ми можемо використовувати стандартний ініціалізатор структури з визначеного property wrapper. Ми передаємо той же ключ, який використовували раніше, і встановлюємо значення за замовчуванням як false. Використання цієї нової властивості просте:
У деяких випадках ви можете захотіти визначити свої власні користувацькі налаштування. Наприклад, у випадках, коли у вас визначені користувацькі налаштування для групи додатків. Наша показана обгортка за замовчуванням використовує стандартні user defaults, але ви можете перевизначити це, щоб використовувати свій контейнер:
Додавання більше властивостей, використовуючи ту ж обгортку
На відміну від старого рішення, додавання більше властивостей, використовуючи property wrapper, дуже просто. Ми можемо повторно використовувати визначену обгортку та ініціалізувати стільки властивостей, скільки нам потрібно.
Як ви можете бачити, обгортка працює з будь-яким типом, який ви визначаєте, допоки тип підтримується для зберігання у UserDefaults.
Зберігання опціоналів за допомогою Property Wrapper для UserDefaults
Звичайна проблема, з якою ви можете зіткнутися при використанні property wrappers, полягає в тому, що загальне значення дозволяє вам визначати всі опціонали або всі розгорнуті значення. Існує поширена техніка, яку можна знайти у спільноті, щоб впоратися з цим, яка використовує спеціально визначений протокол AnyOptional:
Ми можемо розширити наш property wrapper UserDefault, щоб він відповідав цьому протоколу:
Це розширення створює додатковий ініціалізатор, який скасовує необхідність визначення значення за замовчуванням і дозволяє працювати з опціоналами.
Нарешті, нам потрібно налаштувати наш сетер для значення обгортки, щоб дозволити видалення об’єктів з користувацьких налаштувань:
Тепер це дозволяє нам визначати опціонали та встановлювати значення на nil:
Чудово! Ми можемо якимось чином обробляти всі сценарії зараз з обгорткою користувацьких налаштувань. Остання річ, яку потрібно додати, — це проектоване значення, яке ми можемо конвертувати в Combine publisher, так само, як обгортка властивості @Published.
Проектування значення з Property Wrapper
Property wrappers можуть додати ще одну властивість поруч із обгорнутим значенням, яка називається проектованим значенням. Це дозволяє нам проектувати інше значення на основі обгорнутого значення. Типовий приклад — визначення Combine publisher, щоб ми могли спостерігати за змінами, коли вони відбуваються.
Щоб зробити це з нашим property wrapper для користувацьких налаштувань, нам потрібно додати publisher, який буде passthrough subject. Все в назві: він просто передасть зміни значення. Реалізація виглядає наступним чином:
Тепер ми можемо почати спостерігати за змінами нашої властивості наступним чином:
Це чудово! Це дозволяє нам реагувати на будь-які зміни. Оскільки ми визначили нашу властивість статично раніше, цей publisher тепер працюватиме по всьому нашому додатку. Якщо ви хочете дізнатися більше про Combine, обов’язково ознайомтеся з статтею “Початок роботи з фреймворком Combine в Swift”.
Доступ до приватно визначених властивостей
Хоча не рекомендується працювати з property wrappers таким чином, у деяких випадках може бути корисно читати визначені властивості обгортки. Я просто покажу, що це можливо, але вам може знадобитися переглянути реалізацію вашого коду, якщо вам потрібно отримати доступ до приватних властивостей.
До приватно визначеної властивості можна отримати доступ, використовуючи префікс підкреслення. Це дозволяє нам отримати доступ до приватного ключа властивості з нашого property wrapper для користувацьких налаштувань:
Сприймайте це з обережністю і подивіться, чи можете ви задовольнити свої потреби, використовуючи інший тип екземпляра. Одна з ідей може полягати в доступі до охоплюючого екземпляра property wrapper, який матиме додаткову вигоду доступу до приватно визначених властивостей з зовнішніх модулів фреймворку.
Краща альтернатива для доступу до всіх змінних property wrapper
Використовуючи проектоване значення property wrapper, ми можемо визначити публічний доступ до всіх визначених властивостей обгортки. Ми можемо зробити це, повертаючи сам екземпляр обгортки наступним чином:
Тепер проектоване значення повертає структуру користувацьких налаштувань з загальним типом значення. Її використання виглядає наступним чином:
Доступ до “охоплюючого” екземпляра property wrapper
Використовуючи спеціальний статичний підсценарій, ви зможете отримати доступ до охоплюючого екземпляра property wrapper, який визначив конкретний property wrapper. Це може призвести до цікавих варіантів використання, у яких ви можете ділитися тими ж основними властивостями екземпляра для кожної визначеної обгортки.
Наприклад, ми могли б визначити клас налаштувань, який представляє один контейнер користувацьких налаштувань:
Як ви можете бачити, ми вже не визначаємо контейнер у нашому ініціалізаторі property wrapper для користувацьких налаштувань. Щоб все одно мати доступ до правильного контейнера, ми можемо додати статичний підсценарій до нашого визначення обгортки:
Цей статичний підсценарій працює лише з властивостями екземпляра класів, щоб мати тип посилання. Визначаючи fatal error всередині значення обгортки, ми гарантуємо, що використання на структурах призведе до виключення (exception). Ця техніка чудова для налагодження, оскільки вона буде безпосередньо надавати feedback.
Статичний підсценарій дозволяє нам отримувати доступ як до контейнера property wrapper, так і до охоплюючого екземпляра, який визначив змінну з атрибутом обгортки користувацьких налаштувань. Іншими словами, він дає нам доступ до структури UserDefault, а також до класу Preferences.
Об’єднуючи їх, ми можемо переписати наш property wrapper для user defaults, щоб використовувати контейнер, як визначено на екземплярі Preferences. Це чудовий спосіб повторно використовувати той самий основний контейнер user defaults для кожної обгортки.
Приєднання Property Wrappers до параметрів функцій та замикань
Ви також можете використовувати Property Wrappers з параметрами функції або замикання, що призводить до цікавих варіантів використання, які можуть допомогти вам видалити більше шаблонного коду або поліпшити debugging.
Наступний приклад демонструє обгортку для debugging:
Log з’явиться завжди, коли властивість доступу або оновлення, при цьому ми також можемо додати точку переривання для покращеного дебагінгу.
Як приклад, ми могли б додати цю обгортку до аргументу функції тривалості в наступному прикладі анімації:
Це може бути чудовим інструментом під час дебагінгу, у якому ви додаєте обгортку тимчасово. Звісно, це лише приклад. Інші поширені варіанти використання дозволяють перетворювати рядкове значення на верхній або нижній регістр у межах аргументу функції.
Нарешті, ви можете використовувати ту ж обгортку у замиканнях:
На жаль, на момент написання цієї статті, ми не можемо використовувати властивості ініціалізатора, коли використовуємо обгортку всередині замикань. Це, ймовірно, помилка і може бути виправлена в майбутньому оновленні Swift.
Інші приклади використання
Property wrappers також використовуються у стандартних API Swift. Особливо в SwiftUI ви знайдете property wrappers, такі як @StateObject та @Binding. У них усіх є щось спільне: вони роблять часто використовувані шаблони легшими для доступу.
Надихнувшись цими вбудованими прикладами, ви можете почати думати про створення ваших property wrappers, наприклад, для включеного авторозміщення:
Часто використовую цей останній приклад у своїх проектах для представлень, які використовують авторозміщення і вимагають, щоб translatesAutoresizingMaskIntoConstraints було встановлено як false. Про цей приклад ви можете дізнатися більше в моєму блозі: Auto Layout in Swift: Writing Constraints Programmatically.
Визначення семпл-файлів за допомогою property wrapper
Основний приклад фокусується на user defaults, але що якщо ви хочете визначити іншу обгортку? Давайте розглянемо інший приклад, який, сподіваюся, надихне вас на деякі ідеї.
Розгляньте наступний property wrapper, в якому ми визначаємо семпл-файл:
Ми можемо використовувати цю обгортку, щоб визначити наші семпл-файли, які ми можемо використовувати для дебагінгу або під час тестування:
Властивість projectedValue дозволяє нам вивести назву файлу, як використовується в property wrapper:
Це може бути корисно в тих випадках, коли ви хочете знати, яке початкове значення (значення) було використано обгорткою для обчислення кінцевого значення. Зверніть увагу, що ми використовуємо тут знак долара як префікс для доступу до проектованого значення.
Висновок
Property wrappers — це чудовий спосіб видалити шаблонний код з вашого коду. Вищезазначений приклад — лише один з багатьох сценаріїв, в яких це може бути корисно. Ви можете спробувати це самостійно, знаючи повторюваний код і замінивши його настроєним обгорткою.
Дякую!
Разом зі студентами мого курсу ми створюємо українську iOS спільноту. Якщо ви займаєтеся вивченням iOS розробки, будь ласка, приєднуйтесь до нас у Discord!
Також проходьте мій курс по iOS розробці, українською: https://www.udemy.com/course/ios-development-for-beginners-native-iosdevelopment/