Swift 如何创建像 UIViewController 这样的通用 MVP

Swift how to create a generic MVP like UIViewController

我想删除重复的代码,所以我想创建一个简单的 MVP 基础视图控制器,它将模型、视图和演示者类型结合在一起并自动连接它们,例如:

class BaseMvpViewController<M: MvpModel, V: MvpView, P: MvpPresenter>: UIViewController {

我的模型和视图是空协议:

protocol MvpModel {}
protocol MvpView: class {} // class is needed for weak property

主持人看起来像这样:

protocol MvpPresenter {
    associatedtype View: MvpView
    weak var view: View? { get set }
    func onAttach(view: View)
    func onDetach(view: View)
}

这是我的全部BaseMvpViewController:

class BaseMvpViewController<M: MvpModel, V, P: MvpPresenter>: UIViewController, MvpView {
    typealias View = V
    var model: M? = nil
    var presenter: P!

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    deinit {
        presenter.onDetach(view: self as! View)
    }

    override func viewDidLoad() {
        createPresenter()
        super.viewDidLoad()
        presenter.onAttach(view: self as! View)
    }

    func createPresenter() {
        guard presenter != nil else {
            preconditionFailure("Presenter was not created or it was not assigned into the `presenter` property!")
        }
    }
}

问题是 V 必须没有协议,即不能 V: MvpView。否则 VC 的特定实现必须有 class/struct 而不仅仅是 MvpView 的协议。我所有的观点都只是协议,我的 VC 将实施它们,例如

class MyViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView

现在编译器在 onAttach()onDetach() 方法中抱怨 "argument type 'V' does not conform to expected type 'MvpView'"

所以我尝试了一个扩展:

extension BaseMvpViewController where V: MvpView {
    override func viewDidLoad() {
        presenter.onAttach(view: self as! View)
    }
}

又一个编译器错误:"cannot invoke 'onAttach' with an argument list of type '(view: V)'"。还有一个小的编译错误"Members of constrained extensions cannot be declared @objc" where I override func viewDidLoad() in the extension.这可以通过我自己的方法修复,并在自定义 class 中从 viewDidLoad 调用那个方法。知道如何实现我想要的吗?

这是一个类似于 的 similar/same 问题,但从那时起,Swift 世界可能已经有所改进。还是我真的遇到了当前 Swift 能力的硬性限制?

终于找到解决办法了,问题出在铸造self as! View,一定是self as! P.View。并且不能有用于查看的基本协议,因为协议在 Swift 中不符合自身。这是我的完整代码:

protocol MvpPresenter {
    associatedtype View
    var view: View? { get set }
    var isAttached: Bool { get }

    func onAttach(view: View)
    func onDetach(view: View)
}

/// Default implementation for the `isAttached()` method just checks if the `view` is non nil.
extension MvpPresenter {
    var isAttached: Bool { return view != nil }
}

class BaseMvpViewController<M, V, P: MvpPresenter>: UIViewController {
    typealias View = V
    var viewModel: M? = nil
    private(set) var presenter: P!

    //MARK: - Initializers

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override public init(nibName: String?, bundle: Bundle?) {
        super.init(nibName: nibName, bundle: bundle)
    }

    deinit {
        presenter.onDetach(view: self as! P.View)
    }

    //MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter = createPresenter()
    }

    override func viewWillAppear(_ animated: Bool) {
        guard let view = self as? P.View else {
            preconditionFailure("MVP ViewController must implement the view protocol `\(View.self)`!")
        }

        super.viewWillAppear(animated)

        if (!presenter.isAttached) {
            presenter.onAttach(view: view)
        }
    }

    //MARK: - MVP

    /// Override and return a presenter in a subclass.
    func createPresenter() -> P {
        preconditionFailure("MVP method `createPresenter()` must be override in a subclass and do not call `super.createPresenter()`!")
    }
}

和一个示例 VC:

class MyGenericViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView {
    ...
    override func createPresenter() -> MainPresenter {
        return MyPresenter()
    }
    ...
}

这个 VC 将自动有一个 viewModel 属性 类型 MyModel (可以是任何东西,例如结构、class、枚举等), 属性 presenter 类型 MyPresenter,此演示者将自动附加到 viewDidLoadviewWillAppear 之间。必须重写一个方法,createPresenter() 您必须在其中创建和 return 一个演示者。这是在自定义 VC 的 viewDidLoad 方法之前调用的。 Presenter 在 deinit.

中分离

最后一个问题是通用视图控制器不能在界面生成器 (IB) 中使用,因为 IB 通过 Objective-C 运行时与代码对话并且它不知道真正的泛型,因此看不到我们的通用 VC。从 storyboard/xib 实例化通用 VC 时应用程序崩溃。虽然有一个技巧可以解决这个问题。在 storyboard/xib 的任何实例化之前,只需将通用 VC 手动加载到 Objective-C 运行时。好的是 AppDelegateinit 方法:

init() {
    ...
    MyGenericViewController.load()
    ...
}

编辑 1: 我在这个 SO answer

中找到了将通用 VC 加载到 Objective-C 运行时

编辑 2: 示例演示者 class。强制性的东西是 typealiasweak var view: View?onAttach & onDetach 方法。还提供了 attach/detach 方法的最小实现。

class SamplePresenter: MvpPresenter {
    // These two are needed!
    typealias View = SampleView
    weak var view: View?

    private let object: SomeObject
    private let dao: SomeDao

    //MARK: - Initializers

    /// Sample init method which accepts some parameters.
    init(someObject id: String, someDao dao: SomeDao) {
        guard let object = dao.getObject(id: id) else {
            preconditionFailure("Object does not exist!")
        }

        self.object = object
        self.dao = dao
    }

    //MARK: - MVP. Both the onAttach and onDetach must assign the self.view property!

    func onAttach(view: View) {
        self.view = view
    }

    func onDetach(view: View) {
        self.view = nil
    }

    //MARK: - Public interface

    /// Sample public method that can be called from the view (e.g. a ViewController)
    /// that will load some data and tell the view to display them.
    func loadData() {
        guard let view = view else {
            return
        }

        let items = dao.getItem(forObject: object)
        view.showItems(items)
    }

    //MARK: - Private
}