Русский
Русский
English
Статистика
Реклама

MFS паттерн построения UI в iOS приложениях

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

MFS - позволяет создавать современный дизайн приложений и при этом избежать такого явления как MassiveViewController.

Фото: 10 years of the App Store: The design evolution of the earliest apps - 9to5Mac

Причины создания паттерна

Времена когда весь интерфейс контроллера настраивался целиком в методеviewDidLoadдавно закончились.
Настройка графических компонентов и пользовательскогоlayoutстала занимать сотни строк кода.
Помимо массивной настройки и инициализации, встает второй немало важный вопрос поддержки и управления созданными активами.

О существующих трудностях знают в Купертино, на изменение требований рынка компания отреагировала выпуском перспективной технологиейSwiftUI.
Которая, к сожалению, имеет ряд серьезных ограничений, например поддержка отiOS 13 и выше.
Что на данный момент - абсолютно неприемлемо для большинства солидных приложений, которые стараются охватить максимально большую аудиторию пользователей.

MFS - же напротив ориентирован на поддержку и разгрузку уже существующих приложений.
А набор используемых технологий позволяет внедрять паттерн на самую большую аудиторию приложений.

Архитектурный паттернMFS(Managment-Frames-Styles) был разработан, для того чтобы соответствовать духу времени и его потребностям.

Кому может понадобиться MFS ?

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

Если же ваше приложение имеет крайне простой интерфейс, то возможно более оптимальным будет использование стандартногоStoryboard иAutolayout.

Отношение к Autolayout и другим подобным технологиям

Поскольку исторически паттерн создавался именно для реализации сложных дизайнов, поэтому в стандартной своей реализацииMFS- имеет ручное вычисление всехframe, без использования каких-либо сторонних библиотек.

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

Обзор паттерна

ПаттернMFS призван равномерно распределить обязанности между категориями класса, с целью избежания возникновенияMassiveViewController.

Название категории

Обязанности

+Managment

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

+Frames

Содержит методы вычисляющие размеры и координаты subviews.

+Styles

Содержит методы графической конфигурации subviews.
Например задает цвет,закругление,шрифт, и т.д.
Как правило, для каждого property существует отдельный метод настройки.

Как стало вам понятно, категория+Managment является главенствующей, она содержит дополнительные методы, которые полностью забирают на себя все хлопоты по созданию и обслуживанию интерфейса контроллера.
Остальные же категории содержат так называемыечистые функции, которые более широко известны под названиемpure function.

Функция является "чистой" если она мутирует только те значения, которые создала самостоятельно или же приняла из собственных параметров.


То есть, если функция изменяет некие глобальные переменные, которые не были переданы ей в параметры, то "чистой" она не является.

Обратите внимание, на то, что методы категории+Frames ВСЕГДА должны быть чистыми.
Методы же категории+Stylesмогут быть чистыми по усмотрению пользователя, поскольку это не так критично.

На главный файл имплементации контроллера (ViewController.m) ложится обязанность выполнять протоколы различных представлений (напр.:UITableViewDelegate,UITableViewDataSource), а также содержать методыIBAction.


Выше были перечисленны основные обязанности, но в случае острой необходимости, вы должны самостоятельно принимать решение - размещать некий функционал в файле имплементации или же вынести его в отдельную категорию.
Также стоит заметить, что вся бизнес-логика содержится во вьюМодели вашего контроллера или же во вьюМоделях егоsubviews.

Подобная целенаправленная политика позволяет умещать такие сложные контроллеры, как профиль пользователя и его стену, всего в около~300 строчек кода наObjC и вероятно еще меньше - наSwift.

В свою очередь, такая лаконичность полностью исключает побочный эффект в виде "бесконечного скроллинга" и поиска нужного метода.

Все методы распределены строго по категориям и если нам, например, потребуется изменить размер шрифта или поменятьlayout, мы с легкостью откроем соответствующий файл и не будем бесконечно скролить основной код контроллера.

Порядок вызова методов построения UI

На схеме ниже показаны методы и порядок их вызовов для построенияUI вUIViewController.
Как мы можем увидеть процесс построенияUI начинается из методаviewDidAppear, который вызывает методprepareUI.

