IOS с использованием VIPER с UITableView
У меня есть контроллер представления, который содержит табличное представление, поэтому я хочу спросить, где я должен поставить источник данных таблицы и делегировать его, должен ли он быть внешним объектом, или я могу написать его в контроллере просмотра, если мы говорим об VIPER шаблон.
Обычно с использованием шаблона я делаю это:
В viewDidLoad я запрашиваю поток из презентатора, например self.presenter.showSongs()
Ведущий содержит интерактор, а в методе showSongs я запрашиваю некоторые данные от интерактора: self.interactor.loadSongs()
Когда песни готовы перейти к просмотру контроллера, я использую презентатор еще раз, чтобы определить, как эти данные должны отображаться в контроллере вида. Но мой вопрос, что мне делать с источником данных табличного представления?
Ответы
Ответ 1
Прежде всего, ваш View не должен запрашивать данные у Presenter - это нарушение архитектуры VIPER.
Вид пассивен. Он ожидает, когда докладчик предоставит контент для отображения; он никогда не запрашивает данные у докладчика.
Что касается вашего вопроса: лучше сохранить текущее состояние просмотра в Presenter, включая все данные. Потому что это обеспечивает связь между частями VIPER на основе состояния.
Но, с другой стороны, Presenter не должен ничего знать о UIKit, поэтому UITableViewDataSource и UITableViewDelegate должны быть частью уровня View.
Чтобы поддерживать ваш ViewController в хорошей форме и делать это "твердым" способом, лучше хранить DataSource и Delegate в отдельных файлах. Но эти части все же должны знать о докладчике и запрашивать данные. Поэтому я предпочитаю делать это в расширении ViewController
Весь модуль должен выглядеть примерно так:
Посмотреть
ViewController.h
extern NSString * const TableViewCellIdentifier;
@interface ViewController
@end
ViewController.m
NSString * const TableViewCellIdentifier = @"CellIdentifier";
@implemntation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.presenter setupView];
}
- (void)refreshSongs {
[self.tableView reloadData];
}
@end
ViewController + TableViewDataSource.h
@interface ViewController (TableViewDataSource) <UITableViewDataSource>
@end
ViewController + TableViewDataSource.m
@implementation ItemsListViewController (TableViewDataSource)
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.presenter songsCount];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
Song *song = [self.presenter songAtIndex:[indexPath.row]];
// Configure cell
return cell;
}
@end
ViewController + TableViewDelegate.h
@interface ViewController (TableViewDelegate) <UITableViewDelegate>
@end
ViewController + TableViewDelegate.m
@implementation ItemsListViewController (TableViewDelegate)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Song *song = [self.presenter songAtIndex:[indexPath.row]];
[self.presenter didSelectItemAtIndex:indexPath.row];
}
@end
Ведущий
Presenter.m
@interface Presenter()
@property(nonatomic,strong)NSArray *songs;
@end
@implementation Presenter
- (void)setupView {
[self.interactor getSongs];
}
- (NSUInteger)songsCount {
return [self.songs count];
}
- (Song *)songAtIndex:(NSInteger)index {
return self.songs[index];
}
- (void)didLoadSongs:(NSArray *)songs {
self.songs = songs;
[self.userInterface refreshSongs];
}
@end
Interactor
Interactor.m
@implementation Interactor
- (void)getSongs {
[self.service getSongsWithCompletionHandler:^(NSArray *songs) {
[self.presenter didLoadSongs:songs];
}];
}
@end
Ответ 2
Пример в Swift 3.1, возможно, будет полезен для кого-то:
Просмотр
class SongListModuleView: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var tableView: UITableView!
// MARK: - Properties
var presenter: SongListModulePresenterProtocol?
// MARK: - Methods
override func awakeFromNib() {
super.awakeFromNib()
SongListModuleWireFrame.configure(self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter?.viewWillAppear()
}
}
extension SongListModuleView: SongListModuleViewProtocol {
func reloadData() {
tableView.reloadData()
}
}
extension SongListModuleView: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter?.songsCount ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SongCell", for: indexPath) as? SongCell, let song = presenter?.song(atIndex: indexPath) else {
return UITableViewCell()
}
cell.setupCell(withSong: song)
return cell
}
}
Presenter
class SongListModulePresenter {
weak var view: SongListModuleViewProtocol?
var interactor: SongListModuleInteractorInputProtocol?
var wireFrame: SongListModuleWireFrameProtocol?
var songs: [Song] = []
var songsCount: Int {
return songs.count
}
}
extension SongListModulePresenter: SongListModulePresenterProtocol {
func viewWillAppear() {
interactor?.getSongs()
}
func song(atIndex indexPath: IndexPath) -> Song? {
if songs.indices.contains(indexPath.row) {
return songs[indexPath.row]
} else {
return nil
}
}
}
extension SongListModulePresenter: SongListModuleInteractorOutputProtocol {
func reloadSongs(songs: [Song]) {
self.songs = songs
view?.reloadData()
}
}
Interactor
class SongListModuleInteractor {
weak var presenter: SongListModuleInteractorOutputProtocol?
var localDataManager: SongListModuleLocalDataManagerInputProtocol?
var songs: [Song] {
get {
return localDataManager?.getSongsFromRealm() ?? []
}
}
}
extension SongListModuleInteractor: SongListModuleInteractorInputProtocol {
func getSongs() {
presenter?.reloadSongs(songs: songs)
}
}
каркасные
class SongListModuleWireFrame {}
extension SongListModuleWireFrame: SongListModuleWireFrameProtocol {
class func configure(_ view: SongListModuleViewProtocol) {
let presenter: SongListModulePresenterProtocol & SongListModuleInteractorOutputProtocol = SongListModulePresenter()
let interactor: SongListModuleInteractorInputProtocol = SongListModuleInteractor()
let localDataManager: SongListModuleLocalDataManagerInputProtocol = SongListModuleLocalDataManager()
let wireFrame: SongListModuleWireFrameProtocol = SongListModuleWireFrame()
view.presenter = presenter
presenter.view = view
presenter.wireFrame = wireFrame
presenter.interactor = interactor
interactor.presenter = presenter
interactor.localDataManager = localDataManager
}
}
Ответ 3
Очень хороший вопрос @Матросов.
Прежде всего, я хочу сказать вам, что все это касается разделения ответственности между компонентами VIPER, такими как View, Controller, Interactor, Presenter, Routing.
Это больше о вкусах, которые происходят за время разработки. Существует много архитектурных шаблонов, таких как MVC, MVVP, MVVM и т.д. Со временем, когда меняется наш вкус, мы меняем с MVC на VIPER. Кто-то изменится с MVVP на VIPER.
Используйте свое звуковое видение, сохраняя размер класса небольшим по количеству строк. Вы можете хранить методы источника данных в самом представлении ViewController или создать настраиваемый объект, соответствующий протоколу UITableViewDatasoruce.
Моя цель - держать контроллеры представлений стройными, и каждый метод и класс следуют принципу единой ответственности.
Viper помогает создавать высокосвязное и низкосоединенное программное обеспечение.
Прежде чем использовать эту модель развития, нужно иметь четкое представление о распределении ответственности между классами.
Как только у вас будет базовое понимание Oops и протоколов в iOS. Вы найдете эту модель такой же простой, как MVC.
Ответ 4
1) Прежде всего, View is passive
a не должен запрашивать данные для Ведущего. Итак, замените self.presenter.showSongs()
на self.presenter.onViewDidLoad()
.
2) На вашем презентаторе, при реализации onViewDidLoad()
, вы обычно должны звонить интерактору для получения некоторых данных. Затем интерактор вызовет, например, self.presenter.onSongsDataFetched()
3) На вашем презентаторе, при реализации onSongsDataFetched()
, вы должны ПОДГОТОВИТЬ данные в соответствии с форматом, необходимым для представления, а затем вызвать self.view.showSongs(listOfSongs)
4) В вашем представлении, при реализации showSongs(listOfSongs)
, вы должны установить self.mySongs = listOfSongs
, а затем вызвать tableView.reloadData()
5) Ваш TableViewDataSource будет работать над вашим массивом mySongs
и заполнить TableView.
Для более сложных советов и полезных рекомендаций по архитектуре VIPER я рекомендую этот пост: https://www.ckl.io/blog/best-practices-viper-architecture (включая примерный проект)
Ответ 5
Создайте класс NSObject и используйте его как пользовательский источник данных. Определите своих делегатов и источники данных в этом классе.
typealias ListCellConfigureBlock = (cell : AnyObject , item : AnyObject? , indexPath : NSIndexPath?) -> ()
typealias DidSelectedRow = (indexPath : NSIndexPath) -> ()
init (items : Array<AnyObject>? , height : CGFloat , tableView : UITableView? , cellIdentifier : String? , configureCellBlock : ListCellConfigureBlock? , aRowSelectedListener : DidSelectedRow) {
self.tableView = tableView
self.items = items
self.cellIdentifier = cellIdentifier
self.tableViewRowHeight = height
self.configureCellBlock = configureCellBlock
self.aRowSelectedListener = aRowSelectedListener
}
Объявите два типалиаса для обратных вызовов относительно одного для данных заполнения в UITableViewCell, а другой - для того, когда пользователь удаляет строку.
Ответ 6
Вот мои разные вопросы из ответов:
1, View никогда не должен запрашивать Presenter для чего-то. Просто нужно передать события (viewDidLoad()/refresh()/loadMore()/generateCell()
) в Presenter, а Presenter отвечает, какие события передал View.
2, я не думаю, что Interactor должен иметь ссылку на Presenter, Presenter связывается с Interactor посредством обратных вызовов (блокировки или закрытия).