iOS VIPER 架构解读

苹果官方推荐的 APP 架构是 MVC 架构,实际上它并不是传统的三层架构,而是两层架构,即整个 APP 由“模型”和“视图控制器”两层构成。因此视图控制器显得尤其重要,它的重要体现在两个方面:

  • ViewController 是 iOS APP 中最重要的“基石”,没有 ViewController 就没有 APP——iOS 9 以后,要求 UIWindow 必须要有一个 rootViewController,否则 APP 不能运行。
  • 后面所有的 MVP、MVVM 和 VIPER 架构都是基于 ViewController 的。
    正因为其重要,所以 ViewController 才会显得“臃肿”。为了给 ViewController 减肥,许多新的架构应运而生。比如 MVVM、MVP,都试图将“视图控制器”拆分成更多的层级,从而减轻视图控制器的负担。
    VIPER 也不例外。

VIPER 架构

下图阐释了所谓的 VIPER 架构:
iOS VIPER 架构解读_第1张图片
首先简单解释一下:
E - Entity,实体。 对应了 MVC 中的模型层。
然后 VIPER 将 MVC 中的视图控制器进一步分为了以下几层,即上图红框中的所有组件,它们其实都是由 MVC 中视图控制器(即 ViewController)中演变而来的:
R - Router,路由器。负责视图控制器相关的导航(即 Segue),包括进来的导航和出去的导航。
P - Presenter,呈现器。负责视图控制器的渲染,比如将数据展现到每个控件。以及单数据被改变后 UI 的刷新。
I - Interactor,反应器。负责对数据的访问和处理,也就是业务逻辑。
V - View,视图/视图控制器。准确说应当是“减负”后的视图控制器。
其中:
Router, 路由器,是入口,View/ViewController 由路由器创建,其它组件也是由路由器中创建并组装成整个 VIPER 链的。
Presenter 呈现器是中枢神经,因为整个 VIPER 链都要通过它串联起来,比如说 View 和 Interactor 都会和它进行双向绑定,即互相引用。

箭头指示的方向表示引用。如果箭头是由 A 指向 B,则表明 A 引用了 B。
当然,Interactor 也非常重要,它是 VIPER 中的引擎,它会和 Presenter、Entity 构成双向绑定。APP 的动态逻辑主要是放在 Interactor 中的,比如数据的获取,比如数据改变后的 UI 刷新。如果数据的获取是来自于数据库或网络,那么 Interactor 中可以划分出单独的数据访问层(DAO)。
如果 ViewController 是纯静态展现的,那么它可以省掉 Interactor。比如 PostDetail 页面。
除去上面的这些内容后,原来的 ViewController 就不剩下多少内容了,这些内容统统都放到了 View 中。
View 状态
视图的状态在 VIPER 中没有表述。在 MVVM 中,视图状态和 ViewModel 双向绑定。在 VIPER 中没有 ViewModel 的概念,那么它是 Entity 吗?值得探讨。

VIPER 的一种实现

VIPER 架构目前还属于比较新的概念,并没有标准实现,github 上有几个参考的实现,但根据上述理论,比较接近的参考可以看这个:
https://github.com/MindorksOpenSource/iOS-Viper-Architecture
我将尝试用这个例子让你明白什么是 VIPER。
首先,checkout 出源代码。在上述地址的 README 页,作者贴出了 VIPER 架构蓝图,那张图其实和本文开头的 VIPER 图基本上是一致的。
后面一张图就显得特别贴心了,README 中还贴出了项目结构。从这张图中,你不难发现以下几点:

  • 项目基本上是以“屏幕”即 ViewController 来组织的,这也符合正常 APP 的习惯,APP 是以一页页“屏幕”构成的,一个 APP 起码有一个屏幕。
  • 这个 APP 只有两个屏幕:一个列表页,相关代码被组织在 PostList 目录下;一个详情页,代码则组织在 PostDetail 目录下。
  • Entity 是区别于其它 VIPER 组件的,它不和特定的 ViewController 绑定,因为它可以被所有 ViewController 共有。
  • PostList 目录下包含6个子目录:Presenter 目录表示呈现器;View 目录是视图/视图控制器;WireFrame = Router 即路由器;Interactor 是反应器;DataManager 是 DAO;Protocol 目录是协议(类似 O-C 中的头文件)。
  • PostDetail 目录和 PostList 差不多,但少了 DataManager 和 Interacter。因为详情页只是一个静态页,它不能响应用户事件,不需要反应器,同时它的数据是从列表页传入的,并不需要从数据库或网络中获取数据,因此也不需要 DAO。因此可以看出 VIPER 是比较灵活的,5 大组件并不是必须全部实现,用到的就实现,用不到的就不实现。
    闲话少说,让我们开始吧。

