Уже довольно давно, начиная с iOS 5, у программистов появился удобный механизм стилизации внешнего вида компонентов интерфейсов – UIAppearance.
Например, вот так легко установить белый цвет для кнопок и заголовка на всех UINavigationBar и наследниках:
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]]; [[UINavigationBar appearance] setTitleTextAttributes: @{NSForegroundColorAttributeName:[UIColor whiteColor]}];
И это замечательный и удобный инструмент, так как влияет глобально на всё приложение. Если вам требуется изменять только определенные элементы, то можно либо сделать для них соответствующий субкласс от стандартного, либо воспользоваться методом
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray> *)containerTypes
который применит указанные параметры только к тем элементам, что вложены в объекты соответствующих классов.
Пример из документации:
[[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[UISplitViewController class]]] setBarTintColor:myColor]; [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[UITabBarController class], [UISplitViewController class]]] setBarTintColor:myTabbedNavBarColor];
И всё работает замечательно, до того момента, когда в дело вступает один нюанс, описанный в документации:
NOTE
iOS applies appearance changes when a view enters a window, it doesn’t change the appearance of a view that’s already in a window. To change the appearance of a view that’s currently in a window, remove the view from the view hierarchy and then put it back.
Как это свойство влияет на подход к использованию данных методов?
Например, сработать установка новых свойств должна обязательно до добавления (а обычно это просто создание) объектов в текущую иерархию представлений на окне приложения. Иначе ничего во внешнем виде объектов интерфейса не изменится. Самая наглядная проблемная ситуация – это разное оформление двух экранов приложения, которые находятся в одном стеке UINavigationController. При переходе с первого на второй экран должен измениться цвет и прочее оформление UINavigationBar как в Apple Music:
или этом примере:
Конечно, можно сразу сказать, что такой подход не идеальный с точки зрения дизайна, и сама по себе постановка такой задачи содержит в себе изъян. Но с практической точки зрения это сейчас не важно.
Поведение и реализация визуального стиля второго, особого экрана должны быть полностью инкапсулированы, но в то же время у нас присутствует прямая зависимость с объектом более высокого уровня, а именно с UINavigationController. Он отображает представления экранов и навигационная панель относится к нему, а не к экрану. Дизайн второго экрана требует иного стиля для навигационной панели. И тут просматривается концептуальная ошибка дизайна. Идеальным решением была бы инкапсуляция особенностей отображения навигационной панели не в классе экрана, а в кастомных классах навигационного контроллера и самого UINavigationBar, аналогично тому, как работает стилизация статусной строки: контроллер экрана в специальном методе возвращает элемент перечисления. А уж кто и когда запрашивает это значение, контроллер экрана не волнует и волновать, в соответствии с «голливудским принципом», не должно.
Проблема реализации такого визуального перехода с точки зрения программиста состоит лишь в одном: как вернуть исходное состояние с минимальным числом зависимостей? Действительно, если изменить состояние навигационной палени «снизу», из кода экрана, например, в такой манере:
self.navigationController.navigationBar.tintColor = [UIColor whiteColor];
где-нибудь в методах -viewDidAppear: или -viewWillAppear: то при возвращении на первый экран свойство tintColor останется равным белому цвету. Наивным решением будет попытка сохранить каким-либо образом оригинальное значение, и вернуть его в методах -viewDidDisappear: или -viewWillDisappear:. При этом еще необходимо проверять, что именно происходит со стеком контроллера. Зависимостей становится всё больше…
Но целью данной статьи является иллюстрирование работы с UIAppearance, поэтому продолжим.
Методы UIAppearance не сработают вместо прямого доступа к свойствам navigationBar при аналогичном вызове:
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];
Потому что, как было сказано ранее, навигационная панель уже находится в иерархии представлений текущего окна приложения. Документация советует удалить представление и вернуть после работы методов UIAppearance:
UINavigationBar *nb = self.navigationController.navigationBar; UIView *nbSuperview = nb.superview; // сохраняем индекс размещения представления NSUInteger zIdx = [nbSuperview.subviews indexOfObject:nb]; // и удаляем [nb removeFromSuperview]; // применяем стилизацию [[UINavigationBar appearance] setTintColor:[UIColor whiteColor]]; // возвращаем представление [nbSuperview insertSubview:nb atIndex:zIdx];
Для переиспользования реализуем на базе данного кода чуть более универсальный метод с лёгкой анимацией:
-(void)applyNavBarAppearance:(void(^)())appearanceBlock { UINavigationBar *nb = self.navigationController.navigationBar; UIView *nbSuperview = nb.superview; NSUInteger zIdx = [nbSuperview.subviews indexOfObject:nb]; [nb removeFromSuperview]; if (appearanceBlock) appearanceBlock(); nb.alpha = 0.; [UIView animateWithDuration:0.5 animations:^{ [nbSuperview insertSubview:nb atIndex:zIdx]; nb.alpha = 1.; }]; }
Здесь appearaceBlock – блок, в котором можно реализовать любые манипуляции с UIAppearance (да и не только с ним). В итоге использование данного метода будет выглядеть примерно так:
// Стилизация для особого экрана -(void)applyCustomAppearance { [self applyNavBarAppearance:^{ [[UINavigationBar appearance] setTintColor:[UIColor whiteColor]]; [[UINavigationBar appearance] setTitleTextAttributes: @{NSForegroundColorAttributeName:[UIColor whiteColor]}]; }]; } // Возвращение глобальных параметров отображения -(void)applyGlobalAppearance { [self applyNavBarAppearance:^{ // Здесь вызывается метод, устанавливающий глобальные свойства [MyAppDelegate applyGlobalAppearance]; }]; }
В данном подходе, который, конечно, не стоит воспринимать как 100% решение, есть пара нюансов:
- -applyCustomAppearance должен менять только те свойства, которые устанавливает и главный -applyGlobalAppearance
- всё равно требуется наблюдать за направлением навигации, если дизайн более глубоких экранов не отличается, чтобы избежать каких-либо нежелательных графических артефактов при переходах между ними
Добавить комментарий
Для отправки комментария вам необходимо авторизоваться.