$30 off During Our Annual Pro Sale. View Details »

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

CodeFest
April 06, 2019

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

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

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

CodeFest

April 06, 2019
Tweet

More Decks by CodeFest

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

  3. Предпосылки 

    от iOS
    Зачем?
    Мессенджер Создать хайп

    View Slide

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

    View Slide

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

    View Slide

  6. Палитра

    View Slide

  7. Палитра!

    View Slide

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

    View Slide

  9. RGB

    View Slide

  10. HUE

    View Slide

  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));

    View Slide

  12. Визуальная проверка

    View Slide

  13. Обновляем код, создаем 

    50+ багов, исправляем их и
    приступаем к следующему
    важному шагу

    View Slide

  14. ТЕМНАЯ 

    ТЕМА

    View Slide

  15. Условия
    Максимально простое решение
    Минимум изменений в текущем коде
    Работа с CoreGraphics (CALayer’s and etc.)
    Работа с TextKit, CoreText (NSAttributedString)
    Работа с UIKit
    VKUI (VK Apps, VK Connect)
    Android (а вдруг?)

    View Slide

  16. Ух, что сейчас будет
    • Быстро подберем формат
    • Подумаем над хранением
    • Определимся с архитектурой (VIPER)
    • Чуть-чуть покодим

    View Slide

  17. Формат для всех

    View Slide

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

    View Slide

  19. Светлая схема
    "client_light": {
    "appearance": "light",
    "colors": {
    "accent": {
    "color_identifier": "blue_300"
    }
    }
    }

    View Slide

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

    View Slide

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

    https://github.com/VKCOM/Appearance

    View Slide

  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

    View Slide

  23. VKUI (VK Apps, VK Connect)
    доволен, дизайнеры
    согласны, мы счастливы

    View Slide

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

    View Slide

  25. Библиотеки
    https://github.com/regexident/Gestalt 

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

    (много кода, нет поддержки NSAttributedString)
    И вообще их нужно поддерживать, в топку такое

    View Slide

  26. if (dark) {} else {}
    Нет

    View Slide

  27. CoreGraphics
    int CGColorCreate(int space, int components) {
    rax = (*_CGColorCreate_ptr)(space, components);
    return rax;
    }
    void CGContextSetFillColorWithColor(CGContextRef, CGColorRef)

    View Slide

  28. UIKit
    @interface UIDeviceRGBColor : UIColor {
    double alphaComponent
    double blueComponent
    double greenComponent
    double redComponent
    }
    @interface UIDeviceWhiteColor : UIColor {
    double alphaComponent
    double whiteComponent
    }

    View Slide

  29. Мы не хотели (конечно хотели,
    кого мы обманываем), но
    придется писать самостоятельно

    View Slide

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

    View Slide

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

    View Slide

  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;
    }

    View Slide

  33. Appearance
    @interface Appearance : NSObject
    @property (nonatomic, strong) Palette *_Nonnull palette;
    + (Appearance *_Nonnull)appearance;
    @end

    View Slide

  34. Как использовать?
    view.backgroundColor = [Color backgroundContent]
    imageView.tintColor = [Color accent]
    @{ NSForegroundColorAttributeName : [Color accent] }

    View Slide

  35. View Slide

  36. Независимая палитра?
    @protocol Painter
    - (nullable Palette *)palette;
    @end

    View Slide

  37. Независимая палитра!
    @interface Appearance : NSObject
    @property (nonatomic, strong, nullable) Palette *palette;
    @end
    @interface UIResponder (Painter)
    @property (nonatomic, strong, nullable) Palette *palette;
    @end

    View Slide

  38. Добавим конструктор
    - (instancetype)dependedWithPainter:(id)painter {
    return [[Color alloc] initWithIdentifier:[self.identifier copy]
    colors:[self.colors copy]
    alpha:self.alpha
    painter:painter];
    }

    View Slide

  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;
    }

    View Slide

  40. Как использовать?
    view.backgroundColor = [[Color backgroundContent] dependedWithPainter:self]

    View Slide

  41. View Slide

  42. Цвета с сервера?
    - (instancetype)overridedValues:(NSDictionary *)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];
    }

    View Slide

  43. Как использовать?
    view.backgroundColor =
    [[Color backgroundContent] overridedValues:@{ @"light" : [UIColor textColor] }]

    View Slide

  44. View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  47. Как использовать?
    view.backgroundColor =
    [[Color backgroundContent] colorWithAlphaComponent:0.2]

    View Slide

  48. View Slide

  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]];
    }

    View Slide

  50. Как использовать?
    UpdateSchemeColor(@selector(background), [UIColor lightGray], @"light")

    View Slide

  51. View Slide

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

    View Slide

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

    View Slide

  54. А теперь..

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  58. > 40% 

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

    тему на iPhone

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. Будем 

    ВКонтакте!
    vk.com/ns

    View Slide