开始

无论什么项目,都是从 AppDelegate 开始的。打开 AppDelegate.swift,让我们来看下 didFinishLanunchingWithOptions 方法:

let postsList = PostListWireFrame.createPostListModule()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = postsList

PostListWireFrame 是 PostList 页面的路由器,createPostListModule() 静态方法,它返回一个导航控制器,其根控制器就是 PostList 的视图控制器。前面提过,ViewController 的导航由路由器负责。很显然,这里把设置 window 的 rootViewController 也当成是一种“导航”。

PostList 的路由器

路由器是每个 VIPER 链的入口,要了解 VIPER 架构,先从这里开始。打开 PostListWireFrame,我们发现里面有两个导航相关的方法:

  • createPostListModule 方法
    这个方法前面提过,返回一个根控制器为 PostListView 的导航控制器。这个方法是个 class 方法,因此不需要实例化就可调用。这是 PostListView 的“入口”导航,主要负责实例化视图控制器(即 PostListView)本身。
  • presentPostDetailScreen 方法
    这个方法是一个实例方法,在路由器的实例上调用。它是一个“出口”导航,将从本视图控制器(即PostListView)导航到其它视图控制器(PostDetailView)。
    由此可见,路由器主要由各种导航方法构成。先来看 createPostListModule 方法。

createPostListModule 方法

