问题
I am using MVP design pattern. I have two ways of passing data to the another view controller which I'll mention below. I don't know which of them is correct and doesn't violate MVP pattern. I know this is very big question but it's really very important.
1) Using init with presenter
, below am creating the view controller by passing the presenter which the view controller needs.
struct HotelTemplate {
var id: String
var name: String
var icon: String
}
class ListHotelPresenter: NSObject {
private var data = [HotelTemplate]()
func getPresenter(_ index: Int) -> HotelDetailsPresenter {
let presenter = HotelDetailsPresenter(id: data[index].id, name: data[index].name, icon: data[index].icon)
return presenter
}
}
// InitialViewController
class ListHotelViewController: UIViewController {
class func `init`(with presenter: ListHotelPresenter) -> ListHotelViewController {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ListHotelViewController") as! ListHotelViewController
vc.presenter = presenter
return vc
}
var presenter: ListHotelPresenter!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailPresenter = presenter.getPresenter(indexPath.row)
let vc = HotelDetailsViewController.init(with: detailPresenter)
self.navigationController?.pushViewController(vc, animated: true)
}
}
// ViewController that will be push
class HotelDetailsViewController: UIViewController {
class func `init`(with presenter: HotelDetailsPresenter) -> HotelDetailsViewController {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HotelDetailsViewController") as! HotelDetailsViewController
return vc
}
var presenter: HotelDetailsPresenter!
override func viewDidLoad() {
super.viewDidLoad()
presenter.loadHoteData()
}
}
class HotelDetailsPresenter: NSObject {
var hotelId: String
var hotelName: String
var hotelIcon: String
init(id: String, name: String, icon: String) {
self.hotelId = id
self.hotelName = name
self.hotelIcon = icon
}
func loadHoteData() {
// Load hotel data.
// Alamofire.request ..................
}
}
2) By sending the id, name, icon
and then initialising the presenter in the viewDidLoad()
struct HotelTemplate {
var id: String
var name: String
var icon: String
}
class ListHotelPresenter: NSObject {
private var data = [HotelTemplate]()
func getHotelName(_ index: Int) -> String {
return data[index].name
}
func getHotelIcon(_ index: Int) -> String {
return data[index].icon
}
func getHotelId(_ index: Int) -> String {
return data[index].id
}
}
class ListHotelViewController: UIViewController {
var presenter: ListHotelPresenter!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HotelDetailsViewController") as! HotelDetailsViewController
vc.id = presenter.getHotelId(indexPath.row)
vc.name = presenter.getHotelName(indexPath.row)
vc.icon = presenter.getHotelIcon(indexPath.row)
self.navigationController?.pushViewController(vc, animated: true)
}
}
class HotelDetailsViewController: UIViewController {
var presenter: HotelDetailsPresenter!
var id = ""
var name = ""
var icon = ""
override func viewDidLoad() {
super.viewDidLoad()
presenter = HotelDetailsPresenter(id: id, name: name, icon: icon)
presenter.loadHoteData()
}
}
class HotelDetailsPresenter: NSObject {
var hotelId: String
var hotelName: String
var hotelIcon: String
init(id: String, name: String, icon: String) {
self.hotelId = id
self.hotelName = name
self.hotelIcon = icon
}
func loadHoteData() {
// Load hotel data.
// Alamofire.request ..................
}
}
So below are my concerns:
1) Which one is correct? (I feel the first method is really very clean, but my senior told me that it violates the MVP pattern. I don't know how.)
2) Should the presenter property of the controller be public or private?
回答1:
In Objective-C, you can have a View that passes a Model around. We could forward-declare a model in the view controller's header file:
@class HotelTemplate;
In the .m file, I'd make sure not to "#import HotelTemplate.h"
. This way, the model remained opaque. You can pass it around, but you can't look inside.
I don't know of any way to enforce this in Swift. So let me follow your example and pass the next presenter, instead of the next model. All we need is a way to set the View on the each presenter in viewDidLoad()
. To prevent retain cycles, this will be a weak property.
First, here's the list view controller. I made it a UITableViewController.
final class ListHotelViewController: UITableViewController {
private var presenter = ListHotelPresenter()
override func viewDidLoad() {
super.viewDidLoad()
presenter.setView(self)
presenter.loadHotelData()
}
}
The presenter will call it back through a protocol:
protocol ListHotelView: class {
func redraw()
func showDetails(nextPresenter: HotelDetailsPresenter)
}
extension ListHotelViewController: ListHotelView {
func redraw() {
tableView.reloadData()
}
func showDetails(nextPresenter: HotelDetailsPresenter) {
let vc = HotelDetailsViewController.init(with: nextPresenter)
navigationController?.pushViewController(vc, animated: true)
}
}
Here's the table view's data source and delegate:
extension ListHotelViewController /* UITableViewDataSource */ {
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter.hotelCount
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Hotel", for: indexPath) as! HotelTableViewCell
presenter.configure(cell: cell, row: indexPath.row)
return cell
}
}
extension ListHotelViewController /* UITableViewDelegate */ {
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
presenter.showDetails(row: indexPath.row)
}
}
At each step, it defers to the Presenter. The Presenter has a weak link back to the View, but only through the protocol. It has no idea that the View is a ListHotelViewController. We should be able to implement the View with a bunch of print(_)
statements instead, for a terminal-based interface.
final class ListHotelPresenter {
private weak var view: ListHotelView?
private var model: [HotelTemplate] = [] {
didSet {
view?.redraw()
}
}
var hotelCount: Int {
return model.count
}
func setView(_ view: ListHotelView) {
self.view = view
}
func loadHotelData() {
// Network request to load data into model. Let's pretend with dummy data:
let hilton = HotelTemplate(id: "hilton", name: "Hilton", icon: "H")
let radisson = HotelTemplate(id: "radisson", name: "Radisson", icon: "R")
model = [hilton, radisson]
}
func configure(cell: HotelCell, row: Int) {
let hotel = model[row]
cell.show(name: hotel.name, icon: hotel.icon)
}
func showDetails(row: Int) {
let nextPresenter = HotelDetailsPresenter(summaryModel: model[row])
view?.showDetails(nextPresenter: nextPresenter)
}
}
In configure(cell:row:)
, the Presenter talks to the given cell. Note that the cell is also a protocol. With MVP, I really try to imagine how I'd use it to make a terminal-based interface. Here's the cell:
protocol HotelCell: class {
func show(name: String, icon: String)
}
final class HotelTableViewCell: UITableViewCell {}
extension HotelTableViewCell: HotelCell {
func show(name: String, icon: String) {
textLabel?.text = name
// Do something to show icon
}
}
In practice, you'd add more to the table view cell. I just used a plain cell and its text label for this example.
Finally, we come to the pushed view controller.
final class HotelDetailsViewController: UIViewController {
private var presenter: HotelDetailsPresenter!
@IBOutlet private var textLabel: UILabel!
static func `init`(with presenter: HotelDetailsPresenter) -> HotelDetailsViewController {
let vc = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "HotelDetailsViewController")
as! HotelDetailsViewController
vc.presenter = presenter
return vc
}
override func viewDidLoad() {
super.viewDidLoad()
presenter.setView(self)
presenter.show()
}
}
I'm making the assumption that we'll immediately show the summary information we have, but that there's more to detail to come from a web service. That's done by this Presenter.
struct HotelDetails {
let location: String
// more details…
}
final class HotelDetailsPresenter {
private weak var view: HotelDetailsView?
private let summaryModel: HotelTemplate
private var detailsModel: HotelDetails? {
didSet {
guard let detailsModel = detailsModel else { return }
view?.showDetails(location: detailsModel.location)
}
}
init(summaryModel: HotelTemplate) {
self.summaryModel = summaryModel
}
func setView(_ view: HotelDetailsView) {
self.view = view
}
func show() {
view?.show(name: summaryModel.name, icon: summaryModel.icon)
// Network request to load data into detailsModel
}
}
As usual, the Presenter tells the View what to do through a protocol:
protocol HotelDetailsView: class {
func show(name: String, icon: String)
func showDetails(location: String)
}
extension HotelDetailsViewController: HotelDetailsView {
func show(name: String, icon: String) {
textLabel?.text = name
// Do something to show icon
}
func showDetails(location: String) {
// Show other hotel details we loaded
}
}
As you can see, the properties are private. To support unit testing, we might need to relax that using private(set)
so that only the setters are private.
回答2:
Hey as per apple guidance Model-View-Controller both are okay but the second one is more readable and easy to understand to first one with some changes please check the code.
class ListHotelViewController: UIViewController{
private var presenter: ListHotelPresenter!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HotelDetailsViewController") as! HotelDetailsViewController
vc.objHotelTemplate = presenter.getHotelTemplate(indexPath.row)
self.navigationController?.pushViewController(vc, animated: true)
}
}
class HotelDetailsViewController: UIViewController {
private var presenter: HotelDetailsPresenter!
var objHotelTemplate:HotelTemplate!
override func viewDidLoad() {
super.viewDidLoad()
presenter = HotelDetailsPresenter(id: objHotelTemplate.id, name: objHotelTemplate.name, icon: objHotelTemplate.icon)
presenter.loadHoteData()
}
}
来源:https://stackoverflow.com/questions/54444627/passing-data-to-another-controller-using-mvp-pattern-ios