Который в свою очередь, по цепочке, вызывает все остальные.
Также надо сказать, что иногда, при перевороте экрана или же при установке новой вьюМодели, нам требуется вызывать разный набор методов - все зависит от непосредственной сложности вашего интерфейса.

На самых простых представлениях, после переворота экрана, нам потребуется вызывать только методresizeSubviews, на более сложных, где нужно, например скрывать некоторые элементы для определенных ориентаций, там может понадобиться и вызовupdateStyles или жеbindDataFrom.

Обзор контроллера

Подобный обзор паттерна будет проводиться на примере демо приложения.
Данный контроллер входа в приложение имеет различную верстку для разных ориентаций.

В портретной ориентации кнопки находятся друг под другом, а в горизонтальной находятся на одной высоте, с разных сторон экрана.

Даже такой минималистичный дизайн является частным случаем труднореализуемого интерфейса с помощью применения стандартногоAutolayout.

Ниже представлены.h/.m контроллера.
Обратите внимание, что помимо стандартного набор проперти, наш контроллер имеет две достаточно необычных, в привычном понимании, переменных.

@interface LoginController : UIViewController// ViewModel@property (nonatomic, strong, nullable) LoginViewModel* viewModel;@property (nonatomic, strong, nullable) LoginViewModel* oldViewModel;// UI@property (nonatomic, strong, nullable) UIImageView* logoImgView;@property (nonatomic, strong, nullable) UIButton* signInButton;@property (nonatomic, strong, nullable) UIButton* signUpButton;@property (nonatomic, strong, nullable) CAGradientLayer *gradient;@property (nonatomic, assign) CGSize oldSize;#pragma mark - Actions- (void) signUpBtnAction:(UIButton*)sender;- (void) signInBtnAction:(UIButton*)sender;#pragma mark - Initialization+ (LoginController*) initWithViewModel:(nullable LoginViewModel*)viewModel;@end


Речь идет оoldViewModel иoldSize - эти проперти помогают избегать лишних перерисовок и вставок данных.
Подробней о них будет рассказано в разборах отдельных категорий.

@interface LoginController ()@end@implementation LoginController#pragma mark - Life cycle- (void) viewDidAppear:(BOOL)animated{    [super viewDidAppear:animated];    [self prepareUI];}- (void)viewWillTransitionToSize:(CGSize)size      withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{    __weak LoginController* weak = self;    [coordinator animateAlongsideTransition:nil             completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {        [UIView animateWithDuration:0.3                               delay:0                            options:UIViewAnimationOptionCurveEaseOut  animations:^{            [weak resizeSubviews:weak.viewModel];        } completion:nil];    }];}#pragma mark - Action- (void) signUpBtnAction:(UIButton*)sender{    [self.viewModel signUpBtnAction];}- (void) signInBtnAction:(UIButton*)sender{    [self.viewModel signInBtnAction];}#pragma mark - Getters/Setters- (void)setViewModel:(LoginViewModel *)viewModel{    _viewModel = viewModel;      if ((!self.oldViewModel) &amp;&amp; (self.view)){         [self prepareUI];    } else if ((self.oldViewModel) &amp;&amp; (self.view)){        [self bindDataFrom:viewModel];        [self resizeSubviews:viewModel];    }}#pragma mark - Initialization+ (LoginController*) initWithViewModel:(nullable LoginViewModel*)viewModel{    LoginController* vc = [[LoginController alloc] init];    if (vc) {        vc.viewModel = (viewModel) ? viewModel : [LoginViewModel defaultMockup];    }    return vc;}@end

Просмотрев код расположенный выше, можно сказать, что в результате декомпозиции основной файл контроллера остается крайне лаконичным.

Обзор методов категории +Managment

Имя метода

Принимает ли вьюМодель

Предназначение

prepareUI

Главный метод построения UI, вызывает нужную последовательность методов.
Данную функцию рекомендуется вызывать из viewDidAppear.

removeSubviews

Удаляет все subviews с superView.
А также обнуляет все проперти на UI элементы.

initSubviews

Инициализирует нужные subviews.

updateStyles

Вызывает индивидуальные методы настройки для каждого subviews, куда также передает вьюМодель, на основании данных которой может быть принято решение относительно стилей.
Например, задает разный цвет для плашек сообщений в зависимости от пола пользователя.

