ios - medium - viper swift 4
iOS usando VIPER con UITableView (6)
Tengo un controlador de vista que contiene una vista de tabla, así que quiero preguntar dónde debo colocar el origen de datos de vista de tabla y delegar, si es un objeto externo o puedo escribirlo en mi controlador de vista si hablamos del patrón VIPER.
Normalmente usando el patrón hago esto:
En viewDidLoad solicito un flujo del presentador como self.presenter.showSongs()
El presentador contiene interactor y en el método showSongs solicito algunos datos de interactor como: self.interactor.loadSongs ()
Cuando las canciones están listas para volver al controlador de vista, uso el presentador una vez más para determinar cómo se deben mostrar estos datos en el controlador de vista. Pero mi pregunta ¿qué debo hacer con el origen de datos de la vista de tabla?
1) En primer lugar, la vista es passive
y no debe solicitar datos para el presentador. Entonces, reemplace self.presenter.showSongs()
por self.presenter.onViewDidLoad()
.
2) En su Presentador, en la implementación de onViewDidLoad()
, normalmente debería llamar al interactor para obtener algunos datos. Y luego interactor llamará, por ejemplo, self.presenter.onSongsDataFetched()
3) En su Presentador, en la implementación de onSongsDataFetched()
debe PREPARAR los datos según el formato requerido por la Vista y luego llamar a self.view.showSongs(listOfSongs)
4) En su Vista, en la implementación de showSongs(listOfSongs)
, debe establecer self.mySongs = listOfSongs
y luego llamar a tableView.reloadData()
5) Su TableViewDataSource se ejecutará sobre su matriz mySongs
y llenará el TableView.
Para obtener consejos más avanzados y buenas prácticas útiles sobre la arquitectura VIPER, recomiendo esta publicación: https://www.ckl.io/blog/best-practices-viper-architecture (proyecto de muestra incluido)
Aquí están mis diferentes puntos de las respuestas:
1, la Vista nunca debe pedirle algo al Presentador, la Vista solo necesita pasar los eventos ( viewDidLoad()/refresh()/loadMore()/generateCell()
) al Presenter, y el Presenter responde a qué eventos pasó la Vista.
2, no creo que el Interactor deba tener una referencia al Presentador, el Presentador se comunica con el Interactor a través de devoluciones de llamada (bloqueo o cierre).
Cree una clase NSObject y úsela como fuente de datos personalizada. Defina sus delegados y fuentes de datos en esta clase.
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
}
Declare dos tipos de devoluciones de llamadas con respecto a uno para datos de relleno en UITableViewCell y otro para cuando el usuario toque una fila.
Ejemplo en Swift 3.1 , tal vez sea útil para alguien:
Ver
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
}
}
Presentador
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)
}
}
Estructura de alambre
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
}
}
En primer lugar, su Vista no debe solicitar datos a Presenter, es una violación de la arquitectura VIPER.
La vista es pasiva. Espera a que el Presentador le dé contenido para mostrar; nunca le pide al presentador los datos.
En cuanto a su pregunta: es mejor mantener el estado de vista actual en Presenter, incluidos todos los datos. Porque proporciona comunicaciones entre partes VIPER basadas en el estado.
Pero de otra manera, Presenter no debería saber nada acerca de UIKit, por lo que UITableViewDataSource y UITableViewDelegate deberían ser parte de la capa de vista.
Para mantener su ViewController en buena forma y hacerlo de forma "SÓLIDA", es mejor mantener DataSource y Delegate en archivos separados. Pero estas partes todavía deben saber sobre el presentador para pedir datos. Así que prefiero hacerlo en Extensión de ViewController.
Todo el módulo debería verse algo así:
Ver
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
Presentador
Presentador.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
Muy buena pregunta @Matrosov. En primer lugar, quiero decirles que se trata de la separación de responsabilidades entre los componentes de VIPER, como Vista, Controlador, Interactor, Presentador, Enrutamiento.
Es más sobre gustos que se cambian a lo largo del tiempo durante el desarrollo. Existen muchos patrones arquitectónicos como MVC, MVVP, MVVM, etc. A lo largo del tiempo, cuando nuestro gusto cambia, cambiamos de MVC a VIPER. Alguien cambia de MVVP a VIPER.
Use su visión de sonido manteniendo un tamaño de clase pequeño en el número de líneas. Puede mantener los métodos de fuente de datos en el propio ViewController O crear un objeto personalizado que se ajuste al protocolo UITableViewDatasoruce.
Mi objetivo es mantener los controladores de visualización delgados y todos los métodos y clases siguen el principio de responsabilidad única.
Viper ayuda a crear software altamente cohesivo y de bajo acoplamiento.
Antes de usar este modelo de desarrollo, uno debe tener una comprensión sólida de la distribución de responsabilidad entre las clases.
Una vez que tenga un conocimiento básico de Oops y Protocolos en iOS. Encontrarás este modelo tan fácil como MVC.