关于 MVVM 和 ReactiveCocoa


背景

我们的架构在App Store上的应用程序的1.5年中一直保持不变。该应用程序主要由RestCit旧版本的Core Data支持。我们还在应用程序的各种模块中使用标准序列化文件以及NSUserDefaults。

随着核心数据通常需要(通过NSFetchedResultsController),我们的视图控制器经常与数据源层高度耦合。我们经常使用UIImageView +网络类型类别直接从视图层提取。我们使用各种技术(内部和外部使用Rest Kit)来序列化,获取和映射数据。一团糟。

但是最后,这种架构使我们能够快速移动,并尝试在应用程序的每一个角落的任何数量的功能和增强功能,这让我们到了今天的一个时期:成长。

数百万的日常开放,我们的目标是一个性能,无碰撞,并且维护非常轻的架构。

新技术

为了实现我们的架构目标,我们一直在评估主流iOS领域之外的新技术。这篇文章的其余部分将详细介绍我们如何尝试将这些技术合并到应用程序中。

ReactiveCocoa

我一直在尝试ReactiveCocoa几个过去的项目,甚至使用它来实现一个名为“Throwbacks”的最近的Timehop​​实验。 ReactiveCocoa真棒它的好处应该是自己的帖子,但足以说这里的团队变得足够舒适,使用ReactiveCocoa技术,它将在下一个版本的Timehop​​中发挥主要作用。

MVVM

ReactiveCocoa与MVVM架构模式并列。我唯一曝光的MVVM已经通过了ReactiveCocoa生态系统。 MVVM是一种困难的模式,无需像ReactiveCocoa这样简洁的绑定框架来同步视图和查看模型层。

测试和依赖注入

而我一直在努力的第三个组件是自动化测试。我们还没有选择一个特定的库,但大多数选项都足够相似,以满足我们对确保稳定性和未来可重构性的要求。随着测试,我一直在阅读关于依赖注入的方式,以确保可测试性,并尽可能保持组件的模块化。

这就是我将开始提出问题,并提出如何构建可测试的MVVM模块的解决方案,并且不会跳过自己的间接层。

我对MVVM的理解

MVVM通常在这个简单的图中引入:
View => View Model => Model
<- <-
其中=>表示所有权,强力引用,直接观察和事件的一些组合。 < - 表示数据流(但不是直接引用,弱或强)。

在Cocoa-land/objc 的世界里:

视图层主要由UIViews和UIViewControllers组成。
视图模型层由纯NSObject组成。
模型层由我实际调用控制器组成,但也可以称为客户端,数据源等。角色比名称更重要。 控制器通常也是NSObject子类。
模型对象是第四个角色,而原始模型是第五个。
我们将模型对象定义为NSObjects属性中的数据的简单愚蠢存储。
我们将原始模型定义为非本地格式的数据表示(例如,JSON字符串,NSDictionary,NSManagedObject等)。

规则

我介绍了每个角色的规则,以澄清问题的分离。以下是分离每个以前角色的关注的规则。没有上下文,规则有点抽象,所以我会立即介绍一下例子。

Views

允许视图访问视图和查看模型。
视图不允许访问控制器或模型对象。
视图直接绑定其显示属性以查看模型属性。
视图通过RACCommands / RACActions传递用户事件以查看模型,或者通过调用视图模型上的方法。

ViewModels

允许查看模型访问视图模型,控制器和模型对象。
视图模型不允许访问视图或原始模型。
查看模型将模型对象从控制器转换为自我或其他视图模型中的可观察属性。
视图模型接受来自视图或其他视图模型的输入,触发对自我,其他视图模型或控制器的操作。

Controllers

允许控制器访问其他控制器,模型对象和原始模型。
控制器不允许访问视图模型或视图。
控制器从其他控制器或直接从系统级原始数据存储(网络,文件系统,数据库等)协调模型对象访问。
控制器通过异步(或者可能更好地将时间无关)数据(通过RACSignals)来查看模型或其他控制器。
再次强调,这些不是视图控制器!