bindDataFrom

Вставляет данные из вьюМодели в subviews.

resizeSubviews

Вызывает индивидуальные методы расчета размеров и координат для каждой subviews.

addSubviewsToSuperView

Добавляет subviews на superView если те были проинициализированы и не добавлены на родительское представление ранее.

postUIsetting

Здесь должна происходить настройкаsubviews для которых не было создано уникальных методов конфигурации по причине их ненадобности.
Например, тут будет настраиватьсяstatusBar,gestures,allowSelectation и тд.

Из выше перечисленных методов явно прослеживается виденье того, как должен строиться UI в приложении:

  1. Удаление всехsubviewsи обнуление всех проперти наUIэлементы.
    (Если того требует ситуация).

  2. Инициализация нужныхsubviews.

  3. Обновление стилейsubviews (цвет/размер шрифта итд).

  4. Вставка данных вsubviews.

  5. Расчет и установка корректныхframesдляsubviews.

  6. Добавление полностью готовыхsubviews на родительское представление.

Реализация методов категории +Managment

Для того чтобы внутриviewDidAppear не вызывать целый набор методов, был придуман метод-оберткаprepareUI,который вызывает методы категории в нужной последовательности.

Обратите внимание, на методыresizeSubviews иbindDataFrom, порядок их вызовов в некоторых ситуациях может быть прямо противоположенный.

Например, некоторые библиотеки, кэширующие изображения из интернета, возвращают картинку не в полном разрешении, а уже заранее подготовленную под размер вашегоUIImageView, тогда, если вы сначала попытаетесь вставить картинку в рамку размером0x0, у вас может произойти ошибка.

В классическом сценарии сначалаsubviews наполняются данными, а потом производится расчет размеров и координат.

/*----------------------------------------------------------------------  Основной метод построения интерфейса.   Вызывает нужную последовательность методов ----------------------------------------------------------------------*/- (void) prepareUI{    if (self.view){        [self removeSubviews];        [self initSubviews:self.viewModel];        [self updateStyles:self.viewModel];        [self bindDataFrom:self.viewModel];        [self resizeSubviews:self.viewModel];        [self addSubviewsToSuperView];        [self postUIsetting];    }}


Метод удаления всехsubviews с родительского представления.
При работе с контроллером нужен - только в исключительных случаях.

Например, если данные во viewModel могут динамически меняться и набор с расположениемsubviews зависит от вариативностиviewModel, то есть для одной вьюМодели у вас будет один наборsubviews, в определенном месте, а для вьюМодел с другим набором данных, будут иные subviews с прочимиUIэффектами.

Тогда при сменеviewModel имеет смысл вызывать неbindDataFrom иresizeSubviews, а полноценный методprepareUI, потому что он вызовет всю цепочку, которая прежде всего удалит все старые представления.

