PropertyWrappers: Lazy, AdaptiveColor
Наконец-то добрался до относительно новой возможности языка Swift. И сейчас мы сначала разберемся на практике как можно написать свой собственный модификатор lazy с помощью @propertyWrapper. А затем напишем действительно полезный модификатор для поддержки темной темы.
Немного о @propertyWrapper
Дословно это обертка над свойством.
Свойство это пара методов: get и set. Первый отвечает за чтение, второй за запись. Опционально свойство может иметь переменную для хранения значения. Также метода записи может и не быть, тогда свойство можно только читать.
Модификатор @propertyWrapper позволяет обернуть свойство, и таким образом добавить логику выполнения его методов get и set. А что это будет за логика решать вам.
Что такое lazy?
Это модификатор, который позволяет создать значение свойства лениво, только в момент обращения к свойству. Другими словами, мы откладываем создание на тот момент, когда свойство действительно нужно.
Но зачем нам это?
- Если есть вероятность, что свойство может быть не использовано. Тогда нет смысла создавать его значение сразу;
- Если создание значения свойства занимает длительное время, то его лучше отложить. Тогда сам объект, где объявлено свойство, будет создан быстро;
- Значение свойства не может быть создано в момент создания объекта, где это свойство объявлено. К примеру, нужно дождаться готовности одной из зависимостей объекта;
- Ваш вариант.
Более подробно вы можете почитать в официальной документации языка Swift или в одной из множества статей на эту тему. На этом закончим со скучной непонятной теорией и перейдем к практике.
Подготовка
Для начала подготовим пример. Пусть у нас есть объект, который может предоставить помощь пользователю. Но возможно пользователь разберется во всем сам, и тогда нет необходимости создавать этот объект сразу.
final class Game { lazy var help: HelpProvider = { print("Taking Fire, Need Assistance") return HelpProvider() }()}let game = Game() // Output:game.help // Output: Taking Fire, Need Assistancegame.help // Output:
Отлично, у нас есть ленивое свойство help и его значение будет создано только в момент обращения к нему. Затем оно переиспользуется без пересоздания.
То же самое, но без lazy
Теперь откажемся от стандартного модификатора lazy и напишем то же самое без него. Для этого нам потребуется ещё одна переменная для хранения значения свойства. А также придется написать всю ленивую логику.
final class Game { var help: HelpProvider { // Сначала проверяем, может значение уже создано if let storage = helpStorage { return storage } // Иначе создаем и сохраняем в отдельной переменной print("Taking Fire, Need Assistance") let newStorage = HelpProvider() helpStorage = newStorage return newStorage } private var helpStorage: HelpProvider?}let game = Game() // Output:game.help // Output: Taking Fire, Need Assistancegame.help // Output:
Всё, теперь осталось завернуть это в @propertyWrapper.
Заворачиваем в @Lazy
Для начала напишем самую простую версию. Затем оценим её минусы и попробуем улучшить. wrappedValue является обязательным и его тип должен совпадать с типом переменной, к которой применяется @propertyWrapper.
@propertyWrapper final class Lazy { var wrappedValue: HelpProvider { if let existStorage = storage { return existStorage } print("Taking Fire, Need Assistance") let newStorage = HelpProvider() self.storage = newStorage return newStorage } private var storage: HelpProvider?}
Использование очень напоминает применение модификатора lazy. Правда создание значения свойства скрыто в @propertyWrapper.
final class Game { @Lazy var help: HelpProvider}let game = Game() // Output:game.help // Output: Taking Fire, Need Assistancegame.help // Output:
Какие минусы вы заметили? На самом деле пока это решение выглядит как один сплошной минус.
- Можно использовать только для HelpProvider - и это ужасно;
- Создание объекта не может быть изменено без изменения реализации @propertyWrapper.
Чуть ниже я привожу более сложную реализацию. Если вы начинающий, то будьте осторожны. Это может вас шокировать.
Убираем минусы
Чтобы избавится от первого минуса, воспользуемся преимуществом Generics. Объявим @propertyWrapper как LazyOnce<T>, где T это тип, который будет подставляться на этапе компиляции в зависимости от контекста. Таким образом можно будет использовать любой тип.
Со вторым минусом всё сложнее. Нам нужно передать closure, в котором будет скрыто создание объекта. Но проблема в том, что тип свойства wrappedValue должен совпадать с типом в методе init.
Здесь на помощь приходит @autoclosure. Это позволяет передавать аргумент, который может быть как функцией () -> T, так и значением c типом T. Во втором случае значение будет завернуто в closure автоматически. Отсюда и название.
@propertyWrapper final class LazyOnce<T> { var wrappedValue: T { if let existStorage = storage { return existStorage } let newStorage = lazyBlock() self.storage = newStorage return newStorage } private var storage: T? private let lazyBlock: () -> T init(wrappedValue: @escaping @autoclosure () -> T) { self.lazyBlock = wrappedValue }}
Возможно вы обратили внимание, что я переименовал Lazy в LazyOnce. Это связанно с тем, что переменная помеченная таким модификатором будет создана лишь один раз. Нельзя присвоить ей другое значение за пределами @propertyWrapper.
На самом деле это баг и мы могли бы реализовать и другую версию похожую на lazy. Но давайте оставим у нашей реализации такую особенность.
final class Game { @LazyOnce var help = HelpProvider()}let game = Game()// Cannot assign to property: 'help' is a get-only propertygame.help = HelpProvider()
Поддержка Dark Mode
И как обещал в самом начале, давайте рассмотрим более полезный пример применения @propertyWrapper в разработке.
Если вам нужно добавить поддержку темной темы, то вам необходимо описать логику выбора цвета. Для этого как раз подойдет @propertyWrapper. Я объявил аргумент dark опциональным, так как есть универсальные цвета, которые не меняются в зависимости от выбора светлой или темной темы.
@propertyWrapper struct AdaptiveColor { private let light: UIColor private let dark: UIColor init(light: UIColor, dark: UIColor? = nil) { self.light = light self.dark = dark ?? light } var wrappedValue: UIColor { UIColor { // Возвращаем цвет в зависимости от выбранной темы $0.userInterfaceStyle == .light ? self.light : self.dark } }}
Затем можно создать палитру и объявить в ней необходимые цвета. Используя @AdaptiveColor получаем очень компактное и простое представление.
final class Pallete { @AdaptiveColor(light: .white, dark: .black) var backgroundColor: UIColor @AdaptiveColor(light: .green) var textColor: UIColor}
В конце хотелось посоветовать не изобретать @propertyWrapper только ради его применения. Да, это интересная возможность языка Swift. Но также этот подход часто требует использования синглтонов и это чаще плохо, чем хорошо. К примеру, @Injected с использованием синглтон контейнера внутри выглядит очень спорно.
Конец
- В этот раз мы узнали что такое lazy и зачем это нужно;
- На практике разобрались с @propertyWrapper;
- Написали почти бесполезную обертку @LazyOnce, которая уже есть — lazy;
- И даже одну полезную @AdaptiveColor.
На этом пока всё, я надеюсь пост был для вас интересным и полезным.
Подписывайтесь в Twitter и ещё увидимся.