<![CDATA[Max Kazakov]]>http://localhost:2368/http://localhost:2368/favicon.pngMax Kazakovhttp://localhost:2368/Ghost 2.22Tue, 04 Jun 2019 17:25:08 GMT60<![CDATA[История факапа #1]]>Планируя работы на февраль 2019 года по основному проекту, на котором работаю, я подумал, что неплохо было бы зарефакторить код, ответственный за парсинг данных. Так вышло, что в процессе рефакторинга я, сам того не подозревая, заложил в кодовую базу бомбу замедленного действия.

Предыстория

2 года назад наша команда внедряла в

]]>
http://localhost:2368/fuckup-1/5cd5d398806356e203eb810fFri, 10 May 2019 19:40:25 GMTПланируя работы на февраль 2019 года по основному проекту, на котором работаю, я подумал, что неплохо было бы зарефакторить код, ответственный за парсинг данных. Так вышло, что в процессе рефакторинга я, сам того не подозревая, заложил в кодовую базу бомбу замедленного действия.

Предыстория

2 года назад наша команда внедряла в функционал одного из модулей приложения технологию Couchbase + Sync Gateway. В двух словах: технология позволяет синхронизировать БД между приложением и сервером. Внедряли мы ее с целью избавиться от недостатков http протокола. Идея простая — http запросов для нас нет, есть постоянная синхронизация БД на сервере и на клиенте. Любой запрос на клиенте/ответ сервера клиенту реализуется при помощи записи в БД. Обе стороны слушают изменения в своей БД и реагируют на них должным образом. Такая абстракция позволяет не задумываться о том, в каком состоянии находятся http запросы. Просто настраиваем репликатор (сущность, которая синхронизирует БД) и поехали. Есть недостатки вида "как решать конфликты", "как управлять потоком синхронизации" и т.д., но сегодня не об этом.

Couchbase Lite (библиотека для работы с БД на устройстве) поддерживает работу с Data Access Object (DTO), например, как NSManagedObject в CoreData, или Object в Realm. Но сроки и нежелание чуть больше завязываться на возможности конкретной библиотеки привели нас к быстрому решению использовать Dictionary<String: Any> для мапинга моделей из БД в код и обратно. Поэтому мы написали несколько удобных расширений для этого типа и были довольны. У нас не было уверенности в технологии, мы пробовали, поэтому абстрагирование реализации моделей данных от конкретной реализации БД выглядело логично.

Работает — не трогай

От технологии мы отказались, перешли на новую, самописную (кросплатформенные с++ фреймворки). Переход был быстрым, в том числе благодаря абстрагированию от источника данных. Новая технология не реализует DTO, и отдает нам в качестве моделей голые строки. Т.е. модели в БД представлены строками, а значит, парсим каждый раз при чтении из БД (очевидно, бьет по производительности, но некритично). Это важно отметить, т.к. в дальнейшем это только усилит последствия факапа.

Парсинг не требовал изменений при переходе, работал отлично. Но было решено переехать на Codable, по ряду причин:

  • Убрать из процесса парсинга лишнее звено в виде Dictionary
  • Избавиться от кода в extension Dictionary<String: Any>
  • Воспользоваться возможностями автогенерации
  • Сделать парсинг, хорошо понятный любому разработчику

Codable

За пару дней был проведен рефакторинг всех структур данных, их в проекте около 50. Здесь поделюсь парой моментов, которые расстроили и которые редко описываются в туторах по Codable.

Структуры данных реализованы как классы, а значит, используют наследование. Есть иерархии по 4 наследника. Codable не дает использовать автогенерацию для классов-потомков, чем очень расстроил и вынудил писать (а точнее переписывать) много кода. Причина, по которой так происходит, — компилятор не в силах понять, нужно ли классу потомку наследовать родительский конструктор или нужно сгенерировать код для Codable. Об этом хорошие размышления здесь.

Второй небольшой, но неприятный момент — Codable не позволяет игнорировать тип данных. Т.е. если по ключу лежит json-объект, то нельзя просто засунуть его в Data/String в процессе парсинга. Иногда хочется забрать данные, а уже потом поглядеть, что там в них. Или, к примеру, парсить вложенный объект должны не мы, а кто-то другой. В таких ситуациях может выручить библиотека вроде этой либо можно попрактиковаться со своей реализацией. Вот также отличная статья, где автор описывает проблемы с Codable, возникшие при внедрении в проект.

Факап

Пример кода на swift, который все поломал

enum State: Int, Decodable {
    case unknown = -1
    case new = 0
    case deleted = 1
}

class Model: Decodable {
    let state: State
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.state = (try container.decodeIfPresent(State.self, forKey: .state)) ?? .unknown
    }
    
    enum CodingKeys: CodingKey {
        case state
    }
}

decodeIfPresent может выкинуть единственную ошибку — type mismatch. Приложение упадет, если по ключу лежит тип данных, который мы не ожидаем. В случае с enum типом, неподдерживаемое значение Int в ключе state вызовет падение.

Так и случилось. На бекенд выкатили обновление с новым кейсом за 5 дней до релиза мобилки, которая этот кейс поддерживает. Старые приложения начали падать. Нужно было срочно решать проблему со стороны бекенда.