进一步解释MVVM

让我们来详细介绍一下Cocoa的MVVM图。

View ========> View Model ========> Controller ========> Data Store
      |                                          |                                    |
   View                             View Model                    Controller

要澄清,===>表示上述所有权。 | 也表示顶层对象对底层对象的所有权。视图可以生成一个或多个子视图,呈现其他视图控制器,并且还绑定到视图模型。类似地,视图模型可以保留视图模型的集合以将其自己的视图分发到该视图的子视图。该视图模型还可以具有控制器并将控制器连接到其子视图模型。

其次,这里是角色之间的对象流。

View <-------- View Model <-------- Controller <-------- Data Store
    (view model)        (model object)        (raw model) 

从第一张图表可以看出,所有关系都是单向的。因此,在一个界面和一个方向上只有直接耦合。现在可以用测试设备替换我们的视图层,并直接测试视图和视图模型之间的接口。也可以测试视图模型和控制器层之间的接口。

从第二张图表中可以看出,每个角色将一类对象转换为另一个类。我们的角色图开始看起来像从右到左转换数据的管道,以及从左到右转换用户意图的管道。

关于同步/异步

在大多数应用程序中,同步工作的方法与执行异步工作的方法之间存在一个隐含的区别。一个最佳实践是编写由异步方法包装的同步方法。

使用ReactiveCocoa,同步和异步都被视为异步。通过将所有内容视为异步的方式,您通常会事先提交项目,以免对调用对象散布的委托或block回调造成不必要的负担。然而,使用具有可链接操作的系统,理智的处理语义(包括内置线程路由操作)和简洁的绑定使得异步数据的工作变得更加容易。因此,当同步和异步操作可以以相同的方式(并组合)处理时,将所有操作视为异步处理将成为胜利。这也是一个胜利,因为操作的消费者不再需要不必要的知道操作可能有多昂贵。

一个简单的例子

我们从一个简单的例子开始,将很快失去控制。 想像一个代表用户个人资料的视图。 它应该显示用户的照片,具有用户名称的标签,具有用户拥有的朋友的数量的标签以及刷新按钮,因为在该示例中用户的朋友数量变化很大。

View

view 很简单

@class HOPProfileViewModel

@interface HOPProfileView : UIView

@property (nonatomic, strong) HOPProfileViewModel *viewModel;

@end

@inteface ProfileView ()

@property (nonatomic, strong) UIImageView *avatarView;
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *friendCountLabel;
@property (nonatomic, strong) UIButton *refreshButton;

@end

@implementation ProfileView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (!self) return nil;

    _avatarView = [[UIImageView alloc] init];
    [self addSubview:_avatarView];

    // ... create and add the other views as subviews

    RAC(self.avatarView, image) = RACObserve(self, viewModel.avatarImage);
    RAC(self.nameLabel, text) = RACObserve(self, viewModel.nameString);
    RAC(self.friendCountLabel, text) = RACObserve(self, viewModel.friendCountString);
    RAC(self.refreshButton, rac_command) = RACObserve(self, viewModel.refreshCommand); 

    return self;
}

- (void)layoutSubviews { /* ... */ }

@end

