web-dev-qa-db-fra.com

iOS utilisant VIPER avec UITableView

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?

17
Matrosov Alexander

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
17
Konstantin

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
    }
}
6
jonaszmclaren

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.

4
Sandeep Ahuja

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)

3
Marcelo Gracietti

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).

0
Meonardo

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.

0
Haspinder Singh