Merge pull request #24 from AshFurrow/reactiveviewmodel

[WIP] Reactive View Model Refactoring & Testing
This commit is contained in:
Ash Furrow
2013-12-28 06:48:53 -08:00
13 changed files with 250 additions and 46 deletions

View File

@@ -16,6 +16,8 @@
5E730B0E1815F3E4003FCB43 /* FRPGalleryViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E730B0D1815F3E4003FCB43 /* FRPGalleryViewModel.m */; };
5E730B101815F78B003FCB43 /* FRPGalleryViewModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E730B0F1815F78B003FCB43 /* FRPGalleryViewModelTests.m */; };
5E730B141815FE97003FCB43 /* FRPFullSizePhotoViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E730B131815FE97003FCB43 /* FRPFullSizePhotoViewModel.m */; };
5E93AD9A186C781000795C9E /* FRPFullSizePhotoViewModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E93AD99186C781000795C9E /* FRPFullSizePhotoViewModelTests.m */; };
5E93AD9C186C80DE00795C9E /* FRPPhotoViewModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E93AD9B186C80DE00795C9E /* FRPPhotoViewModelTests.m */; };
5EAD1F5818173A3200C67860 /* FRPPhotoViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EAD1F5718173A3200C67860 /* FRPPhotoViewModel.m */; };
5EAD1F5C18173F1500C67860 /* FRPLoginViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EAD1F5B18173F1500C67860 /* FRPLoginViewModel.m */; };
5EAD1F601817418800C67860 /* FRPPhotoDetailViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EAD1F5F1817418800C67860 /* FRPPhotoDetailViewModel.m */; };
@@ -63,6 +65,8 @@
5E730B0F1815F78B003FCB43 /* FRPGalleryViewModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FRPGalleryViewModelTests.m; sourceTree = "<group>"; };
5E730B121815FE97003FCB43 /* FRPFullSizePhotoViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FRPFullSizePhotoViewModel.h; sourceTree = "<group>"; };
5E730B131815FE97003FCB43 /* FRPFullSizePhotoViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FRPFullSizePhotoViewModel.m; sourceTree = "<group>"; };
5E93AD99186C781000795C9E /* FRPFullSizePhotoViewModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FRPFullSizePhotoViewModelTests.m; sourceTree = "<group>"; };
5E93AD9B186C80DE00795C9E /* FRPPhotoViewModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FRPPhotoViewModelTests.m; sourceTree = "<group>"; };
5EAD1F5618173A3200C67860 /* FRPPhotoViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FRPPhotoViewModel.h; sourceTree = "<group>"; };
5EAD1F5718173A3200C67860 /* FRPPhotoViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FRPPhotoViewModel.m; sourceTree = "<group>"; };
5EAD1F5A18173F1500C67860 /* FRPLoginViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FRPLoginViewModel.h; sourceTree = "<group>"; };
@@ -282,6 +286,8 @@
isa = PBXGroup;
children = (
5E730B0F1815F78B003FCB43 /* FRPGalleryViewModelTests.m */,
5E93AD99186C781000795C9E /* FRPFullSizePhotoViewModelTests.m */,
5E93AD9B186C80DE00795C9E /* FRPPhotoViewModelTests.m */,
5EBE2B1A180B07D0007B6BF3 /* Supporting Files */,
);
path = FRPTests;
@@ -482,7 +488,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5E93AD9A186C781000795C9E /* FRPFullSizePhotoViewModelTests.m in Sources */,
5E730B101815F78B003FCB43 /* FRPGalleryViewModelTests.m in Sources */,
5E93AD9C186C80DE00795C9E /* FRPPhotoViewModelTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -13,11 +13,11 @@
@interface FRPFullSizePhotoViewModel : RVMViewModel
-(instancetype)initWithPhotoArray:(NSArray *)photoArray initialPhotoIndex:(NSInteger)initialPhotoIndex;
-(FRPPhotoModel *)photoModelAtIndex:(NSInteger)index;
@property (nonatomic, readonly, strong) NSArray *model;
@property (nonatomic, readonly) NSInteger initialPhotoIndex;
@property (nonatomic, readonly) NSString *initialPhotoName;
-(FRPPhotoModel *)photoModelAtIndex:(NSInteger)index;
@end

View File

@@ -30,9 +30,14 @@
}
-(NSString *)initialPhotoName {
return [self.model[self.initialPhotoIndex] photoName];
FRPPhotoModel *photoModel = [self initialPhotoModel];
return [photoModel photoName];
}
//-(NSString *)initialPhotoName {
// return [self.model[self.initialPhotoIndex] photoName];
//}
-(FRPPhotoModel *)photoModelAtIndex:(NSInteger)index {
if (index < 0 || index > self.model.count - 1) {
// Index was out of bounds, return nil
@@ -42,4 +47,10 @@
}
}
#pragma mark - Private Methods
-(FRPPhotoModel *)initialPhotoModel {
return [self photoModelAtIndex:self.initialPhotoIndex];
}
@end

View File

@@ -1,4 +1,4 @@
//
//
// FRPGalleryViewModel.m
// FRP
//
@@ -21,9 +21,13 @@
self = [super init];
if (!self) return nil;
RAC(self, model) = [[[FRPPhotoImporter importPhotos] logError] catchTo:[RACSignal empty]];
RAC(self, model) = [self importPhotosSignal];
return self;
}
-(RACSignal *)importPhotosSignal {
return [[[FRPPhotoImporter importPhotos] logError] catchTo:[RACSignal empty]];
}
@end

View File

@@ -107,9 +107,9 @@
}
}
+(NSString *)urlForImageSize:(NSInteger)size inDictionary:(NSDictionary *)dictionary {
+(NSString *)urlForImageSize:(NSInteger)size inDictionary:(NSArray *)array {
/*
images = (
(
{
size = 3;
url = "http://ppcdn.500px.org/49204370/b125a49d0863e0ba05d8196072b055876159f33e/3.jpg";
@@ -117,7 +117,7 @@
);
*/
return [[[[[dictionary rac_sequence] filter:^BOOL(NSDictionary *value) {
return [[[[[array rac_sequence] filter:^BOOL(NSDictionary *value) {
return [value[@"size"] integerValue] == size;
}] map:^id(id value) {
return value[@"url"];

View File

@@ -38,8 +38,7 @@
return self;
}
-(void)viewDidLoad
{
-(void)viewDidLoad {
[super viewDidLoad];
// Configure self's view
@@ -47,18 +46,22 @@
// Configure subviews
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
RAC(imageView, image) = self.viewModel.photoImageSignal;
RAC(imageView, image) = RACObserve(self.viewModel, photoImage);
imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];
self.imageView = imageView;
[self.viewModel.didBecomeActiveSignal subscribeNext:^(id x) {
[SVProgressHUD dismiss];
[RACObserve(self.viewModel, loading) subscribeNext:^(NSNumber *loading){
if (loading.boolValue) {
[SVProgressHUD show];
} else {
[SVProgressHUD dismiss];
}
}];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.viewModel.active = YES;
}

View File

@@ -14,7 +14,8 @@
@property (nonatomic, readonly) FRPPhotoModel *model;
@property (nonatomic, readonly) RACSignal *photoImageSignal;
@property (nonatomic, readonly) UIImage *photoImage;
@property (nonatomic, readonly, getter = isLoading) BOOL loading;
-(NSString *)photoName;

View File

@@ -14,7 +14,8 @@
@interface FRPPhotoViewModel ()
@property (nonatomic, strong) RACSignal *photoImageSignal;
@property (nonatomic, strong) UIImage *photoImage;
@property (nonatomic, assign, getter = isLoading) BOOL loading;
@end
@@ -27,22 +28,35 @@
@weakify(self);
[self.didBecomeActiveSignal subscribeNext:^(id x) {
@strongify(self);
[[FRPPhotoImporter fetchPhotoDetails:self.model] subscribeError:^(NSError *error) {
NSLog(@"Could not fetch photo details: %@", error);
} completed:^{
NSLog(@"Fetched photo details.");
}];
[self downloadPhotoModelDetails];
}];
self.photoImageSignal = [RACObserve(self.model, fullsizedData) map:^id(id value) {
RAC(self, photoImage) = [RACObserve(self.model, fullsizedData) map:^id(id value) {
return [UIImage imageWithData:value];
}];
return self;
}
#pragma mark - Public Methods
-(NSString *)photoName {
return self.model.photoName;
}
#pragma mark - Private Methods
-(void)downloadPhotoModelDetails {
self.loading = YES;
@weakify(self);
[[FRPPhotoImporter fetchPhotoDetails:self.model] subscribeError:^(NSError *error) {
NSLog(@"Could not fetch photo details: %@", error);
} completed:^{
@strongify(self);
self.loading = NO;
NSLog(@"Fetched photo details.");
}];
}
@end

View File

@@ -0,0 +1,77 @@
//
// FRPFullSizePhotoViewModelTests.m
// FRP
//
// Created by Ash Furrow on 12/26/2013.
// Copyright (c) 2013 Ash Furrow. All rights reserved.
//
#import <Specta/Specta.h>
#define EXP_SHORTHAND
#import <Expecta/Expecta.h>
#import <OCMock/OCMock.h>
#import "FRPFullSizePhotoViewModel.h"
#import "FRPPhotoModel.h"
@interface FRPFullSizePhotoViewModel ()
-(FRPPhotoModel *)initialPhotoModel;
@end
SpecBegin(FRPFullSizePhotoViewModel)
describe(@"FRPFullSizePhotomodel", ^{
it (@"should assign correct attributes when initialized", ^{
NSArray *model = @[];
NSInteger initialPhotoIndex = 1337;
FRPFullSizePhotoViewModel *viewModel = [[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
expect(model).to.equal(viewModel.model);
expect(initialPhotoIndex).to.equal(viewModel.initialPhotoIndex);
});
it (@"should return nil for an out-of-bounds photo index", ^{
NSArray *model = @[[NSObject new]];
NSInteger initialPhotoIndex = 0;
FRPFullSizePhotoViewModel *viewModel = [[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
id subzeroModel = [viewModel photoModelAtIndex:-1];
expect(subzeroModel).to.beNil();
id aboveBoundsModel = [viewModel photoModelAtIndex:model.count];
expect(aboveBoundsModel).to.beNil();
});
it (@"should return the correct model for photoModelAtIndex:", ^{
id photoModel = [NSObject new];
NSArray *model = @[photoModel];
NSInteger initialPhotoIndex = 0;
FRPFullSizePhotoViewModel *viewModel = [[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
id returnedModel = [viewModel photoModelAtIndex:0];
expect(returnedModel).to.equal(photoModel);
});
it (@"should return the correct initial photo model", ^{
NSArray *model = @[[NSObject new]];
NSInteger initialPhotoIndex = 0;
FRPFullSizePhotoViewModel *viewModel = [[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
id mockViewModel = [OCMockObject partialMockForObject:viewModel];
[[[mockViewModel expect] andReturn:model[0]] photoModelAtIndex:initialPhotoIndex];
id returnedObject = [mockViewModel initialPhotoModel];
expect(returnedObject).to.equal(model[0]);
[mockViewModel verify];
});
});
SpecEnd

View File

@@ -10,22 +10,24 @@
#import <OCMock/OCMock.h>
#import "FRPGalleryViewModel.h"
#import "FRPPhotoImporter.h"
@interface FRPGalleryViewModel ()
-(RACSignal *)importPhotosSignal;
@end
SpecBegin(FRPGalleryViewModel)
describe(@"FRPGalleryViewModel", ^{
beforeAll(^{
// This is run once and only once before all of the examples
// in this group and before any beforeEach blocks.
});
beforeEach(^{
// This is run before each example.
});
it(@"should be initialized and call importPhotos", ^{
STAssertTrue(false, @"Test not implemented.");
id mockObject = [OCMockObject mockForClass:[FRPGalleryViewModel class]];
[[[mockObject expect] andReturn:[RACSignal empty]] importPhotosSignal];
mockObject = [mockObject init];
[mockObject verify];
[mockObject stopMocking];
});
});

View File

@@ -0,0 +1,84 @@
//
// FRPPhotoViewModelTests.m
// FRP
//
// Created by Ash Furrow on 12/26/2013.
// Copyright (c) 2013 Ash Furrow. All rights reserved.
//
#import <Specta/Specta.h>
#define EXP_SHORTHAND
#import <Expecta/Expecta.h>
#import <OCMock/OCMock.h>
#import "FRPPhotoViewModel.h"
#import "FRPPhotoModel.h"
@interface FRPPhotoViewModel ()
-(void)downloadPhotoModelDetails;
@end
SpecBegin(FRPPhotoViewModel)
describe(@"FRPPhotoViewModel", ^{
it (@"should return the photo's name property when photoName is invoked", ^{
NSString *name = @"Ash";
id mockPhotoModel = [OCMockObject mockForClass:[FRPPhotoModel class]];
[[[mockPhotoModel stub] andReturn:name] photoName];
FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:nil];
id mockViewModel = [OCMockObject partialMockForObject:viewModel];
[[[mockViewModel stub] andReturn:mockPhotoModel] model];
id returnedName = [mockViewModel photoName];
expect(returnedName).to.equal(name);
[mockPhotoModel stopMocking];
});
it (@"should download photo model details when it becomes active", ^{
FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:nil];
id mockViewModel = [OCMockObject partialMockForObject:viewModel];
[[mockViewModel expect] downloadPhotoModelDetails];
[mockViewModel setActive:YES];
[mockViewModel verify];
});
it (@"should correctly map image data to UIImage", ^{
UIImage *image = [[UIImage alloc] init];
NSData *imageData = [NSData data];
id mockImage = [OCMockObject mockForClass:[UIImage class]];
[[[mockImage stub] andReturn:image] imageWithData:imageData];
FRPPhotoModel *photoModel = [[FRPPhotoModel alloc] init];
photoModel.fullsizedData = imageData;
__unused FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:photoModel];
[mockImage verify];
[mockImage stopMocking];
});
it (@"should return the correct photo name", ^{
NSString *name = @"Ash";
FRPPhotoModel *photoModel = [[FRPPhotoModel alloc] init];
photoModel.photoName = name;
FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:photoModel];
NSString *returnedName = [viewModel photoName];
expect(name).to.equal(returnedName);
});
});
SpecEnd

View File

@@ -16,8 +16,8 @@ pod 'ReactiveCocoa', '2.1.4'
pod 'ReactiveViewModel', '0.1.1'
pod 'libextobjc', '0.3'
pod '500px-iOS-api', '1.0.5'
pod 'Specta', '~> 0.1.11'
pod 'Specta', '~> 0.2.1'
pod 'Expecta', '~> 0.2'
pod 'OCMock', '~> 2.0.1'
pod 'OCMock', '~> 2.2.2'
end
end

View File

@@ -86,7 +86,7 @@ PODS:
- libextobjc/RuntimeExtensions (0.3)
- libextobjc/UmbrellaHeader (0.3)
- libffi (3.0.13)
- OCMock (2.0.1)
- OCMock (2.2.2)
- ReactiveCocoa (2.1.4):
- ReactiveCocoa/Core
- ReactiveCocoa/no-arc
@@ -94,18 +94,18 @@ PODS:
- ReactiveCocoa/no-arc
- ReactiveCocoa/no-arc (2.1.4)
- ReactiveViewModel (0.1.1):
- ReactiveCocoa (= 2.1.4)
- Specta (0.1.11)
- ReactiveCocoa (~> 2.1)
- Specta (0.2.1)
- SVProgressHUD (0.9)
DEPENDENCIES:
- 500px-iOS-api (= 1.0.5)
- Expecta (~> 0.2)
- libextobjc (= 0.3)
- OCMock (~> 2.0.1)
- OCMock (~> 2.2.2)
- ReactiveCocoa (= 2.1.4)
- ReactiveViewModel (= 0.1.1)
- Specta (~> 0.1.11)
- Specta (~> 0.2.1)
- SVProgressHUD (= 0.9)
SPEC CHECKSUMS:
@@ -113,10 +113,10 @@ SPEC CHECKSUMS:
Expecta: dbc4a27fabb853bdd2e907e33f11ee43a9a47d0c
libextobjc: 820a79dbbbc498611e04fffd07d2d52a5588e7ac
libffi: 64ef39353e747bb2b25e1026afd96a157bf9231c
OCMock: f0c099603f851d07f8d7f2efe26d05da721ec43f
OCMock: ffba68873fd32cfd35d885bddad23bfa816da4a3
ReactiveCocoa: 0e8725dd3c609128144c15192f4dbdab827f54ae
ReactiveViewModel: 43714642e73dc029f4a23ca28cad6f4c57eb8487
Specta: 82746c6fd70b104c5d37cfbadb7bf15a2cbc4da0
ReactiveViewModel: e6cba4e138cfdaac19ea326bf42f6d6a61245f2c
Specta: 2d06220591110c6d9757d8be8ecf8e63aa40dc2a
SVProgressHUD: 03d4845ec8e64591726428a08236c6a5489d45f8
COCOAPODS: 0.26.2
COCOAPODS: 0.29.0