CodeFest 2019. Антон Спивак (ВКонтакте) — iOS. Переход на тёмную сторону

16b6c87229eaf58768d25ed7b2bbbf52?s=47 CodeFest
April 06, 2019

CodeFest 2019. Антон Спивак (ВКонтакте) — iOS. Переход на тёмную сторону

У нас было три сотни экранов, страх все сломать, монструозный UIKit, огромная куча старого кода и жгучее желание сделать тёмную тему.

Этот рассказ будет о том, как подружить свою архитектуру с дизайнерами и JS-разработчиками, добавить цветовые схемы, не прогореть по производительности и в итоге, зауважать создателей UIKit.

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 06, 2019
Tweet

Transcript

  1. ПЕРЕХОД НА ТЁМНУЮ СТОРОНУ

  2. ПЕРЕХОД НА ТЁМНУЮ СТОРОНУ

  3. Предпосылки 
 от iOS Зачем? Мессенджер Создать хайп

  4. Но сперва..

  5. СВЕТЛАЯ ТЕМА

  6. Палитра

  7. Палитра!

  8. Алгоритмически ищем похожие цвета Сжимаем Заменяем по всему проекту

  9. RGB

  10. HUE

  11. Автоподбор HUEColor ls = [leftColor HSV]; HUEColor rs = [rightColor

    HSV]; CGFloat dh = fmin(fabs(ls.hue - rs.hue), 360.0f - fabs(ls.hue - rs.hue)) / 180.0f; CGFloat ds = fabs(ls.saturation - rs.saturation); CGFloat dv = fabs(ls.brightness - rs.brightness) / 255.0f; CGFloat distance = sqrtf((dh * dh) + (ds * ds) + (dv * dv));
  12. Визуальная проверка

  13. Обновляем код, создаем 
 50+ багов, исправляем их и приступаем

    к следующему важному шагу
  14. ТЕМНАЯ 
 ТЕМА

  15. Условия Максимально простое решение Минимум изменений в текущем коде Работа

    с CoreGraphics (CALayer’s and etc.) Работа с TextKit, CoreText (NSAttributedString) Работа с UIKit VKUI (VK Apps, VK Connect) Android (а вдруг?)
  16. Ух, что сейчас будет • Быстро подберем формат • Подумаем

    над хранением • Определимся с архитектурой (VIPER) • Чуть-чуть покодим
  17. Формат для всех

  18. Палитра { "azure_100_muted": "#67A5EB", "azure_300": "#3F8AE0", "azure_350": "#2975CC", "azure_A100": "#2787F5",

    "azure_A400": "#4986CC" }
  19. Светлая схема "client_light": { "appearance": "light", "colors": { "accent": {

    "color_identifier": "blue_300" } } }
  20. Темная схема "client_dark": { "appearance": "dark", "colors": { "accent": {

    "color_identifier": "sky_300" } } }
  21. Как хранить? Голый JSON Одетый JSON в классы (лайки?) Кодогенерация

    и 
 https://github.com/VKCOM/Appearance
  22. Colors.h @interface UIColor (Palette) @property (nonatomic, readonly, strong, class) UIColor

    *azure300; @end @interface Color (Scheme) @property (nonatomic, readonly, strong, class) Color *accent; @end
  23. VKUI (VK Apps, VK Connect) доволен, дизайнеры согласны, мы счастливы

  24. Готовимся писать код, но сначала немного подумаем

  25. Библиотеки https://github.com/regexident/Gestalt 
 (много кода, нет поддержки NSAttributedString) https://github.com/draveness/DKNightVersion 


    (много кода, нет поддержки NSAttributedString) И вообще их нужно поддерживать, в топку такое
  26. if (dark) {} else {} Нет

  27. CoreGraphics int CGColorCreate(int space, int components) { rax = (*_CGColorCreate_ptr)(space,

    components); return rax; } void CGContextSetFillColorWithColor(CGContextRef, CGColorRef)
  28. UIKit @interface UIDeviceRGBColor : UIColor { double alphaComponent double blueComponent

    double greenComponent double redComponent } @interface UIDeviceWhiteColor : UIColor { double alphaComponent double whiteComponent }
  29. Мы не хотели (конечно хотели, кого мы обманываем), но придется

    писать самостоятельно
  30. А как напишем? Спросим у Objective-C

  31. NSProxy - (void)forwardInvocation:(NSInvocation *)aInvocation { [aInvocation invokeWithTarget:[self _keyedColor]]; } -

    (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [[self _keyedColor] methodSignatureForSelector:aSelector]; }
  32. NSProxy - (UIColor *)_keyedColor { UIColor *color = [self.colors valueForKey:[Appearance

    appearance].palette.name]; // Here we need to cache CGColor for system usage [color CGColor]; return color; }
  33. Appearance @interface Appearance : NSObject @property (nonatomic, strong) Palette *_Nonnull

    palette; + (Appearance *_Nonnull)appearance; @end
  34. Как использовать? view.backgroundColor = [Color backgroundContent] imageView.tintColor = [Color accent]

    @{ NSForegroundColorAttributeName : [Color accent] }
  35. None
  36. Независимая палитра? @protocol Painter <NSObject> - (nullable Palette *)palette; @end

  37. Независимая палитра! @interface Appearance : NSObject <Painter> @property (nonatomic, strong,

    nullable) Palette *palette; @end @interface UIResponder (Painter) <Painter> @property (nonatomic, strong, nullable) Palette *palette; @end
  38. Добавим конструктор - (instancetype)dependedWithPainter:(id<Painter>)painter { return [[Color alloc] initWithIdentifier:[self.identifier copy]

    colors:[self.colors copy] alpha:self.alpha painter:painter]; }
  39. И обновим хитрый цвет - (UIColor *)_keyedColor { UIColor *color

    = [self.colors valueForKey:[self _painter].palette.name]; // Here we need to cache CGColor for system usage [color CGColor]; return color; }
  40. Как использовать? view.backgroundColor = [[Color backgroundContent] dependedWithPainter:self]

  41. None
  42. Цвета с сервера? - (instancetype)overridedValues:(NSDictionary<NSString *, UIColor *> *)v {

    NSMutableDictionary *currentColors = [self.colors mutableCopy]; [v enumerateKeysAndObjectsUsingBlock:^(NSString *k, UIColor *o, BOOL *stop) { [currentColors setObject:o forKey:k]; }]; return [[Color alloc] initWithIdentifier:[self.identifier copy] colors:[currentColors copy] alpha:self.alpha painter:self.painter]; }
  43. Как использовать? view.backgroundColor = [[Color backgroundContent] overridedValues:@{ @"light" : [UIColor

    textColor] }]
  44. None
  45. Другое значение alpha? - (UIColor *)colorWithAlphaComponent:(CGFloat)alpha { Color *color =

    [[Color alloc] initWithIdentifier:[self.identifier copy] colors:[self.colors copy] alpha:alpha painter:self.painter]; return (id)color; }
  46. KPACUBO - (UIColor *)_keyedColor { UIColor *color = [self.colors valueForKey:[self

    _painter].palette.name]; color = self.alpha < 1.0 ? [color colorWithAlphaComponent:self.alpha] : color; // Here we need to cache CGColor for system usage [color CGColor]; return color; }
  47. Как использовать? view.backgroundColor = [[Color backgroundContent] colorWithAlphaComponent:0.2]

  48. None
  49. DEBUG void UpdateSchemeColor(SEL selector, UIColor *c, NSString *paletteName) { Color

    *color = (Color *)[Color performSelector:selector]; NSMutableDictionary *colors = [[color colors] mutableCopy]; [colors setValue:color forKey:paletteName]; [color updateColorsWithDictionary:[colors copy]]; }
  50. Как использовать? UpdateSchemeColor(@selector(background), [UIColor lightGray], @"light")

  51. None
  52. Шлем NSNotification, делаем -setNeedsDisplay и льем в прод

  53. Ну почти :D • -[UIView backgroundColor] - вычислимое свойство •

    -[UIView tintColor] - нужно дернуть tintColorDidChange • -[UILabel attributedText] - нужно немного зафорсить • И другие небольшие особенности UIKit’а • Дизассемблируй это - https://hopperapp.com
  54. А теперь..

  55. Обновляем 300+ экранов в проекте и спустя месяц все готово

  56. Результатики

  57. Авантюры это круто

  58. > 40% 
 наших пользователей регулярно используют темную 
 тему

    на iPhone
  59. Всегда есть решение проще (ну или почти всегда)

  60. Уважение к разработчикам UIKit’а

  61. Дизайнеры могут использовать git :D

  62. Будем 
 ВКонтакте! vk.com/ns