这个方法主要是 AppDelegate 调用,用来设置 window 的根控制器,我们可以从 storyboard 中加载一个 NavigationController 来作为 window 的根控制器:
let navController = mainStoryboard.instantiateViewController(withIdentifier: “PostsNavigationController”)
然后获取这个 NavigationController 的第一个控制器,也就是 PostListView:
if let view = navController.childViewControllers.first as? PostListView {
然后构建 VIPER 链中的各个组件。首先是 Presenter 组件:
let presenter: PostListPresenterProtocol & PostListInteractorOutputProtocol = PostListPresenter()
接下来是 Interactor 组件:
let interactor: PostListInteractorInputProtocol & PostListRemoteDataManagerOutputProtocol = PostListInteractor()
以及 DAO 组件:
let localDataManager: PostListLocalDataManagerInputProtocol = PostListLocalDataManager()
let remoteDataManager: PostListRemoteDataManagerInputProtocol = PostListRemoteDataManager()

注意,前面提过“如果数据的获取是来自于数据库或网络,那么 Interactor 中可以划分出单独的数据访问层(DAO)”,所以 localDataManager 和remoteDataManager 从 Interactor 中分离出来了,localDataManager 负责本地数据库的访问,remoteDataManager 负责网络数据的访问。
最后是路由器组件:
let wireFrame: PostListWireFrameProtocol = PostListWireFrame()
注意这是一个 class 方法,self 不可用,所以我们必须实例化一个路由器对象。
接下来是创建 PostListView 的 VIPER 链。
根据本文一开始的架构图,先创建 Presenter 和 View 的双向绑定:
view.presenter = presenter
presenter.view = view
然后是 Presenter 到 Router 的单向绑定。注意箭头指向的方向,是呈现器引用路由器,而非相反方向:
presenter.wireFrame = wireFrame

以及 Presenter 和 Interactor 的双向绑定:
presenter.interactor = interactor
interactor.presenter = presenter
Interactor 和 Entity 之间的双向绑定:
presenter.interactor = interactor
interactor.presenter = presenter
remoteDataManager.remoteRequestHandler = interactor
这里是用 DAO 暂时替换了 Entity,因为 Interactor 可以借助 DAO 来实现数据的获取。同时因为本地数据获取是同步的,localDataManager不需要回调 Interactor,所以也没有必要建立对 Presenter 的引用了。
最后,返回导航控制器:
return navController

presentPostDetailScreen 方法

这个方法就简单了,直接 push 一个 PostDetailView:

let postDetailViewController = PostDetailWireFrame.createPostDetailModule(forPost: post)
if let sourceView = view as? UIViewController {
           	sourceView.navigationController?.pushViewController(postDetailViewController, animated: true)
}

这里,PostDetailWireFrame 是 PostDetailView 的路由器,createPostDetailModule 会返回一个 PostDetailView 实例。
from 参数从方法外部传入,代表导航之前的 ViewController,在本 APP 中实际上就是 PostListView。

PostList 的呈现器

打开 PostListPresenter.swift,它扩展了两个协议。Swift 和 O-C 不同的一点是,它的类没有单独的接口文件(.h文件),因此它用协议 Potocol 来模拟 .h 文件。这样它就可以通过协议而不是具体实现的方式,实现接口和实现的分离。某个类想使用其它类的方法,就可以调用该类所实现的协议方法(即所谓的面向接口编程,不需要关心具体实现)。
首先是 PostListPresenterProtocol 协议,它会被其它 VIPER 组件所调用。它定义了 3 个属性和 2 个方法。
这 3 个属性代表了 Presenter 对 View、Interactor 和 Router 的引用关系(请参考架构图),在路由器的第一个导航方法中,我们见到了这 3 个属性的使用。它们是用来构建 VIPER 链的。这 3 个属性分别是:
weak var view: PostListViewProtocol?
var interactor: PostListInteractorInputProtocol?
var wireFrame: PostListWireFrameProtocol?
viewDidLoad 方法并不是 ViewController 中的那个同名方法,但是这个方法会被 PostListView 的 viewDidLoad 方法所调用,也许这就是它之所以被命名为 viewDidLoad 的原因,但其实你可以将它改成完全不同的另外一个名字。这个方法的主要目的是加载数据,即调用 Interactor 来获取数据:
interactor?.retrievePostList()
showPostDetail 方法也是一个公开方法,它会被 View 所调用。它的作用是调用路由器的第二个导航方法,即导航到 PostDetail 页:

func showPostDetail(forPost post: PostModel) {
	wireFrame?.presentPostDetailScreen(from: view!, forPost: post)
}

第二个协议是 PostListInteractorOutputProtocol 协议。里面有 2 个方法。第一个是 didRetrievePosts 方法,这个方法的作用是在获取到数据后根据数据刷新 UI,因此它会被 Interactor 所调用。因为反应器负责和 Entity 打交道,而网络数据的获取往往是异步的,那么当数据加载成功后需要调用这个方法更新 UI。
didRetrievePosts 方法很简单,它会关闭小菊花,然后通知 View 去更新列表:

func didRetrievePosts(_ posts: [PostModel]) {
        view?.hideLoading()
        view?.showPosts(with: posts)
}

PostList 的 View

VIPER 中的 View 其实是“瘦身”后的 ViewController。因此 PostListView 类跟一般的 ViewController 无异,继承了 UIViewController,也拥有 viewDidLoad 方法。在这个方法中,它调用 Presenter 来抓取数据:

presenter?.viewDidLoad()
tableView.tableFooterView = UIView()

其实是由 Interactor 而非 Presenter 来加载数据,但 View 和 Interactor 之间并没有直接引用关系,所以要通过 Presenter 这个“桥梁”进行。
PostListView 有一个属性表明它和 Presenter 的绑定关系:
var presenter: PostListPresenterProtocol
PostListView 扩展了 PostListViewProtocol 协议,这样其它 VIPER 组件就可以通过 PostListViewProtocol 协议来和它通讯了。
这个协议与数据更新后 UI 的刷新有关,比如显示/隐藏小菊花、刷新列表:

    func showPosts(with posts: [PostModel]) {
        postList = posts
        tableView.reloadData()
    }
    
    func showError() {
        HUD.flash(.label("Internet not connected"), delay: 2.0)
    }
    
    func showLoading() {
        HUD.show(.progress)
    }
    
    func hideLoading() {
        HUD.hide()
    }

接下来扩展 UITableViewDataSource 和 UITableViewDelegate 就不用多说了。

PostList 的反应器

PostListInteractor 扩展了两个协议。
首先看 PostListInteractorInputProtocol 协议。这个协议是给 Presenter 调用的,Presenter 用该协议调用反应器的数据访问方法。
这个协议定义了 3 个属性和一个方法。第 1 个属性绑定了 Presenter:
weak var presenter: PostListInteractorOutputProtocol?
剩余 2 个属性绑定了 DAO:
var localDatamanager: PostListLocalDataManagerInputProtocol?
var remoteDatamanager: PostListRemoteDataManagerInputProtocol?
一个用于访问本地数据库,一个用于访问网络数据。
一个方法是 retrievePostList 方法,用于抓取数据,具体的抓取数据动作则是委托给 DAO 来做:

	do {
            if let postList = try localDatamanager?.retrievePostList() {
                let postModelList = postList.map() {
                    return PostModel(id: Int($0.id), title: $0.title!, imageUrl: $0.imageUrl!, thumbImageUrl: $0.thumbImageUrl!)
                }
                if  postModelList.isEmpty {
                    remoteDatamanager?.retrievePostList()
                }else{
                   presenter?.didRetrievePosts(postModelList)
                }
            } else {
                remoteDatamanager?.retrievePostList()
            }
            
        } catch {
            presenter?.didRetrievePosts([])
        }

retrievePostList 方法中,如果本地数据库中缓存过数据,使用数据库中缓存的数据,否则获取网络数据。获取本地数据是同步的,所以它会立即调用 Presenter 的 didRetrievePosts 方法通知 Presenter 刷新列表。获取网络数据是异步的,所以不会立即调用 didRetrievePosts 方法。
PostListInteractor 还扩展 PostListInteractorInputProtocol 协议。这个协议主要是给 remoteDataManager 调用的,当网络数据返回时,remoteDataManager 会调用这个协议。这个协议有两个方法:

func onPostsRetrieved(_ posts: [PostModel]) {
        presenter?.didRetrievePosts(posts)
        
        for postModel in posts {
            do {
                try localDatamanager?.savePost(id: postModel.id, title: postModel.title, imageUrl: postModel.imageUrl, thumbImageUrl: postModel.thumbImageUrl)
            } catch  {
                
            }
        }
    }
    func onError() {
        presenter?.onError()
    }

第一个方法在数据加载成功时调用,第二个方法在加载失败时调用。这两个方法都会调用 Presenter。同时第一个方法还会将加载到的数据缓存到 CoreData。

PostListReactor 的 DAO

PostListPresenter 通过两个 DAO 访问数据。一个访问 CoreData,一个访问的是网络接口。
具体的实现与 VIPER 并不相干,就不详细介绍了。请自行去看 PostListLocalDataManager 类和 PostListRemoteDataManager 类。需要注意的是,前者是同步访问,所以获取数据后直接通过方法返回值返回数据,所以不需要持有对 Presenter 的引用。而后者是异步访问,所以获取到的数据后不能通过返回值立即返回,必须持有对 Presenter 的引用,以在恰当时机通知 UI 刷新。

PostList 的 Entity

实体不属于 ViewController 专有,所以 Entities 目录位于 PostList 目录以外。这里实际上有两个实体:

  • Post 类
    这个类是 CoreData 托管对象,继承了 NSManagedObject,专门用于本地数据。这类本身是个空类,它的具体实现是通过 extension 实现的(Post+CoreDataProperties.swift)。CoreData 不是本文的主题,不多做介绍了。
  • PostModel 结构
    这个结构是网络数据专用的,用于将网络 JSON 数据映射成 Swift 结构。JSON 解析使用的是 Alamfire 的 ObjectMapper 框架,所以。你会看到一个 mapping 方法。ObjectMapper 框架不是本文的主题,所以也不多说了。
    需要注意的是,PostListPresenter 的 didRetrievePosts 方法只接收 [PostModel] 类型的参数,这样本地数据的 [Post] 必须要转成 [PostModel] 才行。这个工作是在反应器里进行的,如果你忘记了,可以再看下这段代码:
let postModelList = postList.map() {
	return PostModel(id: Int($0.id), title: $0.title!, imageUrl: $0.imageUrl!, thumbImageUrl: $0.thumbImageUrl!)
}

接下来做什么

我们还没有查看过 PostDetail 目录,但实际上这个目录和 PostList 的结构是一样的——除了缺少两个用不到的 VIPER 组件以外。所以要了解 VIPER,只看 PostList 就够了。
你可以为项目中每个 ViewController 进行拆解,然后在路由器中组装自己的 VIPER 链。实际上使用 VIPER 并不会让你的代码变少——VIPER 不是为这个目的而设计的,但每个组件的代码都会变得更简单和清晰,目的只有一个:让水平不一的 iOS 程序员生成质量接近的代码。当然让代码变得更加可测试也是它的主要目的之一,那是另外一个话题。

你可能感兴趣的:(iPhone开发)