Все преимущества MFS в полной мере раскрываются при работе с ячейками таблицы. Цель MFS: рационально распределять обязанности между категориями ячейки, чтобы обеспечить максимальную контролируемость всех процессов и плавность в 60 FPS при прокрутке таблицы на любых устройствах. (⚠️)  Дорогие коллеги, встроенный редактор кода в Хабре является непредсказуемым.Вставки кода во время предосмотра и по итогу могут отличаться.Это значит, что могут появиться отступы и переносы в самых необычных местах, что мешает при чтении.Если вам это причиняет дискомфорт, приглашаю вас на мой гитхаб, где подобных эксцессов нет. MFS был создан потому, что стандартные методы UITableViewCell, которые нам предлагают переопределить, не отвечают современным потребностям сложных интерфейсов. Вопросы следующего характера, перечисленные ниже, по-прежнему оставались не закрытыми: В каком методе скрывать ненужные subviews у переиспользованной ячейки ? В каком методе производить перерасчет размеров и координат ? В каком методе осуществлять вставку контент в subviews ячейки ? В конечном итоге, было принято решение проанализировать опыт работы и систематизировать его на основании приобретенных знаний в готовый паттерн, который можно было бы использовать как шпаргалку при построении новых проектов. Чтобы более глубоко ознакомиться с причинами и историей создания паттерна, а также узнать, как паттерн функционирует при работе с контроллерами, рекомендуем ознакомиться с  первой частью данной статьи. Изначально паттерн получил свое название от сокращения названий категорий Managment-Frames-Styles, которые расширяли стандартный класс вью контроллера, возлагая на себя определенные функциональные обязанности. При работе с ячейками подобный подход был пересмотрен и код из категории +Managment было решено оставить в главном файле имплементации ячейки.Потому что, традиционно, класс ячейки менее нагружен, чем самый простой пример контроллера. Однако, если ваша ячейка, к примеру, вынуждена выполнять протоколы UIScrollViewDelegate,UITableViewDataSource,UITableViewDelegate и вы считаете, что главный имплементационный файл и так нагружен, то вы можете смело создать категорию +Managment и вынести весь код отвечающий за построения UI в нее. Предназначение каждой из категорий хорошо описано в таблице ниже: Название категории Обязанности +Management (Optional for cells) Содержит методы дополнительного жизненного цикла интерфейса, которые инициализируют, добавляют на экран, наполняют контентом и совершают прочие действия. +Frames Содержит pure функции вычисляющие размеры и координаты subviews. +Styles Содержит методы графической конфигурации subviews.Например задает цвет,закругление,шрифт, итд.Как правило для каждого property существует отдельный метод настройки.  Не секрет, что имея целый набор специальных методов для создания UI, в зависимости от функциональных обязанностей, которые стоят перед отдельной ячейкой, порядок их вызовов может несколько отличаться. На схеме, расположенной выше, приведен усредненный пример цепочки вызванных методов для построения UI ячейки. Различий, о которых шла речь, может быть множество, мы же перечислим только некоторые из них: Если все ячейки таблицы содержат однотипный контент (например список друзей пользователя), то вызывать каждый раз метод removeSubviews, который удаляет все subviews с contentView, является нецелесообразно. При перевороте таблицы системой вызывается метод layoutSubviews, что также порождает ветвление алгоритма в зависимости от ситуации, где-то требуется вызывать только resizeSubviews, где-то еще и bindDataFrom, а в некоторых случаях требуется полностью перерисовать интерфейс ячейки.  Функциональные обязанности главных методов Имя метода Принимает ли вьюМодель Предназначение prepareUI ❌ Главный метод построения UI, вызывает нужную последовательность методов.Данный метод рекомендуется вызывать из initWithStyle:reuseIdentifier:. removeSubviews ❌ Удаляет все subviews с superView.А также обнуляет все проперти на UI элементы. initSubviews ✅ Инициализирует нужные subviews. updateStyles ✅ Вызывает индивидуальные методы настройки для каждого subviews, куда также передает вьюМодель, на основании данных которой может быть принято решение относительно стилей.Например, задает разный цвет для плашек сообщений, в зависимости от пола пользователя. bindDataFrom ✅ Вставляет данные из вьюМодели в subviews. resizeSubviews ✅ Вызывает индивидуальные методы расчета размеров и координат для каждой subviews. addSubviewsToSuperView ❌ Добавляет subviews на superView ,если те были проинициализированы и не добавлены на родительское представление ранее. Из выше перечисленных методов явно прослеживается виденье того, как должен строиться UI. Порядок действий при первой инициализации ячейки: Инициализация нужных subviews. Обновление стилей subviews (цвета/размер шрифта итд). Вставка данных в subviews. Расчет и установка корректных frames для subviews. Добавление полностью готовых subviews на родительское представление. Порядок действий при пере-использовании ячейки: Вызывается сеттер установки вьюМодели setViewModel, который вызывает остальные методы по цепочке. Обновляются стили subviews (например увеличивается отступ когда пост содержит четырехзначное число лайков)updateStyles. Происходит расчет новыхframes для subviews на основании данных новой вьюМодели. Осуществляется биндинг данных. Порядок действий при перевороте таблицы: Системой вызывается метод layoutSubviews. А в переопределенной реализации layoutSubviews мы вызываем resizeSubviews.  ⚠️ Еще раз вынужден повторить, порядок и набор вызываемых методов описанных выше, ЯВЛЯЕТСЯ ОБОБЩЕННЫМ примером, в зависимости от разной сложности интерфейса, вам придется самостоятельно конфигурировать схему вызовов.  Разбор методов мы будем осуществлять на примере ячейки поста из социальной сети.Обратите внимание, на то, что демо-приложение из примера использует архитектуру MVVM.Где: View - отвечает исключительно за UI пользователя. ViewModel - является промежуточным звеном между view и model.Хранит методы обертки (преобразует данные из модели для отображения во view), хранит различные кэшируемые данные (высоту ячейки, frames внутренних элементов итд). Model - хранит сырые данные.    Главным камнем преткновения использования таблицы является вопрос пере-использования ячеек. Перед началом работы вы должны решить - ячейки в вашей таблице буду всегда инициализировать один и тот же набор subviews, а потом, если это требуется, скрывать их в отдельных ячейках (например кнопку read more... вы можете проинициализировать в каждой ячейки, но в тех ячейках, в которых текст не превышает лимит, эту кнопку вам потребуется скрыть) - или вы будете каждый раз удалять не нужные subviews с contentView и инициализировать нужные. От этого решения (о котором подробнее будет написано ниже) будет зависеть порядок вызовов методов рисования. Первым в нашей цепочке идет системный метод инициализации ячейки.В нем мы сразу вызываем метод prepareUI, который подготовит весь UI ячейки.Помните, что initWithStyle:reuseIdentifier: как правило вызывается всего несколько раз за все время использования таблицы, так как ячейки пере-используются. Метод создания ячейки всегда вызывается раньше, чем сеттер ее вьюМодели, поэтому, на момент исполнения метода prepareUI ее вьюМодел равняется nil.Следовательно, у вас имеется всего два пути: Вызывать prepareUI из initWithStyle:reuseIdentifier:.Так можно делать если каждая ячейка содержит один и тот же ограниченный набор subviews.Если же ячейки все-таки имеют некую вариативность, например в посте может отсутствовать либо присутствовать фото, тогда рекомендуется не удалять каждый раз subviews, а просто в методе bindDataFrom скрывать ненужные элементы. Вызывать prepareUI из setViewModel.Так нужно делать, если содержимое ячейки очень вариативное и содержать общий набор проинициализированных subviews вы просто не можете. Обратите внимание, что в этом методе происходит чистая инициализация, без каких- либо настроек. Также алгоритм имеет встроенную защиту от повторной инициализации subviews. В данном методе мы вызываем функции определенные в категории +Styles.Они настраивают цвет, шрифт, иконки, селекторы и т.д. для subviews.Обратите внимание на использование некого проперти isStylized.Данное проперти динамически добавлено в класс UIView соответствующей категорией.Дело в том, что некоторые subview имеет смысл конфигурировать всего один раз, ввиду крайней трудоемкости этого процесса, а isStylized используется как флаг, который оповещает алгоритм, о том, что данное проперти уже настроено. Далее в нашей цепочке идут методы resizeSubviews и bindDataFrom, которые имеют одну схожую проблему.Так, как выполнение данных методов может быть достаточно трудозатратным процессом, нам необходимо выработать условия, которые не будут позволять выполнять код внутри этих функций, если не произошло критическое изменение данных или каких-либо других внешних условий. "Холостой" вызов методов может произойти по вине самой системы, которая часто вызывает такие системные методы как layoutSubviews или же setFrame по нескольку раз без видимых на то для нас причин. Для предотвращения "холостых" вызовов мы должны добавить два проперти в нашу ячейку. Чтобы предотвращать "холостой" биндинг данных, ваша viewModel должна самостоятельно реализовать метод isEqualToModel, который должен сравнивать две вьюмодели и определять, имеют ли они критическое отличие друг от друга, которое нужно отобразить на UI, или нет.Если бы мы в качестве примера делали простую ячейку, которая содержит только текст, то в методе isEqualToModel проверяли бы текст на идентичность… Реализовываем ту самую конструкцию, которая ограничивает повторные выполнения кода внутри метода. И вызываем функции из категории +Frames, которые пересчитывают размеры и координаты для subviewsячейки. Вставляем преобразованные данные из viewModel в subviews, также скрываем или раскрываем subviews. Алгоритм имеет защиту от повторного добавления subviews на superView. Переопределяем layoutSubviews и вызываем из него resizeSubviews для адаптации subviews под новый размер таблицы.  Так же, как и было сказано ранее, на примере других функций, та цепочка методов, которая вызывается из данного сеттера, является индивидуальной для каждого случая.Где-то понадобится вызывать только bindDataFrom, где-то понадобится вызывать целый набор, который будет удалять, растягивать, пересчитывать и вставлять. Для того, чтобы обеспечить максимальную плавность интерфейса, нам необходимо предпринять следующие шаги: Постараться вынести всю работу по вычислению координат subviews в фоновые потоки. Постараться кэшировать ранее вычисленные данные, чтобы не нагружать процессор вычислениями, которые он совершал прежне. В том числе, по этой же причине мы не используем Autolayout, потому что он представляет собой систему линейных неравенств, что негативно сказывается на скорости вычисления frames для subviews, поскольку при каждом добавлении даже, как нам кажется, самого незначительного элемента на viewячейки, происходит стремительно усложнение неравенства.На которое тратится все больше и больше процессорного времени. В нашей парадигме каждый кэш может принадлежать одной из двух сущностей: Если subviews ячеек данного класса всегда имееют одни и те же размеры и коордианты, вне зависимости от модели данных, тогда кэш имеет смысыл сделать собственностью класса ячейки. Если subviews основываясь на собственную модель данных может иметь разные frames, тогда кэш со значениями будет хранить объект viewModel данной конкретной ячейки. Для того, чтобы начать кэшировать вычисленные значения размеров и координат subviews, нам нужно классифицировать все возможные типы ячеек, что поможет нам понять, по какому принципу и сколько нужно создавать словарей для кэширования. Первый тип Для ячеек первого типа характерны следующие черты: Как правило, данные ячейки однотипны и однообразны. Размеры и координаты всех subviews заранее предопределены.Пример: таблица контактов, frames аватарки и ФИО предопределены заранее. Если и будет иметь место какое-то различие между ячейками данного класса на основании данных полученных из их viewModel, то все frames при таком сценарии тоже должны быть жестко определены.Пример: отображение времени последнего выхода в сеть (если оно не скрыто настройками приватности), тогда этот UILabel будет отображать под основным. Не всегда, но как правило, ячейки этого типа имеют одну и ту же высоту. Если имеется возможность "развернуть" ячейку, то параметры данного разворота также строго описаны. Рациональным подходом будет всего один раз вычислить frames каждого из subviews, поскольку для каждой ячейки они будут одними и тем же, а затем сохранить значение в кэш. Второй тип Для ячеек второго типа характерны следующие черты: Размеры и координаты subviews всегда индивидуальны и зависят от данных представленных во viewModel ячейки. Высота ячейки является статичной и зависит от данных внутри viewModel (может меняться в зависимости от ориентации). В качестве примера можно привести ячейку, которая содержит текстовое сообщение пользователя.Высота ячейки зависит от длины сообщения.Непосредственный frame для UILabel будет храниться внутри словаря subviewFrames во viewModelячейки. Третий тип Для ячеек третьего типа характерны следующие черты: имеет сложный, составной UI. могут содержать subviews с динамическими размерами и координатами.(то есть могут разворачиваться и сворачиваться). ячейка может менять высоту. при смене ориентации могут изменяться как размеры, так и координаты subviews. Хорошим примером является ячейка, которая содержит пост пользователя. Текст может расширяться, что, во-первых, изменяет размеры самого текста и также меняет координаты subviews расположенных ниже его самого. И во-вторых, это меняет высоту самой ячейки. Для поддержки кэширования значений ячеек нам потребуется три различных кэша.Чтобы избежать путаницы в названиях, ниже будет приведена таблица с определениями каждого из кэшей. Название Кто хранит strong ссылку Функциональная нагрузка +subviewStaticFrames UITableViewCell Хранят frames тех subviews, которые не зависят от данных из viewModel. -subviewFrames ViewModel Хранят frames тех subviews, которые напрямую зависят от данных из viewModel.В кэше хранятся frames только тех subviews , которые не меняют свое местоположение относительно других subviews.Хороший пример это поле с текстом в посте, его {x,y} всегда одни и те же. -subviewSizes ViewModel Хранит размеры динамических subviews, например текст, который может разворачиваться и сворачиваться.А также хранит размеры тех subviews, которые располагаются под каким-либо динамическим элементом.  Самой основополагающей чертой является тот факт, что словари не должны иметь строгой типизации.Это делается для того, чтобы пользователь мог хранить дополнительные флаги или значения, которые будут помогать строить ему UI.В кэшах имеется только одно традиционное деление - это деление на ширину экрана, оно нужно для того, чтобы поддерживать вариативность размеров и координат для разных ориентаций. Структура +subviewStaticFrames и -subviewFrames Кэши имеют одинаковую структуру, а разница заключается только в том, что первый хранит универсальные значения для всех ячеек данного класса, а второй хранит уникальные значения для каждой отдельной ячейки.  Структура -subviewSizes Как было написано выше, словари не должны иметь строгой типизации, ключ "baseCellHeight", который находится в словаре, является наглядной демонстрацией этого правила. В данном примере мы видим деление на minSize и maxSize, все деления являются условными и могут быть адаптируемые под ваш случай, например, вам ничто не мешает внедрить ключ middleSize, если на то будет надобность.  Для более глубокого понимания какие кэши использовать в каких ситуациях, предлагаю обезличить пост пользователя и взглянуть на него как на набор прямоугольников, которые могут меняться в размерах и координатах. Зеленым цветом представлены элементы frames которых хранятся в словаре +subviewStaticFrames,который принадлежит классу ячейки.Это сделано потому, что frames этих элементов всегда один и тот же, вне зависимости от модели данных, различие может быть только при смене ориентации. Синим цветом представлены элементы frames которых хранятся в словаре -subviewFrames,который принадлежит вьюМодели конкретного экземпляра ячейки.Это сделано потому, что size этих элементов может различаться, потому что напрямую зависит от модели данных.Обратите внимание, на то что, у данных subviews могут отличаться только размеры, а {x.y} для каждой из ориентаций являются статичными. Красным цветом представлены элементы sizes которых хранятся в словаре -subviewSizes, который принадлежит вьюМодели конкретного экземпляра ячейки. В словаре хранятся именно их размеры, а не координаты, это сделано потому что, их координаты могут изменяться в зависимости от frames их соседних subviews. То есть, хранить {x,y} галереи фотографий не имеет смыслы, потому что они изменятся, если текст будет развернут.Соответственно их {x,y} вычисляется каждый раз путем вызова метода, который вычисляет framesвысшего subview, а затем мы самостоятельно прибавляем отступ к эту frame. Демонстрация работы с словарем +subviewsStaticFrames  Традиционно в качестве примера мы будем использовать ячейку, отображающую фото и имя друга нашего пользователя. Сам словарь непосредственно объявляется как внутренние проперти в категории +Frames. В итоге после вычислений для первой ячейки в нашем кэше будут значения, которые мы будем использовать при построении других экземпляров ячеек данного класса.  Демонстрация работы с словарем -subviewsFrames Для примера случая, где может пригодиться -subviewsFrames мы выбрали ячейку, которая содержит текстовое сообщение пользователя. На этот раз кэш-словарь содержит непосредственно экземпляр вьюМодели ячейки. Ну и собственно - сам метод вычисления.Структура -subviewsFrames будет такая же, как и у +subviewsStaticFrames.  Демонстрация работы с словарем -subviewSizes На сей раз, в качестве примера, будет рассмотрен случай, когда UI элемент может разворачиваться.Это будет UILabel в ячейке поста пользователя. Кэш-словарь также содержит вьюМодель ячейки. Если вы ранее вызывали метод и он уже закэшировал размер текстового поля, то метод также способен возвращать разные размеры, в зависимости от значения внутри флага isExpandText, хранящегося в viewModel. По нажатию на кнопку "read more" вы будете менять значение переменной, и тогда метод будет возвращать вам нужный размер.  Важной составляющей частью работы таблицы является вычисление высоты ячеек.В нашей парадигме этот процесс должен выглядеть следующим образом: Поскольку мы используем MVVM в качестве архитектурного паттерна для всего приложения, то каждое view (в том числе и ViewController) должно иметь собственную viewModel.В случае с TableViewController его viewModel должен содержать массив с вьюМоделями ячеек, на основании которых строится сама таблица. Обязательной для каждой ячейки является наличие метода +calculateCellHeightFromVM:tableSize:, в который мы передаем вьюМодель ячейки, а данный метод вычисляет и кэширует ее высоту. Непосредственно процесс вычисления выглядит так: Получаем viewModel ячейки по индексу из массива. Вызываем вспомогательный метод +getClassNameByViewModelCell, который возвращает название UIкласса ячейки по ее вьюМодели. Конструкция if проверяет наичие данного метода у класса ячейки, и если он имеется, то вызывает соответствующий метод. Метод-помощник возвращает название класса ячейки пользуясь функцией NSStringFromClass().  К слову говоря метод cellForRowAtIndexPath имеет такую же философию, что и heightForRowAtIndexPath. Реализация метода +calculateCellHeightFromVM:tableSize: может быть свободной. В некоторых случаях вы можете возвращать строго заданную константу, в некоторых вычислять координаты самого нижнего UI элемента ячейки и прибавлять к нему отступ.Ниже будут приведены несколько типичных реализаций: FriendCell Для этой ячейки высота задана несколькими константами.  MessageCell В реализации расчета высоты для ячейки сообщения у нас вычисляется размеры текстового поля и к нему каждый раз прибавляется отступ.То есть, сама высота не кэшируется, кэшируется только размер главного subview.  WallPostCell В случае с ячейкой содержащей пост пользователя нам нужно обеспечить максимальную плавность и производительность при быстрой прокрутке.Для достижения этой цели мы будем кэшировать не только размеры subviews, а еще и высоту самой ячейки, чтобы выиграть дополнительные миллисекунды для избежания доп.вычислений. Реализация вспомогательных методов по типу -isCalculatedHeight: тут приведена не будет, по причине своей ненадобности.Стоит лишь только сказать, что все это разного рода методы обертки, которые сокращают синтаксис при работе со словарями содержащими значения. Для сложно составных ячеек, как правило, не хватает просто вызывать rectFor_ метод возвращающий frame самого нижнего элемента, а требуется вызвать все методы вычисляющие все необходимые framesдля всех subviews.Что собственно и делает метод обертка calculateCoordinatesForVM, который просто вызывается все rectFor_ методы подряд, тем самым кэширует значения для всех subviews.  Итак, последнюю вещью, которую мы можем предпринять для обеспечения максимальной плавности и производительности - это инициировать процесс вычисления размеров и координат в фоновом потоке. Как правило, подобная возможность у нас появляется в методе scrollViewDidScroll, когда по достижению нижней границы таблицы мы запускаем процесс подгрузки новых данных.Тогда, после получения вьюМоделей мы можем вызвать методы кэширования, которые будут выполняться на фоновом потоке.  Фото: Петр Дудченко  Выпустив SwiftUI, Apple конечно же частично купирует проблему поддержки сложного UI для будущих приложений. Но оставляет без всякого внимания абсолютное большинство уже существующих, которые по естественным причинам не могут использовать SwiftUI в своих проектах. Паттерн MFS является своего рода ответом на кричащие потребности сегодняшнего многомиллионного рынка приложений. Архитектурная грамотность, с которой был построен паттерн, дает возможность имплементировать его на устройствах, начиная буквально с самых первых версий iOS. Что, в свою очередь, позволяет использовать решение таким гигантам рынка, как банковские приложения и социальные сети, которые должны охватывать максимальное количество действующих устройства. Уверен в том, что в ближайшем обозримом будущем отрасль породит около десятка похожих решений, где философия MFS будет играть не последнюю роль. Одна из главных задач MFS - это инициировать диалог в среде разработчиков и архитекторов, о том как всем нам грамотно отреагировать на данные изменения рынка.