Решение

Чтобы правильно понять, где именно нужно фиксить, очень схематично представим, как взаимодействуют сервисы и приложения

В этой схеме наша моделька с неподдерживаемым enum-ом летит из прикладных сервисов (Service 1, 2, 3) в сервис событий (Event Service), сохраняется в БД этого сервиса и дальше летит в приложение и сохраняется в локальную БД.

Решения пошагово:

1) Выкатываем на прикладные сервисы хотфикс, который отдает по ключу "state" только те значения, которые поддерживаются текущим приложением. Новый расширенный enum теперь будет передаваться в новом ключе (есть достаточно времени, чтобы поддержать новый ключ в грядущих релизах мобилок).

2) Проходимся скриптом по БД в сервисе событий и делаем фикс, аналогичный пункту 1.

Предприняв эти шаги, видим, что в Firebase продолжают падать краши. Есть некоторая часть пользователей, которые успели в локальную БД затянуть невалидные значения. Проблемы можно было избежать, если бы при получении данных из сети мы парсили ее в нормальную структуру БД, а не в строку.

Теперь нужно как-то почистить локальную БД. Обновление приложения решило бы эту проблему, но до релиза еще пара дней. Переустановить приложения, обзвонив пользователей, — жестко. При логауте чистится локальная БД, значит можно попросить пользователей перелогиниться, но они не могут, т.к. приложение падает сразу же при запуске (падающий модуль запускается первым). Значит, можно разлогинить пользователей удаленно. В крашах Firebase находим id пользователей, которые упали, бекендер пишет скрипт для логаута, запускает его. Проблема решена.

Вместо вывода

Причина ошибки не в том, что я слишком строго парсил данные с сервера, а в том, что элементарно забыл про обратную совместимость. На мой взгляд, кодогенерация для Codable (например Sourcery) не позволила бы избежать проблем в данном случае, т.к. ошибка была сделана именно на уровне логики парсинга. Учитывая тот факт, что число моделей постоянно растет, есть планы прикрутить кодогенерацию, чтобы в том числе исключить человеческий фактор при рефаторинге кода.

]]>
<![CDATA[Autoresizing Layout]]>На мой взгляд, инструменты верстки UI в iOS далеко не идеальны и обладают высоким порогом вхождения (в отличии, например, от html). Будучи начинающим iOS разработчиком, я тратил огромное количество времени на попытки понять принципы работы constraints, циклы layout-a и т.д. Autoresizing — один из инструментов, который помогает iOS разработчику покрыть

]]>
http://localhost:2368/autoresizing-layout/5cd4125f806356e203eb80c1Thu, 09 May 2019 11:44:18 GMTНа мой взгляд, инструменты верстки UI в iOS далеко не идеальны и обладают высоким порогом вхождения (в отличии, например, от html). Будучи начинающим iOS разработчиком, я тратил огромное количество времени на попытки понять принципы работы constraints, циклы layout-a и т.д. Autoresizing — один из инструментов, который помогает iOS разработчику покрыть часть простых кейсов, связанных с версткой. Цель данной статьи — поближе познакомиться с этим инструментом.

Способы верстки в iOS

На сегодняшний день UIKit предоставляет разработчикам 2 способа управления версткой: ручной (или по-другому верстка на фреймах) и Autolayout.

Под ручной версткой понимается управление размерами и позицией view вручную, путем изменения свойств frame (либо center и bounds). Как правило, данный способ реализуется при помощи переопределения метода layoutSubviews UIView, на момент вызова которого, нам известно свойство bounds текущего экземпляра. В этом методе мы вручную определяем позицию и размеры дочерних вью.

Autolayout использует набор ограничений (constraints), которые описывают, как будут располагаться вью относительно друг друга. В отличие от ручного способа, здесь нет необходимости высчитывать frame каждый раз, когда в нашей верстке что-то изменилось, Autolayout сделает это за нас. Данный способ также допускает ручную настройку frame в случае, если мы хотим подкорректировать результат лейаута. Для этого нужно переопределить layoutSubviews, вызывать родительскую реализацию этого метода (здесь дочерние вью получат свои размеры из движка Autolayout), скорректировать фреймы дочерних вью.

Каждый из способов обладает своими преимуществами и недостатками, описание которых достойно отдельной статьи, мы же остановим наше внимание на Autoresizing.

Основные принципы Autoresizing

Autoresizing работает всегда, когда мы решили использовать ручную верстку (и в некоторых случаях при использовании Autolayout, но об этом чуть позже). Autoresizing основан на двух основных понятиях: springs (пружины) и struts (распорки). Перевод этих терминов точно отражает их смысл: springs — что-то, что растягивается, struts — что-то, что всегда имеет фиксированный размер. Данные понятия применимы к следующим параметрам разметки:
1) Внутренние — Width, Height. Описывают размер вью.
2) Внешние — Left, Right, Top, Bottom. Описывают положение вью внутри родителя — отступ от левого, правого, верхнего и нижнего края соответственно.

