Как стать автором
Обновить

Комментарии 6

По этой статье можно небольшой учебник писать.
Однако, спасибо, это было интересно и полезно для меня.
До конца (пока) не дочитал, но в первом же примере вижу нарушение рекомендуемых практик по реализации MVC в iOS SDK.

View, в том числе и UITableViewCell, в идеале, не должны знать о модели приложения. И view controller должен выступать медиатором между слоями модели и представления.

Применительно к вашему примеру, ETRDocumentCell вообще не должна иметь ссылок на model layer и ETRDocument в частности. Подписываться на события об изменениях документа, соответственно, должен view controller, и вызывать -reloadRowsAtIndexPaths: из обработчика.
от автора статьи:

Статья вовсе не о том, как проектировать вьюконтроллеры, а о том, как не поиметь проблем от неосторожного использования KVO.
Тем не менее, я с вами не соглашусь, и подробно объясню, почему.
По ссылке написано только что «view objects are typically decoupled from model objects». «Обычно отделены» не значит, что их всегда необходимо отделять (иначе что?).
Если не ограничиваться схоластикой, а посмотреть на практические следствия использования конкретных наследников UITableViewCell не только как наследников UIView, но и как контроллеров для сопоставляемых им модельных объектов, то имеем вот что:
  1. Конкретные классы ячеек (так же, как и вьюконтроллеров) зачастую проектируются под конкретные модельные классы: если у вас есть ячейка с лэйблами, однозначно сопоставленными атрибутам конкретного модельного класса, то говорить о переиспользуемости или независимости такой «вьюхи» от вашей модели (а это главная причина отделения view от model) не приходится.
  2. Даже если вы можете представить себе сценарий, где какая-нибудь ячейка с полями «Фамилия», «Имя», «Отчество» будет переиспользована для представления разных модельных объектов в разных приложениях или частях одного приложения, то это можно успешно реализовать, объединив модельные классы общим протоколом (это можно сделать и в категориях), и имея в ячейке ссылку на модельные объекты, типизированную только протоколом. Если же вы не планируете ее так переиспользовать, то зачем усложнять себе жизнь?
  3. Название класса ETRDocumentCell как бы намекает на класс ETRDocument. Если бы ячейка не имела к нему отношения, ее следовало бы назвать ETRTableViewCellWithALabelAndAStarShapedButton, что бы в точности отражало ее суть. Без шуток.
  4. UIViewController как таковой не имеет отношения к модели. Он, конечно, называется контроллером, но в своей базовой реализации он контролирует вовсе не модель, а свою вьюху и другие вьюконтроллеры. В конкретных реализациях он имеет то же право работать с моделью, что и ячейки, или любые другие пользовательские реализации классов, имеющих непосредственное отношение к слою View. И критерий здесь не только в наличии класса UIView среди предков.
  5. Нетривиальные операции с моделью должны делаться не во вьюконтроллерах, а в классах-контроллерах, не имеющих к View никакого отношения вообще. Только так они будут переиспользуемы и независимы. Чтение значений атрибутов в этом смысле операция тривиальная.
  6. При работе с UIKit существует негативная тенденция к перегрузке вьюконтроллеров различными функциями (см. Massive View Controller, http://www.objc.io/issue-1/, http://doing-it-wrong.mikeweller.com/). Вынося из них любую логику, программист совершает «Добро».
  7. Только ячейка должна знать, какие у нее есть сабвьюхи, и как на них ложатся атрибуты модельного объекта. Имея эту логику снаружи ячейки, мы нарушаем ее целостность и инкапсуляцию, разносим знание о ее внутреннем устройстве по большему числу мест в проекте, и вообще творим «Зло».
  8. Вынося из UITableViewController в ячейку логику по отображению модельного объекта, можно полностью избавить вьюконтроллер от знаний об отдельных объектах. То есть вьюконтроллер работает со списком, а ячейка — с его элементами. Чем не single responsibility principle?
  9. Имея непосредственную привязку ячеек к модельным объектам, гораздо проще реализовывать любую асинхронную логику. Например, показывать прогресс бар в ячейке, соответствующей какой-нибудь загрузке. Если эта логика будет во вьюконтроллере, она будет содержать вложеннные условия и циклы с indexPath'ами и прочие радости. Да и тот же KVO по сути является такой асинхронной логикой. Можете для сравнения реализовать весь пример «по-вашему», и посмотреть, что будет аккуратнее. Вам придется либо всегда наблюдать за всеми документами, либо каким-то неочевидным образом отслеживать, для каких из них существуют ячейки. Среднестатистический программист, начав по-вашему, скорее всего сделает одну нотификацию, по которой будет делать [self.tableView reloadData], и прощай выделение ячеек, анимации, плавный скролл и так далее.
  10. Даже reloadRowsAtIndexPaths потенциально может иметь нежелательные последствия. Да и нужно-то всего лишь поменять selected у UIButton.
  11. Имея непосредственную привязку ячеек к модельным объектам, гораздо проще обрабатывать user interaction. Нажатие кнопок в ячейке может обрабатываться самой ячейкой без усложения вьюконтроллера. Да и в prepareForSegue:sender: не нужно никаких получений indexPath по ячейке с доставанием модельного объекта по нему.
  12. Я уже давно использую этот паттерн во всех ячейках, и никогда не имел от этого никаких проблем, а имел один только вышеперечисленный профит, включая вьюконтроллеры не больше 250 строк. Поэтому считаю, что имею право его всем рекомендовать.
Спасибо автору за статью и последованное изложение, погружение в KVO, для начинающих это клад.

По аналогии реализовал класс KVO всё отлично работает. По рекомендации с сеттером пришлось написать ещё небольшой макрос, который собственно и генерирует сеттер с вызовом метода stopObservation. Также по моим задачам пришлось добавить возможность следить за несколькими keyPath с одним блоком обработки, но после этого возникла необходимоть в блоке сразу передавать конкретно keyPath, для которого сработало событие. И для блока лучше сразу сделать определение в хидере класса, так удобнее его указывать в параметрах и в случае чего менять сигнатуру.

typedef void (^ETRKVOBlock)(ETRKVO *kvo, NSDictionary *change));

@property (nonatomic, copy) ETRKVOBlock block;

- (id)initWithSubject:(id)subject keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(ETRKVOBlock)block;
Прочитал статью и очень рад что пользуюсь KVO исключительно в виде оберток ReactiveCocoa. В любом случае статья очень подробная, спасибо.
Отличная статья. В общем-то, обо всех этих деталях уже знал. Но на своем горьком опыте.
Хорошо было бы прочитать эту статью в начале работы, поэтому для молодого поколения обязательно к изучению.
Ну и за большое количество пруфов огромное уважение.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий