转自
Di Wu’s blog
,原文:
Architecting iOS Apps with VIPER
VIPER
的 iOS 应用架构的方式。VIPER 已经在很多大型的项目上成功实践,但是出于本文的目的我们将通过一个待办事项清单 (to-do app) 来介绍 VIPER 。你可以在
GitHub
上关注这个项目。
什么是 VIPER?
Mutual Mobile
) 着手改善我们的测试实践时,我们发现给 iOS 应用写测试代码非常困难。因此如果想要设法改变测试的现状,我们首先需要一个更好的方式来架构应用,我们称之为 VIPER。
简明构架
的程序。VIPER 可以是视图 (View),交互器 (Interactor),展示器 (Presenter),实体 (Entity) 以及路由 (Routing) 的首字母缩写。简明架构将一个应用程序的逻辑结构划分为不同的责任层。这使得它更容易隔离依赖项 (如数据库),也更容易测试各层间的边界处的交互:
![](http://static.oschina.net/uploads/img/201509/25165439_NuTo.jpg)
重量级视图控制器
的问题,在这里,视图控制器做了太多工作。为这些重量级视图控制器
瘦身
并不是 iOS 开发者寻求提高代码的质量所要面临的唯一挑战,但至少这是一个很好的开端。
基于用例的应用设计
VIPER 的主要部分
![](http://static.oschina.net/uploads/img/201509/25165439_9F1R.png)
交互器
- (
void
)findUpcomingItems
{
__weak
typeof
(self) welf = self;
NSDate* today = [self.clock today];
NSDate* endOfNextWeek = [[NSCalendar currentCalendar] dateForEndOfFollowingWeekWithDate:today];
[self.dataManager todoItemsBetweenStartDate:today endDate:endOfNextWeek completionBlock:^(NSArray* todoItems) {
[welf.output foundUpcomingItems:[welf upcomingItemsFromToDoItems:todoItems]];
}];
}
实体
@
interface
VTDTodoItem : NSObject
@property (nonatomic, strong) NSDate* dueDate;
@property (nonatomic, copy) NSString* name;
+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;
@end
展示器
- (
void
)addNewEntry
{
[self.listWireframe presentAddInterface];
}
- (
void
)foundUpcomingItems:(NSArray*)upcomingItems
{
if
([upcomingItems count] == 0)
{
[self.userInterface showNoContentMessage];
}
else
{
[self updateUserInterfaceWithUpcomingItems:upcomingItems];
}
}
视图
@protocol VTDAddViewInterface
- (
void
)setEntryName:(NSString *)name;
- (
void
)setEntryDueDate:(NSDate *)date;
@end
@protocol VTDAddModuleInterface
- (
void
)cancelAddAction;
- (
void
)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate
@end
路由
@implementation VTDAddWireframe
- (
void
)presentAddInterfaceFromViewController:(UIViewController *)viewController
{
VTDAddViewController *addViewController = [self addViewController];
addViewController.eventHandler = self.addPresenter;
addViewController.modalPresentationStyle = UIModalPresentationCustom;
addViewController.transitioningDelegate = self;
[viewController presentViewController:addViewController animated:YES completion:nil];
self.presentedViewController = viewController;
}
#pragma mark - UIViewControllerTransitioningDelegate Methods
- (id)animationControllerForDismissedController:(UIViewController *)dismissed
{
return
[[VTDAddDismissalTransition alloc] init];
}
- (id)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source
{
return
[[VTDAddPresentationTransition alloc] init];
}
@end
利用 VIPER 组织应用组件
@implementation VTDAddViewController
- (
void
)viewDidAppear:(BOOL)animated
{
[
super
viewDidAppear:animated];
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(dismiss)];
[self.transitioningBackgroundView addGestureRecognizer:gestureRecognizer];
self.transitioningBackgroundView.userInteractionEnabled = YES;
}
- (
void
)dismiss
{
[self.eventHandler cancelAddAction];
}
- (
void
)setEntryName:(NSString *)name
{
self.nameTextField.text = name;
}
- (
void
)setEntryDueDate:(NSDate *)date
{
[self.datePicker setDate:date];
}
- (IBAction)save:(id)sender
{
[self.eventHandler saveAddActionWithName:self.nameTextField.text
dueDate:self.datePicker.date];
}
- (IBAction)cancel:(id)sender
{
[self.eventHandler cancelAddAction];
}
#pragma mark - UITextFieldDelegate Methods
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
return
YES;
}
@end
@
interface
VTDListDataManager : NSObject
@property (nonatomic, strong) VTDCoreDataStore *dataStore;
- (
void
)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(
void
(^)(NSArray *todoItems))completionBlock;
@end
@implementation VTDListDataManager
- (
void
)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate*)endDate completionBlock:(
void
(^)(NSArray *todoItems))completionBlock
{
NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@
"(date >= %@) AND (date <= %@)"
, [calendar dateForBeginningOfDay:startDate], [calendar dateForEndOfDay:endDate]];
NSArray *sortDescriptors = @[];
__weak
typeof
(self) welf = self;
[self.dataStore
fetchEntriesWithPredicate:predicate
sortDescriptors:sortDescriptors
completionBlock:^(NSArray* entries) {
if
(completionBlock)
{
completionBlock([welf todoItemsFromDataStoreEntries:entries]);
}
}];
}
- (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries
{
return
[entries arrayFromObjectsCollectedWithBlock:^id(VTDManagedTodoItem *todo) {
return
[VTDTodoItem todoItemWithDueDate:todo.date name:todo.name];
}];
}
@end
static
NSString *ListViewControllerIdentifier = @
"VTDListViewController"
;
@implementation VTDListWireframe
- (
void
)presentListInterfaceFromWindow:(UIWindow *)window
{
VTDListViewController *listViewController = [self listViewControllerFromStoryboard];
listViewController.eventHandler = self.listPresenter;
self.listPresenter.userInterface = listViewController;
self.listViewController = listViewController;
[self.rootWireframe showRootViewController:listViewController
inWindow:window];
}
- (VTDListViewController *)listViewControllerFromStoryboard
{
UIStoryboard *storyboard = [self mainStoryboard];
VTDListViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:ListViewControllerIdentifier];
return
viewController;
}
- (UIStoryboard *)mainStoryboard
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@
"Main"
bundle:[NSBundle mainBundle]];
return
storyboard;
}
@end
使用 VIPER 构建模块
@protocol VTDAddModuleInterface
- (
void
)cancelAddAction;
- (
void
)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
@end
@protocol VTDAddModuleDelegate
- (
void
)addModuleDidCancelAddAction;
- (
void
)addModuleDidSaveAddAction;
@end
利用 VIPER 进行测试
- (
void
)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek
{
[[self.dataManager expect] todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY];
[self.interactor findUpcomingItems];
}
- (
void
)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday
{
NSArray *todoItems = @[[VTDTodoItem todoItemWithDueDate:self.today name:@
"Item 1"
]];
[self dataStoreWillReturnToDoItems:todoItems];
NSArray *upcomingItems = @[[VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:self.today title:@
"Item 1"
]];
[self expectUpcomingItems:upcomingItems];
[self.interactor findUpcomingItems];
}
- (
void
)testFoundZeroUpcomingItemsDisplaysNoContentMessage
{
[[self.ui expect] showNoContentMessage];
[self.presenter foundUpcomingItems:@[]];
}
- (
void
)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay
{
VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@
"Today"
sectionImageName:@
"check"
itemTitle:@
"Get a haircut"
itemDueDay:@
""
];
[[self.ui expect] showUpcomingDisplayData:displayData];
NSCalendar *calendar = [NSCalendar gregorianCalendar];
NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29];
VTDUpcomingItem *haircut = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:dueDate title:@
"Get a haircut"
];
[self.presenter foundUpcomingItems:@[haircut]];
}
- (
void
)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay
{
VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@
"Tomorrow"
sectionImageName:@
"alarm"
itemTitle:@
"Buy groceries"
itemDueDay:@
"Thursday"
];
[[self.ui expect] showUpcomingDisplayData:displayData];
NSCalendar *calendar = [NSCalendar gregorianCalendar];
NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29];
VTDUpcomingItem *groceries = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationTomorrow dueDate:dueDate title:@
"Buy groceries"
];
[self.presenter foundUpcomingItems:@[groceries]];
}
- (
void
)testAddNewToDoItemActionPresentsAddToDoUI
{
[[self.wireframe expect] presentAddInterface];
[self.presenter addNewEntry];
}
- (
void
)testShowingNoContentMessageShowsNoContentView
{
[self.view showNoContentMessage];
XCTAssertEqualObjects(self.view.view, self.view.noContentView, @
"the no content view should be the view"
);
}
- (
void
)testShowingUpcomingItemsShowsTableView
{
[self.view showUpcomingDisplayData:nil];
XCTAssertEqualObjects(self.view.view, self.view.tableView, @
"the table view should be the view"
);
}
结论
‘bunny’
对象,或者你的应用使用了故事板的 segues。没关系的,在这些情况下,你只需要在做决定时稍微考虑下 VIPER 所代表的精神就好。VIPER 的核心在于它是建立在
单一责任原则
上的架构。如果你碰到了些许麻烦,想想这些原则再考虑如何前进。
Swift 补充
Swift
的编程语言来作为 Cocoa 和 Cocoa Touch 开发的未来。现在发表关于 Swift 的完整意见还为时尚早,但众所周知编程语言对我们如何设计和构建应用有着重大影响。我们决定使用
Swift 重写我们的待办事项清单
,帮助我们学习它对 VIPER 意味着什么。至今为止,收获颇丰。Swift 中的一些特性对于构建应用的体验有着显著的提升。
结构体
struct UpcomingDisplayItem : Equatable, Printable {
let title : String =
""
let dueDate : String =
""
var
description : String { get {
return
"\(title) -- \(dueDate)"
}}
init(title: String, dueDate: String) {
self.title = title
self.dueDate = dueDate
}
}
func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool {
var
hasEqualSections =
false
hasEqualSections = rightSide.title == leftSide.title
if
hasEqualSections ==
false
{
return
false
}
hasEqualSections = rightSide.dueDate == rightSide.dueDate
return
hasEqualSections
}
类型安全
扩展阅读
转载于:https://my.oschina.net/hejunbinlan/blog/511286