iOS 将 VIPER 与 UITableView 结合使用
iOS using VIPER with UITableView
我有一个包含 table 视图的视图控制器,所以我想问一下我应该把 table 视图数据源和委托放在哪里,它应该是外部对象还是我可以写如果我们谈论 VIPER 模式,它在我的视图控制器中。
通常使用模式我这样做:
在 viewDidLoad 中,我请求来自演示者的一些流程,例如 self.presenter.showSongs()
Presenter 包含交互器,在 showSongs 方法中我从交互器请求一些数据,例如:self.interactor.loadSongs()
当歌曲准备好传回视图控制器时,我再次使用 Presenter 来确定这些数据应该如何在视图控制器中显示。但我的问题是我应该如何处理 table 视图的数据源?
创建一个 NSObject Class 并将其用作自定义数据源。在此 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
}
声明两个类型别名用于回调,一个用于 UITableViewCell 中的填充数据,另一个用于当用户点击一行时。
很好的问题@Matrosov。
首先我想告诉你,这都是关于 VIPER 组件之间的职责分离,如 View、Controller、Interactor、Presenter、Routing。
更多的是关于口味在开发过程中随着时间的推移而发生的变化。有许多架构模式,如 MVC、MVVP、MVVM 等。随着时间的推移,当我们的品味发生变化时,我们会从 MVC 变为 VIPER。有人从 MVVP 变成了 VIPER。
通过保持 class 行数较小的大小来使用您的声音视觉。您可以将数据源方法保留在 ViewController 本身中,或者创建一个符合 UITableViewDatasoruce 协议的自定义对象。
我的目标是让视图控制器保持苗条,并且每个方法和 class 都遵循单一责任原则。
Viper 有助于创建高内聚和低耦合的软件。
在使用这种开发模式之前,应该对 class 之间的责任分配有充分的了解。
一旦您对 iOS 中的 Oops 和协议有了基本的了解。你会发现这个模型和 MVC 一样简单。
首先,您的视图不应该向 Presenter 请求数据——这违反了 VIPER 架构。
视图是被动的。它等待 Presenter 给它显示内容;它从不向 Presenter 询问数据。
至于你的问题:
最好在 Presenter 中保留当前视图状态,包括所有数据。因为它提供基于状态的 VIPER 部件之间的通信。
但换句话说,Presenter 不应该知道任何关于 UIKit 的信息,所以 UITableViewDataSource 和 UITableViewDelegate 应该是 View 层的一部分。
为了让您 ViewController 保持良好状态并以 'SOLID' 方式进行,最好将 DataSource 和 Delegate 保存在单独的文件中。但是这些部分仍然应该知道presenter 请求数据。所以我更喜欢在 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.m
@implementation Interactor
- (void)getSongs {
[self.service getSongsWithCompletionHandler:^(NSArray *songs) {
[self.presenter didLoadSongs:songs];
}];
}
@end
1) 首先,View 是 passive
不应该为 Presenter 请求数据。因此,将 self.presenter.showSongs()
替换为 self.presenter.onViewDidLoad()
.
2) 在您的 Presenter 上,在执行 onViewDidLoad()
时,您通常应该调用交互器来获取一些数据。然后交互器将调用,例如 self.presenter.onSongsDataFetched()
3) 在您的 Presenter 上,在执行 onSongsDataFetched()
时,您应该根据视图所需的格式准备数据,然后调用 self.view.showSongs(listOfSongs)
4) 在您的视图中,在执行 showSongs(listOfSongs)
时,您应该设置 self.mySongs = listOfSongs
然后调用 tableView.reloadData()
5) 您的 TableViewDataSource 将 运行 覆盖您的数组 mySongs
并填充 TableView。
有关 VIPER 架构的更多高级技巧和有用的良好实践,我推荐此 post:https://www.ckl.io/blog/best-practices-viper-architecture(包含示例项目)
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
}
}
主持人
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()
}
}
交互者
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
}
}
以下是我与答案的不同点:
1、View永远不要向Presenter索要东西,View只需要将事件(viewDidLoad()/refresh()/loadMore()/generateCell()
)传递给Presenter,Presenter响应View传递给哪些事件。
2,我认为Interactor不应该引用Presenter,Presenter通过回调(块或闭包)与Interactor通信。
我有一个包含 table 视图的视图控制器,所以我想问一下我应该把 table 视图数据源和委托放在哪里,它应该是外部对象还是我可以写如果我们谈论 VIPER 模式,它在我的视图控制器中。
通常使用模式我这样做:
在 viewDidLoad 中,我请求来自演示者的一些流程,例如 self.presenter.showSongs()
Presenter 包含交互器,在 showSongs 方法中我从交互器请求一些数据,例如:self.interactor.loadSongs()
当歌曲准备好传回视图控制器时,我再次使用 Presenter 来确定这些数据应该如何在视图控制器中显示。但我的问题是我应该如何处理 table 视图的数据源?
创建一个 NSObject Class 并将其用作自定义数据源。在此 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
}
声明两个类型别名用于回调,一个用于 UITableViewCell 中的填充数据,另一个用于当用户点击一行时。
很好的问题@Matrosov。 首先我想告诉你,这都是关于 VIPER 组件之间的职责分离,如 View、Controller、Interactor、Presenter、Routing。
更多的是关于口味在开发过程中随着时间的推移而发生的变化。有许多架构模式,如 MVC、MVVP、MVVM 等。随着时间的推移,当我们的品味发生变化时,我们会从 MVC 变为 VIPER。有人从 MVVP 变成了 VIPER。
通过保持 class 行数较小的大小来使用您的声音视觉。您可以将数据源方法保留在 ViewController 本身中,或者创建一个符合 UITableViewDatasoruce 协议的自定义对象。
我的目标是让视图控制器保持苗条,并且每个方法和 class 都遵循单一责任原则。
Viper 有助于创建高内聚和低耦合的软件。
在使用这种开发模式之前,应该对 class 之间的责任分配有充分的了解。
一旦您对 iOS 中的 Oops 和协议有了基本的了解。你会发现这个模型和 MVC 一样简单。
首先,您的视图不应该向 Presenter 请求数据——这违反了 VIPER 架构。
视图是被动的。它等待 Presenter 给它显示内容;它从不向 Presenter 询问数据。
至于你的问题: 最好在 Presenter 中保留当前视图状态,包括所有数据。因为它提供基于状态的 VIPER 部件之间的通信。
但换句话说,Presenter 不应该知道任何关于 UIKit 的信息,所以 UITableViewDataSource 和 UITableViewDelegate 应该是 View 层的一部分。
为了让您 ViewController 保持良好状态并以 'SOLID' 方式进行,最好将 DataSource 和 Delegate 保存在单独的文件中。但是这些部分仍然应该知道presenter 请求数据。所以我更喜欢在 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.m
@implementation Interactor
- (void)getSongs {
[self.service getSongsWithCompletionHandler:^(NSArray *songs) {
[self.presenter didLoadSongs:songs];
}];
}
@end
1) 首先,View 是 passive
不应该为 Presenter 请求数据。因此,将 self.presenter.showSongs()
替换为 self.presenter.onViewDidLoad()
.
2) 在您的 Presenter 上,在执行 onViewDidLoad()
时,您通常应该调用交互器来获取一些数据。然后交互器将调用,例如 self.presenter.onSongsDataFetched()
3) 在您的 Presenter 上,在执行 onSongsDataFetched()
时,您应该根据视图所需的格式准备数据,然后调用 self.view.showSongs(listOfSongs)
4) 在您的视图中,在执行 showSongs(listOfSongs)
时,您应该设置 self.mySongs = listOfSongs
然后调用 tableView.reloadData()
5) 您的 TableViewDataSource 将 运行 覆盖您的数组 mySongs
并填充 TableView。
有关 VIPER 架构的更多高级技巧和有用的良好实践,我推荐此 post:https://www.ckl.io/blog/best-practices-viper-architecture(包含示例项目)
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
}
}
主持人
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()
}
}
交互者
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
}
}
以下是我与答案的不同点:
1、View永远不要向Presenter索要东西,View只需要将事件(viewDidLoad()/refresh()/loadMore()/generateCell()
)传递给Presenter,Presenter响应View传递给哪些事件。
2,我认为Interactor不应该引用Presenter,Presenter通过回调(块或闭包)与Interactor通信。