/*---------------------------------------------------------------------- Удаляем все `subviews` и обнуляем все проперти на UI элементы. ----------------------------------------------------------------------*/- (void) removeSubviews{    // removing subviews from superview    for (UIView* subview in self.view.subviews){        [subview removeFromSuperview];    }    // remove sublayers from superlayer    for (CALayer* sublayer in self.view.layer.sublayers) {        [sublayer removeFromSuperlayer];    }    self.logoImgView   = nil;    self.signInButton  = nil;    self.signUpButton  = nil;    self.gradient      = nil;}


Обратите внимание, что в этом методе происходит чистая инициализация, без каких-либо настроек.

/*---------------------------------------------------------------------- Инициализирует нужные subviews на основе данных из viewModel ----------------------------------------------------------------------*/- (void) initSubviews:(LoginViewModel*)viewModel{  if (self.view)  {   if (!self.logoImgView)  self.logoImgView  = [[UIImageView alloc] init];   if (!self.signInButton) self.signInButton = [UIButton buttonWithType:UIButtonTypeCustom];   if (!self.signUpButton) self.signUpButton = [UIButton buttonWithType:UIButtonTypeCustom];  }}


МетодupdateStyles вызывает индивидуальные методы для каждого изsubviews, с целью настроить их внешний вид.

/*----------------------------------------------------------------------  Задает стили для subviews. Цвета/размера шрифта/селекторы для кнопок ----------------------------------------------------------------------*/- (void) updateStyles:(LoginViewModel*)viewModel{    if (!viewModel) return;    if (self.logoImgView)  [self styleFor_logoImgView:self.logoImgView   vm:viewModel];    if (self.signInButton) [self styleFor_signInButton:self.signInButton vm:viewModel];    if (self.signUpButton) [self styleFor_signUpButton:self.signUpButton vm:viewModel];}


Во время декларации.hфайла контроллера, фигурировало пропертиoldViewModel.
В данном случае оно понадобилось нам для осуществления проверки на идентичность моделей.
Если вьюМодели идентичны, то биндинга данных не произойдет.

Традиционно подобная конструкция чаще используется при работе с ячейками, но в некоторых случаях может потребоваться и при работе с контроллером.

/*---------------------------------------------------------------------- Связывает данные из вьюМодели в subviews ----------------------------------------------------------------------*/- (void) bindDataFrom:(LoginViewModel*)viewModel{    // Если модели идентичны, то биндинга данных не происходит    if (([self.oldViewModel isEqualToModel:viewModel]) || (!viewModel)){        return;    }    [self.logoImgView setImage:[UIImage imageNamed:viewModel.imageName]];    [self.signInButton setTitle:viewModel.signInBtnTitle forState:UIControlStateNormal];    [self.signUpButton setTitle:viewModel.signUpBtnTitle forState:UIControlStateNormal];    self.oldViewModel = viewModel;}


МетодisEqualToModel в каждом отдельном случае имеет разную реализацию.
Например, может возникнуть ситуация, когда в ваш контроллер устанавливается новая вьюМодель, но основные данные, критически важные данные не отличаются, а были изменены только второстепенные проперти, которые не отображаются в вашемUI.

Тогда методisEqualToModel должен вернуть значениеNO, чтобы избежать повторного биндинга данных.

В нашем случае он имеет подобную реализацию:

/*---------------------------------------------------------------------- Сравнивает модели данных на индетичность. ----------------------------------------------------------------------*/- (BOOL) isEqualToModel:(LoginViewModel*)object{    BOOL isEqual = YES;    if (![object.imageName isEqualToString:self.imageName]){        return NO;    }    if (![object.signInBtnTitle isEqualToString:self.signInBtnTitle]){        return NO;    }    if (![object.signUpBtnTitle isEqualToString:self.signUpBtnTitle]){        return NO;    }    return isEqual;}

Так же как и вbindDataFrom, методresizeSubviews в самом начале имеет условие проверки, которое не позволяет повторно вычислять размеры и координаты дляsubviews, если модель данных или размер родительского представления не был изменен.

/*---------------------------------------------------------------------- Вызывает индивидуальные методы расчета размеров и координат для subviews.  После изменения ориентации или после первой инициализации. ----------------------------------------------------------------------*/- (void) resizeSubviews:(LoginViewModel*)viewModel{    // Выходим если модель данных и размеры одни и те же    if ((([self.oldViewModel isEqualToModel:self.viewModel]) &&        (CGSizeEqualToSize(self.oldSize, self.view.frame.size))) || (!viewModel)) {        return;    }    if (self.view){      if (self.logoImgView)  self.logoImgView.frame  = [LoginController rectFor_logoImgView:viewModel  parentFrame:self.view.frame];      if (self.signInButton) self.signInButton.frame = [LoginController rectFor_signInButton:viewModel parentFrame:self.view.frame];      if (self.signUpButton) self.signUpButton.frame = [LoginController rectFor_signUpButton:viewModel parentFrame:self.view.frame];      if (self.gradient)     self.gradient.frame     =  self.view.bounds;    }    self.oldSize = self.view.frame.size;}


Добавляемsubviews на родительскоеview.

/*---------------------------------------------------------------------- Добавляет subviews на superView ----------------------------------------------------------------------*/- (void) addSubviewsToSuperView{    if (self.view){        if ((self.logoImgView)  &amp;&amp; (!self.logoImgView.superview))   [self.view addSubview:self.logoImgView];        if ((self.signInButton) &amp;&amp; (!self.signInButton.superview))  [self.view addSubview:self.signInButton];        if ((self.signUpButton) &amp;&amp; (!self.signUpButton.superview))  [self.view addSubview:self.signUpButton];    }}


На этом этапе все методы категории+Managment были разобраны и остался единственный метод пост-настройки, который принадлежит категории+Styles.

В методеpostUIsetting мы настраиваемUI компоненты, для которых не создали индивидуальных методов.
Например, в нем можно добавлятьgestures, настраивать таблицу, устанавливать цвет статус бара и т.д.

- (void) postUIsetting{    UIColor* firstColor  =    [UIColor colorWithRed: 0.54 green: 0.36 blue: 0.79 alpha: 1.00];       UIColor* secondColor =    [UIColor colorWithRed: 0.41 green: 0.59 blue: 0.88 alpha: 1.00];;    self.gradient = [CAGradientLayer layer];    self.gradient.frame      = self.view.bounds;    self.gradient.startPoint = CGPointZero;    self.gradient.endPoint   = CGPointMake(1, 1);    self.gradient.colors     = [NSArray arrayWithObjects:(id)firstColor.CGColor,                           (id)secondColor.CGColor, nil];    [self.view.layer insertSublayer:self.gradient atIndex:0];}

Реализация методов категории +Styles

В отличии от категории+Managment,+Styles не имеет системных методов, а лишь содержит индивидуальные методы настройкиUI компонентов.
Ниже будет приведен один из методов.

- (void) styleFor_logoImgView:(UIImageView*)imgView   vm:(LoginViewModel*)viewModel{    if (!imgView.isStylized){        imgView.contentMode = UIViewContentModeScaleAspectFit;        imgView.backgroundColor = [UIColor clearColor];        imgView.opaque = YES;        imgView.clipsToBounds       = YES;        imgView.layer.masksToBounds = YES;        imgView.alpha      = 1.0f;        imgView.isStylized = YES;    }}


Обратите внимание на некое пропертиisStylized, оно было добавлено категорией к каждому наследнику классаUIView.

Традиция использовать данную переменную при настройкеUIэлементов пришла от опыта работы с ячейками в таблице.

Создана была для того, чтобы при переиспользовании однотипных ячеек не производилась повторная настройка (уже настроенных элементов), то есть, чтобы еще раз не добавлялись тени, блюры и т.д.

Реализация методов категории +Frames

Категория+Frames также как и+Styles не имеет системных методов, но может иметь словари класса, в которых могут быть расположены закэшированные размеры и координатыsubviews.

Подробный листинг данных методов публиковать не имеет смысла из-за их громоздкости, по сути, там не происходит ничего интересного, стандартное ручное вычисление координат и размеров.

Но стоит также обратить внимание, что по сравнению с другими категориями,+FramesсодержитИСКЛЮЧИТЕЛЬНОметоды класса (+).

Данная традиция также пришла из опыта работы с ячейками, первоначально методы класса были созданы для того, чтобы в фоне можно было заранее вычислить координаты и размеры для всехsubviewsне имея фактического объекта ячейки, а имея только лишь ее вьюМодель.

+ (CGRect) rectFor_signUpButton:(LoginViewModel*)viewModel                     parentFrame:(CGRect)parentFrame{    if (CGRectEqualToRect(CGRectZero, parentFrame)) return CGRectZero;    // Calculating...    return rect;}

Советы и рекомендации

По собственному опыту использования могу сказать, что имплементироватьMFSможно выборочно.

Например, как правило, имеет смысл создавать подобную архитектуру в тех случаях, когда мы имеем сложныйUIViewController(UIдля которого ввиду его сложности мы создаем кодом), или же когда имеем сложные ячейки таблицы.

То есть, для контроллера классаUITableViewController, за исключением особых случаев - смысла имплементировать данное решение нет.

Заключение

В данной статье вы имели возможность ознакомиться с паттерномMFS на примере работы с вьюКонтроллерами.

Во второй части статьи мы поговорим о примененииMFSпри работе с ячейками таблицы, как обеспечивать60FPSпри быстром скроллинге сложных таблиц на старых девайсах.

Источник: habr.com
К списку статей
Опубликовано: 26.01.2021 12:18:23
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка под ios

Objective c

Дизайн мобильных приложений

Ios

Ui

Objective-c

Mfs

Autolayout

Uikit

Massive view controller

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru