iOS using VIPER with UITableView
Asked Answered
R

6

23

I have a view controller which contains a table view, so I want to ask where should I put table view data source and delegate, should it be an external object or I can write it in my view controller if we say about VIPER pattern.

Normally using pattern I do this:

In viewDidLoad I request some flow from presenter like self.presenter.showSongs()

Presenter contains interactor and in showSongs method I request some data from interactor like: self.interactor.loadSongs()

When songs are ready to passing back to view controller I use presenter one more time to determine how this data should be display in view controller. But my question what should I do with datasource of table view?

Roughspoken answered 21/7, 2016 at 17:5 Comment(2)
What approach did you end up taking?Grogshop
@Ríomhaire, just for now I have instances in my presenter that called tableViewDataSource and tableViewDelegate.Roughspoken
W
22

First of all your View shouldn't ask data from Presenter - it's violation of VIPER architecture.

The View is passive. It waits for the Presenter to give it content to display; it never asks the Presenter for data.

As for you question: It's better to keep current view state in Presenter, including all data. Because it's providing communications between VIPER parts based on state.

But in other way Presenter shouldn't know anything about UIKit, so UITableViewDataSource and UITableViewDelegate should be part of View layer.

To keep you ViewController in good shape and do it in 'SOLID' way, it's better to keep DataSource and Delegate in separate files. But these parts still should know about presenter to ask data. So I prefer to do it in Extension of ViewController

All module should look something like that:

View

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

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
Whist answered 4/8, 2016 at 12:36 Comment(6)
thanks for answer. I also agree with you that datasource/delegate should be as a separate file. Nice solution with extension!Roughspoken
As stated here mutualmobile.github.io/blog/2013/12/04/viper-introduction Interactor never passes Entities to the presentation layer (i.e. Presenter). In your case it does. How to resolve this problem?Interlock
In these article it means that you shouldn't pass any objects except PONSO to Presenter, for example objects from CoreData should be converted to PONSO Objects. You still need pins entities in presenter to convert them to display data for View. They write about it in the article: "The Presenter also receives results from an Interactor and converts the results into a form that is efficient to display in a View."Whist
In this call Song *song = [self.presenter songAtIndex:[indexPath.row]];, it is the view that is actually asking the presenter for data. Runs contrary to your first sentence.Fuscous
I know this is old but I would really like to know the answer to @Fuscous statement. Im at this juncture right now. Anyone know the answerConfocal
This is fine but what happens/should happen when presenter gets data from model which first loads data locally starts updating view, then at the same time call server for update of the data, saves this data and starts update again? What happens to concurrency and how is this supposed to be handled? Basically issue is that after numberOfRows has been returned data in presenter changes and number of rows has changed since update from the server.Novena
P
11

Example in Swift 3.1, maybe will be useful for someone:

View

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

Wireframe

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
    }
}
Plyler answered 16/8, 2017 at 9:13 Comment(0)
S
5

1) First of all, View is passive an should not ask data for the Presenter. So, replace self.presenter.showSongs() by self.presenter.onViewDidLoad().

2) On your Presenter, on the implementation of onViewDidLoad() you should normally call the interactor to fetch some data. And interactor will then call, for example, self.presenter.onSongsDataFetched()

3) On your Presenter, on the implementation of onSongsDataFetched() you should PREPARE the data as per the format required by the View and then call self.view.showSongs(listOfSongs)

4) On your View, on the implementation of showSongs(listOfSongs), you should set self.mySongs = listOfSongs and then call tableView.reloadData()

5) Your TableViewDataSource will run over your array mySongs and populate the TableView.

For more advanced tips and helpful good practices on VIPER architecture, I recommend this post: https://www.ckl.io/blog/best-practices-viper-architecture (sample project included)

Struck answered 11/4, 2017 at 1:48 Comment(1)
View shouldn't store the data array. In your example you have to set self.mySongs = listOfSongs. I guess we have to get all the data from the presenter.Midrash
W
4

Very good question @Matrosov. First of all I want to tell you that, it's all about responsibility segregation among VIPER components such as View, Controller, Interactor, Presenter, Routing.

It's more about tastes one changes over the time during development. There are many architectural patterns out there like MVC, MVVP, MVVM etc. Over time when our taste changes, we change from MVC to VIPER. Someone changes from MVVP To VIPER.

Use your sound vision by keeping class size small in number of lines. You can keep datasource methods in ViewController itself Or create a custom object that conforms to UITableViewDatasoruce protocol.

My aim to keep view controllers slim and every method and class follow Single responsibility principle.

Viper helps to create highly cohesive and low coupled software.

Before using this model of development, one should have sound understanding of distribution of responsibility among classes.

Once you have basic understanding of Oops and Protocols in iOS. You will find this model as easy as MVC.

Womenfolk answered 3/8, 2016 at 11:2 Comment(2)
thanks for answer! yea I understood how VIPER works, but usually we get data in interactor form services and then we need to update data model, so I guess then my datasource should has interactor instance, and then when interactor says that it contains data we need to reload datasource via view controller.Roughspoken
@MatrosovAlexander Use interactor for communication with persistent and network layer. Once data is available, inform to presenter. Presenter can inform to object that conforms to datasource and delegate protocol.Womenfolk
P
0

Create an NSObject Class and use it as the custom data source. Define your delegates and datasources in this class.

 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 two typealias for call backs regarding one for fill data in UITableViewCell and another one for when the user taps a row.

Proudlove answered 1/8, 2016 at 14:22 Comment(0)
Q
0

Here are my different points from the answers:

1, View should never ask Presenter for something, View just need to pass the events(viewDidLoad()/refresh()/loadMore()/generateCell()) to the Presenter, and the Presenter responses which events the View passed to.

2, I don't think the Interactor should have a reference to the Presenter, the Presenter communicates with Interactor via callbacks(block or closure).

Quantize answered 8/11, 2017 at 14:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.