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 посредством обратных вызовов (блокировки или закрытия).