隔离 View 和 Model (Swift)

Posted 极光开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了隔离 View 和 Model (Swift)相关的知识,希望对你有一定的参考价值。

价值 | 思考 | 共鸣

简评:我们经常需要在编写 方便的代码 和 易于维护的代码 之间取得平衡,当然如果能兼顾两者是最好的。

在平衡便利性和可维护性时,往往会遇到 View 和 Model 需要建立联系的情况,如果 View 和 Model 建立太强的连接会导致代码难以重构和难以重用。

专用 View

先看一个例子,在构建应用时,我们需要创建用于显示特定类型数据的专用视图,假设我们需要在表格中显示用户列表。一个常见的方法是创建一个专门用于渲染用户数据的 UserTableViewCell,如下:

 
   
   
 
  1. class UserTableViewCell: UITableViewCell {

  2.    override func layoutSubviews() {

  3.        super.layoutSubviews()

  4.        let imageView = self.imageView!

  5.        imageView.layer.masksToBounds = true

  6.        imageView.layer.cornerRadius = imageView.bounds.height / 2

  7.    }

  8. }

我们还需要使用 User model 来填充 Cell 里面的内容,一般会添加一个 configure 方法,用于填充内容,代码如下:

 
   
   
 
  1. extension UserTableViewCell {

  2.    func configure(with user: User) {

  3.        textLabel?.text = "\(user.firstName) \(user.lastName)"

  4.        imageView?.image = user.profileImage

  5.    }

  6. }

上面的代码在功能上没有任何的问题,但是技术上讲,我们实际上已经将 Model 层泄露给我们的 View 层。我们的 UserTableViewCell class 不仅专门用于呈现 User 的信息(不能是其他的数据模型)而且还需要知道 User 对象的具体内容,起初,这可能不是问题,但是如果我们继续沿着这条路走,我们的 View 很容易演变成包含 app 逻辑的代码:

 
   
   
 
  1. extension UserTableViewCell {

  2.    func configure(with user: User) {

  3.        textLabel?.text = "\(user.firstName) \(user.lastName)"

  4.        imageView?.image = user.profileImage

  5.        // Since this is where we do our model->view binding,

  6.        // it may seem like the natural place for setting up

  7.        // UI events and responding to them.

  8.        if !user.isFriend {

  9.            let addFriendButton = AddFriendButton()

  10.            addFriendButton.closure = {

  11.                FriendManager.shared.addUserAsFriend(user)

  12.            }

  13.            accessoryView = addFriendButton

  14.        } else {

  15.            accessoryView = nil

  16.        }

  17.    }

  18. }

编写如上所示的代码可能看起来非常方便,但通常会让程序难以测试和维护。

通用 View

解决上述问题的一个办法是让 View 和 Model 之间进行严格的分离(代码层面和概念层面进行分离)。

我们再回去看看我们 UserTableViewCell 。他不再与 User 耦合,我们可以该名为 RoundedImageTableViewCell 并删除 configure 这个与 User 耦合的方法。

 
   
   
 
  1. class RoundedImageTableViewCell: UITableViewCell {

  2.    override func layoutSubviews() {

  3.        super.layoutSubviews()

  4.        let imageView = self.imageView!

  5.        imageView.layer.masksToBounds = true

  6.        imageView.layer.cornerRadius = imageView.bounds.height / 2

  7.    }

  8. }

进行上述更改的好处是,我们现在可以轻松的将 RoundedImageTableViewCell 和 其他 model 配合使用。

但是,在将我们的模型代码与我们的视图代码进行分离时,损失了便利性。之前我们可以使用 configure(with:) 方法来渲染 User,现在我们需要找一种新方法来实现这一点。

我们可以做的是创建一个专用对象来配置 View 的显示。我们来构建一个 UserTableViewCellConfigurator 类,代码如下(不同的架构实现有所不同):

 
   
   
 
  1. class UserTableViewCellConfigurator {

  2.    private let friendManager: FriendManager

  3.    init(friendManager: FriendManager) {

  4.        self.friendManager = friendManager

  5.    }

  6.    func configure(_ cell: UITableViewCell, forDisplaying user: User) {

  7.        cell.textLabel?.text = "\(user.firstName) \(user.lastName)"

  8.        cell.imageView?.image = user.profileImage

  9.        if !user.isFriend {

  10.            // We create a local reference to the friend manager so that

  11.            // the button doesn't have to capture the configurator.

  12.            let friendManager = self.friendManager

  13.            let addFriendButton = AddFriendButton()

  14.            addFriendButton.closure = {

  15.                friendManager.addUserAsFriend(user)

  16.            }

  17.            cell.accessoryView = addFriendButton

  18.        } else {

  19.            cell.accessoryView = nil

  20.        }

  21.    }

  22. }

这样我们能够重复使用通用 UI 代码,以及方便的在 View 中呈现 model 数据。并且我们的代码通过依赖注入可以很方便的进行测试,而不是使用单例(这部分内容可以参考这篇文章)。

无论在哪个地方想使用 User 渲染 Cell,我们都可以简单地使用我们的 configurator。

 
   
   
 
  1. class UserListViewController: UITableViewController {

  2.    override func tableView(_ tableView: UITableView,

  3.                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {

  4.        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

  5.        let user = users[indexPath.row]

  6.        configurator.configure(cell, forDisplaying: user)

  7.        return cell

  8.    }

  9. }

View 工厂

configurator 非常适合可复用的 View,例如 TableViewCell ,因为他们重用的时候需要不断 re-configurator(重新配置)来显示新的 model,但对于更多的 “static” View,通常只需要配置一次就够了,因为它们渲染的模型在他的生命周期内不会改变。

这种情况下,使用工程模式可能是一个不错的选择。通过这种方式,我们可以将视图创建和配置捆绑在一起,同时仍然保持 UI 代码简单(与任何 model 分离)。

假设我们想要重建一种简单的方式呈现应用消息。我们可能会有一个视图控制器来显示一条消息。以及某种形式的通知视图。当用户收到一条新消息时弹出一个 View,为了避免重复代码我们创建一个 MessageViewFactory 让我们轻松为给定的 message 创建视图。

 
   
   
 
  1. class MessageViewFactory {

  2.    func makeView(for message: Message) -> UIView {

  3.        let view = TextView()

  4.        view.titleLabel.text = message.title

  5.        view.textLabel.text = message.text

  6.        view.imageView.image = message.icon

  7.        return view

  8.    }

  9. }

如上所示我们不仅使用通用的 TextView 来显示我们的消息(而不是专用类 MessageView),我们对外隐藏了具体的视图(我们的方法只返回 UIView)。就像在 Swift 中的代码封装 看到的一样,API 中删除具体类型可以在后期更便于修改。

▼点击阅读原文获取文中链接

以上是关于隔离 View 和 Model (Swift)的主要内容,如果未能解决你的问题,请参考以下文章

Swift MVVM模式·图层分离

swift 自定义view VFL 设置约束冲突

Swift:结构实例不更新视图

不应发生的 Swift 内存冲突

将 Swift 应用程序的组件划分为 Swift 模块

AppArchitecture-8种