这里有几件事情

  • 该视图在其整个生命周期中未被绑定到视图模型。这种情况更为罕见。大多数视图应该在整个生命周期中被绑定到一个特定的视图模型。较少的可变性大大降低了视图的复杂性。您通常会将视图模型要求在init中传递给接收者。但是,在这种情况下,我们允许视图模型在此视图生命周期中被换出,因此我们必须正确重新配置其数据。您通常会在可重复使用的视图(如UITableViewCells)中看到此模式。
  • 我们正在使用ReactiveCocoa从我们的视图模型属性创建一种单向绑定,以查看属性。注意在这个阶段没有数据转换。
    ReactiveCocoa将确保self.avatarView.image与self.viewModel.avatarImage属性中的当前图像一起设置。即使viewModel对象本身在此视图的生命周期内发生变化,也将确保这一点。如果我们的视图用视图模型初始化,我们可以编写RAC(self.avatarView,image)= RACObserve(self.viewModel,avatarImage),而只会观察到avatarImage属性。
  • label的工作方式与imageView相同。
  • RACCommand是一个有点神奇的对象,可以透明地管理动作和异步结果之间的状态。这里要注意的重要部分是视图模型拥有并配置有问题的RACCommand对象。在幕后,UIButton上的rac_command帮助器类别执行三个任务(大量简化):
    通过touchUpInside操作调用视图模型的RACCommand上的[execute:]。
    RACCommand正在执行时禁用自身。
    当RACCommand完成执行时,重新启用它。
    想象一下,有标准的layoutSubviews和sizeThatFits:方法。
    您可能会问:这是迄今为止,通过setter(?HOPUser *)]设置器将模型对象传递给我们的视图的典型模式呢?

  • 向这个视图/视图模型对添加功能是直接的。需要在占位符图像后跟高分辨率网络图像时加载低分辨率缓存图像?视图层不变。它将自动采用视图模型当前存储的任何图像。永远不会出现从浏览网络的观点引发的回调。

  • 我们的朋友数量存储在我们的模型中作为NSNumber,但是我们的标签需要一个格式化的NSString。视图层不用转换,无论是简单的@ 25 - >“25”还是@ 25 - >“此用户有25个朋友”。
  • 我们可以通过允许测试台比较UIImages和NSStrings直接测试视图模型。
  • 在这个视图的更合适的版本中,超级视图会将视图模型绑定到更通用的子视图,从而使一组超可重用内容块在整个应用程序中与一个或多个各种视图模型一起使用。胶水代码是简单的一对一绑定。
    简而言之,我们已经将数据操纵阶段与演示文稿分开了。

ViewModel

现在我们来处理视图模型。 界面应该看起来很熟悉:

 @interface HOPProfileViewModel : NSObject 
             @property (nonatomic, strong, readonly) UIImage *avatarImage;
   @property (nonatomic, copy, readonly) NSString *nameString;
  @property (nonatomic, copy, readonly) NSString *friendCountString;
 @property (nonatomic, strong, readonly) RACCommand *refreshCommand;
  -(instancetype)initWithUser:(HOPUser *)user;