Рассмотрим простой пример того, как это работает. Допустим, у нас есть задача: необходимо, чтобы дочерняя вью всегда располагалась по центру родительской и пропорционально изменяла размер при изменении размера родителя (Рис. 1). Решением в данном случае будет установить struts для всех внешних параметров, и springs для всех внутренних параметров.

Рис. 1. Синий прямоугольник — родительская вью, красный — дочерняя.

Другой пример: необходимо разместить вью с фиксированным размером в правом нижнем углу родительской вью (Рис. 2). Для это необходимо установить struts для параметров Right и Bottom, а также для всех внутренних параметров, т.к. расстояния от нижнего и правого края до родителя и размер нашей вью строго зафиксированы. Также нужно установить springs для Left и Top, т.к. эти параметры будут изменяться при изменении размеров родительской вью.

Рис. 2. Вью с фиксированным размером и привязкой к правому нижнему углу

Autoresizing в коде

Единственный способ регулировать Autoresizing в коде — свойство autoresizingMask у UIView. Данное свойство имеет тип OptionSet со следующим набором опций:

  • flexibleLeftMargin
  • flexibleWidth
  • flexibleRightMargin
  • flexibleTopMargin
  • flexibleHeight
  • flexibleBottomMargin

Как данное свойство соотносится со stings и struts, внешними и внутренними параметрами разметки? Каждая опция в autoresizingMask — это springs для определенного параметра, а отсутствие опции — автоматически означает struts.

Теперь попробуем решить первую задачу с центрированием дочерней вью (Рис. 1). Для этого нам необходимо задать свойство autoresizingMask, как показано на фрагменте кода ниже.

    // Создаем родительскую view
    let v1 = UIView(frame: CGRect(x: 100, y: 250, width: 200, height: 200))
    v1.backgroundColor = .blue
    
    // Создаем дочернюю вью
    let v2 = UIView(frame: v1.bounds.insetBy(dx: 30, dy: 30))
    v2.backgroundColor = .red
    
    // Добавление в иерархию
    self.view.addSubview(v1)
    v1.addSubview(v2)
    
    // Устанавливаем autoresizing
    v2.autoresizingMask = [.flexibleWidth, .flexibleHeight]

Установив значение свойства [.flexibleWidth, .flexibleHeight], мы говорим, что хотим сделать изменяемые значения для параметров width и height (springs), а для остальных свойств оставить фиксированные значения (struts).

Вторая задача решается при помощи задания значения [.flexibleTopMargin, .flexibleLeftMargin]. Мы не указали в опциях размер, а также нижний и правый отступ, а значит, эти параметры будут оставаться постоянными (Рис. 2).

По умолчанию свойство autoresizingMasks является пустым. Что произойдет, если оставить значение по умолчанию? Дочерняя вью будет всегда сохранять фиксированные отступы сверху и слева, а также фиксированный размер, при этом расстояние снизу и справа будут меняться. Это означает, что пустой autoresizingMasks равносилен [.flexibleBottomMargin, .flexibleRightMargin].

Autoresizing в Interface Builder

Interface Builder также позволяет конфигурировать autoresizingMasks. Для этого необходимо перейти во вкладку Size Inspectors, редактор Autoresizing выглядит как показано на рисунке ниже.

Рис 3. Редактирование Autoresizing в Interface Builder

Здесь активные внешние параметры означают struts, неактивные — springs. Для внутренних параметров все ровно наоборот.

translatesAutoresizingMaskIntoConstraints

Вью начинает быть задействованной в расчете на Autolayout как только мы добавили ей или одной из ее дочерних вью constraint. В таком случае, если задать constraints только для дочерней вью, но не задавать для родительской, мы можем получить предупреждение, которое сообщает о том, что для родительской вью Autolayout не смог определить размер и позицию ("Position and size are ambiguous"). Такая ситуация могла возникнуть когда Autolayout только появился в iOS — новые вью верстались с использованием constraints, а старые оставались на ручном лейауте (он же Autoresizing, как мы выяснили ранее). Для того, чтобы избежать этого, разработчики Apple ввели свойство translatesAutoresizingMaskIntoConstraints. Если свойство имеет значение true, то для вью будут автоматически созданы constraints, которые однозначно определяют позицию и размер. Во View Hierarchy мы можем увидеть автоматически сгенерированные constraints.

Рис. 4. Автоматически сгенерированные constraints

Значение false отключает эту магию. При создании вью из кода — значение свойства — true, при создании из xib/storyboard — false. В Interface Builder свойство регулируется при помощи флага “Use Auto Layout", которое является инвертированным значением translatesAutoresizingMaskIntoConstraints.

Очень часто возникает ситуация, когда мы хотим создать нашу вью из кода и сверстать ее на Autolayout, т.е. явно задать ей constraints. Чтобы наши и автоматически сгенерированные constraints не конфликтовали, необходимо задать значение translatesAutoresizingMaskIntoConstraints в false.

Заключение

Очевидно, что с приходом Autolayout механизм Autoresizng утратил актуальность. Однако, его понимание необходимо как минимум, чтобы осознанно избегать конфликтов c constraints. Также данных механизм может быть полезен для простой верстки вью относительно родителя.

]]>