J'ai un contrôleur de vue qui contient une vue de table. Je souhaite donc savoir où placer la source de données de vue de table et le déléguer, qu'il s'agisse d'un objet externe ou que je puisse l'écrire dans mon contrôleur de vue si nous parlons du modèle VIPER.
Normalement, en utilisant un motif, je fais ceci:
Dans viewDidLoad, je demande un flux au présentateur tel que self.presenter.showSongs()
Presenter contient interactor et dans la méthode showSongs, je demande des données à un interacteur comme: self.interactor.loadSongs ()
Lorsque les chansons sont prêtes à être redirigées vers le contrôleur de vue, j'utilise encore une fois le présentateur pour déterminer le mode d'affichage de ces données dans le contrôleur de vue. Mais ma question que dois-je faire avec la source de données de la vue table?
Tout d’abord, votre vue ne doit pas demander de données à Presenter - c’est une violation de l’architecture de VIPER.
La vue est passive. Il attend que le présentateur lui donne le contenu à afficher; il ne demande jamais de données au présentateur.
En ce qui concerne votre question: Il est préférable de conserver l’état actuel de la vue dans Presenter, y compris toutes les données. Parce qu'il fournit des communications entre les parties VIPER en fonction de l'état.
Mais d’une autre manière, Presenter ne devrait rien savoir d’UIKit, donc UITableViewDataSource et UITableViewDelegate devraient faire partie de la couche View.
Pour maintenir ViewController en bon état et le faire de manière "SOLID", il est préférable de conserver DataSource et Delegate dans des fichiers séparés. Mais ces parties doivent encore savoir sur le présentateur pour demander des données. Donc, je préfère le faire dans l'extension de ViewController
Tous les modules devraient ressembler à quelque chose comme ça:
Vue
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
Présentateur
Présentateur.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
Interacteur
Interactor.m
@implementation Interactor
- (void)getSongs {
[self.service getSongsWithCompletionHandler:^(NSArray *songs) {
[self.presenter didLoadSongs:songs];
}];
}
@end
Exemple dans Swift 3.1 , sera peut-être utile pour quelqu'un:
Vue
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
}
}
Présentateur
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()
}
}
Interacteur
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)
}
}
Filaire
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
}
}
Très bonne question @Matrosov. Tout d’abord, je tiens à vous dire que c’est la séparation des responsabilités entre les composants VIPER tels que View, Controller, Interactor, Presenter, Routing.
Il s’agit plus de goûts que l’on change au cours du développement. Il existe de nombreux modèles architecturaux tels que MVC, MVVP, MVVM, etc. Au fil du temps, lorsque notre goût change, nous passons de MVC à VIPER. Quelqu'un passe de MVVP à VIPER.
Utilisez votre vision sonore en limitant le nombre de lignes à la taille de la classe. Vous pouvez conserver les méthodes de source de données dans ViewController même ou créer un objet personnalisé conforme au protocole UITableViewDatasoruce.
Mon objectif est de garder les contrôleurs de vue minces et chaque méthode et classe suivent le principe de responsabilité unique.
Viper aide à créer un logiciel hautement cohésif et faiblement couplé.
Avant d'utiliser ce modèle de développement, il faut bien comprendre la répartition des responsabilités entre les classes.
Une fois que vous avez compris les notions de base de Oops et de Protocoles sous iOS. Vous trouverez ce modèle aussi simple que MVC.
1) Tout d’abord, View est passive
et ne doit pas demander de données pour le présentateur. Donc, remplacez self.presenter.showSongs()
par self.presenter.onViewDidLoad()
.
2) Sur votre présentateur, lors de l’implémentation de onViewDidLoad()
, vous devriez normalement appeler l’interacteur pour récupérer des données. Et l'interacteur appellera alors, par exemple, self.presenter.onSongsDataFetched()
3) Sur votre présentateur, lors de l’implémentation de onSongsDataFetched()
, vous devriez PRÉPARER les données selon le format requis par la vue, puis appeler self.view.showSongs(listOfSongs)
.
4) Sur votre vue, lors de l'implémentation de showSongs(listOfSongs)
, vous devez définir self.mySongs = listOfSongs
, puis appeler tableView.reloadData()
.
5) Votre TableViewDataSource s’exécutera sur votre tableau mySongs
et remplira le TableView.
Pour des astuces plus avancées et des bonnes pratiques utiles sur l'architecture VIPER, je vous recommande cet article: https://www.ckl.io/blog/best-practices-viper-architecture (exemple de projet inclus)
Voici mes différents points des réponses:
1, View ne doit jamais demander quelque chose à l'animateur, View doit simplement transmettre les événements (viewDidLoad()/refresh()/loadMore()/generateCell()
) à l'animateur et le présentateur répond aux événements auxquels il a été transmis.
2, je ne pense pas que l'interacteur devrait avoir une référence au présentateur, le présentateur communique avec Interactor via des rappels (blocage ou fermeture).
Créez une classe NSObject et utilisez-la en tant que source de données personnalisée. Définissez vos délégués et sources de données dans cette classe.
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
}
Déclarez deux typealias pour les rappels concernant l'un pour les données de remplissage dans UITableViewCell et l'autre pour l'utilisateur lorsque vous tapez sur une ligne.