@end ```
注意所有这些属性都是只读的。 该视图可以自由地观察所有这些属性,并在RACCommand上调用execute。 视图模型掩盖了其所有的内部操作,并向其观察者提供了一个有限的窗口(其视图)。

有一个指定的初始化程序接受HOPUser模型对象。 现在,假设另一个视图模型在它绑定到其视图之前,使用模型对象创建了这个HOPProfileViewModel(我将把它作为我关于MVVM的最明显的问题)。

```@interface HOPProfileViewModel ()
@property (nonatomic, strong) UIImage *avatarImage;
@property (nonatomic, copy) NSString *nameString;
@property (nonatomic, copy) NSString *friendCountString;
@property (nonatomic, strong) RACCommand *refreshCommand;
@end
@implementation HOPProfileViewModel
- (instancetype)initWithUser:(HOPUser *)user {
    self = [super init]
    if (!self) return nil;

    RAC(self, avatarImage) = 
        [[[[[RACObserve(self, user.avatarURL)
            ignore:nil]
            flattenMap:(RACSignal *)^(NSURL *avatarURL) {
                return [[HOPImageController sharedController] imageSignalForURL:avatarURL];
            }]
            startWith:[UIImage imageNamed:@"avatar-placeholder"]]
            deliverOn:[RACScheduler mainThreadScheduler]];

    RAC(self, nameString) = 
        [[RACObserve(self, user.name)
            ignore:nil]
            map:(NSString *)^(NSString *name) {
                return [name uppercaseString];
            }];

    RAC(self, friendCountString) = 
        [[RACObserve(self, user.friendCount)
            ignore:nil]
            map:(NSString *)^(NSNumber *friendCount) {
                return [NSString stringWithFormat:@"This user has %@ friends", friendCount];
            }];

    @weakify(self);
    _refreshCommand = [[RACCommand alloc] initWithSignalBlock:(RACSignal *)^(id _) {
        @strongify(self);
        return [[HOPNetworkController sharedController] fetchUserWithId:self.user.userId];
    }

    RAC(self, user) = 
        [[[_refreshCommand executionSignals] 
            switchToLatest] 
            startWith:user];
}

@end




<div class="se-preview-section-delimiter"></div>

好的,在这个视图模型中还有更多的观点。 这是一件好事。 有一些稍微高级的ReactiveCocoa,但是不要讨论这个。 目标是了解视图,视图模型和控制器之间的关系。

  • 首先,我们将外部属性重新声明为内部的readwrite。
    我们的第一个属性绑定是avatarImage。我们看到我们的图像在HOPUser模型中被表示为一个URL。我们首先观察任何视图模型当前用户模型的avatarURL属性。每次更改时,我们将该URL并将其提供给我们的单例HOPImageController。图像控制器负责缓存缩略图,完整图像,以及从网络中获取图像。该信号将最多发送三个不同的图像,最终将被分配给self.avatarImage。图像可以在后台线程上获取,所以我们确保它们被传递到主线程上最终的目标imageView。
  • 下一个属性绑定是nameString。我们只对这个字符串执行一个映射操作:uppercasing。
  • 我们将朋友计数映射到人类可读的字符串。
  • refreshCommand是从头创建的。每次执行命令时,它会预订信号块(在我们的情况下,当按下按钮时)。该命令自动跟踪信号的状态,直到内部信号完成才会再次执行。在这种情况下,我们假设我们的数据来自一个共享的HOPNetworkController,它发送一个HOPUser对象并完成。
  • self.user映射首先分配传递给init方法的用户对象,然后从命令的执行中获取最新的结果。

这个例子中有很多坑。 注意事项:

所有的代码都是令人难以置信的声明。 我们在任何给定的时间准确地说明了我们的每个属性。 他们都只从一个地方。
我们有很大的灵活性,可以根据产品变化改变我们的模型对象属性的操作。
为了我们的观点,嘲笑这个对象是非常容易的。 它只有四个外部属性。 例如,我们的假实现可以将我们的self.avatarImage属性映射到[[[RACSignal return:[UIImage imageNamed:@“final”]] delay:4] startWith:[UIImage imageNamed:@“placeholder”]]; 这将模拟占位符图像,四秒假网络延迟和最终图像。
我会为另一篇文章留下错误处理,但是作为一个快速总结,RACSignal的合同使得将错误提升到视图模型层几乎是微不足道的,并以适当的方式呈现。

控制器

在控制器层面上,我有更多的答案。 我将介绍我们上面使用的两个类的头文件,我们将从那里开始。
“`
@interface HOPImageController : NSObject // The shared instance of this class.
// Inside it has three functions:
// * It maintains a separate network client for fetching raw image data.
// * It maintains a key/value store of imageURLs and images on disk.
// * It adds images from the network to the cache.
+ (instancetype)sharedController;
// The returned signal sends an image from the cache if available,
// then an image from the network, then completes.
// The signal sends an error if there was a network error.
- (RACSignal )imageSignalForURL:(NSURL )URL;
@end
@interface HOPNetworkController : NSObject
// The shared instance of this class.
// Inside it manages a network session.
+ (instancetype)sharedController;
// The returned signal sends a HOPUser, then completes.
// The signal sends an error if there was a network error.
- (RACSignal )fetchUserWithUserId:(NSNumber )userId;
@end

“`

概要

我试图以一种高水平的方式来解释MVVM,我现在认识它。我写了一个平面的例子,每个角色都有一个组件。然后,我探讨了从这个练习和其他一些情况出发的几个问题。

我非常感谢任何关于这篇文章的反馈。特别是,我真的想在大型体系结构中丰富我对MVVM的理解,可以扩展到多个数据源,数百个观点,以及数百万用户,同时保持快速和无瑕疵。
原文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值