diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..07e3d52ac69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Xcode +.DS_Store +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +!default.xcworkspace +xcuserdata +profile +*.moved-aside +DerivedData +.idea/ +Pods +.pt +.build +tmtags +tmtagsHistory +config/releasenotes.txt +.bundle +Artsy.xcodeproj/xcshareddata/xcschemes/Artsy.xcscheme +/Classes/View\ Controllers/ARTopMenuViewController+DeveloperExtras.m +Podfile.local +chairs/ +.github +Artsy.xcodeproj/project.xcworkspace/xcshareddata/ +vendor/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..cff3a82b24e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +osx_image: xcode61 +language: objective-c +env: + global: + secure: "WCW/8qlLgH94wxvF86DrfBgzhAKFRtMIEBfj3bDWk8I1GUNkzUpU6Mya4zIgKS/WRVT31j5sVj4Do/9z2ffREnlAcWZPIurzyOm6IcJygVQN6uRlbI6m9J2L2WLu8W2XXQyYGIoEWMvznYZ9raY12QFnM+3LIvYFBckNZFhBUZA=" +cache: + - bundler + - cocoapods +env: + - UPLOAD_IOS_SNAPSHOT_BUCKET_NAME=eigen-ci UPLOAD_IOS_SNAPSHOT_BUCKET_PREFIX=snapshots + +before_install: + - 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc' + + - echo "machine github.com login $GITHUB_API_KEY" > ~/.netrc + - chmod 600 .netrc + - pod repo add artsy https://github.com/artsy/Specs.git + +install: + - bundle install + - make pods + +before_script: + - make ci + +script: + - make test + - make lint + +notifications: + slack: + secure: "fXmNnx6XW5OvT/j2jSSHYd3mHwbL+GzUSUSWmZVT0Vx/Ga5jXINTOYRY/9PYgJMqdL8a/L0Mf/18ZZ+tliPlWQ/DnfTz1a3Q/Pf94hfYSGhSGlQC/eXYcpOm/dNOKYQ3sr4tqXtTPylPUDXHeiM2D59ggdlUvVwcALGgHizajPQ=" diff --git a/AppIcon_114.png b/AppIcon_114.png new file mode 100644 index 00000000000..3e29ca766d0 Binary files /dev/null and b/AppIcon_114.png differ diff --git a/AppIcon_57.png b/AppIcon_57.png new file mode 100644 index 00000000000..6086047f657 Binary files /dev/null and b/AppIcon_57.png differ diff --git a/Artsy Tests/ARAnimatedTickViewTest.m b/Artsy Tests/ARAnimatedTickViewTest.m new file mode 100644 index 00000000000..eaa5b888490 --- /dev/null +++ b/Artsy Tests/ARAnimatedTickViewTest.m @@ -0,0 +1,41 @@ +#import "ARAnimatedTickView.h" + +@interface ARTickViewFrontLayer : CAShapeLayer +@end + +SpecBegin(ARAnimatedTickView) + +describe(@"initWithSelection", ^{ + it(@"inits with selected", ^{ + ARAnimatedTickView *tickView = [[ARAnimatedTickView alloc] initWithSelection:YES]; + expect(tickView).to.haveValidSnapshotNamed(@"selected"); + }); + + it(@"inits with deselected", ^{ + ARAnimatedTickView *tickView = [[ARAnimatedTickView alloc] initWithSelection:NO]; + expect(tickView).to.haveValidSnapshotNamed(@"deselected"); + }); +}); + +describe(@"set selected", ^{ + it(@"changes deselected to selected", ^{ + ARAnimatedTickView *tickView = [[ARAnimatedTickView alloc] initWithSelection:NO]; + [tickView setSelected:YES animated:NO]; + expect(tickView).to.haveValidSnapshotNamed(@"selected"); + }); + + it(@"changes selected to deselected", ^{ + ARAnimatedTickView *tickView = [[ARAnimatedTickView alloc] initWithSelection:YES]; + [tickView setSelected:NO animated:NO]; + expect(tickView).to.haveValidSnapshotNamed(@"deselected"); + }); +}); + +describe(@"ARTickViewFrontLayer layer", ^{ + it(@"returns an instance of ARTickViewFrontlayer", ^{ + ARTickViewFrontLayer *frontLayer = [ARTickViewFrontLayer layer]; + expect(frontLayer).to.beKindOf([ARTickViewFrontLayer class]); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARAppDelegate+Testing.h b/Artsy Tests/ARAppDelegate+Testing.h new file mode 100644 index 00000000000..a5b33600ba9 --- /dev/null +++ b/Artsy Tests/ARAppDelegate+Testing.h @@ -0,0 +1,7 @@ +#import "ARAppDelegate.h" + +@interface ARAppDelegate (Testing) + +- (BOOL)swizzled_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions; + +@end diff --git a/Artsy Tests/ARAppDelegate+Testing.m b/Artsy Tests/ARAppDelegate+Testing.m new file mode 100644 index 00000000000..fd6c62f51b4 --- /dev/null +++ b/Artsy Tests/ARAppDelegate+Testing.m @@ -0,0 +1,55 @@ +#import + +#import "ARRouter.h" +#import "ARSwitchBoard.h" +#import "ARLogger.h" +#import "ARAppDelegate+Testing.h" +#import "ARDispatchManager.h" + +@implementation ARAppDelegate (Testing) + +// Swizzle out -application:willFinishLaunchingWithOptions: and +// -application:didFinishLaunchingWithOptions: to not have the normal +// app logic interfere with the tests. +// +// As per mxcl's comment here: http://stackoverflow.com/a/12709123/1254854 + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self swapImplementationOf:@selector(application:didFinishLaunchingWithOptions:) + with:@selector(swizzled_application:didFinishLaunchingWithOptions:)]; + + [self swapImplementationOf:@selector(application:willFinishLaunchingWithOptions:) + with:@selector(swizzled_application:willFinishLaunchingWithOptions:)]; + }); +} + ++ (void)swapImplementationOf:(SEL)old with:(SEL)new +{ + Class class = [self class]; + Method oldMethod = class_getInstanceMethod(class, old); + Method newMethod = class_getInstanceMethod(class, new); + + if (class_addMethod(class, old, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { + class_replaceMethod(class, new, method_getImplementation(oldMethod), method_getTypeEncoding(oldMethod)); + } else { + method_exchangeImplementations(oldMethod, newMethod); + } +} + +- (BOOL)swizzled_application:(id)app willFinishLaunchingWithOptions:(id)opts +{ + [ARRouter setup]; + [ARDispatchManager sharedManager].useSyncronousDispatches = YES; + return YES; +} + +- (BOOL)swizzled_application:(id)app didFinishLaunchingWithOptions:(id)opts +{ + [[ARLogger sharedLogger] startLogging]; + return YES; +} + +@end diff --git a/Artsy Tests/ARAppNotificationsDelegateTests.m b/Artsy Tests/ARAppNotificationsDelegateTests.m new file mode 100644 index 00000000000..c01c7174fca --- /dev/null +++ b/Artsy Tests/ARAppNotificationsDelegateTests.m @@ -0,0 +1,188 @@ +#import "ARAppNotificationsDelegate.h" +#import "ARSwitchBoard.h" +#import +#import "ARAnalyticsConstants.h" +#import "ARNotificationView.h" +#import "ARTopMenuViewController.h" + +SpecBegin(ARAppNotificationsDelegate) + +// TODO: This is our slowest test by far, we should try speed it up. + +describe(@"registerForDeviceNotificationsOnce", ^{ + it(@"only registers for device notifications once", ^{ + id app = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + BOOL respondsToRegisterForRemoteNotifications = [app respondsToSelector:@selector(registerForRemoteNotifications)]; + if (respondsToRegisterForRemoteNotifications) { + [[app expect] registerForRemoteNotifications]; + } else { + UIRemoteNotificationType allTypes = (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert); + [[app expect] registerForRemoteNotificationTypes:allTypes]; + } + + ARAppNotificationsDelegate *delegate = (ARAppNotificationsDelegate *) [JSDecoupledAppDelegate sharedAppDelegate].remoteNotificationsDelegate; + [delegate registerForDeviceNotificationsOnce]; + [app verifyWithDelay:1]; + + if (respondsToRegisterForRemoteNotifications) { + [[app reject] registerForRemoteNotifications]; + } else { + UIRemoteNotificationType allTypes = (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert); + [[app reject] registerForRemoteNotificationTypes:allTypes]; + } + [delegate registerForDeviceNotificationsOnce]; + [app verify]; + [app stopMocking]; + }); +}); + +describe(@"receiveRemoteNotification", ^{ + + __block id mockApplication = nil; + __block id mockAnalytics = nil; + + beforeEach(^{ + mockApplication = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + mockAnalytics = [OCMockObject mockForClass:[ARAnalytics class]]; + [[mockAnalytics stub] event:OCMOCK_ANY withProperties:OCMOCK_ANY]; + }); + + afterEach(^{ + [mockAnalytics stopMocking]; + [mockApplication stopMocking]; + }); + + describe(@"brought back from the background", ^{ + beforeEach(^{ + UIApplicationState state = UIApplicationStateBackground; + [[[mockApplication stub] andReturnValue:OCMOCK_VALUE(state)] applicationState]; + }); + + it(@"navigates to the url provided", ^{ + id classMock = [OCMockObject mockForClass:[ARTopMenuViewController class]]; + // Just to silence runtime assertion failure. + [[[classMock stub] andReturn:nil] sharedController]; + + id JSON = @{ @"url" : @"http://artsy.net/feature" }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification =[NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + UIApplication *app = [UIApplication sharedApplication]; + + id mock = [OCMockObject partialMockForObject:ARSwitchBoard.sharedInstance]; + [[mock expect] loadPath:@"http://artsy.net/feature"]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + [classMock stopMocking]; + }); + + it(@"triggers an analytics event for a notification with an url received and tapped", ^{ + id classMock = [OCMockObject mockForClass:[ARTopMenuViewController class]]; + // Just to silence runtime assertion failure. + [[[classMock stub] andReturn:nil] sharedController]; + id JSON = @{ @"url" : @"http://artsy.net/feature" }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSMutableDictionary *notificationWithAppState = [[NSMutableDictionary alloc] initWithDictionary:notification]; + [notificationWithAppState setObject:@"background" forKey:@"UIApplicationState"]; + UIApplication *app = [UIApplication sharedApplication]; + id mock = [OCMockObject mockForClass:[ARAnalytics class]]; + [[mock expect] event:ARAnalyticsNotificationReceived withProperties:notificationWithAppState]; + [[mock expect] event:ARAnalyticsNotificationTapped withProperties:notificationWithAppState]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + [classMock stopMocking]; + }); + + it(@"triggers an analytics event for a notification without a url received and tapped", ^{ + id JSON = @{ @"aps" : @{ @"alert" : @"hello world" } }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSMutableDictionary *notificationWithAppState = [[NSMutableDictionary alloc] initWithDictionary:notification]; + [notificationWithAppState setObject:@"background" forKey:@"UIApplicationState"]; + UIApplication *app = [UIApplication sharedApplication]; + id mock = [OCMockObject mockForClass:[ARAnalytics class]]; + [[mock expect] event:ARAnalyticsNotificationReceived withProperties:notificationWithAppState]; + [[mock expect] event:ARAnalyticsNotificationTapped withProperties:notificationWithAppState]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + }); + + it(@"does not display the message in aps/alert", ^{ + id JSON = @{ @"aps" : @{ @"alert" : @"hello world" } }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + id mock = [OCMockObject mockForClass:[ARNotificationView class]]; + [[mock reject] + showNoticeInView:OCMOCK_ANY + title:OCMOCK_ANY + hideAfter:0 + response:OCMOCK_ANY]; + UIApplication *app = [UIApplication sharedApplication]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + }); + }); + + describe(@"running in the foreground", ^{ + beforeEach(^{ + UIApplicationState state = UIApplicationStateActive; + [[[mockApplication stub] andReturnValue:OCMOCK_VALUE(state)] applicationState]; + }); + + it(@"triggers only an analytics event for the notification received", ^{ + id JSON = @{ @"url" : @"http://artsy.net/feature" }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSMutableDictionary *notificationWithAppState = [[NSMutableDictionary alloc] initWithDictionary:notification]; + [notificationWithAppState setObject:@"active" forKey:@"UIApplicationState"]; + UIApplication *app = [UIApplication sharedApplication]; + id mock = [OCMockObject mockForClass:[ARAnalytics class]]; + [[mock expect] event:ARAnalyticsNotificationReceived withProperties:notificationWithAppState]; + [[mock reject] event:ARAnalyticsNotificationTapped withProperties:notificationWithAppState]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + }); + + it(@"displays message in aps/alert", ^{ + id JSON = @{ @"url" : @"http://artsy.net/feature", @"aps" : @{ @"alert" : @"hello world" } }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + id mock = [OCMockObject mockForClass:[ARNotificationView class]]; + [[mock expect] + showNoticeInView:OCMOCK_ANY + title:@"hello world" + hideAfter:0 + response:OCMOCK_ANY]; + UIApplication *app = [UIApplication sharedApplication]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + }); + + it(@"defaults message to url", ^{ + id JSON = @{ @"url" : @"http://artsy.net/feature" }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + NSDictionary *notification = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + id mock = [OCMockObject mockForClass:[ARNotificationView class]]; + [[mock expect] + showNoticeInView:OCMOCK_ANY + title:@"http://artsy.net/feature" + hideAfter:0 + response:OCMOCK_ANY]; + UIApplication *app = [UIApplication sharedApplication]; + [[app delegate] application:app didReceiveRemoteNotification:notification]; + [mock verify]; + [mock stopMocking]; + }); + + pending(@"displays message in aps/alert and navigates to url provided"); + }); +}); + +SpecEnd + diff --git a/Artsy Tests/ARAppSearchViewControllerSpec.m b/Artsy Tests/ARAppSearchViewControllerSpec.m new file mode 100644 index 00000000000..f35a0084042 --- /dev/null +++ b/Artsy Tests/ARAppSearchViewControllerSpec.m @@ -0,0 +1,127 @@ +#import "ARAppSearchViewController.h" +#import "ARTopMenuViewController.h" + +@interface ARAppSearchViewController(Testing) +@property (readwrite, nonatomic) BOOL shouldAnimate; +- (void)clearTapped:(id)sender; +- (void)closeSearch:(id)sender; +@end + + +SpecBegin(ARAppSearchViewController) + +__block ARAppSearchViewController *sut; + +dispatch_block_t sharedBefore = ^{ + sut = [[ARAppSearchViewController alloc] init]; + sut.shouldAnimate = NO; + [sut ar_presentWithFrame:[UIScreen mainScreen].bounds]; + + [sut beginAppearanceTransition:YES animated:NO]; + [sut endAppearanceTransition]; + [sut.view setNeedsLayout]; + [sut.view layoutIfNeeded]; +}; + +itHasSnapshotsForDevices(@"looks correct", ^{ + sharedBefore(); + return sut; +}); + +context(@"searching", ^{ + context(@"with results", ^{ + + + itHasSnapshotsForDevices(@"displays search results", ^{ + + sharedBefore(); + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/match" withResponse:@[ + @{ + @"model": @"artist", + @"id": @"aes-plus-f", + @"display": @"AES+F", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"Russian, Founded 1987", + @"published": @(YES), + @"highlights": @[] + }, + @{ + @"model": @"artist", + @"id": @"john-f-carlson", + @"display": @"John F. Carlson", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"Swedish-American, 1875-1947", + @"published": @(YES), + @"highlights": @[] + }, + @{ + @"model": @"artist", + @"id": @"f-scott-hess", + @"display": @"F. Scott Hess", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"American, born 1955", + @"published": @(YES), + @"highlights": @[] + }] + ]; + + sut.textField.text = @"f"; + [sut.textField sendActionsForControlEvents:UIControlEventEditingChanged]; + + expect(sut.searchResults.count).will.equal(3); + + return sut; + }); + }); + + context(@"with no results", ^{ + itHasSnapshotsForDevices(@"displays zero state", ^{ + + sharedBefore(); + sut.searchDataSource.searchResults = [NSOrderedSet orderedSetWithObjects:[SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"f-scott-hess", + @"display": @"F. Scott Hess", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"American, born 1955", + @"published": @(YES), + @"highlights": @[] + }], nil]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/match" withResponse:@[]]; + + sut.textField.text = @"f"; + [sut.textField sendActionsForControlEvents:UIControlEventEditingChanged]; + + expect(sut.searchResults.count).will.equal(0); + return sut; + }); + }); +}); + +it(@"clears search", ^{ + // custom clear button for dark color scheme + sut = [[ARAppSearchViewController alloc] init]; + sut.shouldAnimate = NO; + [sut ar_presentWithFrame:[UIScreen mainScreen].bounds]; + sut.textField.text = @"s"; + [sut clearTapped:nil]; + expect(sut.textField.text).to.equal(@""); + expect(sut.searchDataSource.searchResults.count).to.equal(0); +}); + +it(@"closes search", ^{ + sut = [[ARAppSearchViewController alloc] init]; + OCMockObject *topMenuViewControllerMock = [OCMockObject partialMockForObject:[ARTopMenuViewController sharedController]]; + sut.shouldAnimate = NO; + [sut ar_presentWithFrame:[UIScreen mainScreen].bounds]; + [[topMenuViewControllerMock expect] returnToPreviousTab]; + [sut closeSearch:nil]; + [topMenuViewControllerMock verify]; +}); + +SpecEnd diff --git a/Artsy Tests/ARArtistViewControllerTests.m b/Artsy Tests/ARArtistViewControllerTests.m new file mode 100644 index 00000000000..6c58863829f --- /dev/null +++ b/Artsy Tests/ARArtistViewControllerTests.m @@ -0,0 +1,133 @@ +#import "ARArtistViewController.h" +#import "ARStubbedArtistNetworkModel.h" + +@interface ARArtistViewController (Tests) +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@property (nonatomic, strong) ARArtistNetworkModel *networkModel; +@end + +SpecBegin(ARArtistViewController) + +__block ARStubbedArtistNetworkModel *networkModel; + +itHasSnapshotsForDevices(@"displays artwork counts", ^{ + + ARArtistViewController *vc = [[ARArtistViewController alloc] initWithArtistID:@"some-artist"]; + + networkModel = [[ARStubbedArtistNetworkModel alloc] initWithArtist:vc.artist]; + networkModel.artistForArtistInfo = [Artist modelWithJSON:@{ + @"id": @"some-artist", + @"name": @"Some Artist", + @"years": @"1928-1987", + @"published_artworks_count": @(396), + @"forsale_artworks_count": @(285), + @"artworks_count": @(919) + }]; + + networkModel.artworksForArtworksAtPage = @[]; + vc.networkModel = networkModel; + + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; +}); + +itHasSnapshotsForDevices(@"no artworks", ^{ + ARArtistViewController *vc = [[ARArtistViewController alloc] initWithArtistID:@"some-artist"]; + networkModel = [[ARStubbedArtistNetworkModel alloc] initWithArtist:vc.artist]; + networkModel.artistForArtistInfo = [Artist modelWithJSON:@{ + @"id": @"some-artist", + @"name": @"Some Artist", + @"years": @"1928-1987", + @"published_artworks_count": @(0), + @"forsale_artworks_count": @(0), + @"artworks_count": @(0) + }]; + + networkModel.artworksForArtworksAtPage = @[]; + + vc.networkModel = networkModel; + + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; +}); + +itHasSnapshotsForDevices(@"artworks masonry", ^{ + + ARArtistViewController *vc = [[ARArtistViewController alloc] initWithArtistID:@"some-artist"]; + networkModel = [[ARStubbedArtistNetworkModel alloc] initWithArtist:vc.artist]; + networkModel.artistForArtistInfo = [Artist modelWithJSON:@{ + @"id": @"some-artist", + @"name": @"Some Artist", + @"years": @"1928-1987", + @"published_artworks_count": @(3), + @"forsale_artworks_count": @(1), + @"artworks_count": @(0) + }]; + + networkModel.artworksForArtworksAtPage = [Artwork arrayOfModelsWithJSON:@[ + @{ @"id" : @"some-artist-artwork-1", @"availability" : @"for sale" }, + @{ @"id" : @"some-artist-artwork-2", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-3", @"availability" : @"not for sale" } + ]]; + + vc.networkModel = networkModel; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; +}); + +itHasSnapshotsForDevices(@"two-rows artworks masonry", ^{ + ARArtistViewController *vc = [[ARArtistViewController alloc] initWithArtistID:@"some-artist"]; + networkModel = [[ARStubbedArtistNetworkModel alloc] initWithArtist:vc.artist]; + networkModel.artistForArtistInfo = [Artist modelWithJSON:@{ + @"id": @"some-artist", + @"name": @"Some Artist", + @"years": @"1928-1987", + @"published_artworks_count": @(10), + @"forsale_artworks_count": @(4), + @"artworks_count": @(10) + }]; + + networkModel.artworksForArtworksAtPage = [Artwork arrayOfModelsWithJSON:@[ + @{ @"id" : @"some-artist-artwork-1", @"availability" : @"for sale" }, + @{ @"id" : @"some-artist-artwork-2", @"availability" : @"for sale" }, + @{ @"id" : @"some-artist-artwork-3", @"availability" : @"for sale" }, + @{ @"id" : @"some-artist-artwork-4", @"availability" : @"for sale" }, + @{ @"id" : @"some-artist-artwork-5", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-6", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-7", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-8", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-9", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-10", @"availability" : @"not for sale" }, + ]]; + + vc.networkModel = networkModel; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; +}); + +itHasSnapshotsForDevices(@"with a bio", ^{ + + ARArtistViewController *vc = [[ARArtistViewController alloc] initWithArtistID:@"some-artist"]; + networkModel = [[ARStubbedArtistNetworkModel alloc] initWithArtist:vc.artist]; + networkModel.artistForArtistInfo = [Artist modelWithJSON:@{ + @"id": @"some-artist", + @"name": @"Some Artist", + @"years": @"1928-1987", + @"published_artworks_count": @(3), + @"forsale_artworks_count": @(1), + @"artworks_count": @(0), + @"blurb": @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + }]; + + networkModel.artworksForArtworksAtPage = [Artwork arrayOfModelsWithJSON:@[ + @{ @"id" : @"some-artist-artwork-1", @"availability" : @"for sale" }, + @{ @"id" : @"some-artist-artwork-2", @"availability" : @"not for sale" }, + @{ @"id" : @"some-artist-artwork-3", @"availability" : @"not for sale" } + ]]; + + vc.networkModel = networkModel; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; +}); + +SpecEnd diff --git a/Artsy Tests/ARArtworkActionsViewTests.m b/Artsy Tests/ARArtworkActionsViewTests.m new file mode 100644 index 00000000000..e7f8faf6e9b --- /dev/null +++ b/Artsy Tests/ARArtworkActionsViewTests.m @@ -0,0 +1,237 @@ +#import "ARArtworkActionsView.h" +#import "ARAuctionBidderStateLabel.h" +#import "ORStackView+ArtsyViews.h" +#import "ARArtworkPriceView.h" +#import "ARArtworkAuctionPriceView.h" + +@interface ARArtworkActionsView () +@property(nonatomic, strong) Artwork *artwork; +@property(nonatomic, strong) SaleArtwork *saleArtwork; +@property(nonatomic, strong) ARBorderLabel *bidderStatusLabel; +@property (nonatomic, strong) ARArtworkPriceView *priceView; +@property (nonatomic, strong) ARArtworkAuctionPriceView *auctionPriceView; +- (void)updateUI; +- (void)setupCountdownView; +@end + +SpecBegin(ARArtworkActionsView) + +__block ARArtworkActionsView *view = nil; +__block id mockView = nil; + +beforeEach(^{ + view = [[ARArtworkActionsView alloc] initWithFrame:CGRectMake(0, 0, 320, 310)]; + mockView = [OCMockObject partialMockForObject:view]; + [[mockView stub] setupCountdownView]; +}); + +afterEach(^{ + [mockView stopMocking]; +}); + +it(@"displays contact gallery for a for sale artwork", ^{ + view.artwork = [Artwork modelWithJSON:@{ + @"id" : @"artwork-id", + @"title" : @"Artwork Title", + @"availability" : @"for sale" + }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + expect(view).to.haveValidSnapshotNamed(@"forSale"); +}); + +it(@"displays buy now for an acquireable work with pricing", ^{ + view.artwork = [Artwork modelWithJSON:@{ + @"id" : @"artwork-id", + @"title" : @"Artwork Title", + @"availability" : @"for sale", + @"acquireable" : @YES + }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + expect(view).to.haveValidSnapshotNamed(@"buy"); +}); + +it(@"displays contact seller when the partner is not a gallery", ^{ + view.artwork = [Artwork modelWithJSON:@{ + @"id" : @"artwork-id", + @"title" : @"Artwork Title", + @"availability" : @"for sale", + @"partner" : @{ + @"id" : @"partner_id", + @"type" : @"Museum", + @"name" : @"Guggenheim Museum" + } + }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + expect(view).to.haveValidSnapshotNamed(@"forSaleByAnInstitution"); +}); + +it(@"does not display contact when artwork is in auction", ^{ + view.artwork = [Artwork modelWithJSON:@{ + @"id" : @"artwork-id", + @"title" : @"Artwork Title", + @"availability" : @"for sale", + @"inquireable" : @YES + }]; + view.saleArtwork = [SaleArtwork modelWithJSON:@{ + @"high_estimate_cents" : @20000, + @"low_estimate_cents" : @10000 + }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + expect(view).to.haveValidSnapshotNamed(@"forSaleAtAuction"); +}); + +it(@"displays both bid and buy when artwork is in auction and is acquireable", ^{ + view.artwork = [Artwork modelWithJSON:@{ + @"id" : @"artwork-id", + @"title" : @"Artwork Title", + @"availability" : @"for sale", + @"price" : @"$5,000", + @"sold" : @NO, + @"acquireable" : @YES + }]; + view.saleArtwork = [SaleArtwork modelWithJSON:@{ + @"high_estimate_cents" : @20000, + @"low_estimate_cents" : @10000 + }]; + view.saleArtwork.auction = [Sale modelWithJSON:@{ + @"start_at" : @"1-12-30 00:00:00", + @"end_at" : @"4001-01-01 00:00:00" + }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + expect(view).to.haveValidSnapshotNamed(@"acquireableAtAuction"); +}); + +it(@"displays sold when artwork is in auction and has been acquired", ^{ + view.artwork = [Artwork modelWithJSON:@{ + @"id" : @"artwork-id", + @"title" : @"Artwork Title", + @"availability" : @"sold", + @"sold" : @YES, + @"price" : @"$5,000", + @"acquireable" : @NO + }]; + view.saleArtwork = [SaleArtwork modelWithJSON:@{ + @"high_estimate_cents" : @20000, + @"low_estimate_cents" : @10000 + }]; + view.saleArtwork.auction = [Sale modelWithJSON:@{ + @"start_at" : @"1-12-30 00:00:00", + @"end_at" : @"4001-01-01 00:00:00" + }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + expect(view).will.haveValidSnapshotNamed(@"soldAtAuction"); +}); + +context(@"bidderStatus", ^{ + __block ARAuctionBidderStateLabel *bidderStateView; + + it(@"high bidder", ^{ + Bid *highBid = [Bid modelWithJSON:@{ @"id" : @"abc", @"amount_cents" : @(10000000) }]; + SaleArtwork *saleArtwork = [SaleArtwork saleArtworkWithHighBid:highBid AndReserveStatus:ARReserveStatusNoReserve]; + BidderPosition *highPosition = [BidderPosition modelFromDictionary:@{ @"highestBid" : highBid, @"maxBidAmountCents" : highBid.cents }]; + highPosition.highestBid = highBid; + saleArtwork.positions = @[ highPosition ]; + bidderStateView = [[ARAuctionBidderStateLabel alloc] initWithFrame:CGRectMake(0, 0, 280, 58)]; + [bidderStateView updateWithSaleArtwork:saleArtwork]; + expect(bidderStateView).to.haveValidSnapshotNamed(@"testHighBidder"); + }); + + it(@"outbid", ^{ + Bid *highBid = [Bid modelWithJSON:@{ @"id" : @"abc", @"amount_cents" : @(10000000) }]; + SaleArtwork *saleArtwork = [SaleArtwork saleArtworkWithHighBid:highBid AndReserveStatus:ARReserveStatusNoReserve]; + BidderPosition *lowPosition = [BidderPosition modelWithJSON:@{ @"max_bid_amount_cents" : @(100) }]; + saleArtwork.positions = @[ lowPosition ]; + bidderStateView = [[ARAuctionBidderStateLabel alloc] initWithFrame:CGRectMake(0, 0, 280, 37)]; + [bidderStateView updateWithSaleArtwork:saleArtwork]; + expect(bidderStateView).to.haveValidSnapshotNamed(@"testOutbid"); + }); +}); + +context(@"price view", ^{ + context(@"not at auction", ^{ + it(@"price", ^{ + view.artwork = [Artwork modelFromDictionary:@{ @"price" : @"$30,000", @"inquireable" : @(true)}]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.priceView).to.haveValidSnapshot(); + }); + + it(@"sold", ^{ + view.artwork = [Artwork modelFromDictionary:@{ @"price" : @"$30,000", @"sold" : @(true) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.priceView).to.haveValidSnapshot(); + }); + + it(@"contact for price", ^{ + view.artwork = [Artwork modelFromDictionary:@{ @"price" : @"$30,000", @"inquireable" : @(true), @"availability" : @(ARArtworkAvailabilityForSale), @"isPriceHidden" : @(true) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.priceView).to.haveValidSnapshot(); + }); + }); + context(@"at auction", ^{ + it(@"no bids", ^{ + view.saleArtwork = [SaleArtwork modelWithJSON:@{ @"opening_bid_cents" : @(1000000) }]; + view.artwork = [Artwork modelFromDictionary:@{ @"sold" : @(false) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.auctionPriceView).to.haveValidSnapshot(); + }); + + it(@"has bids", ^{ + Bid *highBid = [Bid modelWithJSON:@{ @"id" : @"abc", @"amount_cents" : @(10000000) }]; + expect(highBid.cents).to.equal(10000000); + view.saleArtwork = [SaleArtwork saleArtworkWithHighBid:highBid AndReserveStatus:ARReserveStatusNoReserve];; + expect(view.saleArtwork.saleHighestBid.cents).to.equal(10000000); + view.artwork = [Artwork modelFromDictionary:@{ @"sold" : @(false) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.auctionPriceView).to.haveValidSnapshot(); + }); + + it(@"reserve met and has bids", ^{ + Bid *highBid = [Bid modelWithJSON:@{ @"id" : @"abc", @"amount_cents" : @(10000000) }]; + view.saleArtwork = [SaleArtwork saleArtworkWithHighBid:highBid AndReserveStatus:ARReserveStatusReserveMet]; + view.artwork = [Artwork modelFromDictionary:@{ @"sold" : @(false) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.auctionPriceView).to.haveValidSnapshot(); + }); + + it(@"current auction reserve not met and has bids", ^{ + Bid *highBid = [Bid modelWithJSON:@{ @"id" : @"abc", @"amount_cents" : @(10000000) }]; + view.saleArtwork = [SaleArtwork saleArtworkWithHighBid:highBid AndReserveStatus:ARReserveStatusReserveNotMet]; + view.saleArtwork.auction = [Sale modelWithJSON:@{ @"start_at" : @"1-12-30 00:00:00", @"end_at" : @"4001-01-01 00:00:00" }]; + view.artwork = [Artwork modelFromDictionary:@{ @"sold" : @(false) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.auctionPriceView).to.haveValidSnapshot(); + }); + + it(@"reserve not met and has no bids", ^{ + view.saleArtwork = [SaleArtwork modelWithJSON:@{ @"opening_bid_cents" : @(1000000), @"reserve_status" : @"reserve_not_met" }]; + view.saleArtwork.auction = [Sale modelWithJSON:@{ @"start_at" : @"1-12-30 00:00:00", @"end_at" : @"1-12-30 00:00:00" }]; + view.artwork = [Artwork modelFromDictionary:@{ @"sold" : @(false) }]; + [view updateUI]; + [view ensureScrollingWithHeight:CGRectGetHeight(view.bounds)]; + [view layoutIfNeeded]; + expect(view.auctionPriceView).to.haveValidSnapshot(); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARArtworkDetailViewTests.m b/Artsy Tests/ARArtworkDetailViewTests.m new file mode 100644 index 00000000000..167aedc3615 --- /dev/null +++ b/Artsy Tests/ARArtworkDetailViewTests.m @@ -0,0 +1,21 @@ +#import "ARArtworkDetailView.h" +#import + +SpecBegin(ARArtworkDetailView) + +it(@"displays both cm and in dimensions", ^{ + Artwork *artwork = [Artwork modelWithJSON:@{ + @"dimensions" : @{ + @"cm":@"100 cm big", + @"in":@"100 inches big", + } + }]; + + ARArtworkDetailView *view = [[ARArtworkDetailView alloc] initWithArtwork:nil andFair:nil]; + view.frame = (CGRect){ 0, 0, 320, 80 }; + [view updateWithArtwork:artwork]; + + expect(view).to.haveValidSnapshotNamed(@"bothDimensions"); +}); + +SpecEnd diff --git a/Artsy Tests/ARArtworkFavoritesNetworkModelTests.m b/Artsy Tests/ARArtworkFavoritesNetworkModelTests.m new file mode 100644 index 00000000000..da83792ac51 --- /dev/null +++ b/Artsy Tests/ARArtworkFavoritesNetworkModelTests.m @@ -0,0 +1,126 @@ +#import "ARArtworkFavoritesNetworkModel.h" +#import "ARUserManager+Stubs.h" +#import "ArtsyAPI.h" +#import "Artwork+Extensions.h" + +@interface ARFavoritesNetworkModel (Tests) +@property (readwrite, nonatomic, assign) NSInteger currentPage; +@property (readwrite, nonatomic, assign) BOOL downloadLock; +- (void)performNetworkRequestAtPage:(NSInteger)page withSuccess:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure; +@end + +SpecBegin(ARArtworkFavoritesNetworkModel) +__block ARArtworkFavoritesNetworkModel *networkModel; + +beforeEach(^{ + networkModel = [[ARArtworkFavoritesNetworkModel alloc] init]; +}); + +beforeAll(^{ + [ARUserManager stubAndLoginWithUsername]; +}); + +afterAll(^{ + [[ARUserManager sharedManager] logout]; +}); + +describe(@"init", ^{ + it(@"sets currentPage to 1", ^{ + expect(networkModel.currentPage).to.equal(1); + }); +}); + + +describe(@"getFavorites", ^{ + it(@"does not make request if another request is in progress", ^{ + id mock = [OCMockObject partialMockForObject:networkModel]; + networkModel.downloadLock = YES; + [[[mock reject] ignoringNonObjectArgs] performNetworkRequestAtPage:0 withSuccess:OCMOCK_ANY failure:OCMOCK_ANY]; + [mock getFavorites:nil failure:nil]; + [mock verify]; + [mock stopMocking]; + }); + + it(@"makes request if no request is in progress", ^{ + id mock = [OCMockObject partialMockForObject:networkModel]; + networkModel.downloadLock = NO; + [[[mock expect] ignoringNonObjectArgs] performNetworkRequestAtPage:0 withSuccess:OCMOCK_ANY failure:OCMOCK_ANY]; + [networkModel getFavorites:nil failure:nil]; + [networkModel getFavorites:nil failure:nil]; + [mock verify]; + [mock stopMocking]; + }); + + describe(@"success with artworks", ^{ + beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/collection/saved-artwork/artworks" withResponse:@[[Artwork stubbedArtworkJSON], [Artwork stubbedArtworkJSON]]]; + }); + + afterEach(^{ + [OHHTTPStubs removeAllStubs]; + }); + + it(@"increments currentPage", ^{ + [networkModel getFavorites:nil failure:nil]; + expect(networkModel.currentPage).will.equal(2); + }); + + it(@"does not set allDownloaded", ^{ + [networkModel getFavorites:nil failure:nil]; + expect(networkModel.allDownloaded).will.beFalsy(); + }); + }); + + describe(@"success without artworks", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/collection/saved-artwork/artworks" withResponse:@[]]; + }); + + it(@"does not increment currentPage", ^{ + [networkModel getFavorites:nil failure:nil]; + expect(networkModel.currentPage).will.equal(1); + }); + + it(@"sets allDownloaded", ^{ + [networkModel getFavorites:nil failure:nil]; + expect(networkModel.allDownloaded).will.beTruthy(); + }); + }); + + describe(@"failure", ^{ + before(^{ + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [[request.URL path] isEqualToString:@"/api/v1/collection/saved-artwork/artworks"]; + } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:404 userInfo:nil]]; + }]; + }); + + it(@"sets allDownloaded", ^{ + [networkModel getFavorites:nil failure:nil]; + expect(networkModel.allDownloaded).will.beTruthy(); + }); + }); + + describe(@"useSampleFavorites", ^{ + it(@"uses sample user id if YES", ^{ + id mock = [OCMockObject mockForClass:[ArtsyAPI class]]; + [[[mock expect] classMethod] getArtworkFromUserFavorites:@"502d15746e721400020006fa" page:1 success:OCMOCK_ANY failure:OCMOCK_ANY]; + networkModel.useSampleFavorites = YES; + [networkModel getFavorites:nil failure:nil]; + [mock verify]; + [mock stopMocking]; + }); + + it(@"uses current user id by default", ^{ + id mock = [OCMockObject mockForClass:[ArtsyAPI class]]; + [[[mock expect] classMethod] getArtworkFromUserFavorites:[User currentUser].userID page:1 success:OCMOCK_ANY failure:OCMOCK_ANY]; + [networkModel getFavorites:nil failure:nil]; + [mock verify]; + [mock stopMocking]; + }); + }); +}); + + +SpecEnd diff --git a/Artsy Tests/ARArtworkRelatedArtworksViewTests.m b/Artsy Tests/ARArtworkRelatedArtworksViewTests.m new file mode 100644 index 00000000000..2a4144bd6f2 --- /dev/null +++ b/Artsy Tests/ARArtworkRelatedArtworksViewTests.m @@ -0,0 +1,48 @@ +#import "ARArtworkRelatedArtworksView.h" +#import "AREmbeddedModelsViewController.h" +#import "ARArtworkMasonryModule.h" + +@interface ARArtworkRelatedArtworksView (Testing) +@property (nonatomic, strong) AREmbeddedModelsViewController *artworksVC; +- (void)renderWithArtworks:(NSArray *)artworks heading:(NSString *)heading; +@end + +SpecBegin(ARArtworkRelatedArtworksView) + +__block ARArtworkRelatedArtworksView *relatedView; + +before(^{ + relatedView = [[ARArtworkRelatedArtworksView alloc] init]; + [relatedView renderWithArtworks:@[[Artwork modelFromDictionary:@{@"title": @"Title"}]] heading:@"Related Heading"]; +}); + +describe(@"iPhone", ^{ + it(@"initializes module with correct layout", ^{ + ARArtworkMasonryLayout layout = [(ARArtworkMasonryModule *)relatedView.artworksVC.activeModule layout]; + expect(layout).to.equal(ARArtworkMasonryLayout2Column); + }); +}); + +describe(@"iPad", ^{ + beforeAll(^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + }); + + afterAll(^{ + [ARTestContext stopStubbing]; + }); + + it(@"initializes the module with correct layout", ^{ + ARArtworkMasonryLayout layout = [(ARArtworkMasonryModule *)relatedView.artworksVC.activeModule layout]; + expect(layout).to.equal(ARArtworkMasonryLayout3Column); + }); + + it(@"returns correct layout for orientation", ^{ + expect([relatedView masonryLayoutForPadWithOrientation:UIInterfaceOrientationLandscapeLeft]).to.equal(ARArtworkMasonryLayout4Column); + expect([relatedView masonryLayoutForPadWithOrientation:UIInterfaceOrientationLandscapeRight]).to.equal(ARArtworkMasonryLayout4Column); + expect([relatedView masonryLayoutForPadWithOrientation:UIInterfaceOrientationPortrait]).to.equal(ARArtworkMasonryLayout3Column); + expect([relatedView masonryLayoutForPadWithOrientation:UIInterfaceOrientationPortraitUpsideDown]).to.equal(ARArtworkMasonryLayout3Column); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARArtworkSetViewControllerSpec.m b/Artsy Tests/ARArtworkSetViewControllerSpec.m new file mode 100644 index 00000000000..a50866146e2 --- /dev/null +++ b/Artsy Tests/ARArtworkSetViewControllerSpec.m @@ -0,0 +1,26 @@ +#import "Artwork+Extensions.h" +#import "ARArtworkSetViewController.h" + +SpecBegin(ARArtworkSetViewController) + +__block ARArtworkSetViewController *controller; + + +describe(@"setting the index", ^{ + it(@"can deal with an out of bound index on init", ^{ + Artwork *artwork = [Artwork stubbedArtwork]; + controller = [[ARArtworkSetViewController alloc] initWithArtworkSet:@[artwork] fair:nil atIndex:2]; + + expect(controller.index).to.equal(0); + }); + + it(@"sets the index correctly", ^{ + Artwork *artwork = [Artwork stubbedArtwork]; + controller = [[ARArtworkSetViewController alloc] initWithArtworkSet:@[artwork, artwork] fair:nil atIndex:1]; + + expect(controller.index).to.equal(1); + }); + +}); + +SpecEnd diff --git a/Artsy Tests/ARArtworkViewControllerTests.m b/Artsy Tests/ARArtworkViewControllerTests.m new file mode 100644 index 00000000000..30fa11529bd --- /dev/null +++ b/Artsy Tests/ARArtworkViewControllerTests.m @@ -0,0 +1,225 @@ +#import "ARArtworkViewController.h" +#import "ARArtworkView.h" + +SpecBegin(ARArtworkViewController) + +__block UIWindow *window; +__block ARArtworkViewController *vc; + +after(^{ + [OHHTTPStubs removeAllStubs]; + vc = nil; +}); + +describe(@"no related data", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/posts" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/fairs" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/sales" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/collection/saved-artwork/artworks" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/layer/synthetic/main/artworks" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/artwork/some-artwork" + withResponse:@{ @"id": @"some-artwork", @"title": @"Some Title" }]; + }); + + it(@"shows artwork on iPhone", ^{ + window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARArtworkViewController alloc] initWithArtworkID:@"some-artwork" fair:nil]; + vc.shouldAnimate = NO; + + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + expect(vc.view).will.haveValidSnapshot(); + }); + + it(@"shows artwork on iPad", ^{ + waitUntil(^(DoneCallback done) { + [ARTestContext stubDevice:ARDeviceTypePad]; + window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARArtworkViewController alloc] initWithArtworkID:@"some-artwork" fair:nil]; + vc.shouldAnimate = NO; + + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + activelyWaitFor(0.5, ^{ + expect(vc.view).will.haveValidSnapshot(); + [ARTestContext stopStubbing]; + done(); + }); + }); + }); +}); + +describe(@"with related artworks", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/posts" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/fairs" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/sales" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/collection/saved-artwork/artworks" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/artwork/some-artwork" + withResponse:@{ @"id": @"some-artwork", @"title": @"Some Title" }]; + + }); + + describe(@"iPhone", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/layer/synthetic/main/artworks" + withParams:@{@"artwork[]": @"some-artwork"} + withResponse:@[ @{ @"id": @"one", @"title": @"One" }, @{ @"id": @"two", @"title": @"Two" } ]]; + + window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARArtworkViewController alloc] initWithArtworkID:@"some-artwork" fair:nil]; + vc.shouldAnimate = NO; + }); + + it(@"displays related artworks", ^{ + [vc.imageView removeFromSuperview];; + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + expect(vc.view).will.haveValidSnapshot(); + }); + + it(@"related artworks view looks correct", ^{ + waitUntil(^(DoneCallback done) { + + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + activelyWaitFor(0.5, ^{ + expect([(ARArtworkView *)vc.view relatedArtworksView]).will.haveValidSnapshot(); + done(); + }); + }); + }); + }); + + describe(@"iPad", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/layer/synthetic/main/artworks" + withParams:@{@"artwork[]": @"some-artwork"} + withResponse:@[ + @{ @"id": @"one", @"title": @"One" }, @{ @"id": @"two", @"title": @"Two" }, + @{ @"id": @"three", @"title": @"Three" }, @{ @"id": @"four", @"title": @"Four" }, + @{ @"id": @"five", @"title": @"Five" }, @{ @"id": @"six", @"title": @"Six" }, + ]]; + + [ARTestContext stubDevice:ARDeviceTypePad]; + window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARArtworkViewController alloc] initWithArtworkID:@"some-artwork" fair:nil]; + vc.shouldAnimate = NO; + }); + + after(^{ + [ARTestContext stopStubbing]; + }); + + it(@"displays related artworks", ^{ + waitUntil(^(DoneCallback done) { + + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + activelyWaitFor(0.5, ^{ + expect(vc.view).will.haveValidSnapshot(); + done(); + }); + }); + }); + + it(@"related artworks view looks correct", ^{ + waitUntil(^(DoneCallback done) { + + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + activelyWaitFor(0.5, ^{ + expect([(ARArtworkView *)vc.view relatedArtworksView]).will.haveValidSnapshot(); + done(); + }); + }); + }); + }); + +}); + +describe(@"at a closed auction", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/posts" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/fairs" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/collection/saved-artwork/artworks" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/sales" withResponse:@[@{ + @"id": @"some-auction", + @"name": @"Some Auction", + @"is_auction": @YES, + @"start_at": @"2000-04-07T16:00:00.000+00:00", + @"end_at": @"2014-04-17T03:59:00.000+00:00", + @"auction_state": @"closed", + @"published": @YES + }]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/bidder_positions" withParams:@{ + @"artwork_id":@"some-artwork", + @"sale_id":@"some-auction" + } withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/bidders" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sale/some-auction/sale_artworks" withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sale/some-auction/sale_artwork/some-artwork" withResponse:@{ + @"id": @"some-artwork", + @"sale_id": @"some-auction", + @"bidder_positions_count": @(1), + @"opening_bid_cents": @(1700000), + @"highest_bid_amount_cents": @(2200000), + @"minimum_next_bid_cents": @(2400000), + @"highest_bid": @{ + @"id": @"highest-bid-id", + @"amount_cents": @(2200000) + } + }]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/artwork/some-artwork" + withResponse:@{ @"id": @"some-artwork", @"title": @"Some Title" }]; + }); + + it(@"displays artwork on iPhone", ^{ + window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARArtworkViewController alloc] initWithArtworkID:@"some-artwork" fair:nil]; + vc.shouldAnimate = NO; + + [vc.imageView removeFromSuperview]; + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + expect(vc.view).will.haveValidSnapshot(); + }); + + it(@"displays artwork on iPad", ^{ + waitUntil(^(DoneCallback done) { + + [ARTestContext stubDevice:ARDeviceTypePad]; + window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARArtworkViewController alloc] initWithArtworkID:@"some-artwork" fair:nil]; + vc.shouldAnimate = NO; + + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + [vc setHasFinishedScrolling]; + activelyWaitFor(0.5, ^{ + expect(vc.view).will.haveValidSnapshot(); + [ARTestContext stopStubbing]; + done(); + }); + }); + }); +}); +pending(@"at a fair"); + +SpecEnd diff --git a/Artsy Tests/ARAspectRatioImageViewTests.m b/Artsy Tests/ARAspectRatioImageViewTests.m new file mode 100644 index 00000000000..99d2dc281fb --- /dev/null +++ b/Artsy Tests/ARAspectRatioImageViewTests.m @@ -0,0 +1,36 @@ +#import "ARAspectRatioImageView.h" +#import "UIImage+ImageFromColor.h" + +SpecBegin(ARAspectRatioImageView) + +__block ARAspectRatioImageView * view = nil; + +describe(@"intrinsicContentSize", ^{ + + describe(@"without an image", ^{ + beforeEach(^{ + view = [[ARAspectRatioImageView alloc] init]; + }); + + it(@"returns no metric", ^{ + CGSize size = (CGSize) { UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric }; + expect([view intrinsicContentSize]).to.equal(size); + }); + }); + + describe(@"with an image", ^{ + beforeEach(^{ + view = [[ARAspectRatioImageView alloc] initWithFrame:CGRectMake(0, 0, 1, 2)]; + view.image = [UIImage imageFromColor:[UIColor whiteColor]]; // 1x1 px + }); + + it(@"returns image size", ^{ + CGSize size = CGSizeMake(1, 1); + expect([view intrinsicContentSize]).to.equal(size); + }); + }); + +}); + +SpecEnd + diff --git a/Artsy Tests/ARBidButtonTests.m b/Artsy Tests/ARBidButtonTests.m new file mode 100644 index 00000000000..0c89da70802 --- /dev/null +++ b/Artsy Tests/ARBidButtonTests.m @@ -0,0 +1,32 @@ +#import "ARBidButton.h" + +SpecBegin(ARBidButton) + +__block ARBidButton * _button = nil; + +beforeEach(^{ + _button = [[ARBidButton alloc] initWithFrame:CGRectMake(0, 0, 300, 46)]; +}); + +it(@"register", ^{ + _button.auctionState = ARAuctionStateDefault; + expect(_button).will.haveValidSnapshotNamed(@"testRegisterState"); +}); + +it(@"registered", ^{ + _button.auctionState = ARAuctionStateUserIsRegistered; + expect(_button).will.haveValidSnapshotNamed(@"testRegisteredState"); +}); + +it(@"bidding open", ^{ + _button.auctionState = ARAuctionStateStarted; + expect(_button).will.haveValidSnapshotNamed(@"testBiddingOpenState"); +}); + +it(@"bidding closed", ^{ + _button.auctionState = ARAuctionStateEnded; + expect(_button).will.haveValidSnapshotNamed(@"testBiddingClosedState"); +}); + +SpecEnd + diff --git a/Artsy Tests/ARBrowseFeaturedLinkInsetCellTests.m b/Artsy Tests/ARBrowseFeaturedLinkInsetCellTests.m new file mode 100644 index 00000000000..109f5b86228 --- /dev/null +++ b/Artsy Tests/ARBrowseFeaturedLinkInsetCellTests.m @@ -0,0 +1,74 @@ +#import "ARBrowseFeaturedLinkInsetCell.h" +#import "FeaturedLink.h" + +@interface ARBrowseFeaturedLinkInsetCell (Test) +@property (nonatomic, strong) UIImageView *overlayImageView; +- (void)setImageWithURL:(NSURL *)url; +@end + +SpecBegin(ARBrowseFeaturedLinkInsetCell) + +__block ARBrowseFeaturedLinkInsetCell *cell = nil; +__block id mockLink = [OCMockObject mockForClass:[FeaturedLink class]]; + +beforeEach(^{ + [[[mockLink stub] andReturn:@"http://example.com/image.jpg"] largeImageURL]; + + cell = [[ARBrowseFeaturedLinkInsetCell alloc] init]; + cell.overlayImageView = nil; +}); + +context(@"errors", ^{ + it(@"does not set overlay if image is nil", ^{ + expect(cell.imageView.image).to.beNil; + expect(cell.overlayImageView).to.beNil; + + }); + + it(@"does not set overlay if request fails", ^{ + expect(cell.imageView.image).to.beNil; + expect(cell.overlayImageView).to.beNil; + + }); +}); + +context(@"success", ^{ + + it(@"does not re-add overlay if one exists", ^{ + UIImageView *overlayImageView = [[UIImageView alloc] init]; + cell.overlayImageView = overlayImageView; + [cell.contentView addSubview:cell.overlayImageView]; + + NSInteger subviews = cell.contentView.subviews.count; + + [cell setImageWithURL:[mockLink largeImageURL]]; + expect(cell.contentView.subviews.count).to.equal(subviews + 0); + expect(cell.overlayImageView).to.equal(overlayImageView); + }); + + context(@"no overlay exists", ^{ + __block NSInteger subviews = 0; + before(^{ + [cell.contentView addSubview:cell.titleLabel]; + subviews = cell.contentView.subviews.count; + [cell setImageWithURL:[mockLink largeImageURL]]; + }); + + it(@"creates overlay view", ^{ + expect(cell.overlayImageView).notTo.beNil; + }); + + pending(@"adds overlay to subviews", ^{ + expect(cell.contentView.subviews.count).will.equal(subviews + 1); + expect(cell.contentView.subviews).will.contain(cell.overlayImageView); + }); + + pending(@"places overlay below text", ^{ + NSInteger overlayIndex = [cell.contentView.subviews indexOfObject:cell.overlayImageView]; + NSInteger labelIndex = [cell.contentView.subviews indexOfObject:cell.titleLabel]; + expect(overlayIndex).to.equal(labelIndex - 1); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARBrowseFeaturedLinksCollectionViewTests.m b/Artsy Tests/ARBrowseFeaturedLinksCollectionViewTests.m new file mode 100644 index 00000000000..a7a4051a41e --- /dev/null +++ b/Artsy Tests/ARBrowseFeaturedLinksCollectionViewTests.m @@ -0,0 +1,310 @@ +#import "ARBrowseFeaturedLinksCollectionView.h" +#import "ARBrowseFeaturedLinksCollectionViewCell.h" +#import "ARBrowseFeaturedLinkInsetCell.h" +#import "UIDevice-Hardware.h" +#import +#import "ARSwitchBoard.h" + +@interface ARBrowseFeaturedLinksCollectionViewDelegateObject : NSObject + +@end + +@implementation ARBrowseFeaturedLinksCollectionViewDelegateObject + +-(void)didSelectFeaturedLink:(FeaturedLink *)featuredLink {} + +@end + +@interface ARBrowseFeaturedLinksCollectionView(Testing) +- (UIScrollView *)secondaryScroll; +- (void)setupPaging; +- (NSString *)reuseIdentifier; +@end + +SpecBegin(ARBrowseFeaturedLinksCollectionView) + +__block ARBrowseFeaturedLinksCollectionView *collectionView = nil; + +describe(@"initWithStyle", ^{ + + sharedExamplesFor(@"general view setup", ^(NSDictionary *data){ + it(@"sets the layout and frame", ^{ + UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout; + expect(layout.class).to.equal([UICollectionViewFlowLayout class]); + expect(layout.scrollDirection).to.equal(UICollectionViewScrollDirectionHorizontal); + }); + + it (@"sets ui options", ^{ + expect(collectionView.showsHorizontalScrollIndicator).to.beFalsy(); + expect(collectionView.backgroundColor).to.equal([UIColor whiteColor]); + }); + + it(@"sets the dataSource and delegate to self", ^{ + expect(collectionView.dataSource).to.equal(collectionView); + expect(collectionView.delegate).to.equal(collectionView); + }); + }); + + describe(@"with ARFeaturedLinkLayoutSingleRow", ^{ + ARFeaturedLinkStyle style = ARFeaturedLinkLayoutSingleRow; + + beforeEach(^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:style]; + }); + + it(@"sets the style", ^{ + expect(collectionView.style).to.equal(style); + }); + + it(@"does not set secondary scroll", ^{ + expect([collectionView secondaryScroll]).to.beNil(); + }); + + + it(@"looks correct", ^{ + collectionView.featuredLinks = @[ + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil] + ]; + expect(collectionView).to.haveValidSnapshot(); + }); + + itBehavesLike(@"general view setup", nil); + }); + + describe(@"with ARFeaturedLinkLayoutDoubleRow", ^{ + ARFeaturedLinkStyle style = ARFeaturedLinkLayoutDoubleRow; + + beforeEach(^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:style]; + }); + + it(@"sets the style", ^{ + expect(collectionView.style).to.equal(style); + }); + + it(@"does not set secondary scroll", ^{ + expect([collectionView secondaryScroll]).to.beNil(); + }); + + it(@"looks correct", ^{ + collectionView.featuredLinks = @[ + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil] + ]; + expect(collectionView).to.haveValidSnapshot(); + }); + + itBehavesLike(@"general view setup", nil); + }); + + describe(@"with ARFeaturedLinkLayoutSinglePaging", ^{ + ARFeaturedLinkStyle style = ARFeaturedLinkLayoutSinglePaging; + + beforeEach(^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:style]; + }); + + it(@"sets the style", ^{ + expect(collectionView.style).to.equal(style); + }); + + it(@"sets up the secondary scroll", ^{ + expect(collectionView.secondaryScroll).notTo.beNil(); + }); + + pending(@"looks correct", ^{ + // This view renders correctly in the app, but it isn't represented accurately in a snapshot. + // We do some weird things with inset and offset, and they do not look right in the snapshot. + collectionView.featuredLinks = @[ + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil] + ]; + + UIViewController *viewController = [[UIViewController alloc] init]; + viewController.view = collectionView; + [viewController ar_presentWithFrame:CGRectMake(0, 0, 768, 1024)]; + expect(collectionView).will.haveValidSnapshot(); + }); + + itBehavesLike(@"general view setup", nil); + }); +}); + +describe(@"setupPaging", ^{ + __block UIScrollView *scrollView = nil; + beforeEach(^{ + CGRect frame = CGRectMake(0, 0, 320, 200); + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithFrame:frame collectionViewLayout:layout]; + [collectionView setupPaging]; + scrollView = collectionView.secondaryScroll; + }); + + it(@"sets up the scroll view", ^{ + expect(scrollView.bounds).to.equal(CGRectMake(0, 0, 208, 195)); + expect(scrollView.clipsToBounds).to.beFalsy(); + expect(scrollView.delegate).to.equal(collectionView); + expect(scrollView.hidden).to.beTruthy(); + expect(scrollView.pagingEnabled).to.beTruthy(); + }); + + it(@"updates its subviews and gestures", ^{ + expect(collectionView.subviews ).to.contain(scrollView); + expect(collectionView.panGestureRecognizer.enabled).to.beFalsy(); + expect(collectionView.gestureRecognizers).to.contain(scrollView.panGestureRecognizer); + }); +}); + +describe(@"scrollViewDidScroll", ^{ + it(@"uses secondary scroll offset if available", ^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSinglePaging]; + collectionView.contentOffset = CGPointMake(0, 0); + collectionView.secondaryScroll.contentOffset = CGPointMake(200, 0); + collectionView.contentInset = UIEdgeInsetsMake(0, 100, 0, 0); + + UIWindow *window = [UIWindow new]; + [window addSubview:collectionView]; + + [collectionView scrollViewDidScroll:collectionView.secondaryScroll]; + expect(collectionView.contentOffset).to.equal(CGPointMake(100, 0)); + }); +}); + +describe(@"numberOfItemsInSection", ^{ + it(@"returns the number of featuredLinks", ^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSingleRow]; + collectionView.featuredLinks = @[@1, @2, @3, @4, @5]; + expect([collectionView collectionView:collectionView numberOfItemsInSection:0]).to.equal(5); + }); +}); + +describe(@"cellForItemAtIndexPath", ^{ + __block NSIndexPath *index = nil; + __block FeaturedLink *link = nil; + __block UICollectionViewCell *cell = nil; + before(^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSingleRow]; + index = [NSIndexPath indexPathForItem:0 inSection:0]; + link = [FeaturedLink modelWithJSON:@{ @"title" : @"one", @"href" : @"/post/one" } error:nil]; + collectionView.featuredLinks = @[link]; + cell = [collectionView collectionView:collectionView cellForItemAtIndexPath:index]; + }); + + it(@"updates cell with link", ^{ + expect([(ARBrowseFeaturedLinksCollectionViewCell*)cell titleLabel].text).to.equal(@"ONE"); + }); + + it(@"assigns reuse identifier", ^{ + expect(cell.reuseIdentifier).to.equal([collectionView reuseIdentifier]); + }); +}); + +describe(@"setFeaturedLinks", ^{ + __block NSArray *links = @[@1, @2, @3]; + it(@"sets featured links", ^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSingleRow]; + collectionView.featuredLinks = links; + expect(collectionView.featuredLinks).to.equal(links.copy); + }); + + it(@"sets secondary scroll size for paging", ^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSinglePaging]; + collectionView.featuredLinks = links; + + CGSize contentSize = [collectionView.collectionViewLayout collectionViewContentSize]; + UIScrollView *scrollView = collectionView.secondaryScroll; + + expect(scrollView.contentSize).to.equal(contentSize); + expect(scrollView.contentOffset).to.equal(CGPointMake(208, 0)); + }); +}); + +describe(@"didSelectItemAtIndexPath", ^{ + it(@"loads item's link", ^{ + id mockDelegate = [OCMockObject mockForClass:[ARBrowseFeaturedLinksCollectionViewDelegateObject class]]; + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSingleRow]; + FeaturedLink *link = [FeaturedLink modelWithJSON:@{ @"title" : @"one", @"href" : @"/post/one" } error:nil]; + collectionView.featuredLinks = @[link]; + collectionView.selectionDelegate = mockDelegate; + + [[mockDelegate expect] didSelectFeaturedLink:link]; + NSIndexPath *index = [NSIndexPath indexPathForItem:0 inSection:0]; + [collectionView collectionView:collectionView didSelectItemAtIndexPath:index]; + [mockDelegate verify]; + }); +}); + +describe(@"reuseIdentifier", ^{ + it(@"returns inset cell for paging style", ^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSinglePaging]; + expect([collectionView reuseIdentifier]).to.equal([ARBrowseFeaturedLinkInsetCell reuseID]); + }); + it(@"returns normal cell otherwise", ^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSingleRow]; + expect([collectionView reuseIdentifier]).to.equal([ARBrowseFeaturedLinksCollectionViewCell reuseID]); + }); +}); + +describe(@"ipad snapshots", ^{ + before(^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + }); + + after(^{ + [ARTestContext stopStubbing]; + }); + + describe(@"with ARFeaturedLinkLayoutSingleRow", ^{ + ARFeaturedLinkStyle style = ARFeaturedLinkLayoutSingleRow; + + beforeEach(^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:style]; + }); + + it(@"looks correct", ^{ + collectionView.featuredLinks = @[ + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + ]; + expect(collectionView).to.haveValidSnapshot(); + }); + + }); + + describe(@"with ARFeaturedLinkLayoutDoubleRow", ^{ + ARFeaturedLinkStyle style = ARFeaturedLinkLayoutDoubleRow; + + beforeEach(^{ + collectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:style]; + }); + + it(@"looks correct", ^{ + collectionView.featuredLinks = @[ + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil], + [[FeaturedLink alloc] initWithDictionary:@{@"title" : @"Title"} error:nil] + ]; + expect(collectionView).to.haveValidSnapshot(); + }); + + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARBrowseViewControllerTests.m b/Artsy Tests/ARBrowseViewControllerTests.m new file mode 100644 index 00000000000..afb71b36da1 --- /dev/null +++ b/Artsy Tests/ARBrowseViewControllerTests.m @@ -0,0 +1,193 @@ +#import "ARBrowseViewController.h" +#import "ARBrowseFeaturedLinksCollectionView.h" +#import +#import "ARUserManager+Stubs.h" + +@interface ARBrowseViewController (Tests) +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@end + +SpecBegin(ARBrowseViewController) + +__block ARBrowseViewController *viewController; +describe(@"iphone", ^{ + before(^{ + // used to find the sets called "Featured Categories", but really these are featured genes + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" + withParams:@{ @"key" : @"browse:featured-genes", @"mobile" : @"true", @"published" : @"true", @"sort" : @"key" } + withResponse:@[ @{ @"id" : @"featured-genes", @"name" : @"Featured Categories", @"item_type" : @"FeaturedLink" } ] + ]; + + // used to find the set called "Gene Categories" + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" + withParams:@{ @"key" : @"browse:gene-categories", @"published" : @"true", @"mobile" : @"true", @"sort" : @"key" } + withResponse:@[ + @{ @"id" : @"featured-categories", @"name" : @"Gene Categories", @"item_type" : @"OrderedSet" } + ] + ]; + + // these are ordered sets of genes + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/featured-categories/items" + withResponse:@[ + @{ @"id" : @"subject-matter-genes", @"name" : @"Subject Matter Genes", @"item_type" : @"FeaturedLink" }, + @{ @"id" : @"mew-media-genes", @"name" : @"New Media Genes", @"item_type" : @"FeaturedLink" } + ] + ]; + + // items inside a featured category, a collection of featured links + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/subject-matter-genes/items" + withResponse:@[ + @{ @"id" : @"s1", @"title" : @"S1", @"href" : @"/gene/s1" }, + @{ @"id" : @"s2", @"title" : @"S2", @"href" : @"/gene/s2" }, + @{ @"id" : @"s3", @"title" : @"S3", @"href" : @"/gene/s3" }, + @{ @"id" : @"s4", @"title" : @"S4", @"href" : @"/gene/s4" }, + @{ @"id" : @"s5", @"title" : @"S5", @"href" : @"/gene/s5" }, + @{ @"id" : @"s6", @"title" : @"S6", @"href" : @"/gene/s6" }, + ] + ]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/mew-media-genes/items" + withResponse:@[ + @{ @"id" : @"m1", @"title" : @"M1", @"href" : @"/gene/m1" }, + @{ @"id" : @"m2", @"title" : @"M2", @"href" : @"/gene/m2" } + ] + ]; + + // featured categories are a collection of genes + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/featured-genes/items" + withResponse:@[ + @{ @"id" : @"modern", @"title" : @"Modern", @"item_type" : @"FeaturedLink" }, + @{ @"id" : @"spumato", @"title" : @"Spumato", @"item_type" : @"FeaturedLink" } + ] + ]; + viewController = [[ARBrowseViewController alloc] init]; + viewController.shouldAnimate = NO; + }); + + it(@"presents featured categories and genes", ^{ + expect(viewController.view).to.beKindOf([ORStackScrollView class]); + ORStackView *stackView = ((ORStackScrollView *)viewController.view).stackView; + expect(stackView).toNot.beNil(); + + expect(stackView.subviews.count).will.equal(6); // label, featured genes, label, genes, label, genes + + expect(stackView.subviews[0]).to.beKindOf([UILabel class]); + expect([((UILabel *)stackView.subviews[0]) text]).to.equal(@"FEATURED CATEGORIES"); + expect(stackView.subviews[1]).to.beKindOf([ARBrowseFeaturedLinksCollectionView class]); + + expect(stackView.subviews[2]).to.beKindOf([UILabel class]); + expect([((UILabel *)stackView.subviews[2]) text]).to.equal(@"SUBJECT MATTER GENES"); + expect(stackView.subviews[3]).to.beKindOf([ARBrowseFeaturedLinksCollectionView class]); + + expect(stackView.subviews[4]).to.beKindOf([UILabel class]); + expect([((UILabel *)stackView.subviews[4]) text]).to.equal(@"NEW MEDIA GENES"); + expect(stackView.subviews[5]).to.beKindOf([ARBrowseFeaturedLinksCollectionView class]); + }); + + it(@"looks correct", ^{ + [viewController ar_presentWithFrame:CGRectMake(0, 0, 320, 480)]; + expect(viewController.view).will.haveValidSnapshot(); + }); +}); + +describe(@"ipad", ^{ + + before(^{ + // used to find the sets called "Featured Categories", but really these are featured genes + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" + withParams:@{ @"key" : @"browse:featured-genes", @"mobile" : @"true", @"published" : @"true", @"sort" : @"key" } + withResponse:@[ @{ @"id" : @"featured-genes", @"name" : @"Featured Categories", @"item_type" : @"FeaturedLink" } ] + ]; + + + // featured categories are a collection of genes + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/featured-genes/items" + withResponse:@[ + @{ @"id" : @"modern", @"title" : @"Modern", @"item_type" : @"FeaturedLink" }, + @{ @"id" : @"spumato", @"title" : @"Spumato", @"item_type" : @"FeaturedLink" } + ] + ]; + + // used to find the set called "Gene Categories" + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" + withParams:@{ @"key" : @"browse:gene-categories", @"published" : @"true", @"mobile" : @"true", @"sort" : @"key" } + withResponse:@[ + @{ @"id" : @"featured-categories", @"name" : @"Gene Categories", @"item_type" : @"OrderedSet" } + ] + ]; + + + // these are ordered sets of genes + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/featured-categories/items" + withResponse:@[ + @{ @"id" : @"subject-matter-genes", @"name" : @"Subject Matter Genes", @"item_type" : @"FeaturedLink" }, + @{ @"id" : @"mew-media-genes", @"name" : @"New Media Genes", @"item_type" : @"FeaturedLink" }, + @{ @"id" : @"some-other-genes", @"name" : @"More Genes", @"item_type" : @"FeaturedLink" } + ] + ]; + + // items inside a featured category, a collection of featured links + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/subject-matter-genes/items" + withResponse:@[ + @{ @"id" : @"s1", @"title" : @"S1", @"href" : @"/gene/s1" }, + @{ @"id" : @"s2", @"title" : @"S2", @"href" : @"/gene/s2" }, + @{ @"id" : @"s3", @"title" : @"S3", @"href" : @"/gene/s3" }, + @{ @"id" : @"s4", @"title" : @"S4", @"href" : @"/gene/s4" }, + @{ @"id" : @"s5", @"title" : @"S5", @"href" : @"/gene/s5" }, + @{ @"id" : @"s6", @"title" : @"S6", @"href" : @"/gene/s6" }, + @{ @"id" : @"s7", @"title" : @"S7", @"href" : @"/gene/s7" }, + @{ @"id" : @"s8", @"title" : @"S8", @"href" : @"/gene/s8" }, + @{ @"id" : @"s9", @"title" : @"S9", @"href" : @"/gene/s9" }, + @{ @"id" : @"s10", @"title" : @"S10", @"href" : @"/gene/s10" } + ] + ]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/mew-media-genes/items" + withResponse:@[ + @{ @"id" : @"m1", @"title" : @"M1", @"href" : @"/gene/m1" }, + @{ @"id" : @"m2", @"title" : @"M2", @"href" : @"/gene/m2" }, + @{ @"id" : @"m3", @"title" : @"M3", @"href" : @"/gene/m3" }, + @{ @"id" : @"m4", @"title" : @"M4", @"href" : @"/gene/m4" }, + @{ @"id" : @"m5", @"title" : @"M5", @"href" : @"/gene/m5" }, + @{ @"id" : @"m6", @"title" : @"M6", @"href" : @"/gene/m6" }, + @{ @"id" : @"m7", @"title" : @"M7", @"href" : @"/gene/m7" }, + @{ @"id" : @"m8", @"title" : @"M8", @"href" : @"/gene/m8" }, + @{ @"id" : @"m9", @"title" : @"M9", @"href" : @"/gene/m9" }, + @{ @"id" : @"m10", @"title" : @"M10", @"href" : @"/gene/m10" } + ] + ]; + + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/some-other-genes/items" + withResponse:@[ + @{ @"id" : @"g1", @"title" : @"G1", @"href" : @"/gene/g1" }, + @{ @"id" : @"g2", @"title" : @"G2", @"href" : @"/gene/g2" }, + @{ @"id" : @"g3", @"title" : @"G3", @"href" : @"/gene/g3" }, + @{ @"id" : @"g4", @"title" : @"G4", @"href" : @"/gene/g4" }, + @{ @"id" : @"g5", @"title" : @"G5", @"href" : @"/gene/g5" }, + @{ @"id" : @"g6", @"title" : @"G6", @"href" : @"/gene/g6" }, + @{ @"id" : @"g7", @"title" : @"G7", @"href" : @"/gene/g7" }, + @{ @"id" : @"g8", @"title" : @"G8", @"href" : @"/gene/g8" }, + @{ @"id" : @"g9", @"title" : @"G9", @"href" : @"/gene/g9" }, + @{ @"id" : @"g10", @"title" : @"G10", @"href" : @"/gene/g10" } + ] + ]; + + viewController = [[ARBrowseViewController alloc] init]; + viewController.shouldAnimate = NO; + }); + + it(@"looks correct", ^{ + waitUntil(^(DoneCallback done) { + [ARTestContext stubDevice:ARDeviceTypePad]; + [viewController ar_presentWithFrame:CGRectMake(0, 0, 768, 1024)]; + activelyWaitFor(0.5, ^{ + expect(viewController.view).will.haveValidSnapshot(); + [ARTestContext stopStubbing]; + done(); + }); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARDeveloperOptionsSpec.m b/Artsy Tests/ARDeveloperOptionsSpec.m new file mode 100644 index 00000000000..2215e361757 --- /dev/null +++ b/Artsy Tests/ARDeveloperOptionsSpec.m @@ -0,0 +1,14 @@ +#import "ARDeveloperOptions.h" + +SpecBegin(ARDeveloperOptions) + +it(@"transforms the correct syntax to options", ^{ + ARDeveloperOptions *sut = [[ARDeveloperOptions alloc] init]; + NSString *data = @"hello:world\ntest:data"; + [sut updateWithStringContents:data]; + + expect(sut[@"hello"]).to.equal(@"world"); + expect(sut[@"test"]).to.equal(@"data"); +}); + +SpecEnd diff --git a/Artsy Tests/ARExpectaExtensions.h b/Artsy Tests/ARExpectaExtensions.h new file mode 100644 index 00000000000..ece506d070e --- /dev/null +++ b/Artsy Tests/ARExpectaExtensions.h @@ -0,0 +1,10 @@ +#define itAsyncronouslyRecordsSnapshotsForDevices(name, ...) _itTestsAsyncronouslyWithDevicesRecording(self, __LINE__, __FILE__, YES, name, (__VA_ARGS__)) +#define itHasAsyncronousSnapshotsForDevices(name, ...) _itTestsAsyncronouslyWithDevicesRecording(self, __LINE__, __FILE__, NO, name, (__VA_ARGS__)) + +#define itRecordsSnapshotsForDevices(name, ...) _itTestsSyncronouslyWithDevicesRecording(self, __LINE__, __FILE__, YES, name, (__VA_ARGS__)) +#define itHasSnapshotsForDevices(name, ...) _itTestsSyncronouslyWithDevicesRecording(self, __LINE__, __FILE__, NO, name, (__VA_ARGS__)) + +/// Usage of this should be limited +void _itTestsAsyncronouslyWithDevicesRecording(id self, int lineNumber, const char *fileName, BOOL record, NSString *name, id (^block)()); + +void _itTestsSyncronouslyWithDevicesRecording(id self, int lineNumber, const char *fileName, BOOL record, NSString *name, id (^block)()); diff --git a/Artsy Tests/ARExpectaExtensions.m b/Artsy Tests/ARExpectaExtensions.m new file mode 100644 index 00000000000..265926c6d38 --- /dev/null +++ b/Artsy Tests/ARExpectaExtensions.m @@ -0,0 +1,57 @@ +#import "ARExpectaExtensions.h" + +void _itTestsAsyncronouslyWithDevicesRecording(id self, int lineNumber, const char *fileName, BOOL record, NSString *name, id (^block)()) { + + void (^snapshot)(id, NSString *) = ^void (id sut, NSString *suffix) { + + EXPExpect *expectation = _EXP_expect(self, lineNumber, fileName, ^id{ return EXPObjectify((sut)); }); + + if (record) { + expectation.will.recordSnapshotNamed([name stringByAppendingString:suffix]); + } else { + expectation.will.haveValidSnapshotNamed([name stringByAppendingString:suffix]); + } + }; + + it([name stringByAppendingString:@" as iphone"], ^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + id sut = block(); + snapshot(sut, @" as iphone"); + [ARTestContext stopStubbing]; + }); + + it([name stringByAppendingString:@" as ipad"], ^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + id sut = block(); + snapshot(sut, @" as ipad"); + [ARTestContext stopStubbing]; + }); +} + +void _itTestsSyncronouslyWithDevicesRecording(id self, int lineNumber, const char *fileName, BOOL record, NSString *name, id (^block)()) { + + void (^snapshot)(id, NSString *) = ^void (id sut, NSString *suffix) { + + EXPExpect *expectation = _EXP_expect(self, lineNumber, fileName, ^id{ return EXPObjectify((sut)); }); + + if (record) { + expectation.to.recordSnapshotNamed([name stringByAppendingString:suffix]); + } else { + expectation.to.haveValidSnapshotNamed([name stringByAppendingString:suffix]); + } + }; + + it([name stringByAppendingString:@" as iphone"], ^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + id sut = block(); + snapshot(sut, @" as iphone"); + [ARTestContext stopStubbing]; + }); + + it([name stringByAppendingString:@" as ipad"], ^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + id sut = block(); + snapshot(sut, @" as ipad"); + [ARTestContext stopStubbing]; + }); +} \ No newline at end of file diff --git a/Artsy Tests/ARFairArtistViewControllerTests.m b/Artsy Tests/ARFairArtistViewControllerTests.m new file mode 100644 index 00000000000..3c779e56b26 --- /dev/null +++ b/Artsy Tests/ARFairArtistViewControllerTests.m @@ -0,0 +1,52 @@ +#import "ARFairAwareObject.h" +#import "ARFairArtistViewController.h" + +@interface ARFairArtistViewController (Tests) +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@end + +SpecBegin(ARFairArtistViewController) + +__block ARFairArtistViewController *fairArtistVC = nil; +__block Fair *fair = nil; + +beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/artist/some-artist" withResponse:@{ @"id" : @"some-artist", @"name" : @"Some Artist" }]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/fair/fair-id/shows" withParams:@{@"artist" : @"some-artist"} withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/follow/artists" withParams:@{@"artists[]" : @"some-artist"} withResponse:@[]]; + fair = [Fair modelWithJSON:@{ @"id" : @"fair-id", @"name" : @"The Armory Show", @"organizer" : @{ @"profile_id" : @"fair-profile-id" } }]; +}); + +describe(@"without maps", ^{ + beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/maps" withParams:@{ @"fair_id" : @"fair-id" } withResponse:@[]]; + fairArtistVC = [[ARFairArtistViewController alloc] initWithArtistID:@"some-artist" fair:fair]; + [fairArtistVC ar_presentWithFrame:CGRectMake(0, 0, 320, 480)]; + }); + + afterEach(^{ + [OHHTTPStubs removeAllStubs]; + }); + + it(@"displays artist title", ^{ + expect(fairArtistVC.view).will.haveValidSnapshot(); + }); +}); + +describe(@"with maps", ^{ + beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/maps" withParams:@{ @"fair_id" : @"fair-id" } withResponse:@[@{ @"id" : @"map-id" }]]; + fairArtistVC = [[ARFairArtistViewController alloc] initWithArtistID:@"some-artist" fair:fair]; + [fairArtistVC ar_presentWithFrame:CGRectMake(0, 0, 320, 480)]; + }); + + afterEach(^{ + [OHHTTPStubs removeAllStubs]; + }); + + it(@"displays artist title and map button", ^{ + expect(fairArtistVC.view).will.haveValidSnapshot(); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairFavoritesNetworkModelTests.m b/Artsy Tests/ARFairFavoritesNetworkModelTests.m new file mode 100644 index 00000000000..edb0d9aa368 --- /dev/null +++ b/Artsy Tests/ARFairFavoritesNetworkModelTests.m @@ -0,0 +1,145 @@ +#import "ARFairFavoritesNetworkModel.h" +#import "ARUserManager+Stubs.h" +#import "ARUserManager.h" +#import "ArtsyAPI.h" +#import "ArtsyAPI+Fairs.h" + +@interface Fair (Testing) + +@property (nonatomic, copy) NSMutableSet *shows; + +@end + +SpecBegin(ARFairFavoritesNetworkModel) + +__block Fair *fair; +beforeEach(^{ + [ARUserManager stubAndLoginWithUsername]; + fair = [Fair modelWithJSON:@{ @"id" : @"fair-id" }]; +}); + +afterEach(^{ + [[ARUserManager sharedManager] logout]; + [OHHTTPStubs removeAllStubs]; +}); + +it(@"ignores artworks without a partner", ^{ + waitUntil(^(DoneCallback done) { + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/collection/saved-artwork/artworks" + withParams:@{ @"fair_id" : @"fair-id", + @"user_id" : [ARUserManager sharedManager].currentUser.userID, + @"private" : @YES } + withResponse:@[ @{ @"id": @"one", @"partner" : @{ @"id" : @"partner-id" } }, + @{ @"id": @"two" } ] + ]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/follow/profiles" + withParams:@{ @"fair_id" : @"fair-id" } + withResponse:@[] + ]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/follow/artists" + withParams:@{ @"fair_id" : @"fair-id" } + withResponse:@[] + ]; + + ARFairFavoritesNetworkModel *favoritesNetworkModel = [[ARFairFavoritesNetworkModel alloc] init]; + [favoritesNetworkModel getFavoritesForNavigationsButtonsForFair:fair artwork:^(NSArray *work) { + expect(work.count).to.equal(2); + done(); + } exhibitors:^(NSArray *exhibitors) { + } artists:^(NSArray *artists) { + } failure:nil]; + }); +}); + +describe(@"when downloading exhibitor data", ^{ + __block PartnerShow *partnerShow; + __block id apiMock; + __block id fairMock; + + beforeEach(^{ + partnerShow = [PartnerShow modelWithJSON:@{ + @"id" : @"show-id", + @"fair_location" : @{ @"display" : @"Pier 1, Booth 2, Section 3, Floor 5" }, + @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila Heller Gallery in New York City" } + }]; + + apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + fairMock = [OCMockObject partialMockForObject:fair]; + + [[[fairMock stub] andDo:^(NSInvocation *invocation) { + [fair willChangeValueForKey:@"shows"]; + fair.shows = [NSMutableSet setWithObject:partnerShow]; + [fair didChangeValueForKey:@"shows"]; + + }] downloadShows]; + }); + + afterEach(^{ + [apiMock stopMocking]; + }); + + it(@"generates random exhibitors when no values are returned from API", ^{ + waitUntil(^(DoneCallback done) { + + [[apiMock stub] getProfileFollowsForFair:OCMOCK_ANY success:[OCMArg checkWithBlock:^BOOL(void (^block)(NSArray *)) { + if (block) { + block(@[]); + } + return true; + }] failure:OCMOCK_ANY]; + + ARFairFavoritesNetworkModel *favoritesNetworkModel = [[ARFairFavoritesNetworkModel alloc] init]; + [favoritesNetworkModel getFavoritesForNavigationsButtonsForFair:fairMock artwork:^(NSArray *work) { + } exhibitors:^(NSArray *exhibitors) { + expect(exhibitors.count).to.equal(1); + NSDictionary *button = exhibitors.firstObject; + expect(button[@"ARNavigationButtonPropertiesKey"][@"title"]).to.equal(@"Leila Heller Gallery in New York City"); + expect(button[@"ARNavigationButtonPropertiesKey"][@"subtitle"]).to.equal(@"Pier 1, Booth 2, Section 3, Floor 5"); + done(); + } artists:^(NSArray *artists) { + } failure:nil]; + }); + }); + + it(@"returns correct exhibitors when values are returned from API", ^{ + waitUntil(^(DoneCallback done) { + + Partner *partner = [Partner modelWithJSON:@{ + @"id" : @"leila-heller", @"name" : @"Leila Heller Gallery in New York City" + }]; + + Profile *profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"FairOrganizer", + }]; + + id profileMock = [OCMockObject partialMockForObject:profile]; + [[[profileMock stub] andReturn:partner] profileOwner]; + + id followMock = [OCMockObject mockForClass:[Follow class]]; + [[[followMock stub] andReturn:profileMock] profile]; + + [[apiMock stub] getProfileFollowsForFair:OCMOCK_ANY success:[OCMArg checkWithBlock:^BOOL(void (^block)(NSArray *)) { + if (block) { + block(@[followMock]); + } + return true; + }] failure:OCMOCK_ANY]; + + ARFairFavoritesNetworkModel *favoritesNetworkModel = [[ARFairFavoritesNetworkModel alloc] init]; + [favoritesNetworkModel getFavoritesForNavigationsButtonsForFair:fairMock artwork:^(NSArray *work) { + } exhibitors:^(NSArray *exhibitors) { + expect(exhibitors.count).to.equal(1); + NSDictionary *button = exhibitors.firstObject; + expect(button[@"ARNavigationButtonPropertiesKey"][@"title"]).to.equal(@"Leila Heller Gallery in New York City"); + expect(button[@"ARNavigationButtonPropertiesKey"][@"subtitle"]).to.equal(@"Pier 1, Booth 2, Section 3, Floor 5"); + done(); + } artists:^(NSArray *artists) { + } failure:nil]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairGuideContainerViewControllerTests.m b/Artsy Tests/ARFairGuideContainerViewControllerTests.m new file mode 100644 index 00000000000..7eaf82a0c64 --- /dev/null +++ b/Artsy Tests/ARFairGuideContainerViewControllerTests.m @@ -0,0 +1,107 @@ +#import "ARFairGuideContainerViewController.h" +#import "ARFairGuideViewController.h" + +@interface ARFairGuideContainerViewController (Testing) + +- (void)didReceiveTap:(UITapGestureRecognizer *)recognizer; + +@property (nonatomic, assign) BOOL mapsLoaded; +@property (nonatomic, assign) BOOL fairLoaded; + +@property (nonatomic, strong) ARFairGuideViewController *fairGuideViewController; + +- (void)downloadContent; + +@end + +SpecBegin(ARFairGuideContainerViewController) + +__block ARFairGuideContainerViewController *_fairGuideVC = nil; + +beforeEach(^{ + Fair *fair = [Fair modelWithJSON:@{ @"id" : @"fair-id", @"name" : @"The Armory Show", @"organizer" : @{ @"profile_id" : @"fair-profile-id" } }]; + id mockFair = [OCMockObject partialMockForObject:fair]; + + NSArray *maps = @[[Map modelWithJSON: @{ + @"map_features": @[ + @{ + @"feature_type": @"lounge", + @"name": @"Public Lounge", + @"x": @0.8, + @"y": @0.7 + }, + @{ + @"feature_type": @"bar", + @"name": @"VIP Bar", + @"x": @0.8, + @"y": @0.8 + }], + @"id": @"map-one", + @"max_tiled_height": @(1000), + @"max_tiled_width": @(2000) + }]]; + + [[[mockFair stub] andReturn:maps] maps]; + [(Fair *)[mockFair stub] updateFair:[OCMArg checkWithBlock:^BOOL(void (^block)(void)) { + if (block) { + block(); + } + + return YES; + }]]; + + [[[mockFair stub] andReturn:maps] maps]; + [(Fair *)[mockFair stub] getFairMaps:[OCMArg checkWithBlock:^BOOL(void (^block)(NSArray *)) { + if (block) { + block(maps); + } + + return YES; + }]]; + + _fairGuideVC = [[ARFairGuideContainerViewController alloc] initWithFair:mockFair]; + _fairGuideVC.animatedTransitions = NO; +}); + +it(@"has the correct fair", ^{ + expect(_fairGuideVC.fair.fairID).to.equal(@"fair-id"); +}); + +describe(@"a loaded view controller", ^{ + beforeEach(^{ + __unused UIView *view = _fairGuideVC.view; + }); + + describe(@"that has appeared", ^{ + + it(@"has a valid snapshot", ^{ + expect(_fairGuideVC).to.haveValidSnapshot(); + }); + + pending(@"after uncollapsing map, it has a valid snapshot", ^{ // TODO: fix this + [_fairGuideVC didReceiveTap:nil]; + expect(_fairGuideVC).to.haveValidSnapshot(); + }); + }); +}); + +it(@"handles a changed user correct", ^{ + __unused UIView *view = _fairGuideVC.view; + [_fairGuideVC beginAppearanceTransition:YES animated:NO]; + [_fairGuideVC endAppearanceTransition]; + + expect(_fairGuideVC.mapsLoaded).to.beTruthy(); + expect(_fairGuideVC.fairLoaded).to.beTruthy(); + + id fairGuideMock = [OCMockObject partialMockForObject:_fairGuideVC]; + [[fairGuideMock expect] downloadContent]; + + [fairGuideMock fairGuideViewControllerDidChangeUser:_fairGuideVC.fairGuideViewController]; + + expect(_fairGuideVC.mapsLoaded).to.beFalsy(); + expect(_fairGuideVC.fairLoaded).to.beFalsy(); + + [fairGuideMock verify]; +}); + +SpecEnd diff --git a/Artsy Tests/ARFairGuideViewControllerTests.m b/Artsy Tests/ARFairGuideViewControllerTests.m new file mode 100644 index 00000000000..69e2d24b78f --- /dev/null +++ b/Artsy Tests/ARFairGuideViewControllerTests.m @@ -0,0 +1,51 @@ +#import "ARFairAwareObject.h" +#import "ARFairGuideViewController.h" +#import "ARSwitchBoard.h" + +@interface ARFairGuideViewController (Testing) + +- (void)userDidSignUp; +@property (nonatomic, assign) NSInteger selectedTabIndex; +@property (nonatomic, strong) User *currentUser; +@end + +SpecBegin(ARFairGuideViewController) + +__block ARFairGuideViewController *_fairGuideVC = nil; + +beforeEach(^{ + Fair *fair = [Fair modelWithJSON:@{ @"id" : @"fair-id", @"name" : @"The Armory Show", @"organizer" : @{ @"profile_id" : @"fair-profile-id" } }]; + _fairGuideVC = [[ARFairGuideViewController alloc] initWithFair:fair]; +}); + +it(@"looks correct with a logged in user", ^{ + _fairGuideVC.currentUser = [User modelWithJSON:@{ @"name" : @"User Name"}]; + _fairGuideVC.view.frame = CGRectMake(0, 0, 320, 480); + [_fairGuideVC fairDidLoad]; + expect(_fairGuideVC).to.haveValidSnapshot(); +}); + +it(@"looks correct with a trial user", ^{ + _fairGuideVC.currentUser = (id)[NSNull null]; + _fairGuideVC.view.frame = CGRectMake(0, 0, 320, 480); + [_fairGuideVC fairDidLoad]; + expect(_fairGuideVC).to.haveValidSnapshot(); +}); + +it(@"adds a top border", ^{ + _fairGuideVC.currentUser = [User modelWithJSON:@{ @"name" : @"User Name"}]; + _fairGuideVC.view.frame = CGRectMake(0, 0, 320, 480); + _fairGuideVC.showTopBorder = YES; + [_fairGuideVC fairDidLoad]; + expect(_fairGuideVC).to.haveValidSnapshot(); +}); + +it(@"calls the appropriate delegate method upon user change", ^{ + id delegateMock = [OCMockObject mockForProtocol:@protocol(ARFairGuideViewControllerDelegate)]; + [[delegateMock expect] fairGuideViewControllerDidChangeUser:OCMOCK_ANY]; + + [_fairGuideVC userDidSignUp]; + expect(_fairGuideVC.selectedTabIndex).to.equal(-1); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairMapAnnotationCallOutViewTests.m b/Artsy Tests/ARFairMapAnnotationCallOutViewTests.m new file mode 100644 index 00000000000..840df339f06 --- /dev/null +++ b/Artsy Tests/ARFairMapAnnotationCallOutViewTests.m @@ -0,0 +1,68 @@ +#import "ARFairMapAnnotationCallOutView.h" +#import + +SpecBegin(ARFairMapAnnotationCallOutView) + +__block ARFairMapAnnotationCallOutView *view; + +beforeEach(^{ + NAMapView *mapView = [[NAMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 280)]; + mapView.zoomStep = 2.5; + mapView.showsVerticalScrollIndicator = NO; + mapView.showsHorizontalScrollIndicator = NO; + Fair *fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"name" : @"The Armory Show", + @"organizer" : @{ @"profile_id" : @"fair-profile-id" } + }]; + view = [[ARFairMapAnnotationCallOutView alloc] initOnMapView:mapView fair:fair]; +}); + +it(@"blank", ^{ + expect(view).to.haveValidSnapshotNamed(@"blank"); +}); + +it(@"title", ^{ + PartnerShow *partnerShow = [PartnerShow modelWithJSON:@{ + @"id" : @"show-id", + @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila Heller" } + }]; + ARFairMapAnnotation *annotation = [[ARFairMapAnnotation alloc] initWithPoint:CGPointZero representedObject:partnerShow]; + [view setAnnotation:annotation]; + expect(view).to.haveValidSnapshotNamed(@"title"); +}); + +it(@"titleWithSubtitle", ^{ + PartnerShow *partnerShow = [PartnerShow modelWithJSON:@{ + @"id" : @"show-id", + @"fair_location" : @{ @"display" : @"Pier 1, Booth 2, Section 3, Floor 5" }, + @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila Heller Gallery in New York City" } + }]; + ARFairMapAnnotation *annotation = [[ARFairMapAnnotation alloc] initWithPoint:CGPointZero representedObject:partnerShow]; + [view setAnnotation:annotation]; + expect(view).to.haveValidSnapshotNamed(@"titleWithSubtitle"); +}); + +it(@"titleWithArrow", ^{ + MapFeature *mapFeature = [MapFeature modelWithJSON:@{ + @"id" : @"feature-id", + @"href" : @"http://example.com", + @"name" : @"Feature", + }]; + ARFairMapAnnotation *annotation = [[ARFairMapAnnotation alloc] initWithPoint:CGPointZero representedObject:mapFeature]; + [view setAnnotation:annotation]; + expect(view).to.haveValidSnapshotNamed(@"titleWithArrow"); +}); + +it(@"long title with arrow", ^{ + MapFeature *mapFeature = [MapFeature modelWithJSON:@{ + @"id" : @"feature-id", + @"href" : @"http://example.com", + @"name" : @"A Red Brown Fox Jumped Over the Fence", + }]; + ARFairMapAnnotation *annotation = [[ARFairMapAnnotation alloc] initWithPoint:CGPointZero representedObject:mapFeature]; + [view setAnnotation:annotation]; + expect(view).to.haveValidSnapshotNamed(@"longTitleWithArrow"); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairMapViewControllerTests.m b/Artsy Tests/ARFairMapViewControllerTests.m new file mode 100644 index 00000000000..ed535cd0c4c --- /dev/null +++ b/Artsy Tests/ARFairMapViewControllerTests.m @@ -0,0 +1,274 @@ +#import "ARFairMapViewController.h" +#import "ARTiledImageDataSourceWithImage.h" +#import "ARFairShowMapper.h" +#import "ARNavigationController.h" +#import "ARFairSearchViewController.h" +#import "ARTopMenuViewController.h" +#import "ArtsyAPI.h" + +@interface ARFairShowMapper () + +- (ARFairMapAnnotationView *)viewForPoint:(CGPoint)point andRepresentedObject:(id)representedObject; + +@end + +@interface ARFairMapViewController (Testing) + +- (void)selectedPartnerShow:(PartnerShow *)partnerShow; +- (void)selectedArtist:(Artist *)artist; + +- (void)hideCallOut; +- (void)hideScreenContents; + +@property (nonatomic, strong, readonly) ARFairSearchViewController *searchVC; + +@end + +SpecBegin(ARFairMapViewController) + +describe(@"on init", ^{ + + __block Fair *fair = nil; + __block ARFairMapViewController *vc = nil; + __block id fairMock = nil; + + beforeEach(^{ + fair = [Fair modelWithJSON:@{ + @"name" : @"fair name", + @"id" : @"fair-id", + }]; + + NSArray *maps = @[ [Map modelWithJSON: @{ + @"map_features": @[ + @{ + @"feature_type": @"lounge", + @"name": @"Public Lounge", + @"x": @0.8, + @"y": @0.7 + }, + @{ + @"feature_type": @"bar", + @"name": @"VIP Bar", + @"x": @0.8, + @"y": @0.8 + }], + @"id": @"map-one", + @"max_tiled_height": @(1000), + @"max_tiled_width": @(2000) + }]]; + + fairMock = [OCMockObject partialMockForObject:fair]; + [[[fairMock stub] andReturn:maps] maps]; + + vc = [[ARFairMapViewController alloc] initWithFair:fairMock]; + [vc ar_presentWithFrame:CGRectMake(0, 0, 320, 480)]; + }); + + it(@"sets its fair", ^{ + expect(vc.fair).to.equal(fairMock); + }); + + it(@"hides screen contents", ^{ + id mockVC = [OCMockObject partialMockForObject:vc]; + + [[mockVC expect] hideCallOut]; + + [vc hideScreenContents]; + + [mockVC verify]; + }); + + describe(@"fair", ^{ + it(@"sets its fair", ^{ + expect(vc.fair).to.equal(fairMock); + }); + + it(@"displays search box", ^{ + expect(vc).to.haveValidSnapshotNamed(@"default"); + }); + + it(@"maps features", ^{ + expect(vc.fair.maps.count).to.equal(1); + Map *map = vc.fair.maps.firstObject; + // updatePosition needs to get a valid relative point + expect(map.mapID).to.equal(@"map-one"); + // load map + OCMockObject *mockMapView = [OCMockObject partialMockForObject:vc.mapShowMapper.mapView]; + [[[mockMapView stub] andReturnValue:OCMOCK_VALUE(CGPointZero)] zoomRelativePoint:CGPointZero]; + // fetch an annotation by a position and feature (copy) + expect([vc.mapShowMapper viewForPoint:CGPointMake(0.8f * 2000, 1000 - 0.7f * 1000) andRepresentedObject:map.features.firstObject]).toNot.beNil(); + [mockMapView stopMocking]; + }); + + it(@"creates and sets its mapDataSource", ^{ + expect(vc.mapDataSource).to.beKindOf([ARTiledImageDataSourceWithImage class]); + }); + }); + + describe(@"fair with selection", ^{ + it(@"adds title", ^{ + vc = [[ARFairMapViewController alloc] initWithFair:fairMock title:@"Selected Artist at Fair" selectedPartnerShows:@[]]; + vc.expandAnnotations = NO; + [vc ar_presentWithFrame:CGRectMake(0, 0, 320, 480)]; + + expect(vc).to.haveValidSnapshot(); + }); + + it(@"constrains the title correctly in a navigation controller", ^{ + vc = [[ARFairMapViewController alloc] initWithFair:fairMock title:@"Selected Artist at Fair" selectedPartnerShows:@[]]; + vc.expandAnnotations = NO; + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; + [navigationController ar_presentWithFrame:CGRectMake(0, 0, 320, 480)]; + + expect(vc).to.haveValidSnapshot(); + + }); + + pending(@"selects a show"); + }); + + describe(@"hides on screen contents", ^{ + __block id mockVC; + __block id apiMock; + + beforeEach(^{ + mockVC = [OCMockObject partialMockForObject:vc]; + [[mockVC expect] hideScreenContents]; + + apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + }); + + afterEach(^{ + [mockVC verify]; + [apiMock stopMocking]; + }); + + it(@"when showing artists", ^{ + [[apiMock stub] getShowsForArtistID:OCMOCK_ANY inFairID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + [mockVC selectedArtist:nil]; + }); + + it(@"when showing partner shows", ^{ + [[apiMock stub] getShowInfo:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + [mockVC selectedPartnerShow:nil]; + }); + }); + + describe(@"calls api", ^{ + __block id apiMock; + + beforeEach(^{ + apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + }); + + afterEach(^{ + [apiMock verify]; + [apiMock stopMocking]; + }); + + it(@"when selecting an artist", ^{ + NSString *artistID = @"artist-id"; + + id mockArtist = [OCMockObject mockForClass:[Artist class]]; + [[[mockArtist stub] andReturn:artistID] artistID]; + + [[apiMock expect] getShowsForArtistID:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqualToString:artistID]; + }] inFairID:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqualToString:fair.fairID]; + }] success:OCMOCK_ANY failure:OCMOCK_ANY]; + [vc selectedArtist:mockArtist]; + }); + + it(@"when selecting a partner show", ^{ + NSString *showId = @"show-id"; + + id mockPartnerShow = [OCMockObject mockForClass:[PartnerShow class]]; + [[[mockPartnerShow stub] andReturn:showId] showID]; + + [[apiMock expect] getShowInfo:[OCMArg checkWithBlock:^BOOL(PartnerShow *obj) { + return [[obj showID] isEqualToString:showId]; + }] success:OCMOCK_ANY failure:OCMOCK_ANY]; + [vc selectedPartnerShow:mockPartnerShow]; + }); + }); + + describe(@"calling execute on the returned command", ^{ + __block id mockCommand; + __block id classMock; + __block id apiMock; + + beforeEach(^{ + apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + + mockCommand = [OCMockObject niceMockForClass:[RACCommand class]]; + [[mockCommand expect] execute:[OCMArg isNil]]; + + id mockNavController = [OCMockObject mockForClass:[ARNavigationController class]]; + [[[mockNavController stub] andReturn:mockCommand] presentPendingOperationLayover]; + + id mockTopViewController = [OCMockObject mockForClass:[ARTopMenuViewController class]]; + [[[mockTopViewController stub] andReturn:mockNavController] rootNavigationController]; + + classMock = [OCMockObject mockForClass:[ARTopMenuViewController class]]; + [[[classMock stub] andReturn:mockTopViewController] sharedController]; + }); + + afterEach(^{ + [mockCommand verify]; + [OHHTTPStubs removeAllStubs]; + [classMock stopMocking]; + [apiMock stopMocking]; + }); + + describe(@"when selecting an artist", ^{ + it(@"and API succeeds", ^{ + [[apiMock stub] getShowsForArtistID:OCMOCK_ANY inFairID:OCMOCK_ANY success:[OCMArg checkWithBlock:^BOOL(void(^block)(NSArray *)) { + if (block) { + block(@[]); + } + return YES; + }] failure:OCMOCK_ANY]; + + [vc selectedArtist:nil]; + }); + + it(@"and API fails", ^{ + [[apiMock stub] getShowsForArtistID:OCMOCK_ANY inFairID:OCMOCK_ANY success:OCMOCK_ANY failure:[OCMArg checkWithBlock:^BOOL(void(^block)(NSArray *)) { + if (block) { + block(@[]); + } + return YES; + }]]; + + [vc selectedArtist:nil]; + }); + }); + + describe(@"when selecting a partner show", ^{ + it(@"and API succeeds", ^{ + [[apiMock stub] getShowInfo:OCMOCK_ANY success:[OCMArg checkWithBlock:^BOOL(void(^block)(NSArray *)) { + if (block) { + block(@[]); + } + return YES; + }] failure:OCMOCK_ANY]; + + [vc selectedPartnerShow:nil]; + }); + + it(@"and API fails", ^{ + [[apiMock stub] getShowInfo:OCMOCK_ANY success:OCMOCK_ANY failure:[OCMArg checkWithBlock:^BOOL(void(^block)(NSArray *)) { + if (block) { + block(@[]); + } + return YES; + }]]; + + [vc selectedPartnerShow:nil]; + }); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairPostsViewControllerTests.m b/Artsy Tests/ARFairPostsViewControllerTests.m new file mode 100644 index 00000000000..80008fd515c --- /dev/null +++ b/Artsy Tests/ARFairPostsViewControllerTests.m @@ -0,0 +1,58 @@ +#import "ARFairPostsViewController.h" +#import "ARPostFeedItemLinkView.h" +#import "ARSwitchBoard.h" + +SpecBegin(ARFairPostsViewController) + +__block ARFairPostsViewController *fairVC = nil; + +beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/profile/fair-profile-id/posts" withResponse:@{ @"results" : @[ @{ @"id": @"post-id", @"title": @"Post Title", @"_type" : @"Post" } ] }]; + Fair *fair = [Fair modelWithJSON:@{ @"name" : @"The Armory Show", @"organizer" : @{ @"profile_id" : @"fair-profile-id" } }]; + fairVC = [[ARFairPostsViewController alloc] initWithFair:fair]; +}); + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +it(@"sets fair and fetches posts", ^{ + expect(fairVC.fair.name).to.equal(@"The Armory Show"); + expect(fairVC.feedTimeline).willNot.beNil(); + expect([fairVC.feedTimeline numberOfItems]).to.equal(1); +}); + +describe(@"rendered", ^{ + beforeEach(^{ + UIWindow *window = [UIWindow new]; + window.rootViewController = fairVC; + [window makeKeyAndVisible]; + expect(fairVC.view.subviews.count).will.equal(3); + }); + + it(@"displays posts", ^{ + // title + UIView *titleView = fairVC.view.subviews[0]; + expect(titleView).to.beKindOf([UILabel class]); + expect(((UILabel *) titleView).text).to.equal(@"POSTS"); + + // first post + UIView *firstPostView = fairVC.view.subviews[2]; + expect(firstPostView).to.beKindOf([ARPostFeedItemLinkView class]); + expect(((ARPostFeedItemLinkView *) firstPostView).targetPath).to.equal(@"/post/post-id"); + }); + + it(@"navigates to the target url when a post is tapped", ^{ + ARPostFeedItemLinkView *firstPostView = (ARPostFeedItemLinkView *) fairVC.view.subviews[2]; + + id mock = [OCMockObject partialMockForObject:ARSwitchBoard.sharedInstance]; + [[mock expect] loadPath:@"/post/post-id"]; + + [firstPostView sendActionsForControlEvents:UIControlEventTouchUpInside]; + + [mock verifyWithDelay:1]; + [mock stopMocking]; + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairSearchViewControllerTests.m b/Artsy Tests/ARFairSearchViewControllerTests.m new file mode 100644 index 00000000000..8659266d949 --- /dev/null +++ b/Artsy Tests/ARFairSearchViewControllerTests.m @@ -0,0 +1,103 @@ +#import "ARFairSearchViewController.h" +#import +#import "SearchResult.h" + +SpecBegin(ARFairSearchViewController) + +describe(@"init", ^{ + + __block ARFairSearchViewController *fairSearchVC = nil; + __block Fair *fair = nil; + + beforeEach(^{ + fair = [Fair modelWithJSON:@{ @"id" : @"fair-id", @"name" : @"The Armory Show", @"organizer" : @{ @"profile_id" : @"fair-profile-id" } }]; + fairSearchVC = [[ARFairSearchViewController alloc] initWithFair:fair]; + [fairSearchVC viewDidLoad]; + }); + + describe(@"searchPartners", ^{ + it(@"partner name prefix", ^{ + id fairMock = [OCMockObject partialMockForObject:fair]; + PartnerShow *partnerShow = [PartnerShow modelWithJSON:@{@"id" : @"show-id", @"name" : @"", @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila Heller" }}]; + NSMutableSet *shows = [NSMutableSet setWithObject:partnerShow]; + [[[fairMock stub] andReturn:shows] shows]; + expect([fairSearchVC searchPartners:@"Leila Heller"].count).to.equal(1); + expect([fairSearchVC searchPartners:@"Heller"].count).to.equal(1); + expect([fairSearchVC searchPartners:@"Invalid"].count).to.equal(0); + }); + + it(@"case insensitive", ^{ + id fairMock = [OCMockObject partialMockForObject:fair]; + PartnerShow *partnerShow = [PartnerShow modelWithJSON:@{@"id" : @"show-id", @"name" : @"", @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila Heller" }}]; + NSMutableSet *shows = [NSMutableSet setWithObject:partnerShow]; + [[[fairMock stub] andReturn:shows] shows]; + expect([fairSearchVC searchPartners:@"leila heller"].count).to.equal(1); + expect([fairSearchVC searchPartners:@"leila"].count).to.equal(1); + expect([fairSearchVC searchPartners:@"heller"].count).to.equal(1); + expect([fairSearchVC searchPartners:@"invalid"].count).to.equal(0); + }); + + it(@"uses short name", ^{ + id fairMock = [OCMockObject partialMockForObject:fair]; + PartnerShow *partnerShow = [PartnerShow modelWithJSON:@{@"id" : @"show-id", @"name" : @"", @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila", @"short_name" : @"Heller" }}]; + NSMutableSet *shows = [NSMutableSet setWithObject:partnerShow]; + [[[fairMock stub] andReturn:shows] shows]; + expect([fairSearchVC searchPartners:@"leila heller"].count).to.equal(0); + expect([fairSearchVC searchPartners:@"leila"].count).to.equal(1); + expect([fairSearchVC searchPartners:@"heller"].count).to.equal(1); + }); + }); + + describe(@"with search results", ^{ + __block id mock; + + beforeEach(^{ + mock = [OCMockObject partialMockForObject:fairSearchVC]; + + // stub local search results + NSArray *localResults = [NSArray arrayWithObject:[SearchResult modelWithJSON:@{ + @"id" : @"leila-heller-gallery-show", + @"display" : @"Leila Heller Gallery", + @"model" : @"partnershow", + @"label" : @"leila-heller", + @"published" : @YES + }]]; + + [[[mock stub] andReturn:localResults] searchPartners:@"Leila"]; + + // stub remote results + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/match" + withParams:@{ @"term" : @"Leila", @"fair_id" : @"fair-id" } + withResponse:@[ @{ + @"model" : @"artist", + @"id" : @"leila-pazooki", + @"display": @"Leila Pazooki", + @"label": @"Artist", + @"published": @YES, + }, @{ + @"model": @"partnershow", + @"id": @"leila-heller-gallery-leila-heller-gallery-at-the-armory-show-2014", + @"display": @"Leila Heller Gallery at The Armory Show 2014", + @"published": @YES + }] andStatusCode:200]; + }); + + afterEach(^{ + [mock stopMocking]; + [OHHTTPStubs removeAllStubs]; + }); + + it(@"combines results from local and remote search", ^{ + [fairSearchVC fetchSearchResults:@"Leila"]; + + // the results are a combination of the local Leila Heller and the remote Leila Pazooki + expect(fairSearchVC.searchResults.count).will.equal(2); + expect(fairSearchVC.searchResults.firstObject).to.beKindOf([SearchResult class]); + SearchResult *result = (id) fairSearchVC.searchResults.firstObject; + expect(result.modelID).to.equal(@"leila-heller-gallery-show"); + }); + }); + +}); + +SpecEnd diff --git a/Artsy Tests/ARFairShowViewControllerTests.m b/Artsy Tests/ARFairShowViewControllerTests.m new file mode 100644 index 00000000000..035678a6a05 --- /dev/null +++ b/Artsy Tests/ARFairShowViewControllerTests.m @@ -0,0 +1,117 @@ +#import "ARFairAwareObject.h" +#import "ARFairShowViewController.h" +#import "UIDevice-Hardware.h" +#import "ARStubbedShowNetworkModel.h" + +@interface ARFairShowViewController () + +- (void)addMapPreview; +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@property (nonatomic, strong) ARShowNetworkModel *showNetworkModel; + +@end + +SpecBegin(ARFairShowViewController) + +__block ARFairShowViewController *fairShowVC = nil; +__block Fair *fair = nil; +__block PartnerShow *show = nil; +__block ARStubbedShowNetworkModel *stubbedNetworkModel; + +describe(@"with map", ^{ + beforeEach(^{ + show = [PartnerShow modelWithJSON:@{ + @"id": @"some-show", + @"name": @"Some Show", + @"partner": @{ @"id" : @"some-partner" }, + @"fair_location": @{ + @"map_points": @[ + @{ + @"x": @(0.15), + @"y": @(0.75) + } + ] + }, + }]; + + fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"name" : @"The Armory Show", + @"organizer" : @{ @"profile_id" : @"fair-profile-id" }, + }]; + + // Required since Fair doesn't parse maps dictionary in Mantle + Map *map = [Map modelWithJSON:@{ + @"id" : @"map-id", + @"tile_size": @(512), + @"map_features": @[], + @"max_tiled_width": @(1000), + @"max_tiled_height": @(2000) + }]; + + stubbedNetworkModel = [[ARStubbedShowNetworkModel alloc] initWithFair:fair show:show maps:@[map]]; + }); + + itHasSnapshotsForDevices(@"displays show title, map, and map button", ^{ + fairShowVC = [[ARFairShowViewController alloc] initWithShow:show fair:fair]; + fairShowVC.showNetworkModel = stubbedNetworkModel; + [fairShowVC ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return fairShowVC; + }); +}); + +describe(@"without map", ^{ + beforeEach(^{ + show = [PartnerShow modelWithJSON:@{ + @"id": @"some-show", + @"name": @"Some Show", + @"partner": @{ @"id" : @"some-partner" }, + }]; + + fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"name" : @"The Armory Show", + @"organizer" : @{ @"profile_id" : @"fair-profile-id" }, + }]; + + + stubbedNetworkModel = [[ARStubbedShowNetworkModel alloc] initWithFair:fair show:show maps:nil]; + }); + + itHasSnapshotsForDevices(@"displays show title", ^{ + fairShowVC = [[ARFairShowViewController alloc] initWithShow:show fair:fair]; + fairShowVC.showNetworkModel = stubbedNetworkModel; + [fairShowVC ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return fairShowVC; + }); + + itHasSnapshotsForDevices(@"displays show images with 1 install image", ^{ + fairShowVC = [[ARFairShowViewController alloc] initWithShow:show fair:fair]; + + NSString *stubbedImagePath = OHPathForFileInBundle(@"stub.jpg", nil); + stubbedNetworkModel.imagesForBoothHeader = [Image arrayOfModelsWithJSON:@[ + @{ @"image_url" : stubbedImagePath, @"image_versions" : @[@"large"] } + ]]; + + fairShowVC.showNetworkModel = stubbedNetworkModel; + [fairShowVC ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return fairShowVC; + }); + + itHasSnapshotsForDevices(@"displays show images with multiple install images", ^{ + fairShowVC = [[ARFairShowViewController alloc] initWithShow:show fair:fair]; + + NSString *stubbedImagePath = OHPathForFileInBundle(@"stub.jpg", nil); + + stubbedNetworkModel.imagesForBoothHeader = [Image arrayOfModelsWithJSON: @[ + @{ @"image_url" : stubbedImagePath, @"image_versions" : @[@"large"] }, + @{ @"image_url" : stubbedImagePath, @"image_versions" : @[@"large"] } + ]]; + + fairShowVC.showNetworkModel = stubbedNetworkModel; + [fairShowVC ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return fairShowVC; + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFairViewControllerTests.m b/Artsy Tests/ARFairViewControllerTests.m new file mode 100644 index 00000000000..9705a47c0bd --- /dev/null +++ b/Artsy Tests/ARFairViewControllerTests.m @@ -0,0 +1,240 @@ +#import "ARFairViewController.h" +#import "ARNavigationButtonsViewController.h" +#import "MTLModel+JSON.h" +#import "Fair.h" +#import "Profile.h" +#import "ARSearchFieldButton.h" +#import "ARFairSearchViewController.h" +#import "ARSearchViewController+Private.h" + +@interface ARFairViewController (Testing) + +@property (nonatomic, strong) ARSearchFieldButton *searchButton; +@property (nonatomic, strong) ARFairSearchViewController *searchVC; + +@property (nonatomic, assign) BOOL hasMap; +@property (nonatomic, strong) ORStackScrollView *stackView; + +@property (nonatomic, assign) BOOL displayingSearch; + +@property (nonatomic, assign) BOOL hidesBackButton; + +@property (nonatomic, strong) ARNavigationButtonsViewController *primaryNavigationVC; + +- (void)searchFieldButtonWasPressed:(ARSearchFieldButton *)sender; +- (BOOL)hasSufficientDataForParallaxHeader; + +@end + +@interface ARFairSearchViewController (Testing) +@property(nonatomic, readwrite, assign) BOOL shouldAnimate; +@end + +SpecBegin(ARFairViewController) + +it(@"maps bindings correctly", ^{ + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:nil]; + + expect(viewController.displayingSearch).to.beFalsy(); + expect(viewController.hidesBackButton).to.beFalsy(); + + viewController.searchVC = [[ARFairSearchViewController alloc] initWithFair:nil]; + + expect(viewController.displayingSearch).to.beTruthy(); + expect(viewController.hidesBackButton).to.beTruthy(); +}); + +__block Fair *bannerlessFair; +__block Fair *bannerFair; +__block Profile *bannerlessProfile; +__block Profile *bannerProfile; + +before(^{ + bannerlessFair = [[Fair alloc] initWithFairID:@"a-fair-affair"]; + bannerFair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"image_url" : @"http://static1.artsy.net/fairs/52617c6c8b3b81f094000013/9/:version.jpg", + @"image_versions" : @[ + @"square", + @"large_rectangle", + @"wide" + ] + }]; + bannerlessProfile = [[Profile alloc] initWithProfileID:@"profile-id"]; + bannerProfile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"default_icon_version" : @"square", + @"icon" : @{ + @"image_url" : @"http://static1.artsy.net/profile_icons/530cc50c9c18dbab9a00005b/:version.jpg", + @"image_versions" : @[ + @"circle", + @"square" + ] + } + }]; +}); + +describe(@"without enough information for a parallax header", ^{ + it(@"doesn't use a parallax header", ^{ + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:bannerlessFair andProfile:bannerlessProfile]; + expect([viewController hasSufficientDataForParallaxHeader]).to.beFalsy(); + }); +}); +describe(@"with some information", ^{ + it(@"uses a parallax header", ^{ + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:bannerFair andProfile:bannerlessProfile]; + expect([viewController hasSufficientDataForParallaxHeader]).to.beTruthy(); + }); + + it(@"uses a parallax header", ^{ + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:bannerlessFair andProfile:bannerProfile]; + expect([viewController hasSufficientDataForParallaxHeader]).to.beTruthy(); + }); +}); + +describe(@"with all available information", ^{ + it(@"uses a parallax header", ^{ + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:bannerFair andProfile:bannerProfile]; + expect([viewController hasSufficientDataForParallaxHeader]).to.beTruthy(); + }); +}); + +context(@"with no map", ^{ + __block ARFairViewController *fairVC = nil; + + beforeEach(^{ + Fair *fair = [[Fair alloc] initWithFairID:@"a-fair-affair"]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/fair/a-fair-affair" withResponse:@{ + @"id" : @"a-fair-affair", + @"name" : @"The Fair Affair", + @"start_at" : @"1976-01-30T15:00:00+00:00", + @"end_at" : @"1976-02-02T15:00:00+00:00" + }]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" withResponse:@[ + @{ + @"description": @"", + @"display_on_mobile": @(1), + @"id": @"set-id", + @"internal_name": @"The Armory Show 2014 Primary Features", + @"item_type": @"FeaturedLink", + @"key": @"primary", + @"name": @"The Armory Show 2014 Primary Features", + @"published": @(1), + } + ]]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/set-id/items" withResponse:@[ + @{ @"id": @"one", @"href": @"/post/moby-my-highlights-from-art-los-angeles-contemporary", @"title" : @"Moby" }, + ]]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/maps" withResponse:@[]]; + + fairVC = [[ARFairViewController alloc] initWithFair:fair]; + fairVC.animatesSearchBehavior = NO; + }); + + afterEach(^{ + [OHHTTPStubs removeAllStubs]; + }); + + context(@"without a profile", ^{ + it(@"sets fair title and dates", ^{ + expect(fairVC.view.subviews.count).will.equal(2); + expect(fairVC.stackView).to.beKindOf(ORStackScrollView.class); + + expect(fairVC.fair.name).will.equal(@"The Fair Affair"); + + ORStackView *stackView = ((ORStackScrollView *) fairVC.stackView).stackView; + expect(stackView.subviews.count).will.beGreaterThan(0); + + UIView *titleView = stackView.subviews[0]; + expect(titleView).to.beKindOf([UILabel class]); + expect(((UILabel *) titleView).text).to.equal(@"The Fair Affair"); + + UIView *subtitleView = stackView.subviews[1]; + expect(subtitleView).to.beKindOf([UILabel class]); + expect(((UILabel *) subtitleView).text).to.equal(@"Jan 30th - Feb 2nd, 1976"); + }); + }); + + context(@"view is loaded", ^{ + beforeEach(^{ + expect(fairVC.view).toNot.beNil(); + }); + + it(@"has no map", ^{ + expect(fairVC.hasMap).will.beFalsy(); + }); + }); +}); + +context(@"with a map", ^{ + __block ARFairViewController *fairVC = nil; + + beforeEach(^{ + Fair *fair = [[Fair alloc] initWithFairID:@"a-fair-affair"]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/fair/a-fair-affair" withResponse:@{ + @"id" : @"a-fair-affair", + @"name" : @"The Fair Affair", + @"start_at" : @"1976-01-30T15:00:00+00:00", + @"end_at" : @"1976-02-02T15:00:00+00:00" + }]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" withResponse:@[ + @{ + @"description": @"", + @"display_on_mobile": @(1), + @"id": @"set-id", + @"internal_name": @"The Armory Show 2014 Primary Features", + @"item_type": @"FeaturedLink", + @"key": @"primary", + @"name": @"The Armory Show 2014 Primary Features", + @"published": @(1), + } + ]]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/set-id/items" withResponse:@[ + @{ @"id": @"one", @"href": @"/post/moby-my-highlights-from-art-los-angeles-contemporary", @"title" : @"Moby" }, + ]]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/maps" withResponse:@[ + @{ + @"id": @"map-id", + } + ]]; + + fairVC = [[ARFairViewController alloc] initWithFair:fair]; + fairVC.animatesSearchBehavior = NO; + fairVC.view.frame = [[UIScreen mainScreen] bounds]; + + }); + + afterEach(^{ + [OHHTTPStubs removeAllStubs]; + }); + + it(@"has a map", ^{ + expect(fairVC.hasMap).will.beTruthy(); + }); + + it(@"has a map button", ^{ + expect(fairVC.primaryNavigationVC).willNot.beNil(); + expect(fairVC.primaryNavigationVC.buttonDescriptions.count).will.beGreaterThan(0); + expect([fairVC.primaryNavigationVC.buttonDescriptions detect:^BOOL(NSDictionary *button) { + return [button[@"ARNavigationButtonPropertiesKey"][@"title"] isEqualToString:@"Map"]; + }]).willNot.beNil(); + }); + + it(@"search view looks correct", ^{ + [fairVC searchFieldButtonWasPressed:nil]; + fairVC.searchVC.shouldAnimate = NO; + [fairVC.searchVC beginAppearanceTransition:YES animated:NO]; + [fairVC.searchVC endAppearanceTransition]; + expect(fairVC.view).to.haveValidSnapshot(); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFavoritesViewControllerTests.m b/Artsy Tests/ARFavoritesViewControllerTests.m new file mode 100644 index 00000000000..71059c291b2 --- /dev/null +++ b/Artsy Tests/ARFavoritesViewControllerTests.m @@ -0,0 +1,275 @@ +#import "ARFavoritesViewController.h" +#import "ARUserManager+Stubs.h" +#import "AREmbeddedModelsViewController.h" +#import "Artwork+Extensions.h" +#import "ARArtworkFavoritesNetworkModel.h" +#import "ARFavoriteItemModule.h" +#import "ARArtistFavoritesNetworkModel.h" +#import "ARSwitchView.h" +#import "ARSwitchView+Favorites.h" +#import "ARGeneFavoritesNetworkModel.h" +#import "ARSwitchBoard.h" + +@interface ARFavoritesViewController () +- (void)getNextItemSet; +- (void)updateView; +@property (nonatomic, strong, readonly) AREmbeddedModelsViewController *embeddedItemsVC; +@property (nonatomic, strong, readonly) ARArtworkMasonryModule *artworksModule; +@property (nonatomic, strong, readonly) ARArtworkFavoritesNetworkModel *artworkFavoritesNetworkModel; +@property (nonatomic, strong, readonly) ARFavoriteItemModule *artistsModule; +@property (nonatomic, strong, readonly) ARArtistFavoritesNetworkModel *artistFavoritesNetworkModel; +@property (nonatomic, strong, readonly) ARFavoriteItemModule *genesModule; +@property (nonatomic, strong, readonly) ARGeneFavoritesNetworkModel *geneFavoritesNetworkModel; +@property (nonatomic, strong, readwrite) ARFavoritesNetworkModel *activeNetworkModel; + +@end + +@interface ARSwitchView () +@property (nonatomic, strong, readwrite) NSArray *buttons; +@end + +SpecBegin(ARFavoritesViewController) + +__block ARFavoritesViewController *favoritesVC = nil; +__block id mock; +__block ARSwitchView *switchView; + +dispatch_block_t sharedBefore = ^{ + favoritesVC = [[ARFavoritesViewController alloc] init]; + mock = [OCMockObject partialMockForObject:favoritesVC]; + [[mock stub] getNextItemSet]; +}; + +after(^{ + [mock stopMocking]; + favoritesVC = nil; +}); + +describe(@"general", ^{ + before(^{ + sharedBefore(); + }); + + it(@"initializes in artwork display mode", ^{ + expect(favoritesVC.view).toNot.beNil(); + [favoritesVC viewWillAppear:NO]; + + expect(favoritesVC.displayMode).to.equal(ARFavoritesDisplayModeArtworks); + expect(favoritesVC.activeNetworkModel).to.equal(favoritesVC.artworkFavoritesNetworkModel); + expect(favoritesVC.embeddedItemsVC.activeModule).to.beKindOf([ARArtworkMasonryModule class]); + }); + + // TODO: implement this functionality. + pending(@"resets network models on viewWillAppear", ^{ + expect(favoritesVC.view).toNot.beNil(); + [favoritesVC viewWillAppear:NO]; + + switchView = favoritesVC.embeddedItemsVC.headerView.subviews[2]; + [[switchView buttons][ARSwitchViewFavoriteCategoriesIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + + ARArtworkFavoritesNetworkModel *initialArtworksNetworkModel = favoritesVC.artworkFavoritesNetworkModel; + ARArtistFavoritesNetworkModel *initialArtistsNetworkModel = favoritesVC.artistFavoritesNetworkModel; + ARGeneFavoritesNetworkModel *initialGenesNetworkModel = favoritesVC.geneFavoritesNetworkModel; + ARArtworkMasonryModule *initialArtworksModule = favoritesVC.artworksModule; + ARFavoriteItemModule *initialArtistsModule = favoritesVC.artistsModule; + ARFavoriteItemModule *initialGenesModule = favoritesVC.genesModule; + + [favoritesVC viewWillAppear:NO]; + expect(favoritesVC.artworkFavoritesNetworkModel).notTo.equal(initialArtworksNetworkModel); + expect(favoritesVC.artistFavoritesNetworkModel).notTo.equal(initialArtistsNetworkModel); + expect(favoritesVC.geneFavoritesNetworkModel).notTo.equal(initialGenesNetworkModel); + + expect(favoritesVC.artworksModule).notTo.equal(initialArtworksModule); + expect(favoritesVC.artistsModule).notTo.equal(initialArtistsModule); + expect(favoritesVC.genesModule).notTo.equal(initialGenesModule); + }); +}); + +describe(@"artworks", ^{ + + itHasSnapshotsForDevices(@"with no artworks", ^{ + sharedBefore(); + + id networkModelMock = [OCMockObject niceMockForClass:[ARArtworkFavoritesNetworkModel class]]; + [[[networkModelMock stub] andReturnValue:OCMOCK_VALUE(YES)] allDownloaded]; + [[[mock stub] andReturn:networkModelMock] artworkFavoritesNetworkModel]; + + return favoritesVC; + }); + + describe(@"with artworks", ^{ + __block NSMutableArray *artworks; + dispatch_block_t artworksBefore = ^{ + sharedBefore(); + artworks = [NSMutableArray array]; + + for(int i = 0; i < 9; i++) { + Artwork *artwork = [[Artwork alloc] initWithDictionary:@{ + @"artworkID" : @"stubbed", + @"title" : NSStringWithFormat(@"Artwork %d", i) + } error:nil]; + + artwork.artist = [[Artist alloc] initWithDictionary:@{ + @"artistID" :@"stubbed", + @"name" : NSStringWithFormat(@"Artist %d", i) + } error:nil]; + + [artworks addObject:artwork]; + } + [favoritesVC beginAppearanceTransition:YES animated:NO]; + [favoritesVC endAppearanceTransition]; + + id moduleMock = [OCMockObject partialMockForObject:favoritesVC.artworksModule]; + [[[moduleMock stub] andReturn:artworks] items]; + }; + + itHasSnapshotsForDevices(@"with artworks", ^{ + artworksBefore(); + return favoritesVC.view; + }); + + it(@"handles tap", ^{ + artworksBefore(); + expect(favoritesVC.view).notTo.beNil(); + id switchboardStub = [OCMockObject partialMockForObject:ARSwitchBoard.sharedInstance]; + [[switchboardStub expect] loadArtworkSet:favoritesVC.embeddedItemsVC.items inFair:nil atIndex:2]; + UICollectionView *collectionView = favoritesVC.embeddedItemsVC.collectionView; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:2 inSection:0]; + [collectionView.delegate collectionView:collectionView didSelectItemAtIndexPath:indexPath]; + [switchboardStub verify]; + }); + }); +}); + +describe(@"artists", ^{ + dispatch_block_t artistsBefore = ^{ + sharedBefore(); + [favoritesVC beginAppearanceTransition:YES animated:NO]; + [favoritesVC endAppearanceTransition]; + switchView = favoritesVC.embeddedItemsVC.headerView.subviews[2]; + }; + + it(@"initializes in artist display mode", ^{ + artistsBefore(); + [[switchView buttons][ARSwitchViewFavoriteArtistsIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + expect(favoritesVC.displayMode).to.equal(ARFavoritesDisplayModeArtists); + expect(favoritesVC.activeNetworkModel).to.equal(favoritesVC.artistFavoritesNetworkModel); + expect(favoritesVC.embeddedItemsVC.activeModule).to.beKindOf([ARFavoriteItemModule class]); + }); + + itHasSnapshotsForDevices(@"with no artists", ^{ + artistsBefore(); + id mock = [OCMockObject partialMockForObject:favoritesVC]; + id networkModelMock = [OCMockObject niceMockForClass:[ARArtistFavoritesNetworkModel class]]; + [[[networkModelMock stub] andReturnValue:OCMOCK_VALUE(YES)] allDownloaded]; + [[[mock stub] andReturn:networkModelMock] artistFavoritesNetworkModel]; + [[switchView buttons][ARSwitchViewFavoriteArtistsIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + + return favoritesVC.view; + }); + + describe(@"with artists", ^{ + __block NSMutableArray *artists; + dispatch_block_t artistsWithArtistsBefore = ^{ + artistsBefore(); + expect(favoritesVC.view).notTo.beNil(); + switchView = favoritesVC.embeddedItemsVC.headerView.subviews[2]; + artists = [NSMutableArray array]; + + for(int i = 0; i< 9; i++) { + Artist *artist = [[Artist alloc] initWithDictionary:@{ + @"artistID" : NSStringWithFormat(@"stubbed_%d", i), + @"name" : NSStringWithFormat(@"Artist %d", i) + } error:nil]; + [artists addObject:artist]; + } + [[switchView buttons][ARSwitchViewFavoriteArtistsIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + id moduleMock = [OCMockObject partialMockForObject:favoritesVC.artistsModule]; + [[[moduleMock stub] andReturn:artists] items]; + }; + + itHasSnapshotsForDevices(@"with artists", ^{ + artistsWithArtistsBefore(); + return favoritesVC.view; + }); + + it(@"handles tap", ^{ + artistsWithArtistsBefore(); + expect(favoritesVC.view).notTo.beNil(); + id switchboardStub = [OCMockObject partialMockForObject:ARSwitchBoard.sharedInstance]; + [[switchboardStub expect] loadArtistWithID:@"stubbed_2"]; + UICollectionView *collectionView = favoritesVC.embeddedItemsVC.collectionView; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:2 inSection:0]; + [collectionView.delegate collectionView:collectionView didSelectItemAtIndexPath:indexPath]; + [switchboardStub verify]; + }); + }); +}); + +describe(@"genes", ^{ + dispatch_block_t genesBefore = ^{ + sharedBefore(); + [favoritesVC beginAppearanceTransition:YES animated:NO]; + [favoritesVC endAppearanceTransition]; + switchView = favoritesVC.embeddedItemsVC.headerView.subviews[2]; + }; + + it(@"initializes in gene display mode", ^{ + genesBefore(); + [[switchView buttons][ARSwitchViewFavoriteCategoriesIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + expect(favoritesVC.displayMode).to.equal(ARFavoritesDisplayModeGenes); + expect(favoritesVC.activeNetworkModel).to.equal(favoritesVC.geneFavoritesNetworkModel); + expect(favoritesVC.embeddedItemsVC.activeModule).to.beKindOf([ARFavoriteItemModule class]); + }); + + itHasSnapshotsForDevices(@"with no genes", ^{ + genesBefore(); + id mock = [OCMockObject partialMockForObject:favoritesVC]; + id networkModelMock = [OCMockObject niceMockForClass:[ARGeneFavoritesNetworkModel class]]; + [[[networkModelMock stub] andReturnValue:OCMOCK_VALUE(YES)] allDownloaded]; + [[[mock stub] andReturn:networkModelMock] geneFavoritesNetworkModel]; + [[switchView buttons][ARSwitchViewFavoriteCategoriesIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + + return favoritesVC.view; + }); + + describe(@"with genes", ^{ + __block NSMutableArray *genes; + + dispatch_block_t genesWithGenesBefore = ^{ + genesBefore(); + [favoritesVC beginAppearanceTransition:YES animated:NO]; + [favoritesVC endAppearanceTransition]; + switchView = favoritesVC.embeddedItemsVC.headerView.subviews[2]; + genes = [NSMutableArray array]; + for(int i = 0; i< 9; i++) { + Gene *gene = [[Gene alloc] initWithDictionary:@{ + @"geneID" : @"stubbed", + @"name" : NSStringWithFormat(@"Gene %d", i) + } error:nil]; + [genes addObject:gene]; + } + id moduleMock = [OCMockObject partialMockForObject:favoritesVC.genesModule]; + [[[moduleMock stub] andReturn:genes] items]; + [[switchView buttons][ARSwitchViewFavoriteCategoriesIndex] sendActionsForControlEvents:UIControlEventTouchUpInside]; + }; + + itHasSnapshotsForDevices(@"with genes", ^{ + genesWithGenesBefore(); + return favoritesVC.view; + }); + + it(@"handles tap", ^{ + genesWithGenesBefore(); + expect(favoritesVC.view).notTo.beNil(); + id switchboardStub = [OCMockObject partialMockForObject:ARSwitchBoard.sharedInstance]; + [[switchboardStub expect] loadGene:favoritesVC.embeddedItemsVC.items[2]]; + UICollectionView *collectionView = favoritesVC.embeddedItemsVC.collectionView; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:2 inSection:0]; + [collectionView.delegate collectionView:collectionView didSelectItemAtIndexPath:indexPath]; + [switchboardStub verify]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFileUtilsTests.m b/Artsy Tests/ARFileUtilsTests.m new file mode 100644 index 00000000000..f6eadbca27b --- /dev/null +++ b/Artsy Tests/ARFileUtilsTests.m @@ -0,0 +1,64 @@ +#import "ARFileUtils.h" +#import "ARUserManager.h" +#import "ARUserManager+Stubs.h" + +SpecBegin(ARFileUtilsTests) + +describe(@"caches", ^{ + it(@"cachesFolder", ^{ + expect([[ARFileUtils cachesFolder] hasSuffix:@"/Caches"]).to.beTruthy(); + }); + + it(@"cachesFolder creates a folder", ^{ + NSString *uuid = [[NSUUID UUID] UUIDString]; + NSString *uuidPath = [ARFileUtils cachesPathWithFolder:uuid filename:@"test.filename"]; + NSString *uuidFolder = NSStringWithFormat(@"%@/%@", [ARFileUtils cachesFolder], uuid); + // path has been created + expect([[NSFileManager defaultManager] fileExistsAtPath:uuidFolder]).to.beTruthy(); + // file doesn't exist + expect([[NSFileManager defaultManager] fileExistsAtPath:uuidPath]).to.beFalsy(); + [[NSFileManager defaultManager] removeItemAtPath:uuidFolder error:nil]; + }); +}); + + +describe(@"logged out user documents folder", ^{ + beforeEach(^{ + [[ARUserManager sharedManager] logout]; + }); + + it(@"userDocumentsFolder", ^{ + expect([ARFileUtils userDocumentsFolder]).to.beNil(); + expect([ARFileUtils userDocumentsPathWithFile:@"folder"]).to.beNil(); + expect([ARFileUtils userDocumentsPathWithFolder:@"folder" filename:@"filename"]).to.beNil(); + }); +}); + +describe(@"logged in user documents folder", ^{ + beforeEach(^{ + [ARUserManager stubAndLoginWithUsername]; + XCTAssert([User currentUser] != nil, @"Current user is nil even after stubbing. "); + }); + + afterEach(^{ + [[NSFileManager defaultManager] removeItemAtPath:[ARFileUtils userDocumentsFolder] error:nil]; + }); + + it(@"userDocumentsFolder", ^{ + expect([[ARFileUtils userDocumentsFolder] hasSuffix:NSStringWithFormat(@"/Documents/%@", [User currentUser].userID)]).to.beTruthy(); + }); + + it(@"userDocumentsFilename", ^{ + NSString *uuid = [[NSUUID UUID] UUIDString]; + NSString *uuidFilePath = [ARFileUtils userDocumentsPathWithFile:uuid]; + expect([uuidFilePath hasSuffix:NSStringWithFormat(@"/Documents/%@/%@", [User currentUser].userID, uuid)]).to.beTruthy(); + }); + + it(@"userDocumentsFolderFilename", ^{ + NSString *uuid = [[NSUUID UUID] UUIDString]; + NSString *uuidPath = [ARFileUtils userDocumentsPathWithFolder:uuid filename:@"filename"]; + expect([uuidPath hasSuffix:NSStringWithFormat(@"/Documents/%@/%@/filename", [User currentUser].userID, uuid)]).to.beTruthy(); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARFollowableButtonTests.m b/Artsy Tests/ARFollowableButtonTests.m new file mode 100644 index 00000000000..ac1c54e0d1b --- /dev/null +++ b/Artsy Tests/ARFollowableButtonTests.m @@ -0,0 +1,20 @@ +#import +#import "ARFollowableButton.h" + +SpecBegin(ARFollowableButton) + +describe(@"ARFollowableButton", ^{ + it(@"following", ^{ + ARFollowableButton *button = [[ARFollowableButton alloc] initWithFrame:CGRectMake(0, 0, 300, 46)]; + [button setFollowingStatus:YES]; + expect(button).to.haveValidSnapshot(); + }); + + it(@"not following", ^{ + ARFollowableButton *button = [[ARFollowableButton alloc] initWithFrame:CGRectMake(0, 0, 300, 46)]; + [button setFollowingStatus:NO]; + expect(button).to.haveValidSnapshot(); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARGeneArtworksNetworkModelTests.m b/Artsy Tests/ARGeneArtworksNetworkModelTests.m new file mode 100644 index 00000000000..5fd5c27d284 --- /dev/null +++ b/Artsy Tests/ARGeneArtworksNetworkModelTests.m @@ -0,0 +1,104 @@ +#import "Artwork+Extensions.h" +#import "ARGeneArtworksNetworkModel.h" + +@interface ARGeneArtworksNetworkModel(Testing) +@property (nonatomic, assign) NSInteger currentPage; +@property (readwrite, nonatomic, assign) BOOL allDownloaded; +@property (readwrite, nonatomic, assign) BOOL downloadLock; +@end + +void stubNetworkingForPartialGeneAtPageWithArray(id gene, NSInteger page, NSArray* content); + +SpecBegin(ARGeneArtworknetworkModel) +__block ARGeneArtworksNetworkModel *networkModel; + +describe(@"init", ^{ + it(@"sets the gene", ^{ + Gene *gene = [[Gene alloc] initWithGeneID:@"hi"]; + networkModel = [[ARGeneArtworksNetworkModel alloc] initWithGene:gene]; + expect(networkModel.gene).to.equal(gene); + }); +}); + +describe(@"networking", ^{ + __block id partialGene; + + before(^{ + Gene *gene = [[Gene alloc] initWithGeneID:@"hi"]; + partialGene = [OCMockObject partialMockForObject:gene]; + networkModel = [[ARGeneArtworksNetworkModel alloc] initWithGene:partialGene]; + }); + + it(@"does not do networking once completed", ^{ + [[partialGene reject] getArtworksAtPage:0 success:nil]; + + networkModel.allDownloaded = YES; + networkModel.downloadLock = NO; + + [networkModel getNextArtworkPage:nil]; + [partialGene verify]; + }); + + it(@"does not do networking if a request is in progress", ^{ + [[partialGene reject] getArtworksAtPage:0 success:nil]; + + networkModel.allDownloaded = NO; + networkModel.downloadLock = YES; + + [networkModel getNextArtworkPage:nil]; + [partialGene verify]; + }); + + describe(@"with stubbed networking", ^{ + __block id partialArtworknetworkModel; + + beforeEach(^{ + networkModel = [[ARGeneArtworksNetworkModel alloc] initWithGene:partialGene]; + partialArtworknetworkModel = [OCMockObject partialMockForObject:networkModel]; + }); + + it(@"asks the gene for artworks at a page", ^{ + NSArray *artworks = @[ [Artwork stubbedArtwork]]; + stubNetworkingForPartialGeneAtPageWithArray(partialGene, 1, artworks); + + [partialArtworknetworkModel getNextArtworkPage:nil]; + [partialGene verify]; + }); + + it(@"adds one to the current page when there is artworks", ^{ + NSArray *artworks = @[ [Artwork stubbedArtwork]]; + NSInteger page = 1; + stubNetworkingForPartialGeneAtPageWithArray(partialGene, page, artworks); + + [partialArtworknetworkModel getNextArtworkPage:nil]; + expect([partialArtworknetworkModel currentPage]).to.beGreaterThan(page); + }); + + it(@"becomes complete when no new artworks arrive", ^{ + NSArray *artworks = @[]; + stubNetworkingForPartialGeneAtPageWithArray(partialGene, 1, artworks); + + [partialArtworknetworkModel getNextArtworkPage:nil]; + expect([partialArtworknetworkModel allDownloaded]).to.equal(YES); + }); + + it(@"does not say it's complete if artworks arrive", ^{ + NSArray *artworks = @[ [Artwork stubbedArtwork]]; + stubNetworkingForPartialGeneAtPageWithArray(partialGene, 1, artworks); + + [partialArtworknetworkModel getNextArtworkPage:nil]; + expect([partialArtworknetworkModel allDownloaded]).to.equal(NO); + }); + }); +}); + +SpecEnd + +void stubNetworkingForPartialGeneAtPageWithArray(id gene, NSInteger page, NSArray* content){ + [[[gene stub] andDo:^(NSInvocation *invocation) { + void (^successBlock)(NSArray *) = nil; + [invocation getArgument:&successBlock atIndex:3]; + successBlock(content); + + }] getArtworksAtPage:page success:[OCMArg any]]; +} diff --git a/Artsy Tests/ARGeneViewControllerTests.m b/Artsy Tests/ARGeneViewControllerTests.m new file mode 100644 index 00000000000..4331d838fe4 --- /dev/null +++ b/Artsy Tests/ARGeneViewControllerTests.m @@ -0,0 +1,40 @@ +#import "ARGeneViewController.h" + +SpecBegin(ARGeneViewController) + +__block ARGeneViewController *vc; + +after(^{ + vc = nil; +}); + +pending(@"with long desciption", ^{ // This works, but on Travis we get a weird autolayout error. +// itHasAsyncronousSnapshotsForDevices(@"with long desciption", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/gene/painting" withResponse:@{ + @"id" : @"painting", + @"name" : @"Painting", + @"description" : @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + }]; + NSArray *artworksJSON = [NSArray array]; + for (int i = 1; i <= 9; i++){ + artworksJSON = [artworksJSON arrayByAddingObject:@{ + @"title" : NSStringWithFormat(@"Artwork %i", i), + @"artist" : @{ + @"name" : NSStringWithFormat(@"Artist %i", i) + }, + @"date" : @"2009" + }]; + } + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/search/filtered/gene/painting" withResponse:artworksJSON]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + vc = [[ARGeneViewController alloc] initWithGeneID:@"painting"]; + vc.shouldAnimate = NO; + window.rootViewController = vc; + expect(vc.view).willNot.beNil(); + [window makeKeyAndVisible]; + return vc; +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARHeartButtonTests.m b/Artsy Tests/ARHeartButtonTests.m new file mode 100644 index 00000000000..5e195e56040 --- /dev/null +++ b/Artsy Tests/ARHeartButtonTests.m @@ -0,0 +1,72 @@ +#import "ARHeartButton.h" + +SpecBegin(ARHeartButton) + +__block ARHeartButton * _button = nil; + +beforeEach(^{ + _button = [[ARHeartButton alloc] init]; + CGFloat buttonSize = [ARCircularActionButton buttonSize]; + _button.frame = CGRectMake(buttonSize / 2, 0, buttonSize, buttonSize); +}); + +it(@"defaults to disabled", ^{ + expect(_button).will.haveValidSnapshotNamed(@"unhearted"); + expect(_button.enabled).to.beFalsy(); +}); + +describe(@"not hearted", ^{ + beforeEach(^{ + _button.hearted = NO; + }); + + it(@"becomes enabled when not hearted", ^{ + expect(_button).will.haveValidSnapshotNamed(@"unhearted"); + expect(_button.enabled).to.beTruthy(); + }); + + it(@"doesn't toggle", ^{ + _button.hearted = NO; + expect(_button).will.haveValidSnapshotNamed(@"unhearted"); + expect(_button.enabled).to.beTruthy(); + }); + + it(@"toggles", ^{ + _button.hearted = YES; + expect(_button).will.haveValidSnapshotNamed(@"hearted"); + _button.hearted = NO; + expect(_button).will.haveValidSnapshotNamed(@"unhearted"); + _button.hearted = YES; + expect(_button).will.haveValidSnapshotNamed(@"hearted"); + expect(_button.enabled).to.beTruthy(); + }); +}); + +describe(@"hearted", ^{ + beforeEach(^{ + _button.hearted = YES; + }); + + it(@"becomes enabled when hearted", ^{ + expect(_button).will.haveValidSnapshotNamed(@"hearted"); + expect(_button.enabled).to.beTruthy(); + }); + + it(@"doesn't toggle", ^{ + _button.hearted = YES; + expect(_button).will.haveValidSnapshotNamed(@"hearted"); + expect(_button.enabled).to.beTruthy(); + }); + + it(@"toggles", ^{ + _button.hearted = NO; + expect(_button).will.haveValidSnapshotNamed(@"unhearted"); + _button.hearted = YES; + expect(_button).will.haveValidSnapshotNamed(@"hearted"); + expect(_button.enabled).to.beTruthy(); + }); +}); + + +SpecEnd + diff --git a/Artsy Tests/ARHeroUnitTests.m b/Artsy Tests/ARHeroUnitTests.m new file mode 100644 index 00000000000..df4b309a723 --- /dev/null +++ b/Artsy Tests/ARHeroUnitTests.m @@ -0,0 +1,212 @@ + +#import "ARHeroUnitViewController.h" +#import "ARHeroUnitsNetworkModel.h" +#import "SiteHeroUnit.h" + +@interface ARSiteHeroUnitViewController : UIViewController +@end + +@interface ARHeroUnitViewController (Test) +@property (nonatomic, strong) UIPageControl *pageControl; +@property (nonatomic, strong) UIPageViewController *pageViewController; +@property (nonatomic, strong) NSTimer *timer; +- (void)startTimer; +- (void)updateViewWithHeroUnits:(NSArray *)heroUnits; +- (ARSiteHeroUnitViewController *)currentViewController; +@end + +@interface ARHeroUnitsNetworkModel (Test) +@property (nonatomic, copy, readwrite) NSArray *heroUnits; +@end + +@interface ARHeroUnitsTestDataSource : ARHeroUnitsNetworkModel +-(void)getHeroUnitsWithSuccess:(void (^)())success failure:(void (^)(NSError *error))failure; +@end + +@implementation ARHeroUnitsTestDataSource +-(void)getHeroUnitsWithSuccess:(void (^)())success failure:(void (^)(NSError *error))failure +{ + success(self.heroUnits); +} +@end + +SpecBegin(ARHeroUnitViewController) +__block ARHeroUnitViewController *heroVC; +__block NSArray *heroUnits; + +dispatch_block_t sharedBefore = ^{ + heroVC = [[ARHeroUnitViewController alloc] init]; + heroVC.heroUnitNetworkModel = [[ARHeroUnitsTestDataSource alloc] init]; + heroVC.heroUnitNetworkModel.heroUnits = heroUnits; + expect(heroVC.view).toNot.beNil(); +}; + +describe(@"with three hero units", ^{ + before(^{ + heroUnits = @[[SiteHeroUnit modelWithJSON:@{ + @"id": @"art-basel1", + @"name": @"Art Basel1", + @"heading": @"Exclusive Preview", + @"mobile_description": @"Discover some artworks.", + @"mobile_title": @"Art Basel", + @"display_on_mobile": @true, + @"position": @1, + @"link":@"/art-basel1", + @"link_text":@"Explore", + @"credit_line":@"Artsy artsy artsy" + }], [SiteHeroUnit modelWithJSON:@{ + @"id": @"art-basel2", + @"name": @"Art Basel2", + @"heading": @"Exclusive Preview", + @"mobile_description": @"Discover some artworks.", + @"mobile_title": @"Art Basel", + @"display_on_mobile": @true, + @"position": @2, + @"link":@"/art-basel2", + @"link_text":@"Explore", + @"credit_line":@"Artsy artsy artsy" + }], [SiteHeroUnit modelWithJSON:@{ + @"id": @"art-basel3", + @"name": @"Art Basel3", + @"heading": @"Exclusive Preview", + @"mobile_description": @"Discover some artworks.", + @"mobile_title": @"Art Basel", + @"display_on_mobile": @true, + @"position": @3, + @"link":@"/art-basel3", + @"link_text":@"Explore", + @"credit_line":@"Artsy artsy artsy" + }]]; + }); + + describe(@"updateViewWithHeroUnits", ^{ + before(^{ + sharedBefore(); + [heroVC updateViewWithHeroUnits:heroUnits]; + }); + + it(@"sets pageControl", ^{ + expect(heroVC.pageControl.hidden).to.beFalsy(); + expect(heroVC.pageControl.numberOfPages).to.equal(3); + }); + + it(@"sets the first view controller", ^{ + expect(heroVC.pageViewController.viewControllers.count).to.equal(1); + }); + + it(@"currentViewController returns a vc", ^{ + ARSiteHeroUnitViewController *currentViewController = [heroVC currentViewController]; + expect(currentViewController).notTo.beNil(); + expect(currentViewController).to.beKindOf([ARSiteHeroUnitViewController class]); + }); + }); + + + it(@"startTimer sets timer for page turn", ^{ + sharedBefore(); + [heroVC startTimer]; + expect(heroVC.timer).toNot.beNil(); + }); + + itHasSnapshotsForDevices(@"with three units", ^{ + sharedBefore(); + [heroVC fetchHeroUnits]; + return heroVC; + }); +}); + +describe(@"with one hero unit", ^{ + before(^{ + heroUnits = @[[SiteHeroUnit modelWithJSON:@{ + @"id": @"art-basel1", + @"name": @"Art Basel1", + @"heading": @"Exclusive Preview", + @"mobile_description": @"Discover some artworks.", + @"mobile_title": @"Art Basel", + @"display_on_mobile": @true, + @"position": @1, + @"link":@"/art-basel1", + @"link_text":@"Explore", + @"credit_line":@"Artsy artsy artsy" + }]]; + }); + + describe(@"updateViewWithHeroUnits", ^{ + before(^{ + sharedBefore(); + [heroVC updateViewWithHeroUnits:heroUnits]; + }); + + it(@"sets pageControl", ^{ + expect(heroVC.pageControl.hidden).to.beTruthy(); + expect(heroVC.pageControl.numberOfPages).to.equal(1); + }); + + it(@"sets the first view controller", ^{ + expect(heroVC.pageViewController.viewControllers.count).to.equal(1); + }); + + it(@"currentViewController returns a vc", ^{ + ARSiteHeroUnitViewController *currentViewController = [heroVC currentViewController]; + expect(currentViewController).notTo.beNil(); + expect(currentViewController).to.beKindOf([ARSiteHeroUnitViewController class]); + }); + }); + + + + it(@"startTimer doesn't set timer for page turn", ^{ + sharedBefore(); + [heroVC startTimer]; + expect(heroVC.timer).to.beNil(); + }); + + itHasSnapshotsForDevices(@"with one unit", ^{ + sharedBefore(); + [heroVC fetchHeroUnits]; + return heroVC; + }); +}); + +describe(@"with no hero units", ^{ + // In practice this should never happen. If there are ever zero hero units, there is a data problem. + before(^{ + heroUnits = @[]; + }); + + describe(@"updateViewWithHeroUnits", ^{ + before(^{ + sharedBefore(); + [heroVC updateViewWithHeroUnits:heroUnits]; + }); + + it(@"sets pageControl", ^{ + expect(heroVC.pageControl.hidden).to.beTruthy(); + expect(heroVC.pageControl.numberOfPages).to.equal(0); + }); + + it(@"sets the first view controller", ^{ + expect(heroVC.pageViewController.viewControllers.count).to.equal(0); + }); + + it(@"currentViewController returns nil", ^{ + ARSiteHeroUnitViewController *currentViewController = [heroVC currentViewController]; + expect(currentViewController).to.beNil(); + }); + }); + + it(@"startTimer doesn't set timer for page turn", ^{ + sharedBefore(); + [heroVC startTimer]; + expect(heroVC.timer).to.beNil(); + }); + + itHasSnapshotsForDevices(@"with no units", ^{ + sharedBefore(); + [heroVC fetchHeroUnits]; + return heroVC; + }); +}); + + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARHeroUnitsNetworkModelTests.m b/Artsy Tests/ARHeroUnitsNetworkModelTests.m new file mode 100644 index 00000000000..08b06e8c956 --- /dev/null +++ b/Artsy Tests/ARHeroUnitsNetworkModelTests.m @@ -0,0 +1,32 @@ +#import "ARHeroUnitsNetworkModel.h" +#import "SiteHeroUnit.h" +SpecBegin(ARHeroUnitsNetworkModel) + +describe(@"heroes", ^{ + __block ARHeroUnitsNetworkModel *_dataSource; + + beforeEach(^{ + _dataSource = [[ARHeroUnitsNetworkModel alloc] init]; + }); + + it(@"only retrieves active hero units", ^{ + waitUntil(^(DoneCallback done) { + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/xapp_token" withResponse:@{ @"xapp_token": @"23123123", @"expires_in": @"2035-01-02T21:42:21-0500" }]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/site_hero_units" + withParams: @{ @"mobile" : @"true", @"enabled" : @"true" } + withResponse:@[ + @{ @"id": @"past", @"enabled" : @YES, @"display_on_mobile" : @YES, @"start_at" : @"1976-01-27T05:00:00+00:00", @"end_at" : @"1976-01-27T05:00:00+00:00" }, + @{ @"id": @"future", @"enabled" : @YES, @"display_on_mobile" : @YES, @"start_at" : @"2099-01-27T05:00:00+00:00", @"end_at" : @"2099-01-27T05:00:00+00:00" }, + @{ @"id": @"current", @"enabled" : @YES, @"display_on_mobile" : @YES, @"start_at" : @"1976-01-27T05:00:00+00:00", @"end_at" : @"2099-01-27T05:00:00+00:00" } + ]]; + + [_dataSource getHeroUnitsWithSuccess:^{ + expect([_dataSource.heroUnits count]).to.equal(1); + expect([[_dataSource.heroUnits firstObject] siteHeroUnitID]).to.equal(@"current"); + done(); + } failure:nil]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARImageItemProviderTests.m b/Artsy Tests/ARImageItemProviderTests.m new file mode 100644 index 00000000000..996c9d111a0 --- /dev/null +++ b/Artsy Tests/ARImageItemProviderTests.m @@ -0,0 +1,31 @@ +#import "ARImageItemProvider.h" + +@interface ARImageItemProvider (Testing) +@property (nonatomic, strong, readwrite) NSString *activityType; +@end + +SpecBegin(ARImageItemProvider) + +describe(@"image provider item", ^{ + __block UIImage *image = [UIImage imageNamed:@"stub.jpg"]; + __block ARImageItemProvider *provider = [[ARImageItemProvider alloc] initWithPlaceholderItem:image]; + __block id providerMock = [OCMockObject partialMockForObject:provider]; + + NSArray *imagelessActivities = @[@"Twitter", @"Facebook"]; + + for (NSString *activity in imagelessActivities) { + it([NSString stringWithFormat:@"%@%@", @"returns nil for ", activity], ^{ + NSString *activityType = [NSString stringWithFormat:@"%@%@",@"com.apple.UIKit.activity.PostTo", activity]; + [[[providerMock stub] andReturn:activityType] activityType]; + expect(provider.item).to.beNil(); + }); + } + + it(@"returns placeholderItem for other activities", ^{ + [[[providerMock stub] andReturn:@"another activity"] activityType];; + expect(provider.item).to.equal(image); + }); + +}); + +SpecEnd diff --git a/Artsy Tests/ARInquireForArtworkViewControllerTests.m b/Artsy Tests/ARInquireForArtworkViewControllerTests.m new file mode 100644 index 00000000000..c538a440a7b --- /dev/null +++ b/Artsy Tests/ARInquireForArtworkViewControllerTests.m @@ -0,0 +1,280 @@ +#import "ARInquireForArtworkViewController.h" +#import "ARUserManager+Stubs.h" + +@interface ARInquireForArtworkViewController (Testing) + +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +@property (nonatomic, strong, readonly) UITextField *emailInput; +@property (nonatomic, strong, readonly) UITextField *nameInput; + +@property (nonatomic, strong, readonly) UILabel *messageTitleLabel; +@property (nonatomic, strong, readonly) UILabel *messageBodyLabel; + +@property (nonatomic, strong, readonly) UIButton *failureDismissButton; +@property (nonatomic, strong, readonly) UIButton *failureTryAgainButton; +@property (nonatomic, strong, readonly) ARModalMenuButton *sendButton; + +- (void)sendButtonTapped:(UIButton *)sender; +- (void)cancelButtonTapped:(UIButton *)sender; +- (void)emailInputHasChanged:(id)sender; +- (void)removeFromHostViewController; +- (void)sendInquiry; +@end + +SpecBegin(ARInquireForArtworkViewController) + +__block Artwork *galleryArtwork, *museumGallery; + +beforeEach(^{ + galleryArtwork = [Artwork modelWithJSON:@{ + @"id" : @"cory-arcangel-photoshop-cs", + @"title" : @"Photoshop CS", + @"artist" : @{ + @"id": @"cory-arcangel", + @"name": @"Cory Arcangel" + }, + @"partner" : @{ + @"id" : @"partner_id", + @"type" : @"Gallery", + @"name" : @"Lisson Gallery" + } + }]; + + museumGallery = [Artwork modelWithJSON:@{ + @"id" : @"cory-arcangel-photoshop-cs", + @"title" : @"Photoshop CS", + @"artist" : @{ + @"id": @"cory-arcangel", + @"name": @"Cory Arcangel" + }, + @"partner" : @{ + @"id" : @"partner_id", + @"type" : @"Museum", + @"name" : @"Guggenheim Museum" + } + }]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/admins/available_representatives" withResponse:@[]]; +}); + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +describe(@"logged in", ^{ + beforeEach(^{ + [ARUserManager stubAndLoginWithUsername]; + }); + + afterEach(^{ + [[ARUserManager sharedManager] logout]; + }); + + itHasAsyncronousSnapshotsForDevices(@"displays Contact Gallery when seller is a gallery", ^{ + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithPartnerInquiryForArtwork:galleryArtwork fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"displays Contact Seller when seller is not a gallery", ^{ + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithPartnerInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"logged out, displays artsy specialist", ^{ + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); +}); + +describe(@"logged out", ^{ + describe(@"contact information", ^{ + beforeEach(^{ + [[ARUserManager sharedManager] logout]; + [ARUserManager sharedManager].trialUserName = @"Trial User"; + [ARUserManager sharedManager].trialUserEmail = @"trial@example.com"; + }); + + afterEach(^{ + [ARUserManager sharedManager].trialUserName = nil; + [ARUserManager sharedManager].trialUserEmail = nil; + }); + + itHasAsyncronousSnapshotsForDevices(@"displays contact gallery", ^{ + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithPartnerInquiryForArtwork:galleryArtwork fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"displays artsy specialist", ^{ + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"works for an artwork without a title", ^{ + museumGallery.title = nil; + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithPartnerInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + }); + + describe(@"send button", ^{ + __block Artwork *artwork; + + beforeEach(^{ + [[ARUserManager sharedManager] logout]; + [ARUserManager sharedManager].trialUserName = @"Trial User"; + }); + + afterEach(^{ + [ARUserManager sharedManager].trialUserName = nil; + [ARUserManager sharedManager].trialUserEmail = nil; + artwork = nil; + }); + + itHasAsyncronousSnapshotsForDevices(@"does not initially enable send if stored email is invalid", ^{ + [ARUserManager sharedManager].trialUserEmail = @"invalidEmail"; + + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"does initially enables send if stored email is valid", ^{ + [ARUserManager sharedManager].trialUserEmail = @"validemail@gmail.com"; + + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"toggles the send button with empty email", ^{ + [ARUserManager sharedManager].trialUserEmail = nil; + + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"toggles the send button when email becomes valid", ^{ + [ARUserManager sharedManager].trialUserEmail = nil; + + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + + vc.emailInput.text = @"validemail@gmail.com"; + [vc emailInputHasChanged:vc.emailInput]; + return vc; + }); + + itHasAsyncronousSnapshotsForDevices(@"toggles the send button when valid email becomes invalid", ^{ + [ARUserManager sharedManager].trialUserEmail = nil; + + ARInquireForArtworkViewController *vc = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:museumGallery fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + + vc.emailInput.text = @"validemail@gmail.com"; + [vc emailInputHasChanged:vc.emailInput]; + vc.emailInput.text = @"invalidEmail"; + [vc emailInputHasChanged:vc.emailInput]; + return vc; + }); + }); +}); + +describe(@"sending", ^{ + __block ARInquireForArtworkViewController *vc; + __block id userMock; + + beforeEach(^{ + [[ARUserManager sharedManager] logout]; + [ARUserManager sharedManager].trialUserName = @"Trial User"; + [ARUserManager sharedManager].trialUserEmail = @"trial@example.com"; + userMock = [OCMockObject mockForClass:[User class]]; + [[[[userMock stub] classMethod] andReturnValue:OCMOCK_VALUE(YES)] isTrialUser]; + + vc = [[ARInquireForArtworkViewController alloc] initWithPartnerInquiryForArtwork:galleryArtwork fair:nil]; + vc.shouldAnimate = NO; + [vc ar_presentWithFrame:[[UIScreen mainScreen] bounds]]; + }); + + afterEach(^{ + [userMock stopMocking]; + [ARUserManager sharedManager].trialUserName = nil; + [ARUserManager sharedManager].trialUserEmail = nil; + }); + + it(@"displays sending message", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/artwork_inquiry_request" withResponse:@{}]; + [vc sendButtonTapped:vc.sendButton]; + expect(vc.messageTitleLabel.hidden).to.beFalsy(); + expect(vc.messageTitleLabel.text).to.equal(@"SENDING…"); + expect(vc.messageBodyLabel.hidden).to.beFalsy(); + expect(vc.messageBodyLabel.text).to.equal(@""); + expect(vc.failureTryAgainButton.hidden).to.beTruthy(); + expect(vc.failureDismissButton.hidden).to.beTruthy(); + }); + + it(@"displays success message", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/artwork_inquiry_request" withResponse:@{}]; + [vc sendButtonTapped:vc.sendButton]; + expect(vc.messageTitleLabel.hidden).to.beFalsy(); + expect(vc.messageTitleLabel.text).will.equal(@"THANK YOU"); + expect(vc.messageBodyLabel.hidden).to.beFalsy(); + expect(vc.messageBodyLabel.text).will.equal(@"Your message has been sent"); + expect(vc.failureTryAgainButton.hidden).to.beTruthy(); + expect(vc.failureDismissButton.hidden).to.beTruthy(); + }); + + describe(@"general failure", ^{ + before(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me/artwork_inquiry_request" withResponse:@{} andStatusCode:400]; + [vc sendButtonTapped:vc.sendButton]; + }); + + it(@"displays failure message", ^{ + expect(vc.messageTitleLabel.hidden).to.beFalsy(); + expect(vc.messageTitleLabel.text).will.equal(@"ERROR SENDING MESSAGE"); + expect(vc.messageBodyLabel.hidden).to.beFalsy(); + expect(vc.messageBodyLabel.text).will.equal(@"Please try again or email\nsupport@artsy.net if the issue persists"); + expect(vc.failureTryAgainButton.hidden).to.beFalsy(); + expect(vc.failureDismissButton.hidden).to.beFalsy(); + }); + + it(@"can be dismissed", ^{ + id vcMock = [OCMockObject partialMockForObject:vc]; + [[vcMock expect] removeFromHostViewController]; + [[vcMock reject] sendInquiry]; + [vc cancelButtonTapped:vc.failureDismissButton]; + [vcMock verify]; + [vcMock stopMocking]; + }); + + it(@"can resend request", ^{ + id vcMock = [OCMockObject partialMockForObject:vc]; + [[vcMock reject] removeFromHostViewController]; + [[vcMock expect] sendInquiry]; + [vc sendButtonTapped:vc.failureTryAgainButton]; + [vcMock verify]; + [vcMock stopMocking]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARInternalMobileWebViewControllerTests.m b/Artsy Tests/ARInternalMobileWebViewControllerTests.m new file mode 100644 index 00000000000..41e1abcdb85 --- /dev/null +++ b/Artsy Tests/ARInternalMobileWebViewControllerTests.m @@ -0,0 +1,204 @@ +#import "AROptions.h" +#import "ARInternalMobileWebViewController.h" +#import "ARUserManager+Stubs.h" +#import "ARUserManager.h" +#import "ARNetworkConstants.h" +#import "ARTrialController.h" +#import "ARSwitchBoard.h" + +SpecBegin(ARInternalMobileViewController) + +it(@"passes on fair context", ^{ + id fair = [OCMockObject mockForClass:[Fair class]]; + id switchboardMock = [OCMockObject partialMockForObject:ARSwitchBoard.sharedInstance]; + [[switchboardMock expect] loadURL:OCMOCK_ANY fair:[OCMArg checkWithBlock:^BOOL(id obj) { + return obj == fair; + }]]; + + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://artsy.net/foo/bar"]]; + controller.fair = fair; + + NSURL *url = [NSURL URLWithString:@"http://artsy.net/foo/bar"]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + [controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeLinkClicked]; + + [switchboardMock verify]; + [switchboardMock stopMocking]; + [fair stopMocking]; +}); + +describe(@"initWithURL", ^{ + describe(@"in production", ^{ + beforeEach(^{ + [AROptions setBool:false forOption:ARUseStagingDefault]; + }); + + it(@"rewrites the scheme", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://m.artsy.net/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://m.artsy.net/foo/bar"); + }); + + it(@"with an artsy.net url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://artsy.net/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://m.artsy.net/foo/bar"); + }); + + it(@"with a relative url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://m.artsy.net/foo/bar"); + }); + + it(@"with an external artsy.net url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"https://2013.artsy.net"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://2013.artsy.net"); + }); + + it(@"with an external url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://example.com/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"http://example.com/foo/bar"); + }); + }); + + describe(@"in production on ipad", ^{ + beforeEach(^{ + [AROptions setBool:false forOption:ARUseStagingDefault]; + [ARTestContext stubDevice:ARDeviceTypePad]; + }); + + afterEach(^{ + [ARTestContext stopStubbing]; + }); + + it(@"with a relative url on ipad", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://artsy.net/foo/bar"); + }); + }); + + describe(@"in staging", ^{ + beforeEach(^{ + [AROptions setBool:true forOption:ARUseStagingDefault]; + }); + + it(@"rewrites the scheme", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"https://m-staging.artsy.net/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"http://m-staging.artsy.net/foo/bar"); + }); + + it(@"with an artsy.net url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://staging.artsy.net/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"http://m-staging.artsy.net/foo/bar"); + }); + + it(@"with a relative url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"http://m-staging.artsy.net/foo/bar"); + }); + + it(@"with an external artsy.net url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"https://2013.artsy.net"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://2013.artsy.net"); + }); + + it(@"with an external url", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"https://example.com/foo/bar"]]; + expect([controller currentURL].absoluteString).to.equal(@"https://example.com/foo/bar"); + }); + }); +}); + +describe(@"authenticated", ^{ + beforeEach(^{ + [ARUserManager stubAndLoginWithUsername]; + }); + + afterEach(^{ + [[ARUserManager sharedManager] logout]; + }); + + it(@"injects an X-Auth-Token header in requests", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://m.artsy.net/"]]; + NSURLRequest *request = [controller requestWithURL:controller.currentURL]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).toNot.beNil(); + }); + + it(@"doesn't leak X-Auth-Token to non-Artsy domains", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://example.com/"]]; + NSURLRequest *request = [controller requestWithURL:controller.currentURL]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).to.beNil(); + }); + + describe(@"shouldStartLoadWithRequest:navigationType", ^{ + __block ARInternalMobileWebViewController *controller; + + beforeEach(^{ + controller = [[ARInternalMobileWebViewController alloc] init]; + }); + + it(@"doesn't show a trial login/signup view on a request to log_in", ^{ + NSURLRequest *request = [controller requestWithURL:[NSURL URLWithString:@"http://m.artsy.net/log_in"]]; + id mockUser = [OCMockObject mockForClass:[User class]]; + [[[mockUser stub] andReturnValue:OCMOCK_VALUE(NO)] isTrialUser]; + id mock = [OCMockObject partialMockForObject:[ARTrialController instance]]; + [[mock reject] presentTrialWithContext:ARTrialContextNotTrial fromTarget:[OCMArg any] selector:[OCMArg anySelector]]; + expect([controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeOther]).to.beFalsy(); + [mock verify]; + [mockUser stopMocking]; + [mock stopMocking]; + }); + }); +}); + +describe(@"unauthenticated", ^{ + beforeEach(^{ + [[ARUserManager sharedManager] logout]; + }); + + it(@"doesn't inject an X-Auth-Token header in requests", ^{ + ARInternalMobileWebViewController *controller = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"http://example.com/"]]; + NSURLRequest *request = [controller requestWithURL:controller.currentURL]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).to.beNil(); + }); + + describe(@"shouldStartLoadWithRequest:navigationType", ^{ + __block ARInternalMobileWebViewController *controller; + + beforeEach(^{ + controller = [[ARInternalMobileWebViewController alloc] init]; + }); + + it(@"handles an internal link being clicked", ^{ + NSURLRequest *request = [controller requestWithURL:[NSURL URLWithString:@"/artwork/andy-warhol-skull"]]; + expect([controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeLinkClicked]).to.beFalsy(); + }); + + it(@"handles an external link being clicked (via a browser)", ^{ + NSURLRequest *request = [controller requestWithURL:[NSURL URLWithString:@"http://example.com"]]; + expect([controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeLinkClicked]).to.beFalsy(); + }); + + it(@"doesn't handle non-link requests", ^{ + NSURLRequest *request = [controller requestWithURL:[NSURL URLWithString:@"http://example.com"]]; + expect([controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeFormSubmitted]).to.beTruthy(); + }); + + it(@"doesn't handle a link with an non-http protocol", ^{ + NSURLRequest *request = [controller requestWithURL:[NSURL URLWithString:@"ftp://example.com"]]; + expect([controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeLinkClicked]).to.beTruthy(); + }); + + it(@"shows a trial login/signup view on a request to log_in", ^{ + NSURLRequest *request = [controller requestWithURL:[NSURL URLWithString:@"http://m.artsy.net/log_in"]]; + id mockUser = [OCMockObject mockForClass:[User class]]; + [[[mockUser stub] andReturnValue:OCMOCK_VALUE(YES)] isTrialUser]; + id mock = [OCMockObject partialMockForObject:[ARTrialController instance]]; + [[mock expect] presentTrialWithContext:ARTrialContextNotTrial fromTarget:[OCMArg any] selector:[OCMArg anySelector]]; + expect([controller webView:nil shouldStartLoadWithRequest:request navigationType:UIWebViewNavigationTypeOther]).to.beFalsy(); + [mock verify]; + [mockUser stopMocking]; + [mock stopMocking]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARLoginViewControllerTests.m b/Artsy Tests/ARLoginViewControllerTests.m new file mode 100644 index 00000000000..40a3b568c55 --- /dev/null +++ b/Artsy Tests/ARLoginViewControllerTests.m @@ -0,0 +1,276 @@ +#import "ARLoginViewController.h" +#import "ARUserManager.h" +#import "ARUserManager+Stubs.h" +#import "ARAuthProviders.h" + +@interface ARLoginViewController (Testing) + +@property (nonatomic, strong) NSString *email; +@property (nonatomic, strong) UITextField *emailTextField; +@property (nonatomic, strong) UITextField *passwordTextField; + +- (void)login:(id)sender; +- (void)fb:(id)sender; +- (void)twitter:(id)sender; +- (void)forgotPassword:(id)sender; +- (void)sendPasswordResetEmail:(NSString *)email; +- (void)textFieldDidChange:(UIView *)textView; +@end + +SpecBegin(ARLoginViewController) + +beforeEach(^{ + [[ARUserManager sharedManager] logout]; +}); + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +describe(@"login view controller", ^{ + __block ARLoginViewController *controller; + + describe(@"snapshots", ^{ + beforeEach(^{ + controller = [[ARLoginViewController alloc] init]; + controller.hideDefaultValues = YES; + }); + + itHasSnapshotsForDevices(@"blank form", ^{ + [controller ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return controller; + }); + + itHasSnapshotsForDevices(@"completed form", ^{ + [controller ar_presentWithFrame:[UIScreen mainScreen].bounds]; + controller.emailTextField.text = [ARUserManager stubUserEmail]; + controller.passwordTextField.text = [ARUserManager stubUserPassword]; + [controller textFieldDidChange:nil]; + + return controller; + }); + }); + + describe(@"initWithEmail", ^{ + beforeEach(^{ + controller = [[ARLoginViewController alloc] initWithEmail:[ARUserManager stubUserEmail]]; + [controller view]; // loads view and calls viewDidLoad + }); + + it(@"sets username text field to the value of the email", ^{ + expect(controller.email).to.equal([ARUserManager stubUserEmail]); + expect(controller.emailTextField.text).to.equal([ARUserManager stubUserEmail]); + }); + }); + + describe(@"login", ^{ + beforeEach(^{ + controller = [[ARLoginViewController alloc] init]; + [controller view]; // loads view and calls viewDidLoad + }); + + describe(@"with username and password", ^{ + it(@"succeeds", ^{ + [ARUserManager stubAccessToken:[ARUserManager stubAccessToken] expiresIn:[ARUserManager stubAccessTokenExpiresIn]]; + [ARUserManager stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + expect([ARUserManager sharedManager].currentUser).to.beNil(); + + controller.emailTextField.text = [ARUserManager stubUserEmail]; + controller.passwordTextField.text = [ARUserManager stubUserPassword]; + [controller login:nil]; + expect([ARUserManager sharedManager].currentUser).willNot.beNil(); + + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); + + it(@"fails and displays error", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/oauth2/access_token" withResponse:@{ @"error": @"invalid_client", @"error_description": @"missing client_id" } andStatusCode:401]; + + controller.emailTextField.text = [ARUserManager stubUserEmail]; + controller.passwordTextField.text = [ARUserManager stubUserPassword]; + + id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]]; + [[mockAlertView expect] setAlertViewStyle:UIAlertViewStyleDefault]; + [[[mockAlertView stub] andReturn:mockAlertView] alloc]; + + (void)[[[mockAlertView expect] andReturn:mockAlertView] + initWithTitle:@"Couldn’t Log In" + message:@"Please check your email and password" + delegate:OCMOCK_ANY + cancelButtonTitle:OCMOCK_ANY + otherButtonTitles:OCMOCK_ANY, nil]; + [[mockAlertView expect] show]; + [controller login:nil]; + [mockAlertView verifyWithDelay:1]; + [mockAlertView stopMocking]; + }); + }); + + describe(@"with facebook", ^{ + it(@"succeeds", ^{ + [ARUserManager stubAccessToken:[ARUserManager stubAccessToken] expiresIn:[ARUserManager stubAccessTokenExpiresIn]]; + [ARUserManager stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + expect([ARUserManager sharedManager].currentUser).to.beNil(); + + id authProviders = [OCMockObject mockForClass:[ARAuthProviders class]]; + [[[authProviders stub] andDo:^(NSInvocation *invocation) { + void(^successBlock)(NSString *token, NSString *email, NSString *name); + [invocation getArgument:&successBlock atIndex:2]; + successBlock(@"facebook token", [ARUserManager stubUserEmail], [ARUserManager stubUserName]); + }] getTokenForFacebook:OCMOCK_ANY failure:OCMOCK_ANY]; + + [controller fb:nil]; + + expect([ARUserManager sharedManager].currentUser).willNot.beNil(); + + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); + + it(@"fails and displays an error", ^{ + id authProviders = [OCMockObject mockForClass:[ARAuthProviders class]]; + [[[authProviders stub] andDo:^(NSInvocation *invocation) { + void(^failureBlock)(NSError *); + [invocation getArgument:&failureBlock atIndex:3]; + failureBlock([NSError errorWithDomain:@"error" code:500 userInfo:nil]); + }] getTokenForFacebook:OCMOCK_ANY failure:OCMOCK_ANY]; + + id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]]; + [[mockAlertView expect] setAlertViewStyle:UIAlertViewStyleDefault]; + [[[mockAlertView stub] andReturn:mockAlertView] alloc]; + (void)[[[mockAlertView expect] andReturn:mockAlertView] + initWithTitle:@"Couldn’t get Facebook credentials" + message:OCMOCK_ANY + delegate:OCMOCK_ANY + cancelButtonTitle:OCMOCK_ANY + otherButtonTitles:OCMOCK_ANY, nil]; + [[mockAlertView expect] show]; + [controller fb:nil]; + [mockAlertView verifyWithDelay:1]; + [mockAlertView stopMocking]; + }); + }); + + describe(@"twitter", ^{ + it(@"succeeds", ^{ + [ARUserManager stubAccessToken:[ARUserManager stubAccessToken] expiresIn:[ARUserManager stubAccessTokenExpiresIn]]; + [ARUserManager stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + + expect([ARUserManager sharedManager].currentUser).to.beNil(); + id authProviders = [OCMockObject mockForClass:[ARAuthProviders class]]; + [[[authProviders stub] andDo:^(NSInvocation *invocation) { + void(^successBlock)(NSString *token, NSString *secret); + [invocation getArgument:&successBlock atIndex:2]; + successBlock(@"twitter token", @"secret"); + }] getReverseAuthTokenForTwitter:OCMOCK_ANY failure:OCMOCK_ANY]; + + [controller twitter:nil]; + expect([ARUserManager sharedManager].currentUser).willNot.beNil(); + + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); + + it(@"fails and displays an error", ^{ + id authProviders = [OCMockObject mockForClass:[ARAuthProviders class]]; + [[[authProviders stub] andDo:^(NSInvocation *invocation) { + void(^failureBlock)(NSError *); + [invocation getArgument:&failureBlock atIndex:3]; + failureBlock([NSError errorWithDomain:@"error" code:500 userInfo:nil]); + }] getReverseAuthTokenForTwitter:OCMOCK_ANY failure:OCMOCK_ANY]; + + id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]]; + [[mockAlertView expect] setAlertViewStyle:UIAlertViewStyleDefault]; + [[[mockAlertView stub] andReturn:mockAlertView] alloc]; + (void)[[[mockAlertView expect] andReturn:mockAlertView] + initWithTitle:@"Couldn’t get Twitter credentials" + message:OCMOCK_ANY + delegate:OCMOCK_ANY + cancelButtonTitle:OCMOCK_ANY + otherButtonTitles:OCMOCK_ANY, nil]; + + [[mockAlertView expect] show]; + [controller twitter:nil]; + [mockAlertView verifyWithDelay:1]; + [mockAlertView stopMocking]; + }); + }); + }); + + describe(@"forgot password", ^{ + beforeEach(^{ + controller = [[ARLoginViewController alloc] init]; + [controller view]; // loads view and calls viewDidLoad + }); + + it(@"displays a password reset form", ^{ + id mockResetPasswordView = [OCMockObject mockForClass:[UIAlertView class]]; + [[[mockResetPasswordView stub] andReturn:mockResetPasswordView] alloc]; + (void)[[[mockResetPasswordView expect] andReturn:mockResetPasswordView] + initWithTitle:@"Forgot Password" + message:@"Please enter your email address and we’ll send you a reset link." + delegate:OCMOCK_ANY + cancelButtonTitle:@"Cancel" + otherButtonTitles:@"Send Link", nil]; + [[[mockResetPasswordView expect] ignoringNonObjectArgs] textFieldAtIndex:0]; + [[mockResetPasswordView expect] setAlertViewStyle:UIAlertViewStylePlainTextInput]; + [[mockResetPasswordView expect] setTapBlock:OCMOCK_ANY]; + [[mockResetPasswordView expect] show]; + + [controller forgotPassword:nil]; + [mockResetPasswordView verify]; + [mockResetPasswordView stopMocking]; + }); + + it(@"displays a confirmation after sending a password reset email", ^{ + [ARUserManager stubXappToken:[ARUserManager stubXappToken] expiresIn:[ARUserManager stubXappTokenExpiresIn]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/users/send_reset_password_instructions" withResponse:@{ @"status": @"success" } andStatusCode:201]; + + id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]]; + [[mockAlertView expect] setAlertViewStyle:UIAlertViewStyleDefault]; + [[[mockAlertView stub] andReturn:mockAlertView] alloc]; + (void)[[[mockAlertView expect] andReturn:mockAlertView] + initWithTitle:@"Please Check Your Email" + message:OCMOCK_ANY + delegate:OCMOCK_ANY + cancelButtonTitle:OCMOCK_ANY + otherButtonTitles:OCMOCK_ANY, nil]; + [[mockAlertView expect] show]; + + [controller sendPasswordResetEmail:@"foo@example.com"]; + + [mockAlertView verifyWithDelay:1]; + [mockAlertView stopMocking]; + }); + + it(@"displays an error on failure to send a reset password email", ^{ + [ARUserManager stubXappToken:[ARUserManager stubXappToken] expiresIn:[ARUserManager stubXappTokenExpiresIn]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/users/send_reset_password_instructions" withResponse:@{ @"error": @"foobar" } andStatusCode:400]; + + id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]]; + [[mockAlertView expect] setAlertViewStyle:UIAlertViewStyleDefault]; + [[[mockAlertView stub] andReturn:mockAlertView] alloc]; + (void)[[[mockAlertView expect] andReturn:mockAlertView] + initWithTitle:@"Couldn’t Reset Password" + message:OCMOCK_ANY + delegate:OCMOCK_ANY + cancelButtonTitle:OCMOCK_ANY + otherButtonTitles:OCMOCK_ANY, nil]; + [[mockAlertView expect] show]; + + [controller sendPasswordResetEmail:@"foo@example.com"]; + + [mockAlertView verifyWithDelay:1]; + [mockAlertView stopMocking]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARMessageItemProviderTests.m b/Artsy Tests/ARMessageItemProviderTests.m new file mode 100644 index 00000000000..1acf7bb3357 --- /dev/null +++ b/Artsy Tests/ARMessageItemProviderTests.m @@ -0,0 +1,78 @@ +#import "ARMessageItemProvider.h" +#import "ARNetworkConstants.h" +#import "ARRouter.h" + +@interface ARMessageItemProvider (Testing) +@property (nonatomic, strong, readonly) NSString *path; +@property (nonatomic, strong, readonly) NSString *message; +@property (nonatomic, strong, readonly) NSURL *url; +@end + + +SpecBegin(ARMessageItemProvider) + +describe(@"message provider", ^{ + __block ARMessageItemProvider *provider; + __block NSString *placeHolderMessage = @"So And So"; + __block NSString *path = @"artist/so-and-so"; + __block UIActivityViewController *activityVC = [[UIActivityViewController alloc] init]; + + describe(@"provider", ^{ + beforeEach(^{ + provider = [[ARMessageItemProvider alloc] initWithMessage:placeHolderMessage path:path]; + }); + + it(@"sets the placeholderItem", ^{ + expect(provider.placeholderItem).to.equal(@"So And So"); + }); + + it(@"sets the path", ^{ + expect(provider.path).to.equal(path); + }); + + it(@"adds ' on Artsy' to the message", ^{ + expect(provider.message).to.equal(@"So And So on Artsy"); + }); + + describe(@"subjectForActivityType", ^{ + it(@"returns message for Mail", ^{ + NSString *subject = [provider activityViewController:activityVC subjectForActivityType:UIActivityTypeMail]; + expect(subject).to.equal(@"So And So on Artsy"); + }); + + it(@"returns nil for other activities", ^{ + NSString *subject = [provider activityViewController:activityVC subjectForActivityType:@"another activity"]; + expect(subject).to.beNil(); + }); + }); + }); + + describe(@"item", ^{ + __block id providerMock; + + before(^{ + provider = [[ARMessageItemProvider alloc] initWithMessage:placeHolderMessage path:path]; + providerMock = [OCMockObject partialMockForObject:provider]; + }); + + it(@"adds Twitter handle for Twitter", ^{ + [[[providerMock stub] andReturn:UIActivityTypePostToTwitter] activityType]; + expect([provider item]).to.equal(@"So And So on @Artsy"); + }); + + it(@"formats HTML for Mail", ^{ + [[[providerMock stub] andReturn:UIActivityTypeMail] activityType]; + NSString *email = [NSString stringWithFormat: @"%@", + [ARRouter baseWebURL].absoluteString, path, @"So And So on Artsy"]; + expect([provider item]).to.equal(email); + }); + + it(@"adds ' on Artsy: for other activities", ^{ + [[[providerMock stub] andReturn:@"another activity"] activityType]; + expect([provider item]).to.equal(@"So And So on Artsy"); + }); + }); +}); + +SpecEnd + diff --git a/Artsy Tests/ARNavigationButtonTests.m b/Artsy Tests/ARNavigationButtonTests.m new file mode 100644 index 00000000000..68ac337020b --- /dev/null +++ b/Artsy Tests/ARNavigationButtonTests.m @@ -0,0 +1,78 @@ +#import "ARNavigationButton.h" + +SpecBegin(ARNavigationButtonSpec) + +__block ARNavigationButton * _view; +CGRect frame = CGRectMake(0, 0, 280, 60); + +describe(@"ARNavigationButton", ^{ + describe(@"default border", ^{ + beforeEach(^{ + _view = [[ARNavigationButton alloc] initWithFrame:frame]; + }); + + it(@"title", ^{ + _view.title = @"Hello World"; + expect(_view).to.haveValidSnapshotNamed(@"navigationButtonWithTitle"); + }); + + it(@"title and subtitle", ^{ + _view.title = @"Title"; + _view.subtitle = @"Subtitle"; + expect(_view).to.haveValidSnapshotNamed(@"navigationButtonWithTitleAndSubtitle"); + }); + }); + + describe(@"thick border", ^{ + beforeEach(^{ + _view = [[ARNavigationButton alloc] initWithFrame:frame withBorder:5]; + }); + + it(@"title", ^{ + _view.title = @"Hello World"; + expect(_view).to.haveValidSnapshotNamed(@"navigationButtonWithTitleAnd5pxBorder"); + }); + + it(@"title and subtitle", ^{ + _view.title = @"Title"; + _view.subtitle = @"Subtitle"; + expect(_view).to.haveValidSnapshotNamed(@"navigationButtonWithTitleAndSubtitleAnd5pxBorder"); + }); + }); + + describe(@"no border", ^{ + beforeEach(^{ + _view = [[ARNavigationButton alloc] initWithFrame:frame withBorder:0]; + }); + + it(@"title", ^{ + _view.title = @"Hello World"; + expect(_view).to.haveValidSnapshotNamed(@"navigationButtonWithTitleAndNoBorder"); + }); + + it(@"title and subtitle", ^{ + _view.title = @"Title"; + _view.subtitle = @"Subtitle"; + expect(_view).to.haveValidSnapshotNamed(@"navigationButtonWithTitleAndSubtitleAndNoBorder"); + }); + }); +}); + +describe(@"ARSerifNavigationButton", ^{ + beforeEach(^{ + _view = [[ARSerifNavigationButton alloc] initWithFrame:frame]; + }); + + it(@"title", ^{ + _view.title = @"Hello World"; + expect(_view).to.haveValidSnapshotNamed(@"serifNavigationButtonWithTitle"); + }); + + it(@"title and subtitle", ^{ + _view.title = @"Title"; + _view.subtitle = @"Subtitle"; + expect(_view).to.haveValidSnapshotNamed(@"serifNavigationButtonWithTitleAndSubtitle"); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARNavigationButtonsViewControllerTests.m b/Artsy Tests/ARNavigationButtonsViewControllerTests.m new file mode 100644 index 00000000000..e66880e3004 --- /dev/null +++ b/Artsy Tests/ARNavigationButtonsViewControllerTests.m @@ -0,0 +1,118 @@ +#import "ARNavigationButtonsViewController.h" +#import "ARNavigationButton.h" +#import "ARButtonWithImage.h" + +SpecBegin(ARNavigationButtonsViewController) + +describe(@"init", ^{ + __block ARNavigationButtonsViewController *vc; + + it(@"should set the button descriptions", ^{ + vc = [[ARNavigationButtonsViewController alloc] initWithButtonDescriptions:@[]]; + + expect(vc.buttonDescriptions).to.equal(@[]); + }); + + it(@"should create buttons based on the descriptions", ^{ + NSArray *buttonDescriptions = @[ + @{ + ARNavigationButtonPropertiesKey: @{ + @"title": @"foo" + } + }, + @{ + ARNavigationButtonPropertiesKey: @{ + @"title": @"bar" + } + } + ]; + + vc = [[ARNavigationButtonsViewController alloc] initWithButtonDescriptions:buttonDescriptions]; + + expect(vc.navigationButtons).to.haveCountOf(2); + }); + + it(@"should call block when tapped", ^{ + __block BOOL passed = NO; + NSArray *buttonDescriptions = @[ + @{ + ARNavigationButtonPropertiesKey: @{ + @"title": @"foo" + }, + ARNavigationButtonHandlerKey: ^(UIButton *button) { + passed = YES; + } + } + ]; + + vc = [[ARNavigationButtonsViewController alloc] initWithButtonDescriptions:buttonDescriptions]; + UIButton *firstButton = vc.navigationButtons.firstObject; + [firstButton sendActionsForControlEvents:UIControlEventTouchUpInside]; + + expect(passed).to.beTruthy(); + }); +}); + +describe(@"creating buttons", ^{ + __block ARNavigationButtonsViewController *vc; + + beforeEach(^{ + NSArray *buttonDescriptions = @[ + @{ + ARNavigationButtonPropertiesKey: @{ + @"backgroundColor": UIColor.redColor + } + }, + @{ + ARNavigationButtonClassKey: ARButtonWithImage.class, + ARNavigationButtonPropertiesKey: @{ + @"title": NSNull.null, + } + } + ]; + + vc = [[ARNavigationButtonsViewController alloc] init]; + + vc.buttonDescriptions = buttonDescriptions; + }); + + it(@"should default to ARNavigationButton as the button class", ^{ + UIButton *firstButton = vc.navigationButtons[0]; + + expect(firstButton).to.beKindOf(ARNavigationButton.class); + }); + + it(@"should allow different button classes", ^{ + UIButton *secondButton = vc.navigationButtons[1]; + + expect(secondButton).to.beKindOf(ARButtonWithImage.class); + }); + + it(@"should set properties of the buttons", ^{ + UIButton *firstButton = vc.navigationButtons.firstObject; + + expect(firstButton.backgroundColor).to.equal(UIColor.redColor); + }); + + it(@"should treat NSNull as nil", ^{ + ARButtonWithImage *secondButton = vc.navigationButtons[1]; + + expect(secondButton.title).to.beNil(); + }); + + it(@"should allow updates", ^{ + vc.buttonDescriptions = @[]; + + expect(vc.navigationButtons).to.beEmpty(); + + vc.buttonDescriptions = @[ + @{ + ARNavigationButtonClassKey: ARButtonWithImage.class + } + ]; + + expect(vc.navigationButtons).to.haveCountOf(1); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARNavigationControllerTests.m b/Artsy Tests/ARNavigationControllerTests.m new file mode 100644 index 00000000000..df3422a8f13 --- /dev/null +++ b/Artsy Tests/ARNavigationControllerTests.m @@ -0,0 +1,95 @@ +#import "ARNavigationController.h" +#import "UIViewController+SimpleChildren.h" +#import "ARPendingOperationViewController.h" + + +@interface ARNavigationController (Testing) + +@property (nonatomic, strong) ARPendingOperationViewController *pendingOperationViewController; + +@end + +SpecBegin(ARNavigationController) + +__block ARNavigationController *navigationController; + + +describe(@"visuals", ^{ + it(@"should be empty for a rootVC", ^{ + UIViewController *viewController = [[UIViewController alloc] init]; + navigationController = [[ARNavigationController alloc] initWithRootViewController:viewController]; + expect(navigationController).to.haveValidSnapshot(); + }); + + it(@"should be show a back button with 2 view controllers", ^{ + UIViewController *viewController = [[UIViewController alloc] init]; + UIViewController *viewController2 = [[UIViewController alloc] init]; + navigationController = [[ARNavigationController alloc] initWithRootViewController:viewController]; + [navigationController pushViewController:viewController2 animated:NO]; + expect(navigationController).to.haveValidSnapshot(); + }); +}); + +describe(@"presenting pending operation layover", ^{ + __block ARNavigationController *navigationController; + + before(^{ + UIViewController *viewController = [[UIViewController alloc] init]; + navigationController = [[ARNavigationController alloc] initWithRootViewController:viewController]; + }); + + it(@"should animate layover transitions", ^{ + expect(navigationController.animatesLayoverChanges).to.beTruthy(); + }); + + it(@"should call through to nil when the message isn't included", ^{ + id mock = [OCMockObject partialMockForObject:navigationController]; + [[mock expect] presentPendingOperationLayoverWithMessage:[OCMArg isNil]]; + + [mock presentPendingOperationLayover]; + + [mock verify]; + }); + + describe(@"without animations", ^{ + beforeEach(^{ + navigationController.animatesLayoverChanges = NO; + }); + + it(@"should present the new view controller", ^{ + id mock = [OCMockObject partialMockForObject:navigationController]; + [[mock expect] ar_addModernChildViewController:[OCMArg checkForClass:[ARPendingOperationViewController class]]]; + + [mock presentPendingOperationLayover]; + + [mock verify]; + }); + + it(@"should dismiss the view controller once the command's execution is completed", ^{ + id mock = [OCMockObject partialMockForObject:navigationController]; + [[mock expect] ar_removeChildViewController:[OCMArg checkForClass:[ARPendingOperationViewController class]]]; + + RACCommand *command = [mock presentPendingOperationLayover]; + [command execute:nil]; + + [mock verify]; + }); + + describe(@"with a message", ^{ + it(@"should present the new view controller", ^{ + NSString *message = @"Hello fine sir or madam"; + + id mock = [OCMockObject partialMockForObject:navigationController]; + [[mock expect] ar_addModernChildViewController:[OCMArg checkWithBlock:^BOOL(ARPendingOperationViewController *obj) { + return [obj.message isEqualToString:message]; + }]]; + + [mock presentPendingOperationLayoverWithMessage:message]; + + [mock verify]; + }); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/AROnboardingViewControllerTests.m b/Artsy Tests/AROnboardingViewControllerTests.m new file mode 100644 index 00000000000..d42243b3cbb --- /dev/null +++ b/Artsy Tests/AROnboardingViewControllerTests.m @@ -0,0 +1,48 @@ +#import "AROnboardingViewController.h" + +@interface AROnboardingViewController (Test) + +- (void)presentCollectorLevel; +- (void)presentWebOnboarding; + +@end + +SpecBegin(AROnboardingViewController) + +__block OCMockObject *mock; +__block AROnboardingViewController *vc; + +describe(@"signup splash", ^{ + + + describe(@"signupDone", ^{ + + before(^{ + vc = [[AROnboardingViewController alloc] init]; + mock = [OCMockObject partialMockForObject:vc]; + }); + + after(^{ + [mock stopMocking]; + [ARTestContext stopStubbing]; + }); + + it(@"ipad", ^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + [[mock reject] presentCollectorLevel]; + [[mock expect] presentWebOnboarding]; + [vc signupDone]; + [mock verify]; + }); + + it (@"not ipad", ^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + [[mock reject] presentWebOnboarding]; + [[mock expect] presentCollectorLevel]; + [vc signupDone]; + [mock verify]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARParallaxHeaderViewControllerTests.m b/Artsy Tests/ARParallaxHeaderViewControllerTests.m new file mode 100644 index 00000000000..c77d0ca4ba8 --- /dev/null +++ b/Artsy Tests/ARParallaxHeaderViewControllerTests.m @@ -0,0 +1,66 @@ +#import "ARParallaxHeaderViewController.h" +#import "Fair.h" +#import "Profile.h" +#import "MTLModel+JSON.h" +#import +#import + +SpecBegin(ARParallaxHeaderViewController) + +describe(@"with a full fair and profile", ^{ + __block ARParallaxHeaderViewController *viewController; + + beforeEach(^{ + [[SDImageCache sharedImageCache] clearDisk]; + [[SDImageCache sharedImageCache] clearMemory]; + + Fair *fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"image_url" : @"http://static1.artsy.net/fairs/52617c6c8b3b81f094000013/9/:version.jpg", + @"image_versions" : @[ + @"wide" + ], + @"name" : @"The Ash Show", + @"start_at" : @"2014-03-06T17:00:00.000+00:00", + @"end_at" : @"2014-03-09T22:00:00.000+00:00", + @"location" : @{ + @"city" : @"Toronto", + @"state" : @"ON" + } + }]; + + Profile *profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"default_icon_version" : @"square", + @"icon" : @{ + @"image_url" : @"http://static1.artsy.net/profile_icons/530cc50c9c18dbab9a00005b/:version.jpg", + @"image_versions" : @[ + @"square" + ] + } + }]; + + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSString *wideImagePath = [bundle pathForResource:@"wide" ofType:@"jpg"]; + UIImage *wideImage = [UIImage imageWithContentsOfFile:wideImagePath]; + NSString *squareImagePath = [bundle pathForResource:@"square" ofType:@"png"]; + UIImage *squareImage = [UIImage imageWithContentsOfFile:squareImagePath]; + + [[SDImageCache sharedImageCache] storeImage:wideImage forKey:@"http://static1.artsy.net/fairs/52617c6c8b3b81f094000013/9/wide.jpg" toDisk:NO]; + [[SDImageCache sharedImageCache] storeImage:squareImage forKey:@"http://static1.artsy.net/profile_icons/530cc50c9c18dbab9a00005b/square.png" toDisk:NO]; + + viewController = [[ARParallaxHeaderViewController alloc] initWithContainingScrollView:nil fair:fair profile:profile]; + }); + + afterEach(^{ + [OHHTTPStubs removeAllStubs]; + [[SDImageCache sharedImageCache] clearMemory]; + }); + + it(@"has a valid snapshot", ^{ + expect(viewController.view).toNot.beNil(); + expect(viewController.view).to.haveValidSnapshot(); + }); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARPendingOperationViewControllerTests.m b/Artsy Tests/ARPendingOperationViewControllerTests.m new file mode 100644 index 00000000000..7e0f3cfc664 --- /dev/null +++ b/Artsy Tests/ARPendingOperationViewControllerTests.m @@ -0,0 +1,37 @@ +#import "ARPendingOperationViewController.h" +#import "ARSpinner.h" + +@interface ARPendingOperationViewController (Testing) + +@property (nonatomic, strong) UILabel *label; +@property (nonatomic, strong) ARSpinner *spinner; + +@end + +SpecBegin(ARPendingOperationViewController) + +it(@"has a valid snapshot", ^{ + ARPendingOperationViewController *viewController = [[ARPendingOperationViewController alloc] init]; + viewController.view.frame = [[UIScreen mainScreen] bounds]; + [viewController.spinner.layer removeAllAnimations]; + + expect(viewController).to.haveValidSnapshot(); +}); + +it(@"has a default message", ^{ + ARPendingOperationViewController *viewController = [[ARPendingOperationViewController alloc] init]; + expect(viewController.message).to.equal(@"locating..."); +}); + +it(@"binds message to the label's text", ^{ + NSString *message = @"Hail Cthulhu"; + + ARPendingOperationViewController *viewController = [[ARPendingOperationViewController alloc] init]; + viewController.message = message; + + // load view + expect(viewController.view).notTo.beNil(); + expect(viewController.label.text).to.equal(message); +}); + +SpecEnd diff --git a/Artsy Tests/ARPostFeedItemLinkViewTests.m b/Artsy Tests/ARPostFeedItemLinkViewTests.m new file mode 100644 index 00000000000..3bdf2adefc8 --- /dev/null +++ b/Artsy Tests/ARPostFeedItemLinkViewTests.m @@ -0,0 +1,21 @@ +#import "ARPostFeedItemLinkView.h" +#import "ARPostFeedItem.h" + +SpecBegin(ARPostFeedItemLinkView) + +__block ARPostFeedItemLinkView *view = nil; +__block ARPostFeedItem *postFeedItem = nil; + +beforeEach(^{ + view = [[ARPostFeedItemLinkView alloc] init]; + postFeedItem = [ARPostFeedItem modelWithJSON:@{ + @"id" : @"post_id" + }]; + [view updateWithPostFeedItem:postFeedItem]; +}); + +it(@"targetURL", ^{ + expect([view targetPath]).to.equal(@"/post/post_id"); +}); + +SpecEnd diff --git a/Artsy Tests/ARProfileViewControllerTests.m b/Artsy Tests/ARProfileViewControllerTests.m new file mode 100644 index 00000000000..8305f2d0e3a --- /dev/null +++ b/Artsy Tests/ARProfileViewControllerTests.m @@ -0,0 +1,280 @@ +#import "ARProfileViewController.h" +#import "ArtsyAPI.h" +#import +#import "ARAnalyticsConstants.h" +#import "UIViewController+SimpleChildren.h" +#import "UIView+FLKAutoLayout.h" +#import "ARFairViewController.h" +#import "ARInternalMobileWebViewController.h" + +@interface ARProfileViewControllerTestsConcreteLayoutGuide : NSObject + +@property (nonatomic, assign) CGFloat length; + +@end + +@implementation ARProfileViewControllerTestsConcreteLayoutGuide + +@end + +@interface ARProfileViewController (Private) + +- (void)loadProfile; +- (void)loadMartsyView; +- (void)showViewController:(UIViewController *)viewController; + +@end + +SpecBegin(ARProfileViewController) + +NSString *profileID = @"id"; + +describe(@"initializer", ^{ + it(@"initializes with the correct profile ID", ^{ + ARProfileViewController *viewController = [[ARProfileViewController alloc] initWithProfileID:profileID]; + + expect(viewController.profileID).to.equal(profileID); + }); +}); + +describe(@"viewDidLoad", ^{ + __block ARProfileViewController *viewController; + + beforeEach(^{ + viewController = [[ARProfileViewController alloc] initWithProfileID:profileID]; + }); + + it(@"Sets up viewDidAppear: to call loadProfile", ^{ + id mockViewController = [OCMockObject partialMockForObject:viewController]; + [[mockViewController expect] loadProfile]; + [viewController viewDidLoad]; + [viewController viewWillAppear:NO]; + [mockViewController verify]; + }); + + it(@"Only calls loadProfile the first time viewDidAppear: is called", ^{ + id mockViewController = [OCMockObject partialMockForObject:viewController]; + [[mockViewController expect] loadProfile]; + [viewController viewDidLoad]; + [viewController viewWillAppear:NO]; + [viewController viewWillAppear:NO]; + [mockViewController verify]; + }); +}); + +describe(@"loadProfile", ^{ + __block ARProfileViewController *viewController; + + beforeEach(^{ + viewController = [[ARProfileViewController alloc] initWithProfileID:profileID]; + }); + + it(@"calls getProfileForProfileID:", ^{ + id apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + + [[apiMock expect] getProfileForProfileID:profileID success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [viewController loadProfile]; + + [apiMock verify]; + [apiMock stopMocking]; + }); + + it(@"loads martsy view on failure", ^{ + id apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + + id viewControllerMock = [OCMockObject partialMockForObject:viewController]; + [[viewControllerMock expect] loadMartsyView]; + + [[apiMock expect] getProfileForProfileID:profileID success:OCMOCK_ANY failure:[OCMArg checkWithBlock:^BOOL(void (^obj)(NSError *error)) { + if (obj) { + obj(nil); + } + return YES; + }]]; + + [viewController loadProfile]; + + [viewControllerMock verify]; + + [apiMock verify]; + [apiMock stopMocking]; + }); + + context(@"fair", ^{ + __block id apiMock; + __block id viewControllerMock; + + before(^{ + apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + viewControllerMock = [OCMockObject partialMockForObject:viewController]; + }); + + it(@"loads a fairvc on iphone with a fair organizer", ^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + [[viewControllerMock expect] showViewController:[OCMArg checkForClass:[ARFairViewController class]]]; + [[viewControllerMock reject] loadMartsyView]; + [[apiMock expect] getProfileForProfileID:profileID success:[OCMArg checkWithBlock:^BOOL(void (^obj)(Profile *profile)) { + Profile *profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"FairOrganizer", + @"owner" : @{ + @"id" : @"user-id", + @"default_fair_id" : @"default-fair-id" + } + }]; + + if (obj) { + obj(profile); + } + return YES; + }] failure:OCMOCK_ANY]; + + [viewController loadProfile]; + [viewControllerMock verify]; + + [apiMock verify]; + [apiMock stopMocking]; + [ARTestContext stopStubbing]; + }); + + it(@"loads a fairvc on iphone with a fair", ^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + [[viewControllerMock expect] showViewController:[OCMArg checkForClass:[ARFairViewController class]]]; + [[viewControllerMock reject] loadMartsyView]; + [[apiMock expect] getProfileForProfileID:profileID success:[OCMArg checkWithBlock:^BOOL(void (^obj)(Profile *profile)) { + Profile *profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"Fair", + @"owner" : @{ + @"id" : @"fair-id", + } + }]; + + if (obj) { + obj(profile); + } + return YES; + }] failure:OCMOCK_ANY]; + + [viewController loadProfile]; + [viewControllerMock verify]; + + [apiMock verify]; + [apiMock stopMocking]; + [ARTestContext stopStubbing]; + }); + + it(@"loads martsy on ipad", ^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + [[viewControllerMock reject] showViewController:[OCMArg checkForClass:[ARFairViewController class]]]; + [[viewControllerMock expect] loadMartsyView]; + [[apiMock expect] getProfileForProfileID:profileID success:[OCMArg checkWithBlock:^BOOL(void (^obj)(Profile *profile)) { + Profile *profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"FairOrganizer", + @"owner" : @{ + @"id" : @"user-id", + @"default_fair_id" : @"default-fair-id" + } + }]; + + if (obj) { + obj(profile); + } + return YES; + }] failure:OCMOCK_ANY]; + + [viewController loadProfile]; + [viewControllerMock verify]; + + [apiMock verify]; + [apiMock stopMocking]; + [ARTestContext stopStubbing]; + }); + }); + + + it(@"loads martsy when a profile's owner is anything but a fair or organizer", ^{ + id apiMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + + id viewControllerMock = [OCMockObject partialMockForObject:viewController]; + [[viewControllerMock expect] loadMartsyView]; + + [[apiMock expect] getProfileForProfileID:profileID success:[OCMArg checkWithBlock:^BOOL(void (^obj)(Profile *profile)) { + Profile *profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"User", + @"owner" : @{ + @"id" : @"user-id", + @"type" : @"User" + } + }]; + + if (obj) { + obj(profile); + } + return YES; + }] failure:OCMOCK_ANY]; + + [viewController loadProfile]; + + [viewControllerMock verify]; + + [apiMock verify]; + [apiMock stopMocking]; + }); +}); + +describe(@"loadMartsyView", ^{ + __block ARProfileViewController *viewController; + + beforeEach(^{ + viewController = [[ARProfileViewController alloc] initWithProfileID:profileID]; + }); + + it(@"creates and shows an internal mobile web browser", ^{ + id viewControllerMock = [OCMockObject partialMockForObject:viewController]; + [[viewControllerMock expect] showViewController:[OCMArg checkForClass:[ARInternalMobileWebViewController class]]]; + + [viewController loadMartsyView]; + + [viewControllerMock verify]; + }); +}); + +describe(@"showViewController:", ^{ + __block ARProfileViewController *viewController; + + beforeEach(^{ + viewController = [[ARProfileViewController alloc] initWithProfileID:profileID]; + }); + + it(@"adds as a child view controller", ^{ + id viewControllerParameter = [[UIViewController alloc] init]; + + id viewControllerMock = [OCMockObject partialMockForObject:viewController]; + + [[viewControllerMock expect] ar_addModernChildViewController:viewControllerParameter]; + + [viewController showViewController:viewControllerParameter]; + [viewControllerMock verify]; + }); + + it(@"Creates an alignment based on the top layout guide", ^{ + ARProfileViewControllerTestsConcreteLayoutGuide *layoutGuide = [[ARProfileViewControllerTestsConcreteLayoutGuide alloc] init]; + layoutGuide.length = 1337.0f; + + id viewMock = [OCMockObject mockForClass:[UIView class]]; + [[viewMock expect] alignTop:[NSString stringWithFormat:@"%f", layoutGuide.length] + leading:@"0" + bottom:@"0" + trailing:@"0" + toView:OCMOCK_ANY]; + + id viewControllerMock = [OCMockObject partialMockForObject:viewController]; + [[[viewControllerMock stub] andReturn:layoutGuide] topLayoutGuide]; + }); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARRouterTests.m b/Artsy Tests/ARRouterTests.m new file mode 100644 index 00000000000..d35b9308d9e --- /dev/null +++ b/Artsy Tests/ARRouterTests.m @@ -0,0 +1,169 @@ +#import "ARRouter.h" +#import "AROptions.h" +#import "ARNetworkConstants.h" + +SpecBegin(ARRouter) + +describe(@"requestForURL", ^{ + describe(@"with auth token", ^{ + beforeEach(^{ + [ARRouter setAuthToken:@"token"]; + }); + + afterEach(^{ + [ARRouter setAuthToken:nil]; + }); + + it(@"sets router auth token for Artsy URLs", ^{ + NSURLRequest *request = [ARRouter requestForURL:[NSURL URLWithString:@"http://m.artsy.net"]]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).to.equal(@"token"); + }); + + it(@"doesn't set auth token for external URLs", ^{ + NSURLRequest *request = [ARRouter requestForURL:[NSURL URLWithString:@"http://example.com"]]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).to.beNil(); + }); + }); + + describe(@"with xapp token", ^{ + beforeEach(^{ + [ARRouter setXappToken:@"token"]; + }); + + afterEach(^{ + [ARRouter setXappToken:nil]; + }); + + it(@"sets router xapp token for Artsy URLs", ^{ + NSURLRequest *request = [ARRouter requestForURL:[NSURL URLWithString:@"http://m.artsy.net"]]; + expect([request valueForHTTPHeaderField:ARXappHeader]).to.equal(@"token"); + }); + + it(@"doesn't set xapp token for external URLs", ^{ + NSURLRequest *request = [ARRouter requestForURL:[NSURL URLWithString:@"http://example.com"]]; + expect([request valueForHTTPHeaderField:ARXappHeader]).to.beNil(); + }); + }); +}); + +describe(@"isInternalURL", ^{ + it(@"returns true with a touch link", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"applewebdata://internal"]; + expect([ARRouter isInternalURL:url]).to.beTruthy(); + }); + + it(@"returns true with an artsy link", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"artsy://internal"]; + expect([ARRouter isInternalURL:url]).to.beTruthy(); + }); + + it(@"returns true with an artsy www", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"http://www.artsy.net/thing"]; + expect([ARRouter isInternalURL:url]).to.beTruthy(); + }); + + it(@"returns true for any artsy url", ^{ + NSSet *artsyHosts = [ARRouter artsyHosts]; + for (NSString *host in artsyHosts){ + NSURL *url = [[NSURL alloc] initWithString:NSStringWithFormat(@"%@/some/path", host)]; + expect([ARRouter isInternalURL:url]).to.beTruthy(); + } + }); + + it(@"returns false for external urls", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"http://externalurl.com/path"]; + expect([ARRouter isInternalURL:url]).to.beFalsy(); + }); + + it(@"returns true for relative urls", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"/relative/url"]; + expect([ARRouter isInternalURL:url]).to.beTruthy(); + }); +}); + +describe(@"isWebURL", ^{ + it(@"returns true with a http link", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"http://internal"]; + expect([ARRouter isWebURL:url]).to.beTruthy(); + }); + + it(@"returns true with a link without a scheme", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"internal"]; + expect([ARRouter isWebURL:url]).to.beTruthy(); + }); + + it(@"returns true with a https link", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"https://internal"]; + expect([ARRouter isWebURL:url]).to.beTruthy(); + }); + + it(@"returns false for mailto: urls", ^{ + NSURL *url = [[NSURL alloc] initWithString:@"mailto:orta.therox@gmail.com"]; + expect([ARRouter isWebURL:url]).to.beFalsy(); + }); +}); + +describe(@"User-Agent", ^{ + __block NSString *userAgent = [[NSUserDefaults standardUserDefaults] valueForKey:@"UserAgent"]; + + it(@"uses Artsy-Mobile hard-coded in Microgravity", ^{ + expect(userAgent).to.beginWith(@"Artsy-Mobile/"); + }); + + it(@"uses Eigen", ^{ + expect(userAgent).to.contain(@"Eigen/"); + }); + + it(@"contains version number", ^{ + expect(userAgent).to.contain([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]); + }); + + it(@"contains build number", ^{ + expect(userAgent).to.contain([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]); + }); + + it(@"preserves simulator information", ^{ + expect(userAgent).to.contain(@"iPhone Simulator"); + }); + + it(@"is contained in requests sent out from router", ^{ + + // Some random requests from the router + OCMockObject *userMock = [OCMockObject mockForClass:[User class]]; + [[[[userMock stub] classMethod] andReturnValue:OCMOCK_VALUE(YES)] isTrialUser]; + + Artwork *artwork = [Artwork modelWithJSON:@{ @"id": @"artwork_id" }]; + NSURLRequest *request = [ARRouter newArtworkInquiryRequestForArtwork:artwork name:@"name" email:@"email.com" message:@"message" analyticsDictionary:@{} shouldContactGallery:NO]; + + expect([request.allHTTPHeaderFields objectForKey:@"User-Agent"]).to.beTruthy(); + expect(request.allHTTPHeaderFields[@"User-Agent"]).to.equal(userAgent); + + request = [ARRouter newOnDutyRepresentativeRequest]; + expect(request.allHTTPHeaderFields[@"User-Agent"]).to.equal(userAgent); + + request = [ARRouter newGenesFromPersonalCollectionAtPage:0]; + expect(request.allHTTPHeaderFields[@"User-Agent"]).to.equal(userAgent); + + request = [ARRouter newShowsRequestForArtist:@"orta"]; + expect(request.allHTTPHeaderFields[@"User-Agent"]).to.equal(userAgent); + }); +}); + +describe(@"baseWebURL", ^{ + beforeEach(^{ + [AROptions setBool:false forOption:ARUseStagingDefault]; + [ARRouter setup]; + }); + + it(@"points to artsy mobile on iphone", ^{ + expect([ARRouter baseWebURL]).to.equal([NSURL URLWithString:@"https://m.artsy.net/"]); + }); + + it(@"points to artsy web on ipad", ^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + expect([ARRouter baseWebURL]).to.equal([NSURL URLWithString:@"https://artsy.net/"]); + [ARTestContext stopStubbing]; + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARSearchFieldButtonTests.m b/Artsy Tests/ARSearchFieldButtonTests.m new file mode 100644 index 00000000000..48d59accbd2 --- /dev/null +++ b/Artsy Tests/ARSearchFieldButtonTests.m @@ -0,0 +1,10 @@ +#import "ARSearchFieldButton.h" + +SpecBegin(ARSearchFieldButton) + +it(@"has a valid snapshot", ^{ + ARSearchFieldButton *button = [[ARSearchFieldButton alloc] initWithFrame:CGRectMake(0, 0, 280, 44)]; + expect(button).to.haveValidSnapshot(); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARSearchViewControllerSpec.m b/Artsy Tests/ARSearchViewControllerSpec.m new file mode 100644 index 00000000000..5de6e0a1dc5 --- /dev/null +++ b/Artsy Tests/ARSearchViewControllerSpec.m @@ -0,0 +1,99 @@ +#import "ARSearchViewController.h" + +@interface ARSearchViewController(Testing) +@property (readwrite, nonatomic) BOOL shouldAnimate; +- (void)presentResultsViewAnimated:(BOOL)animated; +@end + +SpecBegin(ARSearchViewController) + +__block ARSearchViewController *sut; +__block id sutMock; + +context(@"add results", ^{ + before(^{ + sut = [[ARSearchViewController alloc] init]; + sut.shouldAnimate = NO; + sutMock = [OCMockObject partialMockForObject:sut]; + [[[sutMock expect] ignoringNonObjectArgs] presentResultsViewAnimated:NO]; + }); + + after(^{ + [sutMock verify]; + }); + + it(@"replaces results", ^{ + sut.searchDataSource.searchResults = [NSOrderedSet orderedSetWithObjects:[SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"f-scott-hess", + @"display": @"F. Scott Hess", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"American, born 1955", + @"published": @(YES), + @"highlights": @[] + }], [SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"john-f-carlson", + @"display": @"John F. Carlson", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"Swedish-American, 1875-1947", + @"published": @(YES), + @"highlights": @[] + }], nil]; + + expect(sut.searchResults.count).to.equal(2); + + [sut addResults:@[[SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"aes-plus-f", + @"display": @"AES+F", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"Russian, Founded 1987", + @"published": @(YES), + @"highlights": @[] + }]] replace:YES]; + expect(sut.searchResults.count).to.equal(1); + }); + + it(@"adds to results", ^{ + sut.searchDataSource.searchResults = [NSOrderedSet orderedSetWithObjects:[SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"f-scott-hess", + @"display": @"F. Scott Hess", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"American, born 1955", + @"published": @(YES), + @"highlights": @[] + }], [SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"john-f-carlson", + @"display": @"John F. Carlson", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"Swedish-American, 1875-1947", + @"published": @(YES), + @"highlights": @[] + }], nil]; + + expect(sut.searchResults.count).to.equal(2); + + [sut addResults:@[[SearchResult modelWithJSON:@{ + @"model": @"artist", + @"id": @"aes-plus-f", + @"display": @"AES+F", + @"label": @"Artist", + @"score": @"excellent", + @"search_detail": @"Russian, Founded 1987", + @"published": @(YES), + @"highlights": @[] + }]] replace:NO]; + + expect(sut.searchResults.count).to.equal(3); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARSharingControllerTests.m b/Artsy Tests/ARSharingControllerTests.m new file mode 100644 index 00000000000..aef26f846f1 --- /dev/null +++ b/Artsy Tests/ARSharingControllerTests.m @@ -0,0 +1,128 @@ +#import "ARSharingController.h" +#import "ARURLItemProvider.h" +#import "ARMessageItemProvider.h" +#import "ARImageItemProvider.h" + +@interface ARSharingController (Testing) +- (NSString *)message; +- (NSString *)objectID; +- (NSArray *)activityItems; +@property (nonatomic, strong) id object; +@property (nonatomic, strong) ARURLItemProvider *urlProvider; +@property (nonatomic, strong) ARImageItemProvider *imageProvider; +@property (nonatomic, strong) ARMessageItemProvider *messageProvider; +- (void)shareWithThumbnailImageURL:(NSURL *)thumbnailImageURL image:(UIImage *)image; +- (instancetype)initWithObject:(id)object; +- (void)presentActivityViewController; +@end + +SpecBegin(ARSharingController) + +describe(@"sharing", ^{ + __block ARSharingController *sharingController; + + describe(@"objectID", ^{ + before(^{ + sharingController = [ARSharingController new]; + }); + + for (Class class in @[[Artwork class], [Artist class], [Gene class], [PartnerShow class]]){ + it([NSString stringWithFormat:@"returns %@ id", class], ^{ + NSString *object_id = NSStringWithFormat(@"id_for_%@", NSStringFromClass(class)); + sharingController.object = [class modelWithJSON:@{@"id" : object_id} error:nil]; + expect(sharingController.objectID).to.equal(object_id); + }); + }; + }); + + describe(@"message", ^{ + before(^{ + sharingController = [ARSharingController new]; + }); + + describe(@"with an Artwork", ^{ + __block Artwork *artwork; + before(^{ + artwork = [Artwork modelWithJSON:@{@"title" : @"Artwork Title"}]; + sharingController.object = artwork; + }); + it(@"formats string when there is no Artist", ^{ + expect([sharingController message]).to.equal(@"\"Artwork Title\""); + }); + it(@"formats string when there is an Artist", ^{ + Artist *artist = [Artist modelWithJSON:@{@"name" : @"An Artist"}]; + artwork.artist = artist; + expect([sharingController message]).to.equal(@"\"Artwork Title\" by An Artist"); + }); + }); + + it(@"formats the string for a PartnerShow", ^{ + PartnerShow *show = [PartnerShow modelWithJSON:@{@"name" : @"The Best Show Ever"}]; + sharingController.object = show; + expect([sharingController message]).to.equal(@"See The Best Show Ever"); + }); + + + it(@"returns a Gene's name", ^{ + Gene *gene = [Gene modelWithJSON:@{@"name" : @"Surrealism"}]; + sharingController.object = gene; + expect([sharingController message]).to.equal(@"Surrealism"); + }); + + it(@"returns an Artist's name", ^{ + Artist *artist = [Artist modelWithJSON:@{@"name" : @"Jeff Koons"}]; + sharingController.object = artist; + expect([sharingController message]).to.equal(@"Jeff Koons"); + }); + }); + + describe(@"activityItems", ^{ + __block ARMessageItemProvider *messageProvider; + __block ARURLItemProvider *urlProvider; + + before(^{ + sharingController = [ARSharingController new]; + messageProvider = [ARMessageItemProvider new]; + urlProvider = [ARURLItemProvider new]; + }); + + it(@"orders items", ^{ + [sharingController shareWithThumbnailImageURL:nil image:nil]; + NSArray *activityItems = [sharingController activityItems]; + expect([activityItems count]).to.equal(3); + expect(activityItems[0]).to.beKindOf([ARMessageItemProvider class]); + expect(activityItems[1]).to.beKindOf([ARURLItemProvider class]); + expect(activityItems[2]).to.beKindOf([ARImageItemProvider class]); + }); + }); + + describe(@"initWithObject", ^{ + it(@"sets object", ^{ + Artist *artistObject = [Artist new]; + sharingController = [[ARSharingController alloc] initWithObject:artistObject]; + expect(sharingController.object).to.equal(artistObject); + }); + }); + + describe(@"sharewithImage", ^{ + before(^{ + sharingController = [ARSharingController new]; + }); + + it(@"sets all providers", ^{ + [sharingController shareWithThumbnailImageURL:nil image:nil]; + expect(sharingController.messageProvider).notTo.beNil(); + expect(sharingController.urlProvider).notTo.beNil(); + expect(sharingController.imageProvider).notTo.beNil(); + }); + + it(@"triggers the View Controller", ^{ + id mock = [OCMockObject partialMockForObject:sharingController]; + [[mock expect] presentActivityViewController]; + [sharingController shareWithThumbnailImageURL:nil image:nil]; + [mock verify]; + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARShowNetworkModelTests.m b/Artsy Tests/ARShowNetworkModelTests.m new file mode 100644 index 00000000000..04ca0a570a5 --- /dev/null +++ b/Artsy Tests/ARShowNetworkModelTests.m @@ -0,0 +1,119 @@ +#import "ARShowNetworkModel.h" +#import "ArtsyAPI.h" +#import "ArtsyAPI+Shows.h" + +SpecBegin(ARShowNetworkModel) + +__block PartnerShow *show; +__block Fair *fair; + +beforeEach(^{ + show = [PartnerShow modelWithJSON:@{ + @"id" : @"show-id", + @"partner" : @{ @"id" : @"leila-heller", @"name" : @"Leila Heller" } + }]; + fair = [Fair modelWithJSON:@{ @"id" : @"fair-id" }]; +}); + +it(@"sets up its properties upon initialization", ^{ + ARShowNetworkModel *model = [[ARShowNetworkModel alloc] initWithFair:fair show:show]; + expect(model.show).to.equal(show); + expect(model.fair).to.equal(fair); +}); + +describe(@"network access", ^{ + __block id mockShow; + __block id mockFair; + __block id APIMock; + __block id evalutationCheck; + + before(^{ + evalutationCheck = [OCMArg checkWithBlock:^BOOL(void (^success)(PartnerShow *show)) { + success(mockShow); + return YES; + }]; + }); + + + beforeEach(^{ + mockShow = [OCMockObject partialMockForObject:show]; + mockFair = [OCMockObject partialMockForObject:fair]; + APIMock = [OCMockObject mockForClass:[ArtsyAPI class]]; + }); + + afterEach(^{ + [mockShow verify]; + [mockFair verify]; + [APIMock verify], [APIMock stopMocking]; + }); + + describe(@"with mocked show and fair", ^{ + __block ARShowNetworkModel *model; + beforeEach(^{ + model = [[ARShowNetworkModel alloc] initWithFair:mockFair show:mockShow]; + }); + + it(@"gets show info", ^{ + [[[APIMock expect] classMethod] getShowInfo:mockShow success:evalutationCheck failure:OCMOCK_ANY]; + + __block PartnerShow *returnedShow; + [model getShowInfo:^(PartnerShow *show) { + returnedShow = show; + } failure:nil]; + + expect(returnedShow).to.equal(mockShow); + }); + + it(@"gets fair maps", ^{ + NSArray *maps = @[[OCMockObject mockForClass:[Map class]]]; + + [[mockFair expect] getFairMaps:[OCMArg checkWithBlock:^BOOL(void (^success)(NSArray *maps)) { + success(maps); + return YES; + }]]; + + __block NSArray *returnedMaps; + [model getFairMaps:^(NSArray *maps) { + returnedMaps = maps; + }]; + + expect(returnedMaps).to.equal(maps); + }); + + it(@"gets artwork pages", ^{ + NSInteger page = 3; + NSArray *artworks = @[[OCMockObject mockForClass:[Artwork class]]]; + + [[[APIMock expect] classMethod] getArtworksForShow:mockShow atPage:page success:[OCMArg checkWithBlock:^BOOL(void (^success)(NSArray *artworks)) { + success(artworks); + return YES; + }] failure:OCMOCK_ANY]; + + __block NSArray *returnedArtworks; + [model getArtworksAtPage:page success:^(NSArray *artworks) { + returnedArtworks = artworks; + } failure:nil]; + + expect(returnedArtworks).to.equal(artworks); + }); + }); + + describe(@"with mocked show and nil fair", ^{ + __block ARShowNetworkModel *model; + + beforeEach(^{ + model = [[ARShowNetworkModel alloc] initWithFair:nil show:mockShow]; + }); + + it(@"sets fair after getting show info", ^{ + [[[mockShow expect] andReturn:mockFair] fair]; + [[[APIMock expect] classMethod] getShowInfo:mockShow success:evalutationCheck failure:OCMOCK_ANY]; + + [model getShowInfo:nil failure:nil]; + + expect(model.fair).to.equal(mockFair); + }); + }); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARSignUpActiveUserViewControllerTests.m b/Artsy Tests/ARSignUpActiveUserViewControllerTests.m new file mode 100644 index 00000000000..2c77bc997e9 --- /dev/null +++ b/Artsy Tests/ARSignUpActiveUserViewControllerTests.m @@ -0,0 +1,98 @@ +#import "ARSignUpActiveUserViewController.h" + +SpecBegin(ARSignUpActiveUserViewController) + +__block ARSignUpActiveUserViewController *vc; + +dispatch_block_t sharedBefore = ^{ + vc = [[ARSignUpActiveUserViewController alloc] init]; + vc.shouldAnimate = NO; +}; + +describe(@"sign up after app launch", ^{ + itHasSnapshotsForDevices(@"ARTrialContextFavoriteArtist", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextFavoriteArtist; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextFavoriteProfile", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextFavoriteProfile; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextFavoriteGene", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextFavoriteGene; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextFavoriteArtwork", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextFavoriteArtwork; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextShowingFavorites", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextShowingFavorites; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextPeriodical", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextPeriodical; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextRepresentativeInquiry", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextRepresentativeInquiry; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextContactGallery", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextContactGallery; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextAuctionBid", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextAuctionBid; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextArtworkOrder", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextArtworkOrder; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextFairGuide", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextFairGuide; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); + + itHasSnapshotsForDevices(@"ARTrialContextNotTrial", ^{ + sharedBefore(); + vc.trialContext = ARTrialContextNotTrial; + [vc ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return vc; + }); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARSignUpSplashViewControllerTests.m b/Artsy Tests/ARSignUpSplashViewControllerTests.m new file mode 100644 index 00000000000..f045658cc32 --- /dev/null +++ b/Artsy Tests/ARSignUpSplashViewControllerTests.m @@ -0,0 +1,34 @@ +#import "ARSignUpSplashViewController.h" +#import "ARCrossfadingImageView.h" + +@interface ARSignUpSplashViewController () +@property (nonatomic, strong, readwrite) UIPageControl *pageControl; +@property (nonatomic) ARCrossfadingImageView *imageView; +@end + +SpecBegin(ARSignUpSplashViewController) + +__block ARSignUpSplashViewController *controller; + +dispatch_block_t sharedBefore = ^{ + controller = [[ARSignUpSplashViewController alloc] init]; +}; + +describe(@"signup splash", ^{ + + it(@"has three pages", ^{ + sharedBefore(); + expect(controller.view).notTo.beNil(); + expect(controller.pageCount).to.equal(3); + expect(controller.pageControl.numberOfPages).to.equal(3); + expect([controller.imageView.images count]).to.equal(3); + }); + + itHasSnapshotsForDevices(@"looks correct", ^{ + sharedBefore(); + [controller ar_presentWithFrame:[UIScreen mainScreen].bounds]; + return controller; + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARStubbedArtistNetworkModel.h b/Artsy Tests/ARStubbedArtistNetworkModel.h new file mode 100644 index 00000000000..b91e18182b2 --- /dev/null +++ b/Artsy Tests/ARStubbedArtistNetworkModel.h @@ -0,0 +1,8 @@ +#import "ARArtistNetworkModel.h" + +@interface ARStubbedArtistNetworkModel : ARArtistNetworkModel + +@property (readwrite, nonatomic, strong) Artist *artistForArtistInfo; +@property (readwrite, nonatomic, copy) NSArray *artworksForArtworksAtPage; + +@end diff --git a/Artsy Tests/ARStubbedArtistNetworkModel.m b/Artsy Tests/ARStubbedArtistNetworkModel.m new file mode 100644 index 00000000000..2900cb9a1b7 --- /dev/null +++ b/Artsy Tests/ARStubbedArtistNetworkModel.m @@ -0,0 +1,16 @@ +#import "ARStubbedArtistNetworkModel.h" + +@implementation ARStubbedArtistNetworkModel + +- (void)getArtistInfoWithSuccess:(void (^)(Artist *artist))success failure:(void (^)(NSError *error))failure +{ + success(self.artistForArtistInfo); +} + +- (void)getArtistArtworksAtPage:(NSInteger)page params:(NSDictionary *)params success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure + +{ + success(self.artworksForArtworksAtPage); +} + +@end diff --git a/Artsy Tests/ARStubbedShowNetworkModel.h b/Artsy Tests/ARStubbedShowNetworkModel.h new file mode 100644 index 00000000000..4ea5ff776de --- /dev/null +++ b/Artsy Tests/ARStubbedShowNetworkModel.h @@ -0,0 +1,12 @@ +#import "ARShowNetworkModel.h" + +@interface ARStubbedShowNetworkModel : ARShowNetworkModel + +- (instancetype)initWithFair:(Fair *)fair show:(PartnerShow *)show maps:(NSArray *)maps; + +@property (nonatomic, strong, readonly) NSArray *maps; + +@property (nonatomic, strong, readwrite) NSArray *imagesForBoothHeader; +@property (nonatomic, strong, readwrite) NSArray *artworksForBoothHeader; + +@end diff --git a/Artsy Tests/ARStubbedShowNetworkModel.m b/Artsy Tests/ARStubbedShowNetworkModel.m new file mode 100644 index 00000000000..a94dcc18aba --- /dev/null +++ b/Artsy Tests/ARStubbedShowNetworkModel.m @@ -0,0 +1,58 @@ +#import "ARStubbedShowNetworkModel.h" + +@interface Fair () + +@property (nonatomic, copy) NSArray *maps; + +@end + +@implementation ARStubbedShowNetworkModel + +- (instancetype)initWithFair:(Fair *)fair show:(PartnerShow *)show maps:(NSArray *)maps +{ + self = [super initWithFair:fair show:show]; + if (self == nil) { return nil; } + + _maps = maps; + + return self; +} + +- (void)getShowInfo:(void (^)(PartnerShow *show))success failure:(void (^)(NSError *error))failure +{ + success(self.show); +} + +- (void)getFairMaps:(void (^)(NSArray *maps))success +{ + self.fair.maps = self.maps; + success(self.maps); +} + +- (void)getArtworksAtPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + success(@[]); +} + + +- (void)getFairBoothArtworksAndInstallShots:(PartnerShow *)show + gotInstallImages:(void (^)(NSArray *images))gotInstallImages + gotArtworks:(void (^)(NSArray *images))gotArtworkImages + noImages:(void (^)(void))noImages +{ + if (self.imagesForBoothHeader.count) { + gotInstallImages(self.imagesForBoothHeader); + } + + if (self.artworksForBoothHeader.count) { + gotArtworkImages([self.artworksForBoothHeader map:^id(Artwork *artwork) { + return artwork.defaultImage; + }]); + } + + if (self.imagesForBoothHeader.count == 0 && self.artworksForBoothHeader.count == 0) { + noImages(); + } +} + +@end diff --git a/Artsy Tests/ARSwitchBoardTests.m b/Artsy Tests/ARSwitchBoardTests.m new file mode 100644 index 00000000000..78c6ed884fe --- /dev/null +++ b/Artsy Tests/ARSwitchBoardTests.m @@ -0,0 +1,351 @@ +#import "ARSwitchBoard.h" +#import "AROptions.h" +#import "ARRouter.h" +#import "ArtsyAPI.h" +#import "ArtsyAPI+Profiles.h" +#import "ARTopMenuViewController.h" +#import "ARFavoritesViewController.h" +#import "ARProfileViewController.h" +#import "ARArtistViewController.h" +#import "ARBrowseViewController.h" +#import "ARArtworkSetViewController.h" +#import "ARExternalWebBrowserViewController.h" +#import "ARGeneViewController.h" +#import "ARInternalMobileWebViewController.h" +#import "ARProfileViewController.h" +#import "ARFairShowViewController.h" +#import "ARFairViewController.h" +#import "ARFairArtistViewController.h" +#import "ARFairGuideContainerViewController.h" + +@interface ARSwitchBoard(Tests) +- (NSURL *) resolveRelativeUrl:(NSString *)path; +- (id)routeInternalURL:(NSURL *)url fair:(Fair *)fair; +- (void) openURLInExternalService:(NSURL *)url; +@end + +@interface ARProfileViewController(Tests) +- (void)showViewController:(UIViewController *)viewController; +@end + +SpecBegin(ARSwitchBoard) + +__block ARSwitchBoard *switchboard; + +describe(@"ARSwitchboard", ^{ + + beforeEach(^{ + switchboard = [[ARSwitchBoard alloc] init]; + }); + + describe(@"resolveRelativeUrl", ^{ + beforeEach(^{ + [AROptions setBool:false forOption:ARUseStagingDefault]; + [ARRouter setup]; + }); + + it(@"resolves absolute artsy.net url", ^{ + NSString *resolvedUrl = [[switchboard resolveRelativeUrl:@"http://artsy.net/foo/bar"] absoluteString]; + expect(resolvedUrl).to.equal(@"http://artsy.net/foo/bar"); + }); + + it(@"resolves absolute external url", ^{ + NSString *resolvedUrl = [[switchboard resolveRelativeUrl:@"http://example.com/foo/bar"] absoluteString]; + expect(resolvedUrl).to.equal(@"http://example.com/foo/bar"); + }); + + it(@"resolves relative url", ^{ + NSString *resolvedUrl = [[switchboard resolveRelativeUrl:@"/foo/bar"] absoluteString]; + expect(resolvedUrl).to.equal(@"https://m.artsy.net/foo/bar"); + }); + }); + + describe(@"loadURL", ^{ + __block id switchboardMock; + + before(^{ + switchboardMock = [OCMockObject partialMockForObject:switchboard]; + }); + + describe(@"with internal url", ^{ + it(@"routes internal urls correctly", ^{ + NSURL *internalURL = [[NSURL alloc] initWithString:@"http://artsy.net/some/path"]; + [[switchboardMock expect] routeInternalURL:internalURL fair:nil]; + [switchboard loadURL:internalURL]; + [switchboardMock verify]; + }); + }); + + describe(@"with non http schemed url", ^{ + it(@"does not load an internal view", ^{ + [[switchboardMock stub] openURLInExternalService:OCMOCK_ANY]; + + NSURL *externalURL = [[NSURL alloc] initWithString:@"mailto:email@mail.com"]; + [[switchboardMock reject] routeInternalURL:OCMOCK_ANY fair:nil]; + [switchboard loadURL:externalURL]; + [switchboardMock verify]; + }); + + it(@"does not load browser", ^{ + id sharedAppMock = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + [[sharedAppMock reject] openURL:OCMOCK_ANY]; + + NSURL *internalURL = [[NSURL alloc] initWithString:@"mailto:email@mail.com"]; + [switchboard loadURL:internalURL]; + [sharedAppMock verify]; + }); + + it(@"opens with the OS for non-http links", ^{ + NSURL *internalURL = [[NSURL alloc] initWithString:@"tel:111111"]; + [[switchboardMock expect] openURLInExternalService:OCMOCK_ANY]; + + [switchboard loadURL:internalURL]; + [switchboardMock verify]; + }); + + }); + + describe(@"with applewebdata urls", ^{ + it(@"does not load browser", ^{ + NSURL *internalURL = [[NSURL alloc] initWithString:@"applewebdata://EF86F744-3F4F-4732-8A4B-3E5E94D6D7DA/some/path"]; + id sharedAppMock = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + [[sharedAppMock reject] openURL:OCMOCK_ANY]; + [switchboard loadURL:internalURL]; + [sharedAppMock verify]; + + }); + + it(@"routes internal urls", ^{ + NSURL *internalURL = [[NSURL alloc] initWithString:@"applewebdata://EF86F744-3F4F-4732-8A4B-3E5E94D6D7DA/some/path"]; + [[switchboardMock expect] routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/some/path"] fair:nil]; + [switchboard loadURL:internalURL]; + [switchboardMock verify]; + }); + }); + + it(@"loads web view for external urls", ^{ + it(@"loads browser", ^{ + NSURL *externalURL = [[NSURL alloc] initWithString:@"http://google.com"]; + id viewController = [switchboard loadURL:externalURL]; + expect([viewController isKindOfClass:[ARExternalWebBrowserViewController class]]).to.beTruthy(); + + }); + + it(@"does not route url", ^{ + NSURL *externalURL = [[NSURL alloc] initWithString:@"http://google.com"]; + [[switchboardMock reject] routeInternalURL:OCMOCK_ANY fair:nil]; + [switchboard loadURL:externalURL]; + [switchboardMock verify]; + }); + + }); + }); + + describe(@"routeInternalURL", ^{ + __block id classMock; + __block __strong id controllerMock; + + before(^{ + controllerMock = [OCMockObject partialMockForObject:[ARTopMenuViewController sharedController]]; + classMock = [OCMockObject mockForClass:[ARTopMenuViewController class]]; + [[[classMock stub] andReturn:controllerMock] sharedController]; + }); + + after(^{ + [controllerMock verify]; + [controllerMock stopMocking]; + [classMock stopMocking]; + }); + + it(@"routes /favorites", ^{ + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARFavoritesViewController class]]]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/favorites"] fair:nil]; + }); + + it(@"routes /browse", ^{ + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARBrowseViewController class]]]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/browse"] fair:nil]; + }); + + it(@"routes profiles", ^{ + // See aditional tests for profile routing below. + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARProfileViewController class]]]; + NSURL *profileURL = [[NSURL alloc] initWithString:@"http://artsy.net/myprofile"]; + [switchboard routeInternalURL:profileURL fair:nil]; + }); + + it(@"routes artsy.net to Home", ^{ + [[controllerMock expect] loadFeed]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net"] fair:nil]; + }); + + + it(@"routes artsy.net/ to Home", ^{ + [[controllerMock expect] loadFeed]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/"] fair:nil]; + }); + + it(@"routes artists", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/artist/artistname/artworks" withParams:@{ @"page" : @"1", @"size" : @"10" } withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/artists" withParams:@{ @"artist[]" : @"artistname" } withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/posts" withParams:@{ @"artist[]" : @"artistname" } withResponse:@[]]; + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARArtistViewController class]]]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/artist/artistname"] fair:nil]; + }); + + it(@"routes artists in a gallery context on iPad", ^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + switchboard = [[ARSwitchBoard alloc] init]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/artist/artistname/artworks" withParams:@{ @"page" : @"1", @"size" : @"10" } withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/artists" withParams:@{ @"artist[]" : @"artistname" } withResponse:@[]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/posts" withParams:@{ @"artist[]" : @"artistname" } withResponse:@[]]; + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARArtistViewController class]]]; + id viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/some-gallery/artist/artistname"] fair:nil]; + expect(viewController).to.beNil(); + [ARTestContext stopStubbing]; + }); + + it(@"does not route artists in a gallery context on iPhone", ^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + switchboard = [[ARSwitchBoard alloc] init]; + [[controllerMock reject] pushViewController:[OCMArg checkForClass:[ARArtistViewController class]]]; + id viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/some-gallery/artist/artistname"] fair:nil]; + expect(viewController).to.beKindOf([ARInternalMobileWebViewController class]); + [ARTestContext stopStubbing]; + }); + + context(@"fairs", ^{ + + context(@"on iphone", ^{ + before(^{ + [ARTestContext stubDevice:ARDeviceTypePhone5]; + switchboard = [[ARSwitchBoard alloc] init]; + }); + + after(^{ + [ARTestContext stopStubbing]; + }); + + it(@"routes shows", ^{ + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARFairShowViewController class]]]; + id viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/show/show-id"] fair:nil]; + expect(viewController).to.beNil(); + }); + + it(@"routes fair guide", ^{ + Fair *fair = [OCMockObject mockForClass:[Fair class]]; + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARFairGuideContainerViewController class]]]; + id viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/fair-id/for-you"] fair:fair]; + expect(viewController).to.beNil(); + }); + + it(@"routes fair artists", ^{ + Fair *fair = [OCMockObject mockForClass:[Fair class]]; + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARFairArtistViewController class]]]; + id viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"/the-armory-show/browse/artist/artist-id"] fair:fair]; + expect(viewController).to.beNil(); + }); + + it(@"forwards fair for non-native views", ^{ + Fair *fair = [OCMockObject mockForClass:[Fair class]]; + ARInternalMobileWebViewController *viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"/the-armory-show/browse/artists"] fair:fair]; + expect(viewController.fair).to.equal(fair); + }); + }); + + context(@"on ipad", ^{ + before(^{ + [ARTestContext stubDevice:ARDeviceTypePad]; + switchboard = [[ARSwitchBoard alloc] init]; + }); + + after(^{ + [ARTestContext stopStubbing]; + }); + + it(@"doesn't route shows", ^{ + Fair *fair = [OCMockObject mockForClass:[Fair class]]; + [[controllerMock reject] pushViewController:OCMOCK_ANY]; + ARInternalMobileWebViewController *viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/show/show-id"] fair:fair]; + expect(viewController).to.beKindOf([ARInternalMobileWebViewController class]); + expect(viewController.fair).to.equal(fair); + }); + + it(@"doesn't route fair guide", ^{ + Fair *fair = [OCMockObject mockForClass:[Fair class]]; + [[controllerMock reject] pushViewController:OCMOCK_ANY]; + ARInternalMobileWebViewController *viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/fair-id/for-you"] fair:fair]; + expect(viewController).to.beKindOf([ARInternalMobileWebViewController class]); + expect(viewController.fair).to.equal(fair); + }); + + it(@"doesn't route fair artists", ^{ + Fair *fair = [OCMockObject mockForClass:[Fair class]]; + [[controllerMock reject] pushViewController:OCMOCK_ANY]; + ARInternalMobileWebViewController *viewController = [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"/the-armory-show/browse/artist/artist-id"] fair:fair]; + expect(viewController).to.beKindOf([ARInternalMobileWebViewController class]); + expect(viewController.fair).to.equal(fair); + }); + }); + }); + + + it(@"routes artworks", ^{ + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARArtworkSetViewController class]]]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/artwork/artworkID"] fair:nil]; + }); + + it(@"routes artworks and retains fair context", ^{ + Fair *fair = [Fair modelWithJSON:@{}]; + [[controllerMock expect] pushViewController:[OCMArg checkWithBlock:^BOOL(ARArtworkSetViewController *sut) { + return sut.fair == fair; + }]]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/artwork/artworkID"] fair:fair]; + }); + + it(@"routes genes", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/gene/surrealism" withResponse:@{ @"id" : @"surrealism", @"name" : @"Surrealism" }]; + [[controllerMock expect] pushViewController:[OCMArg checkForClass:[ARGeneViewController class]]]; + [switchboard routeInternalURL:[[NSURL alloc] initWithString:@"http://artsy.net/gene/surrealism"] fair:nil]; + }); + }); + + + describe(@"routeProfileWithID", ^{ + __block id mockProfileVC; + + before(^{ + mockProfileVC = [OCMockObject mockForClass:[ARProfileViewController class]]; + }); + + describe(@"with nil profileID", ^{ + it(@"raises exception", ^{ + expect(^{[switchboard routeProfileWithID:nil];}).to.raise(@"NSInternalInconsistencyException"); + }); + }); + + describe(@"with a non-fair profile", ^{ + }); + + describe(@"with a fair profile", ^{ + beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/profile/myfairprofile" withResponse:@{ + @"id" : @"myfairprofile", + @"owner": @{ @"default_fair_id" : @"armory-show-2013" }, + @"owner_type" : @"FairOrganizer" }]; + }); + + it(@"does not load martsy", ^{ + [[mockProfileVC reject] showViewController:[OCMArg checkForClass:[ARInternalMobileWebViewController class]]]; + [switchboard routeProfileWithID:@"myfairprofile"]; + }); + + it(@"routes fair profiles specially", ^{ + [[mockProfileVC expect] showViewController:[OCMArg checkForClass:[ARFairViewController class]]]; + [switchboard routeProfileWithID:@"myfairprofile"]; + }); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARSwitchViewTests.m b/Artsy Tests/ARSwitchViewTests.m new file mode 100644 index 00000000000..e1794a1e848 --- /dev/null +++ b/Artsy Tests/ARSwitchViewTests.m @@ -0,0 +1,34 @@ +#import "ARSwitchView.h" + +SpecBegin(ARSwitchView) + +__block NSArray *titles; + +it (@"looks correct configured with two buttons", ^{ + titles = @[@"First title", @"Second Title"]; + + ARSwitchView *switchView = [[ARSwitchView alloc] initWithButtonTitles:titles]; + switchView.frame = (CGRect){.origin = CGPointZero, .size = CGSizeMake(280, switchView.intrinsicContentSize.height)}; + + expect(switchView).to.haveValidSnapshot(); +}); + +it (@"accepts any number of items", ^{ + NSArray *titles = @[@"First title", @"Second Title", @"Third Title", @"Forth Title"]; + + ARSwitchView *switchView = [[ARSwitchView alloc] initWithButtonTitles:titles]; + switchView.frame = (CGRect){.origin = CGPointZero, .size = CGSizeMake(280, switchView.intrinsicContentSize.height)}; + + expect(switchView).to.haveValidSnapshot(); +}); + +it (@"adjusts buttons to any switch width", ^{ + NSArray *titles = @[@"First title", @"Second Title", @"Third Title", @"Forth Title"]; + + ARSwitchView *switchView = [[ARSwitchView alloc] initWithButtonTitles:titles]; + switchView.frame = (CGRect){.origin = CGPointZero, .size = CGSizeMake(728, switchView.intrinsicContentSize.height)}; + + expect(switchView).to.haveValidSnapshot(); +}); + +SpecEnd diff --git a/Artsy Tests/ARSystemTimeTests.m b/Artsy Tests/ARSystemTimeTests.m new file mode 100644 index 00000000000..850361ea7b0 --- /dev/null +++ b/Artsy Tests/ARSystemTimeTests.m @@ -0,0 +1,67 @@ +#import "ArtsyAPI.h" +#import "ARSystemTime.h" +#import "ARNetworkConstants.h" + +SpecBegin(ARSystemTimeTests) + +beforeEach(^{ + [ARSystemTime reset]; + [OHHTTPStubs stubJSONResponseAtPath:ARSystemTimeURL withResponse:@{ + @"time": @"2422-03-24T13:29:34Z", + @"day": @(24), + @"wday": @(4), + @"month": @(3), + @"year": @(2422), + @"hour": @(13), + @"min": @(29), + @"sec": @(34), + @"dst": @(NO), + @"unix": @(1395926974), + @"utc_offset": @(0), + @"zone": @"UTC", + @"iso8601": @"2422-03-24T13:29:34Z" + }]; +}); + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; + [ARSystemTime reset]; +}); + +describe(@"not in sync", ^{ + it(@"returns NO", ^{ + expect(ARSystemTime.inSync).to.beFalsy(); + }); + + it(@"returns current date/time", ^{ + NSInteger currentYear = [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[NSDate date]].year; + expect([[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[ARSystemTime date]].year).to.equal(currentYear); + }); +}); + +describe(@"in sync", ^{ + beforeEach(^{ + [ARSystemTime sync]; + }); + + it(@"returns YES", ^{ + expect(ARSystemTime.inSync).will.beTruthy(); + }); + + it(@"returns a date time in the future", ^{ + // the delta between now and 2422 will put [ARSystemTime date] into the future + NSInteger currentYear = [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[NSDate date]].year; + expect([[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[ARSystemTime date]].year).will.beGreaterThan(currentYear); + }); + + it(@"returns a new date every time", ^{ + [ARSystemTime sync]; + NSDate *now = [ARSystemTime date]; + // eventually the new time will be ahead of the previous one + expect(now).will.beLessThan([ARSystemTime date]); + }); +}); + + +SpecEnd + diff --git a/Artsy Tests/ARTabContentViewSpec.m b/Artsy Tests/ARTabContentViewSpec.m new file mode 100644 index 00000000000..de0043b1942 --- /dev/null +++ b/Artsy Tests/ARTabContentViewSpec.m @@ -0,0 +1,58 @@ +#import "ARTabContentView.h" +#import "ARTestTopMenuNavigationDataSource.h" + +SpecBegin(ARTabView) + +__block UIViewController *outerController, *innerController1, *innerController2; +__block ARTabContentView *sut; +__block ARTestTopMenuNavigationDataSource *dataSource; + +before(^{ + CGRect frame = CGRectMake(0, 0, 320, 480); + + outerController = [[UIViewController alloc] init]; + innerController1 = [[UIViewController alloc] init]; + innerController1.view.backgroundColor = [UIColor blueColor]; + innerController2 = [[UIViewController alloc] init]; + innerController2.view.backgroundColor = [UIColor redColor]; + + dataSource = [[ARTestTopMenuNavigationDataSource alloc] init]; + dataSource.controller1 = innerController1; + dataSource.controller2 = innerController2; + + sut = [[ARTabContentView alloc] initWithFrame:frame hostViewController:outerController delegate:nil dataSource:dataSource]; + [outerController.view addSubview:sut]; + + [sut setCurrentViewIndex:0 animated:NO]; +}); + +it(@"sets current and previous view index", ^{ + [sut setCurrentViewIndex:1 animated:NO]; + expect(sut.currentViewIndex).to.equal(1); + expect(sut.previousViewIndex).to.equal(0); + + [sut setCurrentViewIndex:0 animated:NO]; + expect(sut.currentViewIndex).to.equal(0); + expect(sut.previousViewIndex).to.equal(1); +}); + +describe(@"correctly shows a navigation controller", ^{ + it(@"at first index" , ^{ + expect(outerController).to.haveValidSnapshot(); + }); + + it(@"at another index" , ^{ + [sut setCurrentViewIndex:1 animated:NO]; + expect(outerController).to.haveValidSnapshot(); + }); + +}); + +it(@"correctly sets the child view controller", ^{ + expect(outerController.childViewControllers).to.contain(innerController1); + [sut setCurrentViewIndex:1 animated:NO]; + expect(outerController.childViewControllers).to.contain(innerController2); + +}); + +SpecEnd diff --git a/Artsy Tests/ARTestContext.h b/Artsy Tests/ARTestContext.h new file mode 100644 index 00000000000..f77b2330e62 --- /dev/null +++ b/Artsy Tests/ARTestContext.h @@ -0,0 +1,12 @@ +NS_ENUM(NSInteger, ARDeviceType){ + ARDeviceTypePhone4, + ARDeviceTypePhone5, + ARDeviceTypePad +}; + +@interface ARTestContext : NSObject + ++ (void)stubDevice:(enum ARDeviceType)device; ++ (void)stopStubbing; + +@end diff --git a/Artsy Tests/ARTestContext.m b/Artsy Tests/ARTestContext.m new file mode 100644 index 00000000000..4370516d21c --- /dev/null +++ b/Artsy Tests/ARTestContext.m @@ -0,0 +1,44 @@ +#import "ARTestContext.h" +#import "UIDevice-Hardware.h" + +static OCMockObject *ARDeviceMock; +static OCMockObject *ARPartialScreenMock; + +@implementation ARTestContext + ++ (void)stubDevice:(enum ARDeviceType)device +{ + CGSize size; + BOOL isClassedAsPhone = YES; + + switch (device) { + case ARDeviceTypePad: + size = (CGSize){ 768, 1024 }; + isClassedAsPhone = NO; + break; + + case ARDeviceTypePhone4: + size = (CGSize){ 320, 480 }; + break; + + case ARDeviceTypePhone5: + size = (CGSize){ 320, 568 }; + break; + } + + ARDeviceMock = [OCMockObject niceMockForClass:UIDevice.class]; + [[[ARDeviceMock stub] andReturnValue:OCMOCK_VALUE((BOOL){ !isClassedAsPhone })] isPad]; + [[[ARDeviceMock stub] andReturnValue:OCMOCK_VALUE((BOOL){ isClassedAsPhone })] isPhone]; + + ARPartialScreenMock = [OCMockObject partialMockForObject:UIScreen.mainScreen]; + NSValue *phoneSize = [NSValue valueWithCGRect:(CGRect)CGRectMake(0, 0, size.width, size.height)]; + [[[ARPartialScreenMock stub] andReturnValue:phoneSize] bounds]; +} + ++ (void)stopStubbing +{ + [ARPartialScreenMock stopMocking]; + [ARDeviceMock stopMocking]; +} + +@end \ No newline at end of file diff --git a/Artsy Tests/ARTestTopMenuNavigationDataSource.h b/Artsy Tests/ARTestTopMenuNavigationDataSource.h new file mode 100644 index 00000000000..13f45b6b7c7 --- /dev/null +++ b/Artsy Tests/ARTestTopMenuNavigationDataSource.h @@ -0,0 +1,8 @@ +#import "ARTopMenuNavigationDataSource.h" + +@interface ARTestTopMenuNavigationDataSource : ARTopMenuNavigationDataSource + +@property (nonatomic, strong, readwrite) UIViewController *controller1; +@property (nonatomic, strong, readwrite) UIViewController *controller2; + +@end diff --git a/Artsy Tests/ARTestTopMenuNavigationDataSource.m b/Artsy Tests/ARTestTopMenuNavigationDataSource.m new file mode 100644 index 00000000000..157c3ddcd8d --- /dev/null +++ b/Artsy Tests/ARTestTopMenuNavigationDataSource.m @@ -0,0 +1,26 @@ +#import "ARTestTopMenuNavigationDataSource.h" + +@implementation ARTestTopMenuNavigationDataSource + +- (UIViewController *)viewControllerForTabContentView:(ARTabContentView *)tabContentView atIndex:(NSInteger)index +{ + if (index == 0 ) { + return self.controller1 ?: [[UIViewController alloc] init]; + } else if (index == 1) { + return self.controller2 ?: [[UIViewController alloc] init]; + } else { + return nil; + } +} + +- (BOOL)tabContentView:(ARTabContentView *)tabContentView canPresentViewControllerAtIndex:(NSInteger)index +{ + return YES; +} + +- (NSInteger)numberOfViewControllersForTabContentView:(ARTabContentView *)tabContentView +{ + return 1; +} + +@end diff --git a/Artsy Tests/ARThemeTests.m b/Artsy Tests/ARThemeTests.m new file mode 100644 index 00000000000..1f8317f6518 --- /dev/null +++ b/Artsy Tests/ARThemeTests.m @@ -0,0 +1,22 @@ +#import "ARTheme.h" + +SpecBegin(ARTheme) + +it(@"automatically loads defaultTheme", ^{ + expect([ARTheme defaultTheme]).toNot.beNil(); + expect([ARTheme defaultTheme]).to.beKindOf([ARTheme class]); +}); + +it(@"loads fonts", ^{ + expect([ARTheme defaultTheme].fonts).notTo.beNil(); +}); + +it(@"loads colors", ^{ + expect([ARTheme defaultTheme].colors).notTo.beNil(); +}); + +it(@"loads layout", ^{ + expect([ARTheme defaultTheme].layout).notTo.beNil(); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ARTiledImageDataSourceWithImageTests.m b/Artsy Tests/ARTiledImageDataSourceWithImageTests.m new file mode 100644 index 00000000000..5e89feaa7f8 --- /dev/null +++ b/Artsy Tests/ARTiledImageDataSourceWithImageTests.m @@ -0,0 +1,60 @@ +#import "UIImage+ImageFromColor.h" +#import "ARTiledImageDataSourceWithImage.h" + +SpecBegin(ARTiledImageDataSourceWithImage) + +describe(@"init", ^{ + + __block Image *image = nil; + __block ARTiledImageDataSourceWithImage *dataSource = nil; + + beforeEach(^{ + image = [Image modelWithJSON:@{ + @"id": @"image-id", + @"max_tiled_height": @(2376), + @"max_tiled_width": @(4224), + @"tile_size" : @(233), + @"tile_base_url" : @"http://static0.artsy.net/maps/map-id/level/dztiles-512-0" + }]; + dataSource = [[ARTiledImageDataSourceWithImage alloc] initWithImage:image]; + }); + + it(@"check for image existing", ^{ + expect(dataSource.image).to.equal(image); + }); + + context(@"delegate methods", ^{ + + it(@"gives the right tile size", ^{ + CGSize tileSize = [dataSource tileSizeForImageView:nil]; + expect(tileSize.width).to.equal(image.tileSize); + expect(tileSize.height).to.equal(image.tileSize); + }); + + it(@"gives the right image size", ^{ + CGSize imageSize = [dataSource imageSizeForImageView:nil]; + expect(imageSize.width).to.equal(image.maxTiledWidth); + expect(imageSize.height).to.equal(image.maxTiledHeight); + }); + + it(@"gives the right minimum zoom level", ^{ + expect([dataSource minimumImageZoomLevelForImageView:nil]).to.equal(11); + }); + + it(@"gives the right maximum zoom level", ^{ + expect([dataSource maximumImageZoomLevelForImageView:nil]).to.equal(13); + }); + + it(@"stores and retrieves a tile", ^{ + UIImage *uiImage = [UIImage imageFromColor:[UIColor whiteColor]]; + NSURL * url = [NSURL URLWithString:@"http://static0.artsy.net/maps/map-id/level/dztiles-512-0/11/1_0.jpg"]; + [dataSource tiledImageView:nil didDownloadTiledImage:uiImage atURL:url]; + UIImage *loadedImage = [dataSource tiledImageView:nil imageTileForLevel:11 x:1 y:0]; + expect(loadedImage).toNot.beNil(); + expect(loadedImage).to.beKindOf([UIImage class]); + }); + }); + +}); + +SpecEnd diff --git a/Artsy Tests/ARTopMenuNavigationDataSourceSpec.m b/Artsy Tests/ARTopMenuNavigationDataSourceSpec.m new file mode 100644 index 00000000000..53736571469 --- /dev/null +++ b/Artsy Tests/ARTopMenuNavigationDataSourceSpec.m @@ -0,0 +1,79 @@ +#import "ARTopMenuNavigationDataSource.h" +#import "ARNavigationController.h" +#import "ARBrowseViewController.h" +#import "ARFavoritesViewController.h" +#import "ARAppSearchViewController.h" +#import "ARShowFeedViewController.h" + +@interface ARTopMenuNavigationDataSource (Testing) +-(ARNavigationController *)navigationControllerForSearch; +-(ARNavigationController *)navigationControllerForFeed; +-(ARNavigationController *)navigationControllerForBrowse; +-(ARNavigationController *)navigationControllerForFavorites; +@end + + +SpecBegin(ARTopMenuNavigationDataSource) +__block ARTopMenuNavigationDataSource *navDataSource; +before(^{ + navDataSource = [[ARTopMenuNavigationDataSource alloc] init]; +}); + +it(@"uses a single search vc", ^{ + ARNavigationController *navigationController = [navDataSource navigationControllerForSearch]; + UIViewController *rootVC = [[navigationController viewControllers] objectAtIndex:0]; + expect(rootVC).to.beKindOf([ARAppSearchViewController class]); + + ARNavigationController *newNavigationController = [navDataSource navigationControllerForSearch]; + UIViewController *newRootVC = [[newNavigationController viewControllers] objectAtIndex:0]; + expect(newNavigationController).to.equal(navigationController); + expect(newRootVC).to.equal(rootVC); +}); + +it(@"uses a single feed vc", ^{ + ARNavigationController *navigationController = [navDataSource navigationControllerForFeed]; + UIViewController *rootVC = [[navigationController viewControllers] objectAtIndex:0]; + expect(rootVC).to.beKindOf([ARShowFeedViewController class]); + + ARNavigationController *newNavigationController = [navDataSource navigationControllerForFeed]; + UIViewController *newRootVC = [[newNavigationController viewControllers] objectAtIndex:0]; + expect(newNavigationController).to.equal(navigationController); + expect(newRootVC).to.equal(rootVC); +}); + + +it(@"uses a single browse vc", ^{ + ARNavigationController *navigationController = [navDataSource navigationControllerForBrowse]; + UIViewController *rootVC = [[navigationController viewControllers] objectAtIndex:0]; + expect(rootVC).to.beKindOf([ARBrowseViewController class]); + + ARNavigationController *newNavigationController = [navDataSource navigationControllerForBrowse]; + UIViewController *newRootVC = [[newNavigationController viewControllers] objectAtIndex:0]; + expect(newNavigationController).to.equal(navigationController); + expect(newRootVC).to.equal(rootVC); +}); + +// TODO: use the same favorites VC. Requires fixing collection view bug. +pending(@"uses a single favorites vc", ^{ + ARNavigationController *navigationController = [navDataSource navigationControllerForFavorites]; + UIViewController *rootVC = [[navigationController viewControllers] objectAtIndex:0]; + expect(rootVC).to.beKindOf([ARFavoritesViewController class]); + + ARNavigationController *newNavigationController = [navDataSource navigationControllerForFavorites]; + UIViewController *newRootVC = [[newNavigationController viewControllers] objectAtIndex:0]; + expect(newNavigationController).to.equal(navigationController); + expect(newRootVC).to.equal(rootVC); +}); + +it(@"reinstantiates favorites vc", ^{ + ARNavigationController *navigationController = [navDataSource navigationControllerForFavorites]; + UIViewController *rootVC = [[navigationController viewControllers] objectAtIndex:0]; + expect(rootVC).to.beKindOf([ARFavoritesViewController class]); + + ARNavigationController *newNavigationController = [navDataSource navigationControllerForFavorites]; + UIViewController *newRootVC = [[newNavigationController viewControllers] objectAtIndex:0]; + expect(newNavigationController).notTo.equal(navigationController); + expect(newRootVC).to.beKindOf([ARFavoritesViewController class]); + expect(newRootVC).notTo.equal(rootVC); +}); +SpecEnd diff --git a/Artsy Tests/ARTopMenuViewControllerSpec.m b/Artsy Tests/ARTopMenuViewControllerSpec.m new file mode 100644 index 00000000000..0c962185e2c --- /dev/null +++ b/Artsy Tests/ARTopMenuViewControllerSpec.m @@ -0,0 +1,54 @@ +#import "ARTopMenuViewController.h" +#import "ARTestTopMenuNavigationDataSource.h" +#import "ARTabContentView.h" +#import "ARTopMenuNavigationDataSource.h" +#import "ARFairViewController.h" + +@interface ARTopMenuViewController(Testing) +@property (readwrite, nonatomic, strong) ARTopMenuNavigationDataSource *navigationDataSource; +@end + +SpecBegin(ARTopMenuViewController) + +__block ARTopMenuViewController *sut; +__block ARTopMenuNavigationDataSource *dataSource; + +dispatch_block_t sharedBefore = ^{ + sut = [[ARTopMenuViewController alloc] init]; + sut.navigationDataSource = dataSource; + [sut ar_presentWithFrame:[UIScreen mainScreen].bounds]; + + [sut beginAppearanceTransition:YES animated:NO]; + [sut endAppearanceTransition]; + [sut.view layoutIfNeeded]; +}; + +itHasSnapshotsForDevices(@"selects 'home' by default", ^{ + dataSource = [[ARTestTopMenuNavigationDataSource alloc] init]; + sharedBefore(); + return sut; +}); + +itHasSnapshotsForDevices(@"should be able to hide", ^{ + dataSource = [[ARTestTopMenuNavigationDataSource alloc] init]; + sharedBefore(); + [sut hideToolbar:YES animated:NO]; + return sut; +}); + +describe(@"navigation", ^{ + it(@"resets the search view controller to the root", ^{ + dataSource = [[ARTopMenuNavigationDataSource alloc] init]; + sharedBefore(); + + [sut.tabContentView setCurrentViewIndex:ARTopTabControllerIndexSearch animated:NO]; + [sut pushViewController:[[ARFairViewController alloc] init] animated:NO]; + + [sut.tabContentView setCurrentViewIndex:ARTopTabControllerIndexFeed animated:NO]; + [sut.tabContentView setCurrentViewIndex:ARTopTabControllerIndexSearch animated:NO]; + + expect(sut.rootNavigationController.viewControllers.count).to.equal(1); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARURLItemProviderTests.m b/Artsy Tests/ARURLItemProviderTests.m new file mode 100644 index 00000000000..8969ef90ba0 --- /dev/null +++ b/Artsy Tests/ARURLItemProviderTests.m @@ -0,0 +1,120 @@ +#import "ARURLItemProvider.h" +#import "ARNetworkConstants.h" + +SpecBegin(ARURLItemProvider) + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +describe(@"url and image thumbnail", ^{ + __block NSString *path = @"/artist/xyz"; + __block NSURL *pathUrl = [NSURL URLWithString:path relativeToURL:[NSURL URLWithString:ARBaseMobileWebURL]]; + __block NSURL *url = [NSURL URLWithString:[pathUrl absoluteString]]; + __block ARURLItemProvider *provider; + __block UIImage *image = [UIImage imageNamed:@"stub.jpg"]; + + describe(@"with valid imageURL", ^{ + __block NSURL *imageURL = [NSURL URLWithString:@"http://image.com/image.jpg"]; + + beforeEach(^{ + provider = [[ARURLItemProvider alloc] initWithMessage:@"Message" path:path thumbnailImageURL:imageURL]; + }); + + it(@"sets the imageURL", ^{ + expect(provider.thumbnailImage).to.beNil(); + expect(provider.thumbnailImageURL).to.equal(imageURL); + }); + + it(@"sets placeholderItem", ^{ + expect(provider.placeholderItem).to.equal(url); + }); + + describe(@"thumbnailImageForActivityType", ^{ + it(@"fetches and returns the thumbnail", ^{ + UIActivityViewController *activityVC = [[UIActivityViewController alloc] init]; + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.path isEqualToString:@"/image.jpg"]; + } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) { + return [[OHHTTPStubsResponse + responseWithFileAtPath:OHPathForFileInBundle(@"stub.jpg", nil) + statusCode:200 + headers:@{@"Content-Type":@"image/jpeg"}] + responseTime:OHHTTPStubsDownloadSpeed3G]; + }]; + + UIImage *thumbnailImage = [provider activityViewController:activityVC thumbnailImageForActivityType:@"any activity" suggestedSize:CGSizeMake(100, 100)]; + expect(thumbnailImage.class).to.equal([UIImage class]); + expect(provider.thumbnailImage).to.equal(thumbnailImage); + }); + + it(@"returns nil for AirDrop sharing", ^{ + UIActivityViewController *activityVC = [[UIActivityViewController alloc] init]; + UIImage *thumbnailImage = [provider activityViewController:activityVC thumbnailImageForActivityType:UIActivityTypeAirDrop suggestedSize:CGSizeMake(100, 100)]; + expect(thumbnailImage).to.beNil(); + }); + }); + + describe(@"item", ^{ + it(@"returns the url", ^{ + expect([provider item]).to.equal(url); + }); + + it(@"returns a file for AirDrop sharing", ^{ + OCMockObject *providerMock = [OCMockObject partialMockForObject:provider]; + [[[providerMock stub] andReturn:UIActivityTypeAirDrop] activityType]; + id file = provider.item; + expect(file).toNot.beNil(); + expect(file).to.beKindOf([NSURL class]); + NSURL *fileURL = (id) file; + expect(fileURL.absoluteString).to.endWith(@".Artsy"); + NSData *fileData = [NSData dataWithContentsOfURL:fileURL]; + NSDictionary *data = [NSJSONSerialization JSONObjectWithData:fileData options:0 error:nil]; + expect([data valueForKey:@"version"]).to.equal(1); + expect([data valueForKey:@"url"]).to.equal(url.absoluteString); + [providerMock stopMocking]; + }); + }); + }); + + describe(@"with invalid imageURL", ^{ + __block NSURL *imageURL = [NSURL URLWithString:@"http://image.com/invalid.jpg"]; + + beforeEach(^{ + provider = [[ARURLItemProvider alloc] initWithMessage:@"Message" path:path thumbnailImageURL:imageURL]; + }); + + it(@"sets the imageURL", ^{ + expect(provider.thumbnailImage).to.beNil(); + expect(provider.thumbnailImageURL).to.equal(imageURL); + }); + + it(@"sets placeholderItem", ^{ + expect(provider.placeholderItem).to.equal(url); + }); + + describe(@"thumbnailImageForActivityType", ^{ + it(@"fetches and returns the thumbnail", ^{ + UIActivityViewController *activityVC = [[UIActivityViewController alloc] init]; + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.path isEqualToString:@"/invalid.jpg"]; + } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:404 userInfo:nil]]; + }]; + + UIImage *thumbnailImage = [provider activityViewController:activityVC thumbnailImageForActivityType:@"any activity" suggestedSize:CGSizeMake(100, 100)]; + expect(UIImageJPEGRepresentation(thumbnailImage, 12)).to.equal(UIImageJPEGRepresentation(image, 12)); + expect(provider.thumbnailImage).to.equal(thumbnailImage); + + }); + }); + + describe(@"item", ^{ + it(@"returns the url", ^{ + expect([provider item]).to.equal(url); + }); + }); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ARUserManager+Stubs.h b/Artsy Tests/ARUserManager+Stubs.h new file mode 100644 index 00000000000..a14cfb1295b --- /dev/null +++ b/Artsy Tests/ARUserManager+Stubs.h @@ -0,0 +1,38 @@ +#import "ARUserManager.h" + +@interface ARUserManager (Stubs) + ++(NSString *) stubAccessToken; ++(NSString *) stubAccessTokenExpiresIn; ++(NSString *) stubXappToken; ++(NSString *) stubXappTokenExpiresIn; ++(NSString *) stubUserID; ++(NSString *) stubUserEmail; ++(NSString *) stubUserPassword; ++(NSString *) stubUserName; + ++ (void)stubAccessToken:(NSString*)accessToken expiresIn:(NSString*)expiresIn; ++ (void)stubXappToken:(NSString*)xappToken expiresIn:(NSString*)expiresIn; ++ (void)stubMe:(NSString*)userID email:(NSString*)email name:(NSString*)name; ++ (void)stubAndLoginWithUsername; ++ (void)stubbedLoginWithUsername:(NSString *)username password:(NSString *)password + successWithCredentials:(void(^)(NSString *accessToken, NSDate *expirationDate))credentials + gotUser:(void(^)(User *currentUser))success + authenticationFailure:(void (^)(NSError *error))authFail + networkFailure:(void (^)(NSError *error))networkFailure; ++ (void)stubAndLoginWithFacebookToken; ++ (void)stubbedLoginWithFacebookToken:(NSString *)token + successWithCredentials:(void (^)(NSString *, NSDate *))credentials + gotUser:(void (^)(User *))success + authenticationFailure:(void (^)(NSError *error))authFail + networkFailure:(void (^)(NSError *))networkFailure; ++ (void)stubAndLoginWithTwitterToken; ++ (void)stubbedLoginWithTwitterToken:(NSString *)token + secret:(NSString *)secret + successWithCredentials:(void (^)(NSString *, NSDate *))credentials + gotUser:(void (^)(User *))success + authenticationFailure:(void (^)(NSError *error))authFail + networkFailure:(void (^)(NSError *))networkFailure; ++ (NSString *)userDataPath; + +@end diff --git a/Artsy Tests/ARUserManager+Stubs.m b/Artsy Tests/ARUserManager+Stubs.m new file mode 100644 index 00000000000..0243c5d3d18 --- /dev/null +++ b/Artsy Tests/ARUserManager+Stubs.m @@ -0,0 +1,195 @@ +#import "ARUserManager+Stubs.h" +#import + +@implementation ARUserManager (Stubs) + ++ (NSString *)stubAccessToken { return @"access token"; }; ++ (NSString *)stubAccessTokenExpiresIn { return @"2035-01-02T21:42:21-0500"; }; ++ (NSString *)stubXappToken { return @"xapp token"; }; ++ (NSString *)stubXappTokenExpiresIn { return @"2035-01-02T21:42:21-0500"; }; ++ (NSString *)stubUserID { return @"4d78f315faf6426b4f000011"; }; ++ (NSString *)stubUserEmail { return @"user@example.com"; }; ++ (NSString *)stubUserPassword { return @"password"; }; ++ (NSString *)stubUserName { return @"Joe Shmoe"; }; + ++ (void)stubAccessToken:(NSString*)accessToken expiresIn:(NSString*)expiresIn +{ + [OHHTTPStubs stubJSONResponseAtPath:@"/oauth2/access_token" withResponse:@{ @"access_token": accessToken, @"expires_in": expiresIn }]; +} + ++ (void)stubXappToken:(NSString*)xappToken expiresIn:(NSString*)expiresIn +{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/xapp_token" withResponse:@{ @"xapp_token": xappToken, @"expires_in": expiresIn }]; +} + ++ (void)stubMe:(NSString*)userID email:(NSString*)email name:(NSString*)name +{ + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/me" withResponse:@{ @"id" : userID, @"email": email, @"name": name }]; +} + ++ (void)stubAndLoginWithUsername +{ + [self stubAccessToken:[ARUserManager stubAccessToken] expiresIn:[ARUserManager stubAccessTokenExpiresIn]]; + [self stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + + [self stubbedLoginWithUsername:[ARUserManager stubUserEmail] + password:[ARUserManager stubUserPassword] + successWithCredentials:nil + gotUser:nil + authenticationFailure:nil + networkFailure:nil]; + +} + ++ (void)stubbedLoginWithUsername:(NSString *)username password:(NSString *)password + successWithCredentials:(void(^)(NSString *accessToken, NSDate *expirationDate))credentials + gotUser:(void(^)(User *currentUser))success + authenticationFailure:(void (^)(NSError *error))authFail + networkFailure:(void (^)(NSError *error))networkFailure +{ + __block BOOL done = NO; + [[ARUserManager sharedManager] + loginWithUsername:[ARUserManager stubUserEmail] password:[ARUserManager stubUserPassword] + successWithCredentials:^(NSString *accessToken, NSDate *tokenExpiryDate) { + if (credentials) { + credentials(accessToken, tokenExpiryDate); + } + } + gotUser:^(User *currentUser) { + if (success) { + success(currentUser); + } + done = YES; + } + authenticationFailure:^(NSError *error) { + if (authFail) { + authFail(error); + } + done = YES; + } + networkFailure:^(NSError *error) { + if (networkFailure) { + networkFailure(error); + } + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } +} + ++ (void)stubAndLoginWithFacebookToken +{ + [self stubAccessToken:[ARUserManager stubAccessToken] expiresIn:[ARUserManager stubAccessTokenExpiresIn]]; + [self stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + + [self stubbedLoginWithFacebookToken:@"facebok token" + successWithCredentials:nil + gotUser:nil + authenticationFailure:nil + networkFailure:nil]; +} + ++ (void)stubbedLoginWithFacebookToken:(NSString *)token + successWithCredentials:(void (^)(NSString *, NSDate *))credentials + gotUser:(void (^)(User *))success + authenticationFailure:(void (^)(NSError *error))authFail + networkFailure:(void (^)(NSError *))networkFailure +{ + __block BOOL done = NO; + [[ARUserManager sharedManager] + loginWithFacebookToken:token + successWithCredentials:^(NSString *accessToken, NSDate *tokenExpiryDate) { + if (credentials) { + credentials(accessToken, tokenExpiryDate); + } + } + gotUser:^(User *currentUser) { + if (success) { + success(currentUser); + } + done = YES; + } + authenticationFailure:^(NSError *error) { + if (authFail) { + authFail(error); + } + done = YES; + } + networkFailure:^(NSError *error) { + if (networkFailure) { + networkFailure(error); + } + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } +} + ++ (void)stubAndLoginWithTwitterToken +{ + [self stubAccessToken:[ARUserManager stubAccessToken] expiresIn:[ARUserManager stubAccessTokenExpiresIn]]; + [self stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + + [self stubbedLoginWithTwitterToken:@"twitter token" + secret:@"twitter secret" + successWithCredentials:nil + gotUser:nil + authenticationFailure:nil + networkFailure:nil]; +} + + ++ (void)stubbedLoginWithTwitterToken:(NSString *)token + secret:(NSString *)secret + successWithCredentials:(void (^)(NSString *, NSDate *))credentials + gotUser:(void (^)(User *))success + authenticationFailure:(void (^)(NSError *error))authFail + networkFailure:(void (^)(NSError *))networkFailure +{ + __block BOOL done = NO; + [[ARUserManager sharedManager] loginWithTwitterToken:token secret: secret + successWithCredentials:^(NSString *accessToken, NSDate *tokenExpiryDate) { + if (credentials) { + credentials(accessToken, tokenExpiryDate); + } + } + gotUser:^(User *currentUser) { + if (success) { + success(currentUser); + } + done = YES; + } + authenticationFailure:^(NSError *error) { + if (authFail) { + authFail(error); + } + done = YES; + } + networkFailure:^(NSError *error) { + if (networkFailure) { + networkFailure(error); + } + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } +} + +#pragma mark - +#pragma mark Utilities + ++ (NSString *)userDataPath { + NSString *userID = [[NSUserDefaults standardUserDefaults] objectForKey:ARUserIdentifierDefault]; + if (!userID) { return nil; } + NSArray *directories =[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; + NSString *documentsPath = [[directories lastObject] relativePath]; + return [[documentsPath stringByAppendingPathComponent:userID] stringByAppendingPathComponent:@"User.data"]; +} + +@end diff --git a/Artsy Tests/ARUserManagerTests.m b/Artsy Tests/ARUserManagerTests.m new file mode 100644 index 00000000000..19af9fa7a9b --- /dev/null +++ b/Artsy Tests/ARUserManagerTests.m @@ -0,0 +1,331 @@ +#import "ARUserManager.h" +#import "ARUserManager+Stubs.h" +#import "ARRouter.h" +#import "ARNetworkConstants.h" + +SpecBegin(ARUserManager) + +beforeEach(^{ + [[ARUserManager sharedManager] logout]; +}); + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +describe(@"login", ^{ + + sharedExamplesFor(@"success", ^(NSDictionary *data) { + it(@"stores user data after login", ^{ + NSString *userDataPath = [ARUserManager userDataPath]; + expect([[NSFileManager defaultManager] fileExistsAtPath:userDataPath]).to.beTruthy(); + User *storedUser = [NSKeyedUnarchiver unarchiveObjectWithFile:userDataPath]; + expect(storedUser).toNot.beNil(); + expect(storedUser.userID).to.equal(ARUserManager.stubUserID); + expect(storedUser.email).to.equal(ARUserManager.stubUserEmail); + expect(storedUser.name).to.equal(ARUserManager.stubUserName); + }); + + it(@"remembers access token", ^{ + expect([[ARUserManager sharedManager] hasValidAuthenticationToken]).to.beTruthy(); + expect([ARUserManager stubAccessToken]).to.equal([UICKeyChainStore stringForKey:AROAuthTokenDefault]); + }); + + it(@"remembers access token expiry date", ^{ + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSDate *expiryDate = [dateFormatter dateFromString:[ARUserManager stubAccessTokenExpiresIn]]; + expect([expiryDate isEqualToDate:[[NSUserDefaults standardUserDefaults] objectForKey:AROAuthTokenExpiryDateDefault]]).to.beTruthy(); + }); + + it(@"sets current user", ^{ + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser).toNot.beNil(); + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); + + it(@"sets router auth token", ^{ + NSURLRequest *request = [ARRouter requestForURL:[NSURL URLWithString:@"http://m.artsy.net"]]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).toNot.beNil(); + }); + }); + + describe(@"with username and password", ^{ + beforeEach(^{ + [ARUserManager stubAndLoginWithUsername]; + }); + itBehavesLike(@"success", nil); + }); + + describe(@"with a Facebook token", ^{ + beforeEach(^{ + [ARUserManager stubAndLoginWithFacebookToken]; + }); + itBehavesLike(@"success", nil); + }); + + describe(@"with a Twitter token", ^{ + beforeEach(^{ + [ARUserManager stubAndLoginWithTwitterToken]; + }); + itBehavesLike(@"success", nil); + }); + + it(@"fails with a missing client id", ^{ + [OHHTTPStubs stubJSONResponseAtPath:@"/oauth2/access_token" withResponse:@{ @"error": @"invalid_client", @"error_description": @"missing client_id" } andStatusCode:401]; + + [ARUserManager stubbedLoginWithUsername:[ARUserManager stubUserEmail] password:[ARUserManager stubUserPassword] + successWithCredentials:^(NSString *accessToken, NSDate *tokenExpiryDate) { + XCTFail(@"Expected API failure."); + } gotUser:^(User *currentUser) { + XCTFail(@"Expected API failure."); + } authenticationFailure:^(NSError *error) { + NSHTTPURLResponse *response = (NSHTTPURLResponse *) error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey]; + expect(response.statusCode).to.equal(401); + NSDictionary *recoverySuggestion = [NSJSONSerialization JSONObjectWithData:[error.userInfo[NSLocalizedRecoverySuggestionErrorKey] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; + expect(recoverySuggestion).to.equal(@{ @"error_description" : @"missing client_id", @"error" : @"invalid_client" }); + } networkFailure:^(NSError *error){ + XCTFail(@"Expected API failure."); + }]; + }); + + it(@"fails with an expired token", ^{ + NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow: -(60.0f*60.0f*24.0f)]; + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSString *expiryDate = [dateFormatter stringFromDate:yesterday]; + + [ARUserManager stubAccessToken:[ARUserManager stubAccessToken] expiresIn:expiryDate]; + [ARUserManager stubMe:[ARUserManager stubUserID] email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName]]; + + [ARUserManager stubbedLoginWithUsername:[ARUserManager stubUserEmail] + password:[ARUserManager stubUserPassword] + successWithCredentials:nil + gotUser:nil + authenticationFailure:nil + networkFailure:nil]; + + expect([[ARUserManager sharedManager] hasValidAuthenticationToken]).to.beFalsy(); + }); +}); + +describe(@"logout", ^{ + describe(@"with email and password", ^{ + __block NSString * _userDataPath; + + beforeEach(^{ + [ARUserManager stubAndLoginWithUsername]; + _userDataPath = [ARUserManager userDataPath]; + [[ARUserManager sharedManager] logout]; + }); + + it(@"resets currentUser", ^{ + expect([[ARUserManager sharedManager] currentUser]).to.beNil(); + }); + + it(@"destroys stored user data", ^{ + expect([[NSFileManager defaultManager] fileExistsAtPath:_userDataPath]).to.beFalsy(); + }); + + it(@"unsets router auth token", ^{ + NSURLRequest *request = [ARRouter requestForURL:[NSURL URLWithString:@"http://m.artsy.net"]]; + expect([request valueForHTTPHeaderField:ARAuthHeader]).to.beNil(); + }); + }); + + it(@"clears artsy.net cookies", ^{ + NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; + for(NSHTTPCookie *cookie in [cookieStorage cookiesForURL:[NSURL URLWithString:@"http://artsy.net"]]) { + [cookieStorage deleteCookie:cookie]; + } + NSInteger cookieCount = cookieStorage.cookies.count; + NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary]; + [cookieProperties setObject:@"name" forKey:NSHTTPCookieName]; + [cookieProperties setObject:@"value" forKey:NSHTTPCookieValue]; + [cookieProperties setObject:@"/" forKey:NSHTTPCookiePath]; + [cookieProperties setObject:@"0" forKey:NSHTTPCookieVersion]; + // will delete a cookie in any of the artsy.net domains + for(NSString *artsyHost in ARRouter.artsyHosts) { + [cookieProperties setObject:artsyHost forKey:NSHTTPCookieDomain]; + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:[NSHTTPCookie cookieWithProperties:cookieProperties]]; + } + // don't touch a cookie in a different domain + [cookieProperties setObject:@"example.com" forKey:NSHTTPCookieDomain]; + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:[NSHTTPCookie cookieWithProperties:cookieProperties]]; + expect(cookieStorage.cookies.count).to.equal(cookieCount + ARRouter.artsyHosts.count + 1); + expect([cookieStorage cookiesForURL:[NSURL URLWithString:@"http://artsy.net"]].count).to.equal(1); + [[ARUserManager sharedManager] logout]; + expect(cookieStorage.cookies.count).to.equal(cookieCount + 1); + expect([cookieStorage cookiesForURL:[NSURL URLWithString:@"http://artsy.net"]].count).to.equal(0); + }); +}); + +describe(@"startTrial", ^{ + beforeEach(^{ + [ARUserManager stubXappToken:[ARUserManager stubXappToken] expiresIn:[ARUserManager stubXappTokenExpiresIn]]; + + __block BOOL done = NO; + [[ARUserManager sharedManager] startTrial:^{ + done = YES; + } failure:^(NSError *error) { + XCTFail(@"startTrial: %@", error); + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }); + + it(@"sets an xapp token", ^{ + expect([[ARUserManager stubXappToken] isEqualToString:[UICKeyChainStore stringForKey:ARXAppTokenDefault]]).to.beTruthy(); + }); + + it(@"sets expiry date", ^{ + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSDate *expiryDate = [dateFormatter dateFromString:[ARUserManager stubXappTokenExpiresIn]]; + expect([expiryDate isEqualToDate:[[NSUserDefaults standardUserDefaults] objectForKey:ARXAppTokenExpiryDateDefault]]).to.beTruthy(); + }); +}); + +describe(@"createUserWithName", ^{ + beforeEach(^{ + [ARUserManager stubXappToken:[ARUserManager stubXappToken] expiresIn:[ARUserManager stubXappTokenExpiresIn]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/user" withResponse:@{ @"id": [ARUserManager stubUserID], @"email": [ARUserManager stubUserEmail], @"name": [ARUserManager stubUserName] } andStatusCode:201]; + + __block BOOL done = NO; + [[ARUserManager sharedManager] createUserWithName:[ARUserManager stubUserName] email:[ARUserManager stubUserEmail] password:[ARUserManager stubUserPassword] success:^(User *user) { + done = YES; + } failure:^(NSError *error, id JSON) { + XCTFail(@"createUserWithName: %@", error); + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }); + + it(@"sets current user", ^{ + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser).toNot.beNil(); + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); +}); + +describe(@"createUserViaFacebookWithToken", ^{ + beforeEach(^{ + [ARUserManager stubXappToken:[ARUserManager stubXappToken] expiresIn:[ARUserManager stubXappTokenExpiresIn]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/user" withResponse:@{ @"id": [ARUserManager stubUserID], @"email": [ARUserManager stubUserEmail], @"name": [ARUserManager stubUserName] } andStatusCode:201]; + + __block BOOL done = NO; + [[ARUserManager sharedManager] createUserViaFacebookWithToken:@"facebook token" email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName] success:^(User *user) { + done = YES; + } failure:^(NSError *error, id JSON) { + XCTFail(@"createUserWithFacebookToken: %@", error); + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }); + + it(@"sets current user", ^{ + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser).toNot.beNil(); + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); +}); + +describe(@"createUserViaTwitterWithToken", ^{ + beforeEach(^{ + [ARUserManager stubXappToken:[ARUserManager stubXappToken] expiresIn:[ARUserManager stubXappTokenExpiresIn]]; + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/user" withResponse:@{ @"id": [ARUserManager stubUserID], @"email": [ARUserManager stubUserEmail], @"name": [ARUserManager stubUserName] } andStatusCode:201]; + + __block BOOL done = NO; + [[ARUserManager sharedManager] createUserViaTwitterWithToken:@"twitter token" secret:@"twitter secret" email:[ARUserManager stubUserEmail] name:[ARUserManager stubUserName] success:^(User *user) { + done = YES; + } failure:^(NSError *error, id JSON) { + XCTFail(@"createUserWithFacebookToken: %@", error); + done = YES; + }]; + + while(!done) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }); + + it(@"sets current user", ^{ + User *currentUser = [[ARUserManager sharedManager] currentUser]; + expect(currentUser).toNot.beNil(); + expect(currentUser.userID).to.equal(ARUserManager.stubUserID); + expect(currentUser.email).to.equal(ARUserManager.stubUserEmail); + expect(currentUser.name).to.equal(ARUserManager.stubUserName); + }); +}); + +describe(@"trialUserEmail", ^{ + beforeEach(^{ + [ARUserManager sharedManager].trialUserEmail = nil; + }); + + afterEach(^{ + [ARUserManager sharedManager].trialUserEmail = nil; + }); + + it(@"is nil", ^{ + expect([ARUserManager sharedManager].trialUserEmail).to.beNil(); + }); + + it(@"sets and reads value", ^{ + [ARUserManager sharedManager].trialUserEmail = @"trial@example.com"; + expect([ARUserManager sharedManager].trialUserEmail).to.equal(@"trial@example.com"); + }); +}); + +describe(@"trialUserName", ^{ + beforeEach(^{ + [ARUserManager sharedManager].trialUserName = nil; + }); + + afterEach(^{ + [ARUserManager sharedManager].trialUserName = nil; + }); + + it(@"is nil", ^{ + expect([ARUserManager sharedManager].trialUserName).to.beNil(); + }); + + it(@"sets and reads value", ^{ + [ARUserManager sharedManager].trialUserName = @"Name"; + expect([ARUserManager sharedManager].trialUserName).to.equal(@"Name"); + }); +}); + +describe(@"trialUserUUID", ^{ + beforeEach(^{ + [[ARUserManager sharedManager] resetTrialUserUUID]; + }); + + afterEach(^{ + [[ARUserManager sharedManager] resetTrialUserUUID]; + }); + + it(@"is not", ^{ + expect([ARUserManager sharedManager].trialUserUUID).notTo.beNil(); + }); + + it(@"is persisted", ^{ + NSString *uuid = [ARUserManager sharedManager].trialUserUUID; + expect([ARUserManager sharedManager].trialUserUUID).to.equal(uuid); + }); +}); + +SpecEnd + diff --git a/Artsy Tests/ARValueTransformerTests.m b/Artsy Tests/ARValueTransformerTests.m new file mode 100644 index 00000000000..26f23add30d --- /dev/null +++ b/Artsy Tests/ARValueTransformerTests.m @@ -0,0 +1,34 @@ +#import "ARValueTransformer.h" + +SpecBegin(ARValueTransformerSpec) + +typedef NS_ENUM(NSInteger, TestFeatureType){ + TestFeatureOne, + TestFeatureTwo, + TestFeatureThree +}; + +NSDictionary *featureTypes = @{ + @"one" : @(TestFeatureOne), + @"two" : @(TestFeatureTwo), + @"three" : @(TestFeatureThree) +}; + +__block MTLValueTransformer * transformer = nil; + +describe(@"without default", ^{ + beforeEach(^{ + transformer = [ARValueTransformer enumValueTransformerWithMap:featureTypes]; + }); + it(@"transforms a known value", ^{ + expect([transformer transformedValue:@"one"]).to.equal(TestFeatureOne); + }); + it(@"returns nil for an unknown value", ^{ + expect([transformer transformedValue:@"unknown"]).to.beNil; + }); + it(@"returns nil for nil", ^{ + expect([transformer transformedValue:nil]).to.beNil; + }); +}); + +SpecEnd diff --git a/Artsy Tests/Artsy Tests-Info.plist b/Artsy Tests/Artsy Tests-Info.plist new file mode 100644 index 00000000000..e65b61be3a5 --- /dev/null +++ b/Artsy Tests/Artsy Tests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + net.artsy.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Artsy Tests/Artsy Tests-Prefix.pch b/Artsy Tests/Artsy Tests-Prefix.pch new file mode 100644 index 00000000000..732788b41f1 --- /dev/null +++ b/Artsy Tests/Artsy Tests-Prefix.pch @@ -0,0 +1,37 @@ +// +// Artsy Tests-Prefix.pch +// Artsy +// +// Created by Orta Therox on 01/01/2013. +// Copyright (c) 2013 Artsy. All rights reserved. +// + +#ifdef __OBJC__ + #import + #import + #define EXP_SHORTHAND + #import + #import "SpectaDSL+Sleep.h" + #import + #import + #import + #import + #import + #import + #import + #import + #import + #import + #import "MTLModel+JSON.h" + #import "MTLModel+Dictionary.h" + #import "Constants.h" + #import "Models.h" + #import "Extensions.h" + #import "ARTestContext.h" + #import "StyledSubclasses.h" + #import + #import + #import + #import + #import "UIViewController+PresentWithFrame.h" +#endif diff --git a/Artsy Tests/ArtsyAPI+ArtworksTests.m b/Artsy Tests/ArtsyAPI+ArtworksTests.m new file mode 100644 index 00000000000..0c6e594a502 --- /dev/null +++ b/Artsy Tests/ArtsyAPI+ArtworksTests.m @@ -0,0 +1,47 @@ +#import "ArtsyAPI.h" +#import "ArtsyAPI+Artworks.h" + +SpecBegin(ArtsyAPIArtworks) + +describe(@"relatedFairs", ^{ + it(@"filters out fairs that do not have an organizer or profile", ^{ + __block NSArray *results; + Artwork *artwork = [Artwork modelWithJSON:@{@"id": @"artworkartwork"}]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/related/fairs" + withParams:@{@"artwork": @[@"artworkartwork"]} + withResponse:@[@{ @"organizer": [NSNull null], + @"id": @"no-organizer", + @"name": @"Fair Without Organizer" + }, + @{ @"organizer": @{ + @"default_fair_id": @"no-profile", + @"profile_id": [NSNull null], + @"id": @"no-profile", + @"name": @"no-profile" }, + @"id": @"no-profile", + @"name": @"Fair Without Profile" + }, + @{ @"organizer": @{ + @"default_fair_id": @"organizer-and-profile", + @"profile_id": @"organizer-and-profile", + @"id": @"organizer-and-profile", + @"name": @"organizer-and-profile" }, + @"id": @"organizer-and-profile", + @"name": @"Fair With Organizer With Profile" + }] + ]; + + [ArtsyAPI getFairsForArtwork:artwork success:^(NSArray *fairs) { + results = fairs.copy; + } failure:nil]; + [Expecta setAsynchronousTestTimeout:2]; + expect(results.count).will.equal(1); + expect([(Fair *)results[0] fairID]).will.equal(@"organizer-and-profile"); + + [OHHTTPStubs removeAllStubs]; + }); +}); + +SpecEnd + diff --git a/Artsy Tests/ArtsyAPI+ErrorHandlers.m b/Artsy Tests/ArtsyAPI+ErrorHandlers.m new file mode 100644 index 00000000000..07cfd83f325 --- /dev/null +++ b/Artsy Tests/ArtsyAPI+ErrorHandlers.m @@ -0,0 +1,107 @@ +#import "ArtsyAPI.h" +#import "MutableNSURLResponse.h" + +SpecBegin(ArtsyAPIErrorHandlers) + +describe(@"handleHTTPError", ^{ + it(@"nil error", ^{ + waitUntil(^(DoneCallback done) { + [ArtsyAPI handleHTTPError:nil statusCode:400 errorMessage:nil success:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + } failure:^(NSError *error) { + done(); + }]; + }); + }); + + it(@"plain NSError", ^{ + waitUntil(^(DoneCallback done) { + NSError *error = [[NSError alloc] init]; + [ArtsyAPI handleHTTPError:error statusCode:400 errorMessage:nil success:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + } failure:^(NSError *error) { + done(); + }]; + }); + }); + + it(@"wrong status code", ^{ + waitUntil(^(DoneCallback done) { + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:302 userInfo:@{}]; + [ArtsyAPI handleHTTPError:error statusCode:404 errorMessage:nil success:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + } failure:^(NSError *error) { + done(); + }]; + }); + }); + + it(@"correct status code and any error message", ^{ + waitUntil(^(DoneCallback done) { + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:401 userInfo:@{ + NSLocalizedRecoverySuggestionErrorKey: @"{\"error\":\"Unauthorized\",\"text\":\"The XAPP token is invalid or has expired.\"}", + AFNetworkingOperationFailingURLResponseErrorKey: [[MutableNSURLResponse alloc] initWithStatusCode:401] + }]; + [ArtsyAPI handleHTTPError:error statusCode:401 errorMessage:nil success:^(NSError *error) { + done(); + } failure:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + }]; + }); + }); + + it(@"correct status code and wrong error message", ^{ + waitUntil(^(DoneCallback done) { + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:401 userInfo:@{ + NSLocalizedRecoverySuggestionErrorKey: @"{\"error\":\"Unauthorized\",\"text\":\"The XAPP token is invalid or has expired.\"}", + AFNetworkingOperationFailingURLResponseErrorKey: [[MutableNSURLResponse alloc] initWithStatusCode:401] + }]; + [ArtsyAPI handleHTTPError:error statusCode:401 errorMessage:@"Unexpected" success:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + } failure:^(NSError *error) { + done(); + }]; + + }); + }); + + it(@"correct status code and correct error message", ^{ + waitUntil(^(DoneCallback done) { + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:401 userInfo:@{ + NSLocalizedRecoverySuggestionErrorKey: @"{\"error\":\"Unauthorized\",\"text\":\"The XAPP token is invalid or has expired.\"}", + AFNetworkingOperationFailingURLResponseErrorKey: [[MutableNSURLResponse alloc] initWithStatusCode:401] + }]; + [ArtsyAPI handleHTTPError:error statusCode:401 errorMessage:@"Unauthorized" success:^(NSError *error) { + done(); + } failure:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + }]; + }); + }); +}); + +describe(@"handleHTTPErrors", ^{ + it(@"correct status code and one correct error message", ^{ + waitUntil(^(DoneCallback done) { + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:401 userInfo:@{ + NSLocalizedRecoverySuggestionErrorKey: @"{\"error\":\"Unauthorized\",\"text\":\"The XAPP token is invalid or has expired.\"}", + AFNetworkingOperationFailingURLResponseErrorKey: [[MutableNSURLResponse alloc] initWithStatusCode:401] + }]; + [ArtsyAPI handleHTTPError:error statusCode:401 errorMessages:@[@"Foo", @"Unauthorized"] success:^(NSError *error) { + done(); + } failure:^(NSError *error) { + expect(false).to.beTruthy(); + done(); + }]; + }); + }); +}); + +SpecEnd + diff --git a/Artsy Tests/ArtsyAPI+PrivateTests.m b/Artsy Tests/ArtsyAPI+PrivateTests.m new file mode 100644 index 00000000000..b965ea4ed24 --- /dev/null +++ b/Artsy Tests/ArtsyAPI+PrivateTests.m @@ -0,0 +1,31 @@ +#import "ArtsyAPI.h" +#import "ArtsyAPI+Private.h" +#import "MutableNSURLResponse.h" + +SpecBegin(ArtsyAPIPrivate) + +describe(@"handleXappTokenError", ^{ + it(@"doesn't reset XAPP token on non-401 errors", ^{ + [UICKeyChainStore setString:@"xapp token" forKey:ARXAppTokenDefault]; + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:302 userInfo:@{}]; + [ArtsyAPI handleXappTokenError:error]; + expect([UICKeyChainStore stringForKey:ARXAppTokenDefault]).to.equal(@"xapp token"); + }); + + it(@"resets XAPP token on error", ^{ + [UICKeyChainStore setString:@"value" forKey:ARXAppTokenDefault]; + id mock = [OCMockObject mockForClass:[ARRouter class]]; + [[mock expect] setXappToken:nil]; + NSError *error = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:401 userInfo:@{ + NSLocalizedRecoverySuggestionErrorKey: @"{\"error\":\"Unauthorized\",\"text\":\"The XAPP token is invalid or has expired.\"}", + AFNetworkingOperationFailingURLResponseErrorKey: [[MutableNSURLResponse alloc] initWithStatusCode:401] + }]; + [ArtsyAPI handleXappTokenError:error]; + [mock verify]; + [mock stopMocking]; + expect([UICKeyChainStore stringForKey:ARXAppTokenDefault]).to.beNil(); + }); +}); + +SpecEnd + diff --git a/Artsy Tests/ArtsyAPI+SystemTimeTests.m b/Artsy Tests/ArtsyAPI+SystemTimeTests.m new file mode 100644 index 00000000000..8cc8b33b302 --- /dev/null +++ b/Artsy Tests/ArtsyAPI+SystemTimeTests.m @@ -0,0 +1,43 @@ +#import "ArtsyAPI.h" +#import "ArtsyAPI+SystemTime.h" +#import "ARNetworkConstants.h" + +SpecBegin(ArtsyAPISystemTime) + +beforeEach(^{ + [OHHTTPStubs stubJSONResponseAtPath:ARSystemTimeURL withResponse:@{ + @"time": @"2422-03-24T13:29:34Z", + @"day": @(24), + @"wday": @(4), + @"month": @(3), + @"year": @(2422), + @"hour": @(13), + @"min": @(29), + @"sec": @(34), + @"dst": @(NO), + @"unix": @(1395926974), + @"utc_offset": @(0), + @"zone": @"UTC", + @"iso8601": @"2422-03-24T13:29:34Z" + }]; +}); + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +it(@"retrieves system time", ^{ + waitUntil(^(DoneCallback done) { + [ArtsyAPI getSystemTime:^(SystemTime *systemTime) { + expect(systemTime).toNot.beNil(); + NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:systemTime.date]; + expect(components.year).to.equal(2422); + expect(components.month).to.equal(3); + expect(components.day).to.equal(24); + done(); + } failure:nil]; + }); +}); + +SpecEnd + diff --git a/Artsy Tests/Artwork+Extensions.h b/Artsy Tests/Artwork+Extensions.h new file mode 100644 index 00000000000..f84d693d344 --- /dev/null +++ b/Artsy Tests/Artwork+Extensions.h @@ -0,0 +1,4 @@ +@interface Artwork (Extensions) ++ (id)stubbedArtwork; ++ (id)stubbedArtworkJSON; +@end diff --git a/Artsy Tests/Artwork+Extensions.m b/Artsy Tests/Artwork+Extensions.m new file mode 100644 index 00000000000..d30ea55d666 --- /dev/null +++ b/Artsy Tests/Artwork+Extensions.m @@ -0,0 +1,15 @@ +#import "Artwork+Extensions.h" + +@implementation Artwork (Extensions) + ++ (id)stubbedArtwork +{ + return [Artwork modelWithJSON:[self stubbedArtworkJSON]]; +} + ++ (id)stubbedArtworkJSON +{ + return @{ @"id" : @"stubbed" }; +} + +@end diff --git a/Artsy Tests/ArtworkTests.m b/Artsy Tests/ArtworkTests.m new file mode 100644 index 00000000000..d387082d680 --- /dev/null +++ b/Artsy Tests/ArtworkTests.m @@ -0,0 +1,105 @@ +SpecBegin(Artwork) + +describe(@"hasHeightAndWidth", ^{ + it(@"with height and width", ^{ + Artwork *artwork = [Artwork modelWithJSON:@{ @"id" : @"artwork-id", @"width" : @(100), @"height" : @(200) }]; + expect(artwork.hasWidth).to.beTruthy(); + expect(artwork.hasHeight).to.beTruthy(); + expect(artwork.hasDepth).to.beFalsy(); + expect(artwork.hasDiameter).to.beFalsy(); + expect(artwork.hasWidthAndHeight).to.beTruthy(); + }); + + it(@"without width", ^{ + Artwork *artwork = [Artwork modelWithJSON:@{ @"id" : @"artwork-id", @"height" : @(200) }]; + expect(artwork.hasWidth).to.beFalsy(); + expect(artwork.hasHeight).to.beTruthy(); + expect(artwork.hasDepth).to.beFalsy(); + expect(artwork.hasDiameter).to.beFalsy(); + expect(artwork.hasWidthAndHeight).to.beFalsy(); + }); + + it(@"with depth", ^{ + Artwork *artwork = [Artwork modelWithJSON:@{ @"id" : @"artwork-id", @"depth" : @(200) }]; + expect(artwork.hasWidth).to.beFalsy(); + expect(artwork.hasHeight).to.beFalsy(); + expect(artwork.hasDepth).to.beTruthy(); + expect(artwork.hasDiameter).to.beFalsy(); + expect(artwork.hasWidthAndHeight).to.beFalsy(); + }); +}); + +describe(@"defaultImage", ^{ + __block Artwork *artwork; + __block NSDictionary *artworkJSON; + before(^{ + artwork = nil; + }); + it(@"sets default image to the appropriate image", ^{ + artworkJSON = @{ + @"id": @"artwork_id", + @"artist":@{ @"id":@"artist_id" }, + @"images":@[ + @{@"id": @"image_1_id", + @"is_default": @NO, + @"image_versions": @[@"small", @"square"]}, + @{@"id": @"image_2_id", + @"is_default": @YES, + @"image_versions": @[@"small", @"square"]}] + }; + expect(^{ + artwork = [Artwork modelWithJSON: artworkJSON]; + }).notTo.raiseAny(); + expect(artwork.defaultImage).notTo.beNil(); + expect(artwork.defaultImage.imageID).to.equal(@"image_2_id"); + }); + + it(@"sets default image to nil if no default", ^{ + artworkJSON = @{ + @"id":@"artwork_id", + @"artist":@{ @"id":@"artist_id" }, + @"images":@[ + @{@"id": @"image_1_id", + @"is_default": @NO, + @"image_versions": @[@"small", @"square"]}, + @{@"id": @"image_2_id", + @"is_default": @NO, + @"image_versions": @[@"small", @"square"]}] + }; + expect(^{ + artwork = [Artwork modelWithJSON: artworkJSON]; + }).notTo.raiseAny(); + expect(artwork.defaultImage).to.beNil(); + + }); + + it(@"sets default image to nil if no images", ^{ + artworkJSON = @{ + @"id":@"artwork_id", + @"artist":@{ @"id":@"artist_id" }, + @"images":@[] + }; + expect(^{ + artwork = [Artwork modelWithJSON: artworkJSON]; + }).notTo.raiseAny(); + expect(artwork.defaultImage).to.beNil(); + + }); + + + it(@"migrates model from version 0 to 1", ^{ + NSString *artworkData_v0 = [[NSBundle bundleForClass:[self class]] pathForResource:@"Artwork_v0" ofType:@"data"]; + Artwork *deserializedArtwork = [NSKeyedUnarchiver unarchiveObjectWithFile:artworkData_v0]; + expect(deserializedArtwork.additionalInfo).to.equal(@"In the collection of European Painting and Sculpture at LACMA.\n\nPaul Rodman Mabury Collection (39.12.3)"); + }); + + it(@"loads model version 1", ^{ + NSString *artworkData_v1 = [[NSBundle bundleForClass:[self class]] pathForResource:@"Artwork_v1" ofType:@"data"]; + Artwork *deserializedArtwork = [NSKeyedUnarchiver unarchiveObjectWithFile:artworkData_v1]; + expect(deserializedArtwork.additionalInfo).to.equal(@"In the collection of European Painting and Sculpture at LACMA.\n\nPaul Rodman Mabury Collection (39.12.3)"); + }); +}); + + + +SpecEnd diff --git a/Artsy Tests/Artwork_v0.data b/Artsy Tests/Artwork_v0.data new file mode 100644 index 00000000000..ab8b6951844 Binary files /dev/null and b/Artsy Tests/Artwork_v0.data differ diff --git a/Artsy Tests/Artwork_v1.data b/Artsy Tests/Artwork_v1.data new file mode 100644 index 00000000000..9f8e0c8e864 Binary files /dev/null and b/Artsy Tests/Artwork_v1.data differ diff --git a/Artsy Tests/Bid+Extensions.h b/Artsy Tests/Bid+Extensions.h new file mode 100644 index 00000000000..b871551e2a4 --- /dev/null +++ b/Artsy Tests/Bid+Extensions.h @@ -0,0 +1,3 @@ +@interface Bid (Extensions) ++ (Bid *)bidWithCents:(NSNumber *)cents bidID:(NSString *)bidID; +@end diff --git a/Artsy Tests/Bid+Extensions.m b/Artsy Tests/Bid+Extensions.m new file mode 100644 index 00000000000..adc6f7224a8 --- /dev/null +++ b/Artsy Tests/Bid+Extensions.m @@ -0,0 +1,10 @@ +#import "Bid+Extensions.h" + +@implementation Bid (Extensions) + ++ (Bid *)bidWithCents:(NSNumber *)cents bidID:(NSString *)bidID +{ + return [Bid modelWithJSON:@{ @"cents" : cents, @"id" : bidID }]; +} + +@end diff --git a/Artsy Tests/Extensions.h b/Artsy Tests/Extensions.h new file mode 100644 index 00000000000..442c96dc404 --- /dev/null +++ b/Artsy Tests/Extensions.h @@ -0,0 +1,6 @@ +#import "Sale+Extensions.h" +#import "Bid+Extensions.h" +#import "SaleArtwork+Extensions.h" +#import "OHHTTPStubs+JSON.h" +#import "OCMArg+ClassChecker.h" +#import "ARExpectaExtensions.h" \ No newline at end of file diff --git a/Artsy Tests/FairTests.m b/Artsy Tests/FairTests.m new file mode 100644 index 00000000000..bbbfa732a02 --- /dev/null +++ b/Artsy Tests/FairTests.m @@ -0,0 +1,177 @@ +#import "ARFeedItems.h" + +@interface Fair () + +@property (nonatomic, copy) NSArray *maps; + +@end + +SpecBegin(Fair) + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +it(@"initialize", ^{ + Fair *fair = [Fair modelWithJSON:@{ @"id" : @"fair-id" }]; + expect(fair.fairID).to.equal(@"fair-id"); + expect(fair.organizer).to.beNil(); +}); + +describe(@"generates a location", ^{ + it(@"has no location", ^{ + Fair *fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + }]; + expect(fair.location).to.beNil(); + }); + + it(@"as a city", ^{ + Fair *fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"location" : @{ @"city": @"Toronto" } + }]; + expect(fair.location).to.equal(@"Toronto"); + }); + + it(@"has a state", ^{ + Fair *fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"location" : @{ @"state": @"ON" } + }]; + expect(fair.location).to.equal(@"ON"); + }); + + it(@"has a city and a state", ^{ + Fair *fair = [Fair modelWithJSON:@{ + @"id" : @"fair-id", + @"location" : @{ @"city": @"Toronto", @"state" : @"ON" } + }]; + expect(fair.location).to.equal(@"Toronto, ON"); + }); +}); + +it(@"getPosts", ^{ + Fair *fair = [Fair modelFromDictionary:@{ + @"organizer" : [FairOrganizer modelFromDictionary:@{ + @"profileID" : @"art-los-angeles-contemporary-profile" + }] + }]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/profile/art-los-angeles-contemporary-profile/posts" + withResponse:@{ @"results" : @[ @{ @"id": @"post-id", @"title": @"Post Title", @"_type" : @"Post" } ] }]; + + __block ARFeedTimeline * _feedTimeline = nil; + [fair getPosts:^(ARFeedTimeline *feedTimeline) { + _feedTimeline = feedTimeline; + }]; + + expect(_feedTimeline).willNot.beNil(); + expect([_feedTimeline numberOfItems]).to.equal(1); + ARPostFeedItem *item = (ARPostFeedItem *) [_feedTimeline itemAtIndex:0]; + expect(item).toNot.beNil(); + expect([item feedItemID]).to.equal(@"post-id"); +}); + +it(@"getOrderedSets", ^{ + Fair *fair = [Fair modelWithDictionary:@{ @"fairID" : @"fair-id" } error:nil]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/sets" withResponse:@[ + @{ @"id": @"52ded6edc9dc24bccc0000a4", @"key": @"curator", @"name" : @"Highlights from Art World Influencers", @"item_type" : @"FeaturedLink" }, + @{ @"id": @"52ded6edc9dc24bccc0000a5", @"key": @"curator", @"name" : @"Other Highlights", @"item_type" : @"FeaturedLink" }, + @{ @"id": @"52ded6edc9dc24bccc0000b5", @"key": @"something else", @"name" : @"Something Else", @"item_type" : @"FeaturedLink" } + ]]; + + __block NSMutableDictionary * _orderedSets = nil; + [fair getOrderedSets:^(NSMutableDictionary *orderedSets) { + _orderedSets = orderedSets; + }]; + + expect(_orderedSets).willNot.beNil(); + expect(_orderedSets.count).to.equal(2); + + NSArray *curatorOrderedSets = _orderedSets[@"curator"]; + expect(curatorOrderedSets).toNot.beNil(); + expect(curatorOrderedSets.count).to.equal(2); + + OrderedSet *first = (OrderedSet *) curatorOrderedSets[0]; + expect(first).toNot.beNil(); + expect(first.orderedSetID).to.equal(@"52ded6edc9dc24bccc0000a4"); + expect(first.name).to.equal(@"Highlights from Art World Influencers"); + expect(first.orderedSetDescription).to.beNil(); + // TODO: item type +}); + +describe(@"getting shows", ^{ + Fair *fair = [Fair modelFromDictionary:@{ @"fairID" : @"fair-id" }]; + + beforeEach(^{ + [OHHTTPStubs + stubJSONResponseAtPath:@"/api/v1/fair/fair-id/shows" + withResponse:@{ + @"results": @[@{ + @"id": @"thomas-solomon-gallery-thomas-solomon-gallery-at-art-los-angeles-contemporary-2014", + @"name": @"Thomas Solomon Gallery at Art Los Angeles Contemporary 2014", + @"_type": @"PartnerShow" + }] + }]; + }); + + it(@"maintains its maps", ^{ + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/fair/fair-id" withResponse:@{ + @"id" : @"fair-id", + @"name" : @"The Fair Name", + @"start_at" : @"1976-01-30T15:00:00+00:00", + @"end_at" : @"1976-02-02T15:00:00+00:00" + }]; + + __block BOOL called = NO; + fair.maps = [NSArray array]; + [fair updateFair:^{ + called = YES; + }]; + + expect(called).will.beTruthy(); + expect(fair.maps).willNot.beNil(); + }); + + it(@"return a fair's shows", ^{ + __block NSArray *shows = nil; + + [fair + onShowsUpdate:^(NSArray *result) { + shows = result; + } + failure:^(NSError *error) { + + }]; + + expect(shows).willNot.beNil(); + expect(shows).will.haveCountOf(1); + expect([shows.firstObject name]).will.equal(@"Thomas Solomon Gallery at Art Los Angeles Contemporary 2014"); + }); +}); + +describe(@"getting image URL string", ^{ + it(@"gets nil for empty raw string", ^{ + Fair *fair = [Fair modelFromDictionary:@{ @"fairID" : @"fair-id" }]; + expect([fair bannerAddress]).to.beNil(); + }); + + it (@"gets nil for non-existent version", ^{ + Fair *fair = [Fair modelFromDictionary:@{ @"fairID" : @"fair-id", @"rawImageURLString": @"http://something/:version.jpg", @"imageVersions": @[ + @"something_that_we_do_not_support" + ]}]; + expect([fair bannerAddress]).to.beNil(); + }); + + it (@"gets wide if availble", ^{ + Fair *fair = [Fair modelFromDictionary:@{ @"fairID" : @"fair-id", @"rawImageURLString": @"http://something/:version.jpg", @"imageVersions": @[ + @"wide", @"square" + ]}]; + expect([fair bannerAddress]).to.equal(@"http://something/wide.jpg"); + }); +}); + +SpecEnd diff --git a/Artsy Tests/MapFeatureTests.m b/Artsy Tests/MapFeatureTests.m new file mode 100644 index 00000000000..8a1af9f0675 --- /dev/null +++ b/Artsy Tests/MapFeatureTests.m @@ -0,0 +1,9 @@ +SpecBegin(MapFeature) + +it(@"maps every feature type to an image", ^{ + for (int mapFeatureType = 0; mapFeatureType < ARMapFeatureTypeMax; mapFeatureType++) { + expect(NSStringFromARMapFeatureType((enum ARMapFeatureType) mapFeatureType)).toNot.beNil(); + } +}); + +SpecEnd diff --git a/Artsy Tests/MutableNSURLResponse.h b/Artsy Tests/MutableNSURLResponse.h new file mode 100644 index 00000000000..49981691560 --- /dev/null +++ b/Artsy Tests/MutableNSURLResponse.h @@ -0,0 +1,4 @@ +@interface MutableNSURLResponse : NSURLResponse +@property(nonatomic, assign) NSInteger statusCode; +-(id)initWithStatusCode:(NSInteger)statusCode; +@end diff --git a/Artsy Tests/MutableNSURLResponse.m b/Artsy Tests/MutableNSURLResponse.m new file mode 100644 index 00000000000..2a26a51eef5 --- /dev/null +++ b/Artsy Tests/MutableNSURLResponse.m @@ -0,0 +1,15 @@ +#import "MutableNSURLResponse.h" + +@implementation MutableNSURLResponse + +-(id)initWithStatusCode:(NSInteger)statusCode +{ + self = [super init]; + if (self) { + _statusCode = statusCode; + } + return self; +} + +@end + diff --git a/Artsy Tests/OCMArg+ClassChecker.h b/Artsy Tests/OCMArg+ClassChecker.h new file mode 100644 index 00000000000..e3de5af6a06 --- /dev/null +++ b/Artsy Tests/OCMArg+ClassChecker.h @@ -0,0 +1,7 @@ +#import "OCMArg.h" + +@interface OCMArg (ClassChecker) + ++ (id)checkForClass:(Class)klass; + +@end diff --git a/Artsy Tests/OCMArg+ClassChecker.m b/Artsy Tests/OCMArg+ClassChecker.m new file mode 100644 index 00000000000..ff40880890b --- /dev/null +++ b/Artsy Tests/OCMArg+ClassChecker.m @@ -0,0 +1,11 @@ +#import "OCMArg+ClassChecker.h" + +@implementation OCMArg (ClassChecker) + ++ (id)checkForClass:(Class)klass { + return [self checkWithBlock:^BOOL(id obj) { + return [obj isKindOfClass:klass]; + }]; +} + +@end diff --git a/Artsy Tests/OHHTTPStubs+JSON.h b/Artsy Tests/OHHTTPStubs+JSON.h new file mode 100644 index 00000000000..9b20762a6ab --- /dev/null +++ b/Artsy Tests/OHHTTPStubs+JSON.h @@ -0,0 +1,8 @@ +@interface OHHTTPStubs(JSON) + ++(void)stubJSONResponseAtPath:(NSString *)path withResponse:(id)response; ++(void)stubJSONResponseAtPath:(NSString *)path withResponse:(id)response andStatusCode:(NSInteger)code; ++(void)stubJSONResponseAtPath:(NSString *)path withParams:(NSDictionary *)params withResponse:(id)response; ++(void)stubJSONResponseAtPath:(NSString *)path withParams:(NSDictionary *)params withResponse:(id)response andStatusCode:(NSInteger)code; + +@end diff --git a/Artsy Tests/OHHTTPStubs+JSON.m b/Artsy Tests/OHHTTPStubs+JSON.m new file mode 100644 index 00000000000..51765647ea4 --- /dev/null +++ b/Artsy Tests/OHHTTPStubs+JSON.m @@ -0,0 +1,47 @@ +#import +#import +#import "OHHTTPStubs+JSON.h" +#import "ARRouter.h" + +@implementation OHHTTPStubs(JSON) + ++(void)stubJSONResponseAtPath:(NSString *)path withResponse:(id)response +{ + [OHHTTPStubs stubJSONResponseAtPath:path withResponse:response andStatusCode:200]; +} + ++(void)stubJSONResponseAtPath:(NSString *)path withResponse:(id)response andStatusCode:(NSInteger)code; +{ + [OHHTTPStubs stubJSONResponseAtPath:path withParams:nil withResponse:response andStatusCode:code]; +} + ++(void)stubJSONResponseAtPath:(NSString *)path withParams:(NSDictionary *)params withResponse:(id)response +{ + [OHHTTPStubs stubJSONResponseAtPath:path withParams:params withResponse:response andStatusCode:200]; +} + ++(void)stubJSONResponseAtPath:(NSString *)path withParams:(NSDictionary *)params withResponse:(id)response andStatusCode:(NSInteger)code +{ + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + NSURLComponents *requestComponents = [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO]; + NSString *urlString = path; + + if (params) { + NSString *stubbedQueryString = AFQueryStringFromParametersWithEncoding(params, NSUTF8StringEncoding); + urlString = [urlString stringByAppendingFormat:@"?%@", stubbedQueryString]; + } + + NSURL *stubbedURL = [NSURL URLWithString:urlString]; + NSURLComponents *stubbedComponents = [NSURLComponents componentsWithURL:stubbedURL resolvingAgainstBaseURL:NO]; + + BOOL pathsMatch = [requestComponents.path isEqualToString:stubbedComponents.path]; + BOOL queriesMatch = params ? [requestComponents.query isEqualToString:stubbedComponents.query] : YES; + return (pathsMatch && queriesMatch); + + } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) { + NSData *data = [NSJSONSerialization dataWithJSONObject:response options:0 error:nil]; + return [OHHTTPStubsResponse responseWithData:data statusCode:(int)code headers:@{ @"Content-Type": @"application/json" }]; + }]; +} + +@end diff --git a/Artsy Tests/ORStackViewArtsyCategoriesTests.m b/Artsy Tests/ORStackViewArtsyCategoriesTests.m new file mode 100644 index 00000000000..048189b131d --- /dev/null +++ b/Artsy Tests/ORStackViewArtsyCategoriesTests.m @@ -0,0 +1,85 @@ +#import +#import "ORStackView+ArtsyViews.h" + +@interface ORStackView(Testing) +@property (nonatomic, strong) NSMutableArray *viewStack; +@end + +SpecBegin(ORStackViewArtsyCategories) + +describe(@"when adding a page title", ^{ + it(@"adds another view to the stack", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + expect(stackView.viewStack.count).to.equal(0); + + [stackView addPageTitleWithString:@"Test"]; + expect(stackView.viewStack.count).to.equal(1); + }); + + it(@"sets the title correctly", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + NSString *title = @"Testering"; + [stackView addPageTitleWithString:title]; + + UIView *titleLabel = [stackView.viewStack.firstObject view]; + expect(titleLabel).to.beKindOf([UILabel class]); + expect([((UILabel *) titleLabel) text]).to.equal(title.uppercaseString); + }); + + it(@"returns the title label", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + NSString *title = @"Testering"; + UIView *titleLabel = [stackView addPageTitleWithString:title]; + + expect(titleLabel).to.beKindOf([UILabel class]); + expect([((UILabel *) titleLabel) text]).to.equal(title.uppercaseString); + }); + +}); + +describe(@"when adding a page subtitle", ^{ + it(@"adds another view to the stack", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + expect(stackView.viewStack.count).to.equal(0); + + [stackView addPageSubtitleWithString:@"Test"]; + expect(stackView.viewStack.count).to.equal(1); + }); + + it(@"sets the title correctly", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + NSString *title = @"Testering2"; + [stackView addPageSubtitleWithString:title]; + + UIView *titleLabel = [stackView.viewStack.firstObject view]; + expect(titleLabel).to.beKindOf([UILabel class]); + expect([((UILabel *) titleLabel) text]).to.equal(title.uppercaseString); + }); +}); + +describe(@"when adding an alt page title and subtitle", ^{ + it(@"adds 2 views to the stack", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + expect(stackView.viewStack.count).to.equal(0); + + [stackView addSerifPageTitle:@"title" subtitle:@"subtitle"]; + expect(stackView.viewStack.count).to.equal(2); + }); + + it(@"sets the title correctly", ^{ + ORStackView *stackView = [[ORStackView alloc] init]; + NSString *title = @"Testering3"; + NSString *subtitle = @"Testering3"; + [stackView addSerifPageTitle:title subtitle:subtitle]; + + UILabel *hopefullyTitleLabel = (id)[stackView.viewStack.firstObject view]; + expect([hopefullyTitleLabel text]).to.equal(title); + + UILabel *hopefullySubtitleLabel = (id)[stackView.viewStack[1] view]; + expect([hopefullySubtitleLabel text]).to.equal(subtitle); + }); +}); + + +SpecEnd + diff --git a/Artsy Tests/OrderedSetTests.m b/Artsy Tests/OrderedSetTests.m new file mode 100644 index 00000000000..91697af6409 --- /dev/null +++ b/Artsy Tests/OrderedSetTests.m @@ -0,0 +1,64 @@ +SpecBegin(OrderedSet) + +afterEach(^{ + [OHHTTPStubs removeAllStubs]; +}); + +it(@"initialize", ^{ + OrderedSet *orderedSet = [OrderedSet modelFromDictionary:@{ @"orderedSetID" : @"set-id" }]; + expect(orderedSet.orderedSetID).to.equal(@"set-id"); +}); + +it(@"getItems(FeaturedLink)", ^{ + OrderedSet *orderedSet = [OrderedSet modelFromDictionary:@{ @"orderedSetID" : @"set-id", @"itemType" : @"FeaturedLink" }]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/set-id/items" withResponse:@[ + @{ @"id": @"one", @"href": @"/post/moby-my-highlights-from-art-los-angeles-contemporary", @"title" : @"Moby" }, + @{ @"id": @"two", @"href": @"/post/iris_marden-my-highlights-from-art-los-angeles-contemporary", @"title" : @"Iris Marden" }, + ]]; + + __block NSArray * _orderedSetItems = nil; + [orderedSet getItems:^(NSArray *items) { + _orderedSetItems = items; + }]; + + expect(_orderedSetItems).willNot.beNil(); + expect(_orderedSetItems.count).to.equal(2); + expect(_orderedSetItems.firstObject).to.beKindOf([FeaturedLink class]); + + FeaturedLink *first = _orderedSetItems[0]; + expect(first).toNot.beNil(); + expect(first.title).to.equal(@"Moby"); + expect(first.href).to.equal(@"/post/moby-my-highlights-from-art-los-angeles-contemporary"); +}); + +it(@"getItems(Sale)", ^{ + OrderedSet *orderedSet = [OrderedSet modelFromDictionary:@{ @"orderedSetID" : @"set-id", @"itemType" : @"Sale" }]; + + [OHHTTPStubs stubJSONResponseAtPath:@"/api/v1/set/set-id/items" withResponse:@[ + @{ @"id": @"frieze-london-sale-2013", @"name": @"Frieze London Sale 2013" } + ]]; + + __block NSArray * _orderedSetItems = nil; + [orderedSet getItems:^(NSArray *items) { + _orderedSetItems = items; + }]; + + expect(_orderedSetItems).willNot.beNil(); + expect(_orderedSetItems.count).to.equal(1); + expect(_orderedSetItems.firstObject).to.beKindOf([Sale class]); + + Sale *first = _orderedSetItems[0]; + expect(first).toNot.beNil(); + expect(first.saleID).to.equal(@"frieze-london-sale-2013"); + expect(first.name).to.equal(@"Frieze London Sale 2013"); +}); + +it(@"getItems raises an exception when retrieving items of an unsupported time", ^{ + OrderedSet *orderedSet = [OrderedSet modelFromDictionary:@{ @"orderedSetID" : @"set-id", @"itemType" : @"Invalid" }]; + expect(^{ + [orderedSet getItems:nil]; + }).to.raiseWithReason(@"NSInternalInconsistencyException", @"Unsupported item type: Invalid"); +}); + +SpecEnd diff --git a/Artsy Tests/PartnerTests.m b/Artsy Tests/PartnerTests.m new file mode 100644 index 00000000000..3087bcb8f80 --- /dev/null +++ b/Artsy Tests/PartnerTests.m @@ -0,0 +1,25 @@ +SpecBegin(Partner) + +describe(@"defaultImage", ^{ + __block Partner *partner; + __block NSDictionary *partnerJSON; + + before(^{ + partner = nil; + }); + + it(@"sets profile_public to NO if nil", ^{ + partnerJSON = @{ + @"id": @"profile_id", + @"default_profile_id":@"profile_id", + @"default_profile_public":[NSNull null] + }; + expect(^{ + partner = [Partner modelWithJSON: partnerJSON]; + }).notTo.raiseAny(); + expect(partner.defaultProfilePublic).notTo.beNil(); + expect(partner.defaultProfilePublic).to.beFalsy; + }); +}); + +SpecEnd \ No newline at end of file diff --git a/Artsy Tests/ProfileTests.m b/Artsy Tests/ProfileTests.m new file mode 100644 index 00000000000..624d960f939 --- /dev/null +++ b/Artsy Tests/ProfileTests.m @@ -0,0 +1,111 @@ +SpecBegin(Profile) + +__block Profile *profile; + +describe(@"User", ^{ + beforeEach(^{ + profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"User", + @"owner" : @{ + @"id" : @"user-id", + @"type" : @"User" + } + }]; + }); + + it(@"creates an owner of type User", ^{ + expect(profile.profileID).to.equal(@"profile-id"); + expect(profile.profileOwner).to.beKindOf([User class]); + expect([(User *)profile.profileOwner userID]).to.equal(@"user-id"); + }); + + it(@"can be serialized", ^{ + NSData *profileData = [NSKeyedArchiver archivedDataWithRootObject:profile]; + Profile *deserializedProfile = [NSKeyedUnarchiver unarchiveObjectWithData:profileData]; + expect(deserializedProfile.profileID).to.equal(profile.profileID); + expect(deserializedProfile.profileOwner).to.beKindOf([User class]); + expect([(User *)deserializedProfile.profileOwner userID]).to.equal(@"user-id"); + }); +}); + +describe(@"FairOrganizer", ^{ + beforeEach(^{ + profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"FairOrganizer", + @"owner" : @{ + @"id" : @"fair-organizer-id", + @"default_fair_id" : @"default-fair-id" + } + }]; + }); + + it(@"creates an owner of type FairOrganizer", ^{ + expect(profile.profileID).to.equal(@"profile-id"); + expect(profile.profileOwner).to.beKindOf([FairOrganizer class]); + expect([(FairOrganizer *)profile.profileOwner fairOrganizerID]).to.equal(@"fair-organizer-id"); + }); + + it(@"can be serialized", ^{ + NSData *profileData = [NSKeyedArchiver archivedDataWithRootObject:profile]; + Profile *deserializedProfile = [NSKeyedUnarchiver unarchiveObjectWithData:profileData]; + expect(deserializedProfile.profileID).to.equal(profile.profileID); + expect(deserializedProfile.profileOwner).to.beKindOf([FairOrganizer class]); + expect([(FairOrganizer *)deserializedProfile.profileOwner fairOrganizerID]).to.equal(@"fair-organizer-id"); + }); +}); + +describe(@"Fair", ^{ + beforeEach(^{ + profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"Fair", + @"owner" : @{ + @"id" : @"fair-id", + } + }]; + }); + + it(@"creates an owner of type Fair", ^{ + expect(profile.profileID).to.equal(@"profile-id"); + expect(profile.profileOwner).to.beKindOf([Fair class]); + expect([(Fair *)profile.profileOwner fairID]).to.equal(@"fair-id"); + }); + + it(@"can be serialized", ^{ + NSData *profileData = [NSKeyedArchiver archivedDataWithRootObject:profile]; + Profile *deserializedProfile = [NSKeyedUnarchiver unarchiveObjectWithData:profileData]; + expect(deserializedProfile.profileID).to.equal(profile.profileID); + expect(deserializedProfile.profileOwner).to.beKindOf([Fair class]); + expect([(Fair *)deserializedProfile.profileOwner fairID]).to.equal(@"fair-id"); + }); +}); + +describe(@"Partner", ^{ + beforeEach(^{ + profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"PartnerGallery", + @"owner" : @{ + @"id" : @"partner-id", + } + }]; + }); + + it(@"creates an owner of type Partner", ^{ + expect(profile.profileID).to.equal(@"profile-id"); + expect(profile.profileOwner).to.beKindOf([Partner class]); + expect([(Partner *)profile.profileOwner partnerID]).to.equal(@"partner-id"); + }); + + it(@"can be serialized", ^{ + NSData *profileData = [NSKeyedArchiver archivedDataWithRootObject:profile]; + Profile *deserializedProfile = [NSKeyedUnarchiver unarchiveObjectWithData:profileData]; + expect(deserializedProfile.profileID).to.equal(profile.profileID); + expect(deserializedProfile.profileOwner).to.beKindOf([Partner class]); + expect([(Partner *)deserializedProfile.profileOwner partnerID]).to.equal(@"partner-id"); + }); +}); + +SpecEnd diff --git a/Artsy Tests/ReferenceImages/ARAnimatedTickViewSpec/deselected@2x.png b/Artsy Tests/ReferenceImages/ARAnimatedTickViewSpec/deselected@2x.png new file mode 100644 index 00000000000..55b2a8a6179 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAnimatedTickViewSpec/deselected@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAnimatedTickViewSpec/selected@2x.png b/Artsy Tests/ReferenceImages/ARAnimatedTickViewSpec/selected@2x.png new file mode 100644 index 00000000000..04d94b4fbeb Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAnimatedTickViewSpec/selected@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/arbitrary_text_wraps_correctly@2x.png b/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/arbitrary_text_wraps_correctly@2x.png new file mode 100644 index 00000000000..52258eab402 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/arbitrary_text_wraps_correctly@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/defaultNavigation_logged_in_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/defaultNavigation_logged_in_looks_correct@2x.png new file mode 100644 index 00000000000..b126865ad74 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/defaultNavigation_logged_in_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/defaultNavigation_logged_out_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/defaultNavigation_logged_out_looks_correct@2x.png new file mode 100644 index 00000000000..ded0655cd73 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppMenuViewControllerSpec/defaultNavigation_logged_out_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays search results as ipad@2x.png b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays search results as ipad@2x.png new file mode 100644 index 00000000000..7a931cbfb10 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays search results as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays search results as iphone@2x.png b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays search results as iphone@2x.png new file mode 100644 index 00000000000..a247cf4b3a7 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays search results as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays zero state as ipad@2x.png b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays zero state as ipad@2x.png new file mode 100644 index 00000000000..712f7f42713 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays zero state as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays zero state as iphone@2x.png b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays zero state as iphone@2x.png new file mode 100644 index 00000000000..34e10dac440 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/displays zero state as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/looks correct as ipad@2x.png b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/looks correct as ipad@2x.png new file mode 100644 index 00000000000..d31ef0c5b06 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/looks correct as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/looks correct as iphone@2x.png b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/looks correct as iphone@2x.png new file mode 100644 index 00000000000..f0835a1b7d4 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARAppSearchViewControllerSpec/looks correct as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/artworks masonry as ipad@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/artworks masonry as ipad@2x.png new file mode 100644 index 00000000000..b11e250514b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/artworks masonry as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/artworks masonry as iphone@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/artworks masonry as iphone@2x.png new file mode 100644 index 00000000000..b6528548d42 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/artworks masonry as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/displays artwork counts as ipad@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/displays artwork counts as ipad@2x.png new file mode 100644 index 00000000000..b33ff3ddb42 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/displays artwork counts as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/displays artwork counts as iphone@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/displays artwork counts as iphone@2x.png new file mode 100644 index 00000000000..bbcb8d11431 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/displays artwork counts as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/no artworks as ipad@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/no artworks as ipad@2x.png new file mode 100644 index 00000000000..ec5cf72512b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/no artworks as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/no artworks as iphone@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/no artworks as iphone@2x.png new file mode 100644 index 00000000000..be7de1f3c01 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/no artworks as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/two-rows artworks masonry as ipad@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/two-rows artworks masonry as ipad@2x.png new file mode 100644 index 00000000000..4611e8d1638 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/two-rows artworks masonry as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/two-rows artworks masonry as iphone@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/two-rows artworks masonry as iphone@2x.png new file mode 100644 index 00000000000..59d2518e598 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/two-rows artworks masonry as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/with a bio as ipad@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/with a bio as ipad@2x.png new file mode 100644 index 00000000000..6bfa2814b27 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/with a bio as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/with a bio as iphone@2x.png b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/with a bio as iphone@2x.png new file mode 100644 index 00000000000..7ba5bf1f0b5 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtistViewControllerSpec/with a bio as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/acquireableAtAuction@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/acquireableAtAuction@2x.png new file mode 100644 index 00000000000..2af0d36df5c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/acquireableAtAuction@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/buy@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/buy@2x.png new file mode 100644 index 00000000000..690d26a46ee Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/buy@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSale@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSale@2x.png new file mode 100644 index 00000000000..3c5bb416cd7 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSale@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSaleAtAuction@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSaleAtAuction@2x.png new file mode 100644 index 00000000000..92043dcb0af Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSaleAtAuction@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSaleByAnInstitution@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSaleByAnInstitution@2x.png new file mode 100644 index 00000000000..6f48e266014 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/forSaleByAnInstitution@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_current_auction_reserve_and_no_bids@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_current_auction_reserve_and_no_bids@2x.png new file mode 100644 index 00000000000..50749c1d911 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_current_auction_reserve_and_no_bids@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_current_auction_reserve_not_met_and_has_bids@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_current_auction_reserve_not_met_and_has_bids@2x.png new file mode 100644 index 00000000000..d9e98ebd05d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_current_auction_reserve_not_met_and_has_bids@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_has_bids@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_has_bids@2x.png new file mode 100644 index 00000000000..3ffa576635a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_has_bids@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_no_bids@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_no_bids@2x.png new file mode 100644 index 00000000000..72afa8d28b1 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_no_bids@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_reserve_met_and_has_bids@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_reserve_met_and_has_bids@2x.png new file mode 100644 index 00000000000..0d53ad68322 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_reserve_met_and_has_bids@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_reserve_not_met_and_has_no_bids@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_reserve_not_met_and_has_no_bids@2x.png new file mode 100644 index 00000000000..2c9feb9cfce Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_at_auction_reserve_not_met_and_has_no_bids@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_contact_for_price@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_contact_for_price@2x.png new file mode 100644 index 00000000000..a247bac1ace Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_contact_for_price@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_price@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_price@2x.png new file mode 100644 index 00000000000..aaedef64e38 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_price@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_sold@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_sold@2x.png new file mode 100644 index 00000000000..854d8b3803c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/price_view_not_at_auction_sold@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/soldAtAuction@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/soldAtAuction@2x.png new file mode 100644 index 00000000000..b092c1e3cd2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/soldAtAuction@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/testHighBidder@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/testHighBidder@2x.png new file mode 100644 index 00000000000..d23c2c8ffa8 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/testHighBidder@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/testOutbid@2x.png b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/testOutbid@2x.png new file mode 100644 index 00000000000..5b52d4f07c7 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkActionsViewSpec/testOutbid@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkDetailViewSpec/bothDimensions@2x.png b/Artsy Tests/ReferenceImages/ARArtworkDetailViewSpec/bothDimensions@2x.png new file mode 100644 index 00000000000..22d11a0bf70 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkDetailViewSpec/bothDimensions@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/at_a_closed_auction_displays_artwork_on_iPad@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/at_a_closed_auction_displays_artwork_on_iPad@2x.png new file mode 100644 index 00000000000..4b9f6748b6f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/at_a_closed_auction_displays_artwork_on_iPad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/at_a_closed_auction_displays_artwork_on_iPhone@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/at_a_closed_auction_displays_artwork_on_iPhone@2x.png new file mode 100644 index 00000000000..d78560a9129 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/at_a_closed_auction_displays_artwork_on_iPhone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/no_related_data_shows_artwork_on_iPad@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/no_related_data_shows_artwork_on_iPad@2x.png new file mode 100644 index 00000000000..aa333e4323a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/no_related_data_shows_artwork_on_iPad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/no_related_data_shows_artwork_on_iPhone@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/no_related_data_shows_artwork_on_iPhone@2x.png new file mode 100644 index 00000000000..d3c750d4682 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/no_related_data_shows_artwork_on_iPhone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPad_displays_related_artworks@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPad_displays_related_artworks@2x.png new file mode 100644 index 00000000000..7bb2500f549 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPad_displays_related_artworks@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPad_related_artworks_view_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPad_related_artworks_view_looks_correct@2x.png new file mode 100644 index 00000000000..e13fbe6b56a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPad_related_artworks_view_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPhone_displays_related_artworks@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPhone_displays_related_artworks@2x.png new file mode 100644 index 00000000000..91427d04cca Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPhone_displays_related_artworks@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPhone_related_artworks_view_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPhone_related_artworks_view_looks_correct@2x.png new file mode 100644 index 00000000000..2292ec2732a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARArtworkViewControllerSpec/with_related_artworks_iPhone_related_artworks_view_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBidButtonSpec/testBiddingClosedState@2x.png b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testBiddingClosedState@2x.png new file mode 100644 index 00000000000..4a93ab79702 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testBiddingClosedState@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBidButtonSpec/testBiddingOpenState@2x.png b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testBiddingOpenState@2x.png new file mode 100644 index 00000000000..fffbd2f4c70 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testBiddingOpenState@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBidButtonSpec/testRegisterState@2x.png b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testRegisterState@2x.png new file mode 100644 index 00000000000..95af182fe24 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testRegisterState@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBidButtonSpec/testRegisteredState@2x.png b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testRegisteredState@2x.png new file mode 100644 index 00000000000..023a44e6745 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBidButtonSpec/testRegisteredState@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/initWithStyle_with_ARFeaturedLinkLayoutDoubleRow_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/initWithStyle_with_ARFeaturedLinkLayoutDoubleRow_looks_correct@2x.png new file mode 100644 index 00000000000..bd8b4554354 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/initWithStyle_with_ARFeaturedLinkLayoutDoubleRow_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/initWithStyle_with_ARFeaturedLinkLayoutSingleRow_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/initWithStyle_with_ARFeaturedLinkLayoutSingleRow_looks_correct@2x.png new file mode 100644 index 00000000000..25c931dca4c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/initWithStyle_with_ARFeaturedLinkLayoutSingleRow_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/ipad_snapshots_with_ARFeaturedLinkLayoutDoubleRow_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/ipad_snapshots_with_ARFeaturedLinkLayoutDoubleRow_looks_correct@2x.png new file mode 100644 index 00000000000..ff17b4813fd Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/ipad_snapshots_with_ARFeaturedLinkLayoutDoubleRow_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/ipad_snapshots_with_ARFeaturedLinkLayoutSingleRow_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/ipad_snapshots_with_ARFeaturedLinkLayoutSingleRow_looks_correct@2x.png new file mode 100644 index 00000000000..83a2666c9f2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBrowseFeaturedLinksCollectionViewSpec/ipad_snapshots_with_ARFeaturedLinkLayoutSingleRow_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBrowseViewControllerSpec/ipad_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARBrowseViewControllerSpec/ipad_looks_correct@2x.png new file mode 100644 index 00000000000..46f31d89291 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBrowseViewControllerSpec/ipad_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARBrowseViewControllerSpec/iphone_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARBrowseViewControllerSpec/iphone_looks_correct@2x.png new file mode 100644 index 00000000000..4022c93013e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARBrowseViewControllerSpec/iphone_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairArtistViewControllerSpec/with_maps_displays_artist_title_and_map_button@2x.png b/Artsy Tests/ReferenceImages/ARFairArtistViewControllerSpec/with_maps_displays_artist_title_and_map_button@2x.png new file mode 100644 index 00000000000..6159e254b9f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairArtistViewControllerSpec/with_maps_displays_artist_title_and_map_button@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairArtistViewControllerSpec/without_maps_displays_artist_title@2x.png b/Artsy Tests/ReferenceImages/ARFairArtistViewControllerSpec/without_maps_displays_artist_title@2x.png new file mode 100644 index 00000000000..221dac576a5 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairArtistViewControllerSpec/without_maps_displays_artist_title@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairGuideContainerViewControllerSpec/a_loaded_view_controller_that_has_appeared_after_uncollapsing_map__it_has_a_valid_snapshot@2x.png b/Artsy Tests/ReferenceImages/ARFairGuideContainerViewControllerSpec/a_loaded_view_controller_that_has_appeared_after_uncollapsing_map__it_has_a_valid_snapshot@2x.png new file mode 100644 index 00000000000..10a4e5393e1 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairGuideContainerViewControllerSpec/a_loaded_view_controller_that_has_appeared_after_uncollapsing_map__it_has_a_valid_snapshot@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairGuideContainerViewControllerSpec/a_loaded_view_controller_that_has_appeared_has_a_valid_snapshot@2x.png b/Artsy Tests/ReferenceImages/ARFairGuideContainerViewControllerSpec/a_loaded_view_controller_that_has_appeared_has_a_valid_snapshot@2x.png new file mode 100644 index 00000000000..d33ad2b554b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairGuideContainerViewControllerSpec/a_loaded_view_controller_that_has_appeared_has_a_valid_snapshot@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/adds_a_top_border@2x.png b/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/adds_a_top_border@2x.png new file mode 100644 index 00000000000..921a633de11 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/adds_a_top_border@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/looks_correct_with_a_logged_in_user@2x.png b/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/looks_correct_with_a_logged_in_user@2x.png new file mode 100644 index 00000000000..aa8ce14fc80 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/looks_correct_with_a_logged_in_user@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/looks_correct_with_a_trial_user@2x.png b/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/looks_correct_with_a_trial_user@2x.png new file mode 100644 index 00000000000..b50bacd997d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairGuideViewControllerSpec/looks_correct_with_a_trial_user@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/blank@2x.png b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/blank@2x.png new file mode 100644 index 00000000000..ed7d8b34ac7 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/blank@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/longTitleWithArrow@2x.png b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/longTitleWithArrow@2x.png new file mode 100644 index 00000000000..52c801c5953 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/longTitleWithArrow@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/title@2x.png b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/title@2x.png new file mode 100644 index 00000000000..96653a1b34e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/title@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/titleWithArrow@2x.png b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/titleWithArrow@2x.png new file mode 100644 index 00000000000..4e9626c5ec6 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/titleWithArrow@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/titleWithSubtitle@2x.png b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/titleWithSubtitle@2x.png new file mode 100644 index 00000000000..845bd38d142 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapAnnotationCallOutViewSpec/titleWithSubtitle@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/default@2x.png b/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/default@2x.png new file mode 100644 index 00000000000..7f360b10628 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/default@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/on_init_fair_with_selection_adds_title@2x.png b/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/on_init_fair_with_selection_adds_title@2x.png new file mode 100644 index 00000000000..00efbbb9a6c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/on_init_fair_with_selection_adds_title@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/on_init_fair_with_selection_constrains_the_title_correctly_in_a_navigation_controller@2x.png b/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/on_init_fair_with_selection_constrains_the_title_correctly_in_a_navigation_controller@2x.png new file mode 100644 index 00000000000..cd6186d40cd Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairMapViewControllerSpec/on_init_fair_with_selection_constrains_the_title_correctly_in_a_navigation_controller@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with 1 install image as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with 1 install image as ipad@2x.png new file mode 100644 index 00000000000..8938e08b65b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with 1 install image as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with 1 install image as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with 1 install image as iphone@2x.png new file mode 100644 index 00000000000..f6397ee565a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with 1 install image as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with multiple install images as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with multiple install images as ipad@2x.png new file mode 100644 index 00000000000..8938e08b65b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with multiple install images as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with multiple install images as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with multiple install images as iphone@2x.png new file mode 100644 index 00000000000..f6397ee565a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show images with multiple install images as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title as ipad@2x.png new file mode 100644 index 00000000000..5c45ba6a70e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title as iphone@2x.png new file mode 100644 index 00000000000..5e69ba5c623 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title, map, and map button as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title, map, and map button as ipad@2x.png new file mode 100644 index 00000000000..0f563d0df48 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title, map, and map button as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title, map, and map button as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title, map, and map button as iphone@2x.png new file mode 100644 index 00000000000..b9f61bbb149 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairShowViewControllerSpec/displays show title, map, and map button as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFairViewControllerSpec/with_a_map_search_view_looks_correct@2x.png b/Artsy Tests/ReferenceImages/ARFairViewControllerSpec/with_a_map_search_view_looks_correct@2x.png new file mode 100644 index 00000000000..b77a32e36e2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFairViewControllerSpec/with_a_map_search_view_looks_correct@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artists as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artists as ipad@2x.png new file mode 100644 index 00000000000..5e06ee1d7c6 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artists as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artists as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artists as iphone@2x.png new file mode 100644 index 00000000000..267282c1a08 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artists as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artworks as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artworks as ipad@2x.png new file mode 100644 index 00000000000..c0f10f0fa8a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artworks as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artworks as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artworks as iphone@2x.png new file mode 100644 index 00000000000..bb48828b0f2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with artworks as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with genes as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with genes as ipad@2x.png new file mode 100644 index 00000000000..11d1918d32d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with genes as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with genes as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with genes as iphone@2x.png new file mode 100644 index 00000000000..f080a97285f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with genes as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artists as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artists as ipad@2x.png new file mode 100644 index 00000000000..091737ed7e2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artists as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artists as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artists as iphone@2x.png new file mode 100644 index 00000000000..2044147c50a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artists as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artworks as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artworks as ipad@2x.png new file mode 100644 index 00000000000..aafb0eff295 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artworks as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artworks as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artworks as iphone@2x.png new file mode 100644 index 00000000000..b027f950ac9 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no artworks as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no genes as ipad@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no genes as ipad@2x.png new file mode 100644 index 00000000000..41c67ea6410 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no genes as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no genes as iphone@2x.png b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no genes as iphone@2x.png new file mode 100644 index 00000000000..7c1bcfc484a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFavoritesViewControllerSpec/with no genes as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFollowableButtonSpec/ARFollowableButton_following@2x.png b/Artsy Tests/ReferenceImages/ARFollowableButtonSpec/ARFollowableButton_following@2x.png new file mode 100644 index 00000000000..4723af87fe8 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFollowableButtonSpec/ARFollowableButton_following@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARFollowableButtonSpec/ARFollowableButton_not_following@2x.png b/Artsy Tests/ReferenceImages/ARFollowableButtonSpec/ARFollowableButton_not_following@2x.png new file mode 100644 index 00000000000..eccdd0a6d89 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARFollowableButtonSpec/ARFollowableButton_not_following@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARGeneViewControllerSpec/with long desciption as ipad@2x.png b/Artsy Tests/ReferenceImages/ARGeneViewControllerSpec/with long desciption as ipad@2x.png new file mode 100644 index 00000000000..e8fac1cb40e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARGeneViewControllerSpec/with long desciption as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARGeneViewControllerSpec/with long desciption as iphone@2x.png b/Artsy Tests/ReferenceImages/ARGeneViewControllerSpec/with long desciption as iphone@2x.png new file mode 100644 index 00000000000..58ad6dabc0c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARGeneViewControllerSpec/with long desciption as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeartButtonSpec/hearted@2x.png b/Artsy Tests/ReferenceImages/ARHeartButtonSpec/hearted@2x.png new file mode 100644 index 00000000000..7af0d30bcd0 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeartButtonSpec/hearted@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeartButtonSpec/unhearted@2x.png b/Artsy Tests/ReferenceImages/ARHeartButtonSpec/unhearted@2x.png new file mode 100644 index 00000000000..8b61a55f544 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeartButtonSpec/unhearted@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with no units as ipad@2x.png b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with no units as ipad@2x.png new file mode 100644 index 00000000000..065361b095f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with no units as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with no units as iphone@2x.png b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with no units as iphone@2x.png new file mode 100644 index 00000000000..a6b565810d5 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with no units as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with one unit as ipad@2x.png b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with one unit as ipad@2x.png new file mode 100644 index 00000000000..7a314f501b8 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with one unit as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with one unit as iphone@2x.png b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with one unit as iphone@2x.png new file mode 100644 index 00000000000..6df79256c26 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with one unit as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with three units as ipad@2x.png b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with three units as ipad@2x.png new file mode 100644 index 00000000000..43a13e0199b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with three units as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with three units as iphone@2x.png b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with three units as iphone@2x.png new file mode 100644 index 00000000000..27e36a0fd62 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARHeroUnitViewControllerSpec/with three units as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Gallery when seller is a gallery as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Gallery when seller is a gallery as ipad@2x.png new file mode 100644 index 00000000000..bd69651239f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Gallery when seller is a gallery as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Gallery when seller is a gallery as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Gallery when seller is a gallery as iphone@2x.png new file mode 100644 index 00000000000..9dfaaa14bd5 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Gallery when seller is a gallery as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Seller when seller is not a gallery as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Seller when seller is not a gallery as ipad@2x.png new file mode 100644 index 00000000000..b6ee25ad21f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Seller when seller is not a gallery as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Seller when seller is not a gallery as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Seller when seller is not a gallery as iphone@2x.png new file mode 100644 index 00000000000..9db7eaf3f96 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays Contact Seller when seller is not a gallery as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays artsy specialist as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays artsy specialist as ipad@2x.png new file mode 100644 index 00000000000..19777b593b3 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays artsy specialist as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays artsy specialist as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays artsy specialist as iphone@2x.png new file mode 100644 index 00000000000..333c7d9e445 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays artsy specialist as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays contact gallery as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays contact gallery as ipad@2x.png new file mode 100644 index 00000000000..91e8292b1e4 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays contact gallery as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays contact gallery as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays contact gallery as iphone@2x.png new file mode 100644 index 00000000000..742b37aeda6 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/displays contact gallery as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does initially enables send if stored email is valid as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does initially enables send if stored email is valid as ipad@2x.png new file mode 100644 index 00000000000..221fa5f4ebf Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does initially enables send if stored email is valid as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does initially enables send if stored email is valid as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does initially enables send if stored email is valid as iphone@2x.png new file mode 100644 index 00000000000..5be42192e7a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does initially enables send if stored email is valid as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does not initially enable send if stored email is invalid as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does not initially enable send if stored email is invalid as ipad@2x.png new file mode 100644 index 00000000000..e679a462859 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does not initially enable send if stored email is invalid as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does not initially enable send if stored email is invalid as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does not initially enable send if stored email is invalid as iphone@2x.png new file mode 100644 index 00000000000..9db7d86a834 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/does not initially enable send if stored email is invalid as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/logged out, displays artsy specialist as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/logged out, displays artsy specialist as ipad@2x.png new file mode 100644 index 00000000000..d1a9359e666 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/logged out, displays artsy specialist as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/logged out, displays artsy specialist as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/logged out, displays artsy specialist as iphone@2x.png new file mode 100644 index 00000000000..e6bc35271ae Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/logged out, displays artsy specialist as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when email becomes valid as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when email becomes valid as ipad@2x.png new file mode 100644 index 00000000000..221fa5f4ebf Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when email becomes valid as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when email becomes valid as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when email becomes valid as iphone@2x.png new file mode 100644 index 00000000000..5be42192e7a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when email becomes valid as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when valid email becomes invalid as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when valid email becomes invalid as ipad@2x.png new file mode 100644 index 00000000000..e679a462859 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when valid email becomes invalid as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when valid email becomes invalid as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when valid email becomes invalid as iphone@2x.png new file mode 100644 index 00000000000..9db7d86a834 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button when valid email becomes invalid as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button with empty email as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button with empty email as ipad@2x.png new file mode 100644 index 00000000000..1761fef791d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button with empty email as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button with empty email as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button with empty email as iphone@2x.png new file mode 100644 index 00000000000..260abc70dd8 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/toggles the send button with empty email as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/works for an artwork without a title as ipad@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/works for an artwork without a title as ipad@2x.png new file mode 100644 index 00000000000..126221bb6ba Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/works for an artwork without a title as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/works for an artwork without a title as iphone@2x.png b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/works for an artwork without a title as iphone@2x.png new file mode 100644 index 00000000000..923e70ed1ee Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARInquireForArtworkViewControllerSpec/works for an artwork without a title as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/blank form as ipad@2x.png b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/blank form as ipad@2x.png new file mode 100644 index 00000000000..e2ac2f69a3c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/blank form as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/blank form as iphone@2x.png b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/blank form as iphone@2x.png new file mode 100644 index 00000000000..f7e44e52206 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/blank form as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/completed form as ipad@2x.png b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/completed form as ipad@2x.png new file mode 100644 index 00000000000..4dfe7947368 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/completed form as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/completed form as iphone@2x.png b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/completed form as iphone@2x.png new file mode 100644 index 00000000000..53018788777 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARLoginViewControllerSpec/completed form as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitle@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitle@2x.png new file mode 100644 index 00000000000..1969822a75a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitle@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAnd5pxBorder@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAnd5pxBorder@2x.png new file mode 100644 index 00000000000..f720d167f97 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAnd5pxBorder@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndNoBorder@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndNoBorder@2x.png new file mode 100644 index 00000000000..209ad93b66f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndNoBorder@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitle@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitle@2x.png new file mode 100644 index 00000000000..11306dbcb9f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitle@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitleAnd5pxBorder@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitleAnd5pxBorder@2x.png new file mode 100644 index 00000000000..4f28bf67ee0 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitleAnd5pxBorder@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitleAndNoBorder@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitleAndNoBorder@2x.png new file mode 100644 index 00000000000..4eb6cb582f2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/navigationButtonWithTitleAndSubtitleAndNoBorder@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/serifNavigationButtonWithTitle@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/serifNavigationButtonWithTitle@2x.png new file mode 100644 index 00000000000..baa576829b8 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/serifNavigationButtonWithTitle@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/serifNavigationButtonWithTitleAndSubtitle@2x.png b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/serifNavigationButtonWithTitleAndSubtitle@2x.png new file mode 100644 index 00000000000..6eb2743d538 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationButtonSpecSpec/serifNavigationButtonWithTitleAndSubtitle@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationControllerSpec/visuals_should_be_empty_for_a_rootVC@2x.png b/Artsy Tests/ReferenceImages/ARNavigationControllerSpec/visuals_should_be_empty_for_a_rootVC@2x.png new file mode 100644 index 00000000000..e24ba3bfad9 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationControllerSpec/visuals_should_be_empty_for_a_rootVC@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARNavigationControllerSpec/visuals_should_be_show_a_back_button_with_2_view_controllers@2x.png b/Artsy Tests/ReferenceImages/ARNavigationControllerSpec/visuals_should_be_show_a_back_button_with_2_view_controllers@2x.png new file mode 100644 index 00000000000..64ce19bae0d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARNavigationControllerSpec/visuals_should_be_show_a_back_button_with_2_view_controllers@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARParallaxHeaderViewControllerSpec/with_a_full_fair_and_profile_has_a_valid_snapshot@2x.png b/Artsy Tests/ReferenceImages/ARParallaxHeaderViewControllerSpec/with_a_full_fair_and_profile_has_a_valid_snapshot@2x.png new file mode 100644 index 00000000000..470f11f2aa2 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARParallaxHeaderViewControllerSpec/with_a_full_fair_and_profile_has_a_valid_snapshot@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARPendingOperationViewControllerSpec/has_a_valid_snapshot@2x.png b/Artsy Tests/ReferenceImages/ARPendingOperationViewControllerSpec/has_a_valid_snapshot@2x.png new file mode 100644 index 00000000000..8648b753f0a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARPendingOperationViewControllerSpec/has_a_valid_snapshot@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSearchFieldButtonSpec/has_a_valid_snapshot@2x.png b/Artsy Tests/ReferenceImages/ARSearchFieldButtonSpec/has_a_valid_snapshot@2x.png new file mode 100644 index 00000000000..5164338362a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSearchFieldButtonSpec/has_a_valid_snapshot@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextArtworkOrder as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextArtworkOrder as ipad@2x.png new file mode 100644 index 00000000000..774d7f47db8 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextArtworkOrder as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextArtworkOrder as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextArtworkOrder as iphone@2x.png new file mode 100644 index 00000000000..9293fa7012e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextArtworkOrder as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextAuctionBid as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextAuctionBid as ipad@2x.png new file mode 100644 index 00000000000..1873d50e95a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextAuctionBid as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextAuctionBid as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextAuctionBid as iphone@2x.png new file mode 100644 index 00000000000..41cf8c36ecd Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextAuctionBid as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextContactGallery as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextContactGallery as ipad@2x.png new file mode 100644 index 00000000000..22a067abd90 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextContactGallery as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextContactGallery as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextContactGallery as iphone@2x.png new file mode 100644 index 00000000000..4ea2b8b750b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextContactGallery as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFairGuide as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFairGuide as ipad@2x.png new file mode 100644 index 00000000000..c94171b7645 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFairGuide as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFairGuide as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFairGuide as iphone@2x.png new file mode 100644 index 00000000000..8bf5c3c129c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFairGuide as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtist as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtist as ipad@2x.png new file mode 100644 index 00000000000..bf4d8d12f8a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtist as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtist as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtist as iphone@2x.png new file mode 100644 index 00000000000..e2660e1bd5b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtist as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtwork as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtwork as ipad@2x.png new file mode 100644 index 00000000000..d42f2858faa Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtwork as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtwork as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtwork as iphone@2x.png new file mode 100644 index 00000000000..9703d928544 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteArtwork as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteGene as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteGene as ipad@2x.png new file mode 100644 index 00000000000..d42f2858faa Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteGene as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteGene as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteGene as iphone@2x.png new file mode 100644 index 00000000000..9703d928544 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteGene as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteProfile as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteProfile as ipad@2x.png new file mode 100644 index 00000000000..d0dd6cd264e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteProfile as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteProfile as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteProfile as iphone@2x.png new file mode 100644 index 00000000000..24a0e60cc6a Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextFavoriteProfile as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextNotTrial as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextNotTrial as ipad@2x.png new file mode 100644 index 00000000000..200fc219db6 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextNotTrial as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextNotTrial as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextNotTrial as iphone@2x.png new file mode 100644 index 00000000000..30dacac961d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextNotTrial as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextPeriodical as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextPeriodical as ipad@2x.png new file mode 100644 index 00000000000..3710d6bd975 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextPeriodical as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextPeriodical as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextPeriodical as iphone@2x.png new file mode 100644 index 00000000000..cc1be07722c Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextPeriodical as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextRepresentativeInquiry as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextRepresentativeInquiry as ipad@2x.png new file mode 100644 index 00000000000..a9f3b766199 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextRepresentativeInquiry as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextRepresentativeInquiry as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextRepresentativeInquiry as iphone@2x.png new file mode 100644 index 00000000000..578005763f9 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextRepresentativeInquiry as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextShowingFavorites as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextShowingFavorites as ipad@2x.png new file mode 100644 index 00000000000..b0d84475807 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextShowingFavorites as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextShowingFavorites as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextShowingFavorites as iphone@2x.png new file mode 100644 index 00000000000..b56336ceb2d Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpActiveUserViewControllerSpec/ARTrialContextShowingFavorites as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpSplashViewControllerSpec/looks correct as ipad@2x.png b/Artsy Tests/ReferenceImages/ARSignUpSplashViewControllerSpec/looks correct as ipad@2x.png new file mode 100644 index 00000000000..1687b51a653 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpSplashViewControllerSpec/looks correct as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSignUpSplashViewControllerSpec/looks correct as iphone@2x.png b/Artsy Tests/ReferenceImages/ARSignUpSplashViewControllerSpec/looks correct as iphone@2x.png new file mode 100644 index 00000000000..4ef690018f3 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSignUpSplashViewControllerSpec/looks correct as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSwitchViewSpec/accepts_any_number_of_items@2x.png b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/accepts_any_number_of_items@2x.png new file mode 100644 index 00000000000..870ecc11735 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/accepts_any_number_of_items@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSwitchViewSpec/adjusts_buttons_to_any_switch_width@2x.png b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/adjusts_buttons_to_any_switch_width@2x.png new file mode 100644 index 00000000000..0bf7509a5ce Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/adjusts_buttons_to_any_switch_width@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSwitchViewSpec/looks_correct_configured_with_two_buttons@2x.png b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/looks_correct_configured_with_two_buttons@2x.png new file mode 100644 index 00000000000..42c77284334 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/looks_correct_configured_with_two_buttons@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARSwitchViewSpec/looks_correct_with_a_style_configured_with_two_buttons@2x.png b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/looks_correct_with_a_style_configured_with_two_buttons@2x.png new file mode 100644 index 00000000000..23128eeaa5b Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARSwitchViewSpec/looks_correct_with_a_style_configured_with_two_buttons@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARTabViewSpec/correctly_shows_a_navigation_controller_at_another_index@2x.png b/Artsy Tests/ReferenceImages/ARTabViewSpec/correctly_shows_a_navigation_controller_at_another_index@2x.png new file mode 100644 index 00000000000..4106ff3ca7e Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARTabViewSpec/correctly_shows_a_navigation_controller_at_another_index@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARTabViewSpec/correctly_shows_a_navigation_controller_at_first_index@2x.png b/Artsy Tests/ReferenceImages/ARTabViewSpec/correctly_shows_a_navigation_controller_at_first_index@2x.png new file mode 100644 index 00000000000..afdfce731a3 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARTabViewSpec/correctly_shows_a_navigation_controller_at_first_index@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/selects 'home' by default as ipad@2x.png b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/selects 'home' by default as ipad@2x.png new file mode 100644 index 00000000000..877ece64c9f Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/selects 'home' by default as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/selects 'home' by default as iphone@2x.png b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/selects 'home' by default as iphone@2x.png new file mode 100644 index 00000000000..7f9188c6e71 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/selects 'home' by default as iphone@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/should be able to hide as ipad@2x.png b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/should be able to hide as ipad@2x.png new file mode 100644 index 00000000000..10aac3364dc Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/should be able to hide as ipad@2x.png differ diff --git a/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/should be able to hide as iphone@2x.png b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/should be able to hide as iphone@2x.png new file mode 100644 index 00000000000..570842878b6 Binary files /dev/null and b/Artsy Tests/ReferenceImages/ARTopMenuViewControllerSpec/should be able to hide as iphone@2x.png differ diff --git a/Artsy Tests/Sale+Extensions.h b/Artsy Tests/Sale+Extensions.h new file mode 100644 index 00000000000..4dac02d3730 --- /dev/null +++ b/Artsy Tests/Sale+Extensions.h @@ -0,0 +1,3 @@ +@interface Sale (Extensions) ++ (Sale *)saleWithStart:(NSDate *)start end:(NSDate *)end; +@end diff --git a/Artsy Tests/Sale+Extensions.m b/Artsy Tests/Sale+Extensions.m new file mode 100644 index 00000000000..3e3aa9f4014 --- /dev/null +++ b/Artsy Tests/Sale+Extensions.m @@ -0,0 +1,8 @@ +#import "Sale+Extensions.h" + +@implementation Sale (Extensions) ++ (Sale *)saleWithStart:(NSDate *)start end:(NSDate *)end +{ + return [Sale modelFromDictionary:@{ @"startDate" : start, @"endDate" : end }]; +} +@end diff --git a/Artsy Tests/SaleArtwork+Extensions.h b/Artsy Tests/SaleArtwork+Extensions.h new file mode 100644 index 00000000000..a58d3454d5e --- /dev/null +++ b/Artsy Tests/SaleArtwork+Extensions.h @@ -0,0 +1,6 @@ +#import "SaleArtwork.h" + +@interface SaleArtwork (Extensions) ++ (SaleArtwork *)saleArtworkWithHighBid:(Bid *)bid AndReserveStatus:(ARReserveStatus)status; +@end + diff --git a/Artsy Tests/SaleArtwork+Extensions.m b/Artsy Tests/SaleArtwork+Extensions.m new file mode 100644 index 00000000000..875df71a7d0 --- /dev/null +++ b/Artsy Tests/SaleArtwork+Extensions.m @@ -0,0 +1,14 @@ +#import "SaleArtwork+Extensions.h" + +@implementation SaleArtwork (Extensions) ++ (SaleArtwork *)saleArtworkWithHighBid:(Bid *)bid AndReserveStatus:(ARReserveStatus)status +{ + return [SaleArtwork modelFromDictionary:@{ + @"artworkNumPositions" : @(5), + @"saleHighestBid" : bid, + @"minimumNextBidCents" : @(11000000), + @"reserveStatus" : @(status) + }]; +} +@end + diff --git a/Artsy Tests/SaleArtworkTests.m b/Artsy Tests/SaleArtworkTests.m new file mode 100644 index 00000000000..d77efea7a75 --- /dev/null +++ b/Artsy Tests/SaleArtworkTests.m @@ -0,0 +1,140 @@ +SpecBegin(SaleArtwork) + +describe(@"artwork for sale", ^{ + __block SaleArtwork *_saleArtwork; + + beforeEach(^{ + _saleArtwork = [[SaleArtwork alloc] init]; + }); + + it(@"has default state", ^{ + expect([_saleArtwork auctionState]).to.equal(ARAuctionStateDefault); + }); + + it(@"says it has no estimate when there is no min/max estimate", ^{ + _saleArtwork = [[SaleArtwork alloc] init]; + expect(_saleArtwork.hasEstimate).to.beFalsy(); + }); + + it(@"says it has an estimate when there is no min/max estimate", ^{ + _saleArtwork = [SaleArtwork modelWithJSON:@{@"high_estimate_cents" : @20000}]; + expect(_saleArtwork.hasEstimate).to.beTruthy(); + + _saleArtwork = [SaleArtwork modelWithJSON:@{@"low_estimate_cents" : @20000}]; + expect(_saleArtwork.hasEstimate).to.beTruthy(); + + _saleArtwork = [SaleArtwork modelWithJSON:@{@"high_estimate_cents" : @20000, @"low_estimate_cents" : @10000}]; + expect(_saleArtwork.hasEstimate).to.beTruthy(); + }); + + describe(@"estimate string", ^{ + + it(@"returns a string showing both low and high ", ^{ + _saleArtwork = [SaleArtwork modelWithJSON:@{ @"high_estimate_cents" : @20000, @"low_estimate_cents" : @10000}]; + expect(_saleArtwork.estimateString).to.equal(@"Estimate: $100 – $200"); + }); + + it(@"returns a string showing low if available ", ^{ + _saleArtwork = [SaleArtwork modelWithJSON:@{ @"low_estimate_cents" : @10000}]; + expect(_saleArtwork.estimateString).to.equal(@"Estimate: $100"); + }); + + it(@"returns a string showing high if available", ^{ + _saleArtwork = [SaleArtwork modelWithJSON:@{ @"high_estimate_cents" : @100000}]; + expect(_saleArtwork.estimateString).to.equal(@"Estimate: $1,000"); + }); + }); + + describe(@"with a bidder", ^{ + beforeEach(^{ + _saleArtwork.auction = nil; + _saleArtwork.bidder = [[Bidder alloc] init]; + }); + + it(@"sets user is registered state", ^{ + expect([_saleArtwork auctionState]).to.equal(ARAuctionStateUserIsRegistered); + }); + }); + + describe(@"with an auction that starts in the future", ^{ + beforeEach(^{ + _saleArtwork.auction = [Sale saleWithStart:[NSDate distantFuture] end:[NSDate distantFuture]]; + }); + + it(@"does not change state", ^{ + expect([_saleArtwork auctionState]).to.equal(ARAuctionStateDefault); + }); + }); + + describe(@"with an auction that has started", ^{ + beforeEach(^{ + _saleArtwork.auction = [Sale saleWithStart:[NSDate distantPast] end:[NSDate distantFuture]]; + }); + + it(@"sets auction started state", ^{ + expect([_saleArtwork auctionState]).to.equal(ARAuctionStateStarted); + }); + }); + + describe(@"with an auction that has ended", ^{ + beforeEach(^{ + _saleArtwork.auction = [Sale saleWithStart:[NSDate distantPast] end:[NSDate distantPast]]; + }); + + it(@"sets auction ended state", ^{ + expect([_saleArtwork auctionState]).to.equal(ARAuctionStateStarted | ARAuctionStateEnded); + }); + }); + + describe(@"with a bid", ^{ + beforeEach(^{ + _saleArtwork.saleHighestBid = [Bid bidWithCents:@(99) bidID:@"lowBid"]; + }); + + it(@"sets has bids state", ^{ + expect([_saleArtwork auctionState] & ARAuctionStateArtworkHasBids).to.beTruthy(); + }); + }); + + describe(@"with a low bidder", ^{ + beforeEach(^{ + _saleArtwork.saleHighestBid = [Bid bidWithCents:@(99) bidID:@"lowBid"]; + _saleArtwork.positions = @[ [BidderPosition modelFromDictionary:@{ @"highestBid" : [Bid bidWithCents:@(99999) bidID:@"highBid"] }] ]; + }); + + it(@"sets bidder state", ^{ + expect([_saleArtwork auctionState] & ARAuctionStateUserIsBidder).to.beTruthy(); + }); + }); + + describe(@"with a highest bidder", ^{ + beforeEach(^{ + _saleArtwork.saleHighestBid = [Bid bidWithCents:@(99999) bidID:@"highBid"]; + _saleArtwork.positions = @[ [BidderPosition modelFromDictionary:@{ @"highestBid" : [Bid bidWithCents:@(99999) bidID:@"highBid"] }] ]; + }); + + it(@"sets bidder state", ^{ + expect([_saleArtwork auctionState] & ARAuctionStateUserIsHighBidder).to.beTruthy(); + }); + }); + + describe(@"with multiple bidder positions", ^{ + __block BidderPosition * _position; + + beforeEach(^{ + _position = [BidderPosition modelFromDictionary:@{ @"maxBidAmountCents" : @(103) }]; + _saleArtwork.positions = @[ + [BidderPosition modelFromDictionary:@{ @"maxBidAmountCents" : @(100) }], + [BidderPosition modelFromDictionary:@{ @"maxBidAmountCents" : @(101) }], + _position + ]; + }); + + it(@"sets max bidder position", ^{ + expect([_saleArtwork userMaxBidderPosition]).to.equal(_position); + }); + }); +}); + +SpecEnd + diff --git a/Artsy Tests/SaleTests.m b/Artsy Tests/SaleTests.m new file mode 100644 index 00000000000..80740b0a580 --- /dev/null +++ b/Artsy Tests/SaleTests.m @@ -0,0 +1,39 @@ +SpecBegin(Sale) + +describe(@"past auction", ^{ + __block Sale *_pastAuction; + + beforeEach(^{ + _pastAuction = [Sale saleWithStart:[NSDate distantPast] end:[NSDate distantPast]]; + }); + + it(@"is not active", ^{ + expect(_pastAuction.isCurrentlyActive).to.beFalsy(); + }); +}); + +describe(@"current auction", ^{ + __block Sale *_currentAuction; + + beforeEach(^{ + _currentAuction = [Sale saleWithStart:[NSDate distantPast] end:[NSDate distantFuture]]; + }); + + it(@"is active", ^{ + expect(_currentAuction.isCurrentlyActive).to.beTruthy(); + }); +}); + +describe(@"future auction", ^{ + __block Sale *_futureAuction; + + beforeEach(^{ + _futureAuction = [Sale saleWithStart:[NSDate distantFuture] end:[NSDate distantFuture]]; + }); + + it(@"is active", ^{ + expect(_futureAuction.isCurrentlyActive).to.beFalsy(); + }); +}); + +SpecEnd diff --git a/Artsy Tests/SiteHeroUnitTests.m b/Artsy Tests/SiteHeroUnitTests.m new file mode 100644 index 00000000000..6513481afc7 --- /dev/null +++ b/Artsy Tests/SiteHeroUnitTests.m @@ -0,0 +1,42 @@ +SpecBegin(SiteHeroUnit) + +describe(@"currentlyActive", ^{ + it(@"past", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"past", @"start_at" : @"1976-01-27T05:00:00+00:00", @"end_at" : @"1976-01-27T05:00:00+00:00" } error:nil]; + expect(siteHeroUnit.currentlyActive).to.beFalsy(); + }); + + it(@"future", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"future", @"start_at" : @"2099-01-27T05:00:00+00:00", @"end_at" : @"2099-01-27T05:00:00+00:00" } error:nil]; + expect(siteHeroUnit.currentlyActive).to.beFalsy(); + }); + + it(@"current", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"current", @"start_at" : @"1976-01-27T05:00:00+00:00", @"end_at" : @"2099-01-27T05:00:00+00:00" } error:nil]; + expect(siteHeroUnit.isCurrentlyActive).to.beTruthy(); + }); +}); + +describe(@"alignment", ^{ + it(@"sets left correctly", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"left_hero_unit", @"type" : @"left" } error:nil]; + expect(siteHeroUnit.alignment).to.equal(ARHeroUnitAlignmentLeft); + }); + + it(@"sets right correctly", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"right_hero_unit", @"type" : @"right" } error:nil]; + expect(siteHeroUnit.alignment).to.equal(ARHeroUnitAlignmentRight); + }); + + it(@"defaults nil to left", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"nil_hero_unit", @"type" : NSNull.null } error:nil]; + expect(siteHeroUnit.alignment).to.equal(ARHeroUnitAlignmentLeft); + }); + + it(@"defaults anything else to left", ^{ + SiteHeroUnit *siteHeroUnit = [SiteHeroUnit modelWithJSON:@{ @"id": @"bad_data_hero_unit", @"type" : @"upside_down" } error:nil]; + expect(siteHeroUnit.alignment).to.equal(ARHeroUnitAlignmentLeft); + }); +}); + +SpecEnd diff --git a/Artsy Tests/SpectaDSL+Sleep.h b/Artsy Tests/SpectaDSL+Sleep.h new file mode 100644 index 00000000000..f46af704772 --- /dev/null +++ b/Artsy Tests/SpectaDSL+Sleep.h @@ -0,0 +1,4 @@ +#import + +void activelyWaitFor(double seconds, void (^block)()); + diff --git a/Artsy Tests/SpectaDSL+Sleep.m b/Artsy Tests/SpectaDSL+Sleep.m new file mode 100644 index 00000000000..bad7579a795 --- /dev/null +++ b/Artsy Tests/SpectaDSL+Sleep.m @@ -0,0 +1,11 @@ +#import "SpectaDSL+Sleep.h" + +void activelyWaitFor(double seconds, void (^block)()){ + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + [NSThread sleepForTimeInterval:seconds]; + dispatch_async(dispatch_get_main_queue(), ^{ + block(); + }); + }); +} diff --git a/Artsy Tests/SystemTimeTests.m b/Artsy Tests/SystemTimeTests.m new file mode 100644 index 00000000000..e50618922e4 --- /dev/null +++ b/Artsy Tests/SystemTimeTests.m @@ -0,0 +1,30 @@ +SpecBegin(SystemTime) + +__block SystemTime *systemTime; + +beforeEach(^{ + systemTime = [SystemTime modelWithJSON:@{ + @"time": @"2022-03-24T13:29:34Z", + @"day": @(24), + @"wday": @(4), + @"month": @(3), + @"year": @(2022), + @"hour": @(13), + @"min": @(29), + @"sec": @(34), + @"dst": @(NO), + @"unix": @(1395926974), + @"utc_offset": @(0), + @"zone": @"UTC", + @"iso8601": @"2022-03-24T13:29:34Z" + }]; +}); + +it(@"converts date", ^{ + NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:systemTime.date]; + expect(components.year).to.equal(2022); + expect(components.month).to.equal(3); + expect(components.day).to.equal(24); +}); + +SpecEnd diff --git a/Artsy Tests/TestData/User_v0.data b/Artsy Tests/TestData/User_v0.data new file mode 100644 index 00000000000..9232f4079f7 Binary files /dev/null and b/Artsy Tests/TestData/User_v0.data differ diff --git a/Artsy Tests/TestData/User_v1.data b/Artsy Tests/TestData/User_v1.data new file mode 100644 index 00000000000..689fa0a81a3 Binary files /dev/null and b/Artsy Tests/TestData/User_v1.data differ diff --git a/Artsy Tests/UIApplicationStateEnumTests.m b/Artsy Tests/UIApplicationStateEnumTests.m new file mode 100644 index 00000000000..bf65c32ce37 --- /dev/null +++ b/Artsy Tests/UIApplicationStateEnumTests.m @@ -0,0 +1,26 @@ + +#import "UIApplicationStateEnum.h" + +SpecBegin(UIApplicationStateEnum) + +describe(@"toString", ^{ + it(@"background", ^{ + expect([UIApplicationStateEnum toString:UIApplicationStateBackground]).to.equal(@"background"); + }); + + it(@"active", ^{ + expect([UIApplicationStateEnum toString:UIApplicationStateActive]).to.equal(@"active"); + }); + + it(@"inactive", ^{ + expect([UIApplicationStateEnum toString:UIApplicationStateInactive]).to.equal(@"inactive"); + }); + + it(@"invalid", ^{ + expect(^{ + [UIApplicationStateEnum toString:(UIApplicationState) -1]; + }).to.raiseWithReason(@"NSGenericException", @"Unexpected UIApplicationState -1"); + }); +}); + +SpecEnd diff --git a/Artsy Tests/UINavigationController_InnermostTopViewControllerSpec.m b/Artsy Tests/UINavigationController_InnermostTopViewControllerSpec.m new file mode 100644 index 00000000000..0ab70783b6c --- /dev/null +++ b/Artsy Tests/UINavigationController_InnermostTopViewControllerSpec.m @@ -0,0 +1,49 @@ +#import "UIViewController+InnermostTopViewController.h" + +SpecBegin(UINavigationController_InnermostTopViewController) + +it(@"returns the nil for empty navigation controllers", ^{ + expect([[[UINavigationController alloc] init] ar_innermostTopViewController]).to.beNil(); +}); + +it(@"returns the direct top view controller if there is no nesting", ^{ + UIViewController *viewController = [[UIViewController alloc] init]; + + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; + + expect(navigationController.ar_innermostTopViewController).to.equal(viewController); +}); + +it(@"returns the inner most top view controller if there is nesting", ^{ + UIViewController *viewController = [[UIViewController alloc] init]; + + UINavigationController *childNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; + + UIViewController *childWrapperviewController = [[UIViewController alloc] init]; + [childWrapperviewController addChildViewController:childNavigationController]; + [childWrapperviewController.view addSubview:childNavigationController.view]; + + UINavigationController *rootNavigationController = [[UINavigationController alloc] initWithRootViewController:childWrapperviewController]; + + expect(rootNavigationController.ar_innermostTopViewController).to.equal(viewController); +}); + +it(@"returns the inner most top view controller if there is multiple levels of nesting", ^{ + UIViewController *viewController = [[UIViewController alloc] init]; + + UINavigationController *childNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; + + UIViewController *childWrapperviewController1 = [[UIViewController alloc] init]; + [childWrapperviewController1 addChildViewController:childNavigationController]; + [childWrapperviewController1.view addSubview:childNavigationController.view]; + + UIViewController *childWrapperviewController2 = [[UIViewController alloc] init]; + [childWrapperviewController2 addChildViewController:childWrapperviewController1]; + [childWrapperviewController2.view addSubview:childWrapperviewController1.view]; + + UINavigationController *rootNavigationController = [[UINavigationController alloc] initWithRootViewController:childWrapperviewController2]; + + expect(rootNavigationController.ar_innermostTopViewController).to.equal(viewController); +}); + +SpecEnd diff --git a/Artsy Tests/UIViewController+PresentWithFrame.h b/Artsy Tests/UIViewController+PresentWithFrame.h new file mode 100644 index 00000000000..94dad55bf55 --- /dev/null +++ b/Artsy Tests/UIViewController+PresentWithFrame.h @@ -0,0 +1,11 @@ +#import + +@interface UIViewController (PresentWithFrame) + +/// Presents and sets the frame for the view controller, if self +/// responds to the removing the animations via shouldAnimate: +/// then this is set before viewDidLoad/viewWill/DidAppear are called. + +- (void)ar_presentWithFrame:(CGRect)frame; + +@end diff --git a/Artsy Tests/UIViewController+PresentWithFrame.m b/Artsy Tests/UIViewController+PresentWithFrame.m new file mode 100644 index 00000000000..6f0ef26221c --- /dev/null +++ b/Artsy Tests/UIViewController+PresentWithFrame.m @@ -0,0 +1,23 @@ +#import "UIViewController+PresentWithFrame.h" + +@interface UIViewController (PresentWithFrameFakery) + +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +@end + +@implementation UIViewController (PresentWithFrame) + +- (void)ar_presentWithFrame:(CGRect)frame +{ + SEL animates = NSSelectorFromString(@"setShouldAnimate:"); + if ([self respondsToSelector:animates]) { + self.shouldAnimate = NO; + } + + [self beginAppearanceTransition:YES animated:NO]; + self.view.frame = frame; + [self endAppearanceTransition]; +} + +@end diff --git a/Artsy Tests/UserTests.m b/Artsy Tests/UserTests.m new file mode 100644 index 00000000000..1c054a3f4ed --- /dev/null +++ b/Artsy Tests/UserTests.m @@ -0,0 +1,43 @@ +SpecBegin(User) + +describe(@"with profile", ^{ + __block User *user; + + beforeEach(^{ + user = [User modelWithJSON:@{ @"id" : @"user-id", @"default_profile_id" : @"user-profile" }]; + user.profile = [Profile modelWithJSON:@{ + @"id" : @"profile-id", + @"owner_type" : @"User", + @"owner" : @{ + @"id" : @"user-id", + @"type" : @"User" + } + }]; + }); + + it(@"can be serialized", ^{ + NSData *userData = [NSKeyedArchiver archivedDataWithRootObject:user]; + User *deserializedUser = [NSKeyedUnarchiver unarchiveObjectWithData:userData]; + expect(deserializedUser.userID).to.equal(@"user-id"); + }); +}); + +it(@"migrates model from version 0 to 1", ^{ + NSString *userData_v0 = [[NSBundle bundleForClass:[self class]] pathForResource:@"User_v0" ofType:@"data"]; + User *deserializedUser = [NSKeyedUnarchiver unarchiveObjectWithFile:userData_v0]; + expect(deserializedUser.userID).to.equal(@"4dc805b18101da0001000489"); + expect(deserializedUser.defaultProfileID).to.equal(@"dblockdotorg"); + expect(deserializedUser.name).to.equal(@"dB."); + expect(deserializedUser.email).to.equal(@"dblock@dblock.org"); +}); + +it(@"loads model version 1", ^{ + NSString *userData_v1 = [[NSBundle bundleForClass:[self class]] pathForResource:@"User_v1" ofType:@"data"]; + User *deserializedUser = [NSKeyedUnarchiver unarchiveObjectWithFile:userData_v1]; + expect(deserializedUser.userID).to.equal(@"4dc805b18101da0001000489"); + expect(deserializedUser.defaultProfileID).to.equal(@"dblockdotorg"); + expect(deserializedUser.name).to.equal(@"dB."); + expect(deserializedUser.email).to.equal(@"dblock@dblock.org"); +}); + +SpecEnd diff --git a/Artsy Tests/en.lproj/InfoPlist.strings b/Artsy Tests/en.lproj/InfoPlist.strings new file mode 100644 index 00000000000..477b28ff8f8 --- /dev/null +++ b/Artsy Tests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Artsy Tests/square.png b/Artsy Tests/square.png new file mode 100644 index 00000000000..31a11e635fb Binary files /dev/null and b/Artsy Tests/square.png differ diff --git a/Artsy Tests/stub.jpg b/Artsy Tests/stub.jpg new file mode 100644 index 00000000000..67c4aa9a5b6 Binary files /dev/null and b/Artsy Tests/stub.jpg differ diff --git a/Artsy Tests/wide.jpg b/Artsy Tests/wide.jpg new file mode 100644 index 00000000000..1fb98af8d34 Binary files /dev/null and b/Artsy Tests/wide.jpg differ diff --git a/Artsy.xcodeproj/project.pbxproj b/Artsy.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..9740fe0dc42 --- /dev/null +++ b/Artsy.xcodeproj/project.pbxproj @@ -0,0 +1,4652 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 062C202116DD76C90095A7EC /* ARZoomView.m in Sources */ = {isa = PBXBuildFile; fileRef = 062C202016DD76C90095A7EC /* ARZoomView.m */; }; + 0631FA6F1705E77F000A5ED3 /* mail.html in Resources */ = {isa = PBXBuildFile; fileRef = 0631FA6E1705E77F000A5ED3 /* mail.html */; }; + 064330E5170F526300FF6C41 /* ARArtistViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 064330E4170F526200FF6C41 /* ARArtistViewController.m */; }; + 342F902302C1CF0FD342AB93 /* NSDate+Util.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F97C153D8524B132D0927 /* NSDate+Util.m */; }; + 342F9061CC93426E7F8579A2 /* ARFeedImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F966C989CF0BE97711FB6 /* ARFeedImageLoader.m */; }; + 342F906FD605E75158D32876 /* ARSharingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9DC4018D17E7378DA607 /* ARSharingController.m */; }; + 342F90AEE6950A52210D2DC2 /* UILabel+Typography.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9F6EDBFE4624755933EB /* UILabel+Typography.m */; }; + 342F90FACF8AABA704AF5FC1 /* PartnerShowFairLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9A17225A44AC4BE26303 /* PartnerShowFairLocation.m */; }; + 342F917E6CFBD49D6F43873D /* ARCollapsableTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F927CDF62DAE64F3A1AD7 /* ARCollapsableTextView.m */; }; + 342F91B952D170DCF5A0CDA4 /* UIImageView+AsyncImageLoading.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9DA91638895F6A6A432C /* UIImageView+AsyncImageLoading.m */; }; + 342F91E3124D3E5EA18AB301 /* ARSplitStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F91131CCE3110E3E44E19 /* ARSplitStackView.m */; }; + 342F92546490FE42643B3060 /* ARFairViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9FD434A78C36B76A85B6 /* ARFairViewControllerTests.m */; }; + 342F925E34CAC3E5DB8E5F52 /* ARFairMapViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F92647D726C0DC289B1F3 /* ARFairMapViewControllerTests.m */; }; + 342F9272F2C534C1DFE2EB95 /* ARParallaxEffect.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9A7210CFDD1C15CFBE3C /* ARParallaxEffect.m */; }; + 342F9276A48EA339833228D8 /* ARArtworkDetailViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9F3C689C41D4B941B665 /* ARArtworkDetailViewTests.m */; }; + 342F92A94769B065A53C6735 /* UIViewController+ARStateRestoration.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F992BB6F28B5BE6477E3F /* UIViewController+ARStateRestoration.m */; }; + 342F92E8397ACCA0D2EFF1CB /* MapPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F976360A7DAA65AF7D755 /* MapPoint.m */; }; + 342F9308EE5B3487AE62276D /* ARFairShowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F966895F2D8BDFA7B39B1 /* ARFairShowViewController.m */; }; + 342F94077E27A45531E7A76F /* ARBidButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9629B03A1BEB6888BD4B /* ARBidButton.m */; }; + 342F949383329F78F589BE53 /* MTLModel+Dictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F98950528061D8AE370EA /* MTLModel+Dictionary.m */; }; + 342F953528DFA7BCEAC0552A /* ArtsyAPI+Shows.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9300791CEBAEC9D58AD0 /* ArtsyAPI+Shows.m */; }; + 342F95D6D6422286C9F5A33A /* ARStandardDateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9F3E8A0C15DD0DB437D1 /* ARStandardDateFormatter.m */; }; + 342F95DB4E709752C0EC1FE2 /* Map.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9C11DC281B1C4DC1FB8C /* Map.m */; }; + 342F95E4121D5C70F42DF9A2 /* ARFeedStatusIndicatorTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F919D0897DFE221EC5E73 /* ARFeedStatusIndicatorTableViewCell.m */; }; + 342F961978DD1D771928472A /* UIDevice-Hardware.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F97B49335330C4FD569B4 /* UIDevice-Hardware.m */; }; + 342F96505F255C0F443B5EAA /* UIViewController+SimpleChildren.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9331632309600346BD90 /* UIViewController+SimpleChildren.m */; }; + 342F9654C7AF4AEF8C21BC11 /* ARActionButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F937DAA05F0E703D7A020 /* ARActionButtonsView.m */; }; + 342F9667FC693B47498549FE /* UIApplicationStateEnum.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9CDCE3E3D1674CA080AD /* UIApplicationStateEnum.m */; }; + 342F9715E899AC7D24E70734 /* ORStackViewArtsyCategoriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9316E861BD2C6EBFA31A /* ORStackViewArtsyCategoriesTests.m */; }; + 342F971CCE09F6261F8460AF /* ARFairViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F951B04D7300332FF3DDB /* ARFairViewController.m */; }; + 342F97971D28FDC703392765 /* MTLModel+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F92D1AEEE18F719695548 /* MTLModel+JSON.m */; }; + 342F979DA15866BB8A70CC23 /* ORStackView+ArtsyViews.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9F32AB53EF2503473315 /* ORStackView+ArtsyViews.m */; }; + 342F97CF4D0BD118671622A0 /* ARFairMapAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9559FD6EB9419E3CEE68 /* ARFairMapAnnotationView.m */; }; + 342F9841DEB316C8E4E3C68B /* ARValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F90BFE6268BEBC0E7BB2E /* ARValueTransformer.m */; }; + 342F98EAAAAA4C3264E90CD2 /* ARGeneArtworksNetworkModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F93F91FB4ACCDC29EB901 /* ARGeneArtworksNetworkModelTests.m */; }; + 342F995E23C13C290DEFF23D /* ARScrollNavigationChief.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F98A73E2E1162514B5A24 /* ARScrollNavigationChief.m */; }; + 342F996B2CF23951168534AD /* Follow.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F92FE487AC8FCA43F5C74 /* Follow.m */; }; + 342F99C0F0921C78A384B0E6 /* UIFont+ArtsyFonts.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9929E6FBF1C0AF1BADD7 /* UIFont+ArtsyFonts.m */; }; + 342F99FA4B25031ABC0613DA /* ARTiledImageDataSourceWithImageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9F73EE9B1DBFE4E88BA3 /* ARTiledImageDataSourceWithImageTests.m */; }; + 342F9A43E9B4295EF2CE07E2 /* UIViewController+FullScreenLoading.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F924737155CB0C5A439F9 /* UIViewController+FullScreenLoading.m */; }; + 342F9A60D7F0C30310535232 /* UIImage+ImageFromColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F987C1CACE95AAD292717 /* UIImage+ImageFromColor.m */; }; + 342F9A63FF3B06029E405043 /* ActiveErrorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 342F9AF3B1FC9DB29D65F3D1 /* ActiveErrorView.xib */; }; + 342F9BBE60676CEC2AD66C5A /* NSString+StringCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F903047176176865D20CA /* NSString+StringCase.m */; }; + 342F9BDEE699AB7F9EA34E9F /* ARFileUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9FEFF4A1B56EE307467B /* ARFileUtils.m */; }; + 342F9C00DD759E6DEA845426 /* ARImagePageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9D38472C06E2C065568E /* ARImagePageViewController.m */; }; + 342F9C27685AF340FF34282B /* ARPageSubTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F940CACC66E85F5F48B96 /* ARPageSubTitleView.m */; }; + 342F9CB876CC63CF8E8C656F /* ARTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F969D222D3AAD67B93FCE /* ARTextView.m */; }; + 342F9CDD0CDC753B394F3CFC /* Fair.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9860A7DDF0C11DC56572 /* Fair.m */; }; + 342F9CEA884D36A98BE6BA96 /* NSString+StringSize.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9DDBC48E0F833DBC76F4 /* NSString+StringSize.m */; }; + 342F9CF7231DFEF359B2D829 /* ARFollowableButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9039177611EDF514D37E /* ARFollowableButton.m */; }; + 342F9D1673764FE7442062AC /* ARNetworkErrorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F97A9F6D9D371703C2B19 /* ARNetworkErrorManager.m */; }; + 342F9D3F000632CC30AC5260 /* ARNavigationButtonsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9C3811C6F12C261F04D4 /* ARNavigationButtonsViewController.m */; }; + 342F9D9296B47F66184FA760 /* NSDate+DateRange.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9175329F0389E644C194 /* NSDate+DateRange.m */; }; + 342F9D9D248E4024524174E8 /* ARFairMapViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F939F8B8D9AC1A4773325 /* ARFairMapViewController.m */; }; + 342F9DDFA581974195A708D3 /* ARTrialController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F96EF94FE8A32662F8BF1 /* ARTrialController.m */; }; + 342F9E0892E99A2F66657964 /* Artwork+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F93309D1B60C53E2E0732 /* Artwork+Extensions.m */; }; + 342F9F54666BFDFF200E31AE /* UIView+HitTestExpansion.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F9F4385F10D19BC94E456 /* UIView+HitTestExpansion.m */; }; + 342F9F5789AD65AD09205030 /* MapFeature.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F93E38025E2D2DA90C6DA /* MapFeature.m */; }; + 342F9FBB86D02FABC8AA9339 /* ARNavigationButtonsViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 342F965F76AA25C6C700FE1C /* ARNavigationButtonsViewControllerTests.m */; }; + 3C11CD28189B07810060B26B /* ARAspectRatioImageViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C11CD27189B07810060B26B /* ARAspectRatioImageViewTests.m */; }; + 3C1266F118BE6CC700B5AE72 /* ARFairGuideViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C1266F018BE6CC700B5AE72 /* ARFairGuideViewControllerTests.m */; }; + 3C205ACD1908041700B3C2B4 /* ARSearchResultsDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C205ACC1908041700B3C2B4 /* ARSearchResultsDataSource.m */; }; + 3C205B59189000A5004280E0 /* ARAppNotificationsDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C205B58189000A5004280E0 /* ARAppNotificationsDelegateTests.m */; }; + 3C22227418C6487C00B7CE3A /* User_v0.data in Resources */ = {isa = PBXBuildFile; fileRef = 3C22227318C6487C00B7CE3A /* User_v0.data */; }; + 3C22227618C64B4A00B7CE3A /* User_v1.data in Resources */ = {isa = PBXBuildFile; fileRef = 3C22227518C64B4A00B7CE3A /* User_v1.data */; }; + 3C28D3DB18BE180B00C846EA /* ARNavigationButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C28D3DA18BE180B00C846EA /* ARNavigationButtonTests.m */; }; + 3C2E6C5C192262A3009DAB28 /* ARRouterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C2E6C5B192262A3009DAB28 /* ARRouterTests.m */; }; + 3C33298D18AD3324006D28C0 /* ARFairSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C33298C18AD3324006D28C0 /* ARFairSearchViewController.m */; }; + 3C33299218AD9399006D28C0 /* ARFairSearchViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C33299118AD9399006D28C0 /* ARFairSearchViewControllerTests.m */; }; + 3C35CC7B189FF05E00E3D8DE /* OrderedSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C35CC7A189FF05E00E3D8DE /* OrderedSet.m */; }; + 3C35CC7D189FF14800E3D8DE /* OrderedSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C35CC7C189FF14800E3D8DE /* OrderedSetTests.m */; }; + 3C35CC80189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C35CC7F189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.m */; }; + 3C3FEA8E1884346D00E1A16F /* ARUserManager+Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C3FEA8D1884346D00E1A16F /* ARUserManager+Stubs.m */; }; + 3C4877B3192A745400F40062 /* ARFairMapAnnotationCallOutViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C4877B2192A745400F40062 /* ARFairMapAnnotationCallOutViewTests.m */; }; + 3C48E2101965AC640077A80B /* ARCustomEigenLabels.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C48E20F1965AC640077A80B /* ARCustomEigenLabels.m */; }; + 3C4AE96719094969009C0E8B /* SearchIcon_HeavyGrey@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96219094969009C0E8B /* SearchIcon_HeavyGrey@2x.png */; }; + 3C4AE96819094969009C0E8B /* SearchIcon_LightGrey@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96319094969009C0E8B /* SearchIcon_LightGrey@2x.png */; }; + 3C4AE96919094969009C0E8B /* SearchIcon_MediumGrey@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96419094969009C0E8B /* SearchIcon_MediumGrey@2x.png */; }; + 3C4AE96A19094969009C0E8B /* SearchThumb_HeavyGrey@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96519094969009C0E8B /* SearchThumb_HeavyGrey@2x.png */; }; + 3C4AE96B19094969009C0E8B /* SearchThumb_LightGrey@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96619094969009C0E8B /* SearchThumb_LightGrey@2x.png */; }; + 3C4AE97C19094DAA009C0E8B /* MapAnnotation_Artsy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96D19094DAA009C0E8B /* MapAnnotation_Artsy@2x.png */; }; + 3C4AE97D19094DAA009C0E8B /* MapAnnotation_CoatCheck@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96E19094DAA009C0E8B /* MapAnnotation_CoatCheck@2x.png */; }; + 3C4AE97E19094DAA009C0E8B /* MapAnnotation_Drink@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE96F19094DAA009C0E8B /* MapAnnotation_Drink@2x.png */; }; + 3C4AE97F19094DAA009C0E8B /* MapAnnotation_Food@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97019094DAA009C0E8B /* MapAnnotation_Food@2x.png */; }; + 3C4AE98019094DAA009C0E8B /* MapAnnotation_GenericEvent@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97119094DAA009C0E8B /* MapAnnotation_GenericEvent@2x.png */; }; + 3C4AE98119094DAA009C0E8B /* MapAnnotation_Highlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97219094DAA009C0E8B /* MapAnnotation_Highlighted@2x.png */; }; + 3C4AE98219094DAA009C0E8B /* MapAnnotation_Info@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97319094DAA009C0E8B /* MapAnnotation_Info@2x.png */; }; + 3C4AE98319094DAA009C0E8B /* MapAnnotation_Installation@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97419094DAA009C0E8B /* MapAnnotation_Installation@2x.png */; }; + 3C4AE98419094DAA009C0E8B /* MapAnnotation_Lounge@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97519094DAA009C0E8B /* MapAnnotation_Lounge@2x.png */; }; + 3C4AE98519094DAA009C0E8B /* MapAnnotation_Restroom@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97619094DAA009C0E8B /* MapAnnotation_Restroom@2x.png */; }; + 3C4AE98619094DAA009C0E8B /* MapAnnotation_Saved@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97719094DAA009C0E8B /* MapAnnotation_Saved@2x.png */; }; + 3C4AE98719094DAA009C0E8B /* MapAnnotation_Search@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97819094DAA009C0E8B /* MapAnnotation_Search@2x.png */; }; + 3C4AE98819094DAA009C0E8B /* MapAnnotation_Transport@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97919094DAA009C0E8B /* MapAnnotation_Transport@2x.png */; }; + 3C4AE98919094DAA009C0E8B /* MapAnnotation_Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97A19094DAA009C0E8B /* MapAnnotation_Default@2x.png */; }; + 3C4AE98A19094DAA009C0E8B /* MapAnnotation_VIP@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE97B19094DAA009C0E8B /* MapAnnotation_VIP@2x.png */; }; + 3C4AE99219094F96009C0E8B /* ViewInRoom_Base@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE98C19094F96009C0E8B /* ViewInRoom_Base@2x.png */; }; + 3C4AE99319094F96009C0E8B /* ViewInRoom_BaseNoBench@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE98D19094F96009C0E8B /* ViewInRoom_BaseNoBench@2x.png */; }; + 3C4AE99419094F96009C0E8B /* ViewInRoom_Bench@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE98E19094F96009C0E8B /* ViewInRoom_Bench@2x.png */; }; + 3C4AE99519094F96009C0E8B /* ViewInRoom_Man_3@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE98F19094F96009C0E8B /* ViewInRoom_Man_3@2x.png */; }; + 3C4AE99619094F96009C0E8B /* ViewInRoom_Wall_Right@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE99019094F96009C0E8B /* ViewInRoom_Wall_Right@2x.png */; }; + 3C4AE99719094F96009C0E8B /* ViewInRoom_Wall@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE99119094F96009C0E8B /* ViewInRoom_Wall@2x.png */; }; + 3C4AE9A419096CED009C0E8B /* ARFairMapAnnotationCallOutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C4AE9A319096CED009C0E8B /* ARFairMapAnnotationCallOutView.m */; }; + 3C4AE9A619098916009C0E8B /* MapAnnotationCallout_Anchor@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE9A519098916009C0E8B /* MapAnnotationCallout_Anchor@2x.png */; }; + 3C4AE9AC1909C3A5009C0E8B /* MapAnnotationCallout_Arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C4AE9AB1909C3A5009C0E8B /* MapAnnotationCallout_Arrow@2x.png */; }; + 3C5C7E4018970E8B003823BB /* UIApplicationStateEnumTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C5C7E3F18970E8B003823BB /* UIApplicationStateEnumTests.m */; }; + 3C6AA7CB1885F38D00501F07 /* SaleArtwork+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AA7CA1885F38D00501F07 /* SaleArtwork+Extensions.m */; }; + 3C6AEE27188F228600DD98FC /* ARInternalMobileWebViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AEE26188F228600DD98FC /* ARInternalMobileWebViewControllerTests.m */; }; + 3C6AEE2D188F2D3E00DD98FC /* ARSwitchBoardTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AEE2C188F2D3E00DD98FC /* ARSwitchBoardTests.m */; }; + 3C6BDCBD18E0AEF60028EF5D /* ArtsyAPI+PrivateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6BDCBC18E0AEF60028EF5D /* ArtsyAPI+PrivateTests.m */; }; + 3C6BDCC018E0B3E40028EF5D /* MutableNSURLResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6BDCBF18E0B3E40028EF5D /* MutableNSURLResponse.m */; }; + 3C6BDCC318E0B8D50028EF5D /* ARInquireForArtworkViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6BDCC218E0B8D50028EF5D /* ARInquireForArtworkViewControllerTests.m */; }; + 3C6CB60518ABC7BB008DFE3B /* UserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6CB60418ABC7BB008DFE3B /* UserTests.m */; }; + 3C6CB60918ABD8D2008DFE3B /* ARPostsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6CB60818ABD8D2008DFE3B /* ARPostsViewController.m */; }; + 3C6CB61018ABFB8D008DFE3B /* ARRelatedArtistsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6CB60F18ABFB8D008DFE3B /* ARRelatedArtistsViewController.m */; }; + 3C7294CC196C3E660073663D /* ARFairShowViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C7294CB196C3E660073663D /* ARFairShowViewControllerTests.m */; }; + 3C7880BC18B9081C00595E30 /* ARNotificationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C7880BB18B9081C00595E30 /* ARNotificationView.m */; }; + 3C7A7FA718C6EDAB00E8D336 /* ArtworkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C7A7FA618C6EDAB00E8D336 /* ArtworkTests.m */; }; + 3C8A916718A299BF0038A5B2 /* ARFairSectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C8A916618A299BF0038A5B2 /* ARFairSectionViewController.m */; }; + 3C94B2DA192BF109008D04DF /* ARFairMapPreview.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C94B2D9192BF109008D04DF /* ARFairMapPreview.m */; }; + 3C990C8818CF8E9000BF4C44 /* ARFairMapAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C990C8718CF8E9000BF4C44 /* ARFairMapAnnotation.m */; }; + 3C9F215F18B25D0D00D8898B /* ARSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C9F215E18B25D0D00D8898B /* ARSearchViewController.m */; }; + 3CA0A17D18EF633900C361E5 /* ARArtistViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA0A17C18EF633900C361E5 /* ARArtistViewControllerTests.m */; }; + 3CA17D591901A4900010C9F5 /* SpectaDSL+Sleep.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA17D581901A4900010C9F5 /* SpectaDSL+Sleep.m */; }; + 3CA1E820188465F0003C622D /* Sale+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA1E81F188465F0003C622D /* Sale+Extensions.m */; }; + 3CA1E8231884663E003C622D /* Bid+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA1E8221884663E003C622D /* Bid+Extensions.m */; }; + 3CA1E82518846B3A003C622D /* SaleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA1E82418846B3A003C622D /* SaleTests.m */; }; + 3CA37E7C1910217500B06E81 /* ARHeartButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA37E7B1910217500B06E81 /* ARHeartButtonTests.m */; }; + 3CA55D8918BFF8F800B44CD3 /* MapFeatureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CA55D8818BFF8F800B44CD3 /* MapFeatureTests.m */; }; + 3CAA412F18D88F2E000EE867 /* UIViewController+InnermostTopViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA412E18D88F2E000EE867 /* UIViewController+InnermostTopViewController.m */; }; + 3CAA413218D8CC32000EE867 /* ARFairFavoritesNetworkModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA413118D8CC32000EE867 /* ARFairFavoritesNetworkModelTests.m */; }; + 3CAC639F190AA87A00B17325 /* MapAnnotationCallout_Partner@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CAC639E190AA87A00B17325 /* MapAnnotationCallout_Partner@2x.png */; }; + 3CACF2AC18F591E40054091E /* ARThemeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CACF2AB18F591E40054091E /* ARThemeTests.m */; }; + 3CAED17C188026AC00840608 /* ARUserManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CAED17B188026AC00840608 /* ARUserManagerTests.m */; }; + 3CB37D97192246B500089A1D /* ArtsyAPI+ErrorHandlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CB37D96192246B500089A1D /* ArtsyAPI+ErrorHandlers.m */; }; + 3CB37D991922483100089A1D /* ArtsyAPI+ErrorHandlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CB37D981922483100089A1D /* ArtsyAPI+ErrorHandlers.m */; }; + 3CB97D8F1887099E008C44FE /* ARLoginViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CB97D8E1887099E008C44FE /* ARLoginViewControllerTests.m */; }; + 3CB9A20D18F303B20056C72B /* ARArtworkActionsViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CB9A20C18F303B20056C72B /* ARArtworkActionsViewTests.m */; }; + 3CBB03A8192BA94C00689F89 /* ARFairArtistViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CBB03A7192BA94C00689F89 /* ARFairArtistViewControllerTests.m */; }; + 3CCCC88D1899657C008015DD /* ARFairPostsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCC88C1899657C008015DD /* ARFairPostsViewController.m */; }; + 3CCCC8941899676E008015DD /* ARFairPostsViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCC8931899676E008015DD /* ARFairPostsViewControllerTests.m */; }; + 3CCCC89B18996DD4008015DD /* Post.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCC89A18996DD4008015DD /* Post.m */; }; + 3CCCC89D18997948008015DD /* FairTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCC89C18997948008015DD /* FairTests.m */; }; + 3CCCC8A21899B412008015DD /* ArtsyAPI+Fairs.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCC8A11899B412008015DD /* ArtsyAPI+Fairs.m */; }; + 3CCCC8A51899B6F9008015DD /* FairOrganizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCC8A41899B6F9008015DD /* FairOrganizer.m */; }; + 3CD0BB8918EB0CDF00A59910 /* ARFavoritesViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CD0BB8818EB0CDF00A59910 /* ARFavoritesViewControllerTests.m */; }; + 3CD36799189A9B7A00285DF7 /* ARPostFeedItemLinkView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CD36798189A9B7A00285DF7 /* ARPostFeedItemLinkView.m */; }; + 3CD3679C189AC4F000285DF7 /* ARAspectRatioImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CD3679B189AC4F000285DF7 /* ARAspectRatioImageView.m */; }; + 3CD3679F189ADBEF00285DF7 /* ARPostFeedItemLinkViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CD3679E189ADBEF00285DF7 /* ARPostFeedItemLinkViewTests.m */; }; + 3CE0DA2F18A12335000E537A /* Video.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE0DA2E18A12335000E537A /* Video.m */; }; + 3CE0DA3218A13604000E537A /* OHHTTPStubs+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE0DA3118A13604000E537A /* OHHTTPStubs+JSON.m */; }; + 3CE75A0B18B6367F00885355 /* ARValueTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE75A0A18B6367F00885355 /* ARValueTransformerTests.m */; }; + 3CE7F34218A99E62002BA993 /* ARHeroUnitsNetworkModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE7F34118A99E62002BA993 /* ARHeroUnitsNetworkModelTests.m */; }; + 3CE7F34418A99E6D002BA993 /* SiteHeroUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE7F34318A99E6D002BA993 /* SiteHeroUnitTests.m */; }; + 3CEE0B5F18A16EA200FEA6E6 /* ArtsyAPI+Profiles.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE0B5E18A16EA200FEA6E6 /* ArtsyAPI+Profiles.m */; }; + 3CEE0B6218A16F6900FEA6E6 /* ArtsyAPI+Posts.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE0B6118A16F6900FEA6E6 /* ArtsyAPI+Posts.m */; }; + 3CEE0B6518A16F7D00FEA6E6 /* ArtsyAPI+Artists.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE0B6418A16F7D00FEA6E6 /* ArtsyAPI+Artists.m */; }; + 3CEE0B6818A16F9000FEA6E6 /* ArtsyAPI+Genes.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE0B6718A16F8F00FEA6E6 /* ArtsyAPI+Genes.m */; }; + 3CEE0B6A18A18CDA00FEA6E6 /* ProfileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE0B6918A18CDA00FEA6E6 /* ProfileTests.m */; }; + 3CF0774618DC6585009E18E4 /* ARKonamiKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF0774518DC6585009E18E4 /* ARKonamiKeyboardView.m */; }; + 3CF144A418E3727F00B1A764 /* UIViewController+ScreenSize.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144A318E3727F00B1A764 /* UIViewController+ScreenSize.m */; }; + 3CF144A718E45F6C00B1A764 /* SystemTime.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144A618E45F6C00B1A764 /* SystemTime.m */; }; + 3CF144A918E4607900B1A764 /* SystemTimeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144A818E4607900B1A764 /* SystemTimeTests.m */; }; + 3CF144AB18E460BE00B1A764 /* ArtsyAPI+SystemTimeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144AA18E460BE00B1A764 /* ArtsyAPI+SystemTimeTests.m */; }; + 3CF144AE18E460EF00B1A764 /* ArtsyAPI+SystemTime.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144AD18E460EF00B1A764 /* ArtsyAPI+SystemTime.m */; }; + 3CF144B418E47F9F00B1A764 /* ARSystemTime.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144B318E47F9F00B1A764 /* ARSystemTime.m */; }; + 3CF144B618E4802400B1A764 /* ARSystemTimeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144B518E4802400B1A764 /* ARSystemTimeTests.m */; }; + 3CF144B818E9E00400B1A764 /* ARSignUpSplashViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CF144B718E9E00400B1A764 /* ARSignUpSplashViewControllerTests.m */; }; + 3CFB078B18EB417F00792024 /* ARSecureTextFieldWithPlaceholder.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB078A18EB417F00792024 /* ARSecureTextFieldWithPlaceholder.m */; }; + 3CFB079118EB585B00792024 /* ARTile+ASCII.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB079018EB585B00792024 /* ARTile+ASCII.m */; }; + 3CFBE32D18C3A3F400C781D0 /* ARNetworkErrorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFBE32C18C3A3F400C781D0 /* ARNetworkErrorView.m */; }; + 3CFBE33618C5848900C781D0 /* ARFileUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFBE33518C5848900C781D0 /* ARFileUtilsTests.m */; }; + 491A4DE2168E4343003B2246 /* Gene.m in Sources */ = {isa = PBXBuildFile; fileRef = 491A4DE1168E4343003B2246 /* Gene.m */; }; + 4938742917BD512700724795 /* ARSignUpSplashViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4938742717BD512700724795 /* ARSignUpSplashViewController.m */; }; + 4938743617BDB2CD00724795 /* ARCrossfadingImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4938743517BDB2CD00724795 /* ARCrossfadingImageView.m */; }; + 49405AB317BEBAFF004F86D8 /* AROnboardingNavBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 49405AB217BEBAFF004F86D8 /* AROnboardingNavBarView.m */; }; + 49405AB617BEC87A004F86D8 /* ARSignupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 49405AB517BEC87A004F86D8 /* ARSignupViewController.m */; }; + 494332FD16692010005AB483 /* VideoContentLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 494332FC16692010005AB483 /* VideoContentLink.m */; }; + 49473F3317C18772004BF082 /* ARSlideshowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 49473F3217C18772004BF082 /* ARSlideshowViewController.m */; }; + 49473F3617C1907F004BF082 /* ARCreateAccountViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 49473F3517C1907F004BF082 /* ARCreateAccountViewController.m */; }; + 49473F3917C192AE004BF082 /* ARTextFieldWithPlaceholder.m in Sources */ = {isa = PBXBuildFile; fileRef = 49473F3817C192AE004BF082 /* ARTextFieldWithPlaceholder.m */; }; + 4953E8321668021D00A09726 /* Image.m in Sources */ = {isa = PBXBuildFile; fileRef = 4953E8311668021D00A09726 /* Image.m */; }; + 4953E8381668179500A09726 /* PostImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4953E8371668179500A09726 /* PostImage.m */; }; + 4953E83B16681A4200A09726 /* ContentLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 4953E83A16681A4200A09726 /* ContentLink.m */; }; + 499A587B166561E9004B0E2F /* ARFeedConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 499A587A166561E8004B0E2F /* ARFeedConstants.m */; }; + 499A588E16658CAA004B0E2F /* Partner.m in Sources */ = {isa = PBXBuildFile; fileRef = 499A588D16658CAA004B0E2F /* Partner.m */; }; + 499A5894166683AC004B0E2F /* Artist.m in Sources */ = {isa = PBXBuildFile; fileRef = 499A5893166683AB004B0E2F /* Artist.m */; }; + 499A58A516669CD6004B0E2F /* ARPostFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 499A58A416669CD5004B0E2F /* ARPostFeedItem.m */; }; + 49A76D0817592C96001D4B81 /* SearchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A76D0717592C96001D4B81 /* SearchResult.m */; }; + 49A76D0E17594E32001D4B81 /* Tag.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A76D0D17594E32001D4B81 /* Tag.m */; }; + 49A77221165ADB6E00BC6FD3 /* ARFeedTimeline.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A77220165ADB6E00BC6FD3 /* ARFeedTimeline.m */; }; + 49A77235165ADB9400BC6FD3 /* ARFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A77224165ADB9300BC6FD3 /* ARFeedItem.m */; }; + 49A77236165ADB9400BC6FD3 /* ARFollowFairFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A77226165ADB9300BC6FD3 /* ARFollowFairFeedItem.m */; }; + 49A77237165ADB9400BC6FD3 /* ARPartnerShowFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A77228165ADB9300BC6FD3 /* ARPartnerShowFeedItem.m */; }; + 49A77238165ADB9400BC6FD3 /* ARPublishedArtworkSetFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A7722A165ADB9300BC6FD3 /* ARPublishedArtworkSetFeedItem.m */; }; + 49A77239165ADB9400BC6FD3 /* ARRepostFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A7722C165ADB9300BC6FD3 /* ARRepostFeedItem.m */; }; + 49A7723A165ADB9400BC6FD3 /* ARSavedArtworkSetFeedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A7722E165ADB9300BC6FD3 /* ARSavedArtworkSetFeedItem.m */; }; + 49BA7E0E1655ABE600C06572 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 49BA7E0C1655ABE600C06572 /* InfoPlist.strings */; }; + 49BA7E141655ABE600C06572 /* ARAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 49BA7E131655ABE600C06572 /* ARAppDelegate.m */; }; + 49EC62181778AF100020D648 /* PartnerShow.m in Sources */ = {isa = PBXBuildFile; fileRef = 49EC62171778AF100020D648 /* PartnerShow.m */; }; + 49EF164716C568EA00460BD7 /* Profile.m in Sources */ = {isa = PBXBuildFile; fileRef = 49EF164616C568EA00460BD7 /* Profile.m */; }; + 49F0C67B17B9706000721244 /* AROnboardingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 49F0C67A17B9706000721244 /* AROnboardingViewController.m */; }; + 49F45188176A71B50041A4B4 /* ARArtworkSetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4917819E176A6B22001E751E /* ARArtworkSetViewController.m */; }; + 540262C618A0FAFB00844AE1 /* ARButtonWithImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 540262C518A0FAFB00844AE1 /* ARButtonWithImage.m */; }; + 54289FEE18AA7F4E00681E49 /* UINavigationController_InnermostTopViewControllerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 54289FED18AA7F4E00681E49 /* UINavigationController_InnermostTopViewControllerSpec.m */; }; + 5435192E18A8E9420060F31E /* UIView+OldSchoolSnapshots.m in Sources */ = {isa = PBXBuildFile; fileRef = 5435192D18A8E9420060F31E /* UIView+OldSchoolSnapshots.m */; }; + 546778EE18A95642002C4C71 /* ARAppDelegate+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = 546778ED18A95642002C4C71 /* ARAppDelegate+Testing.m */; }; + 546A858217763349006D489B /* ARHeroUnitsNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 546A858117763349006D489B /* ARHeroUnitsNetworkModel.m */; }; + 54B7477518A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B7477418A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.m */; }; + 5E0AEB7819B9EA43009F34DE /* ARShowNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E0AEB7719B9EA43009F34DE /* ARShowNetworkModel.m */; }; + 5E0AEB7A19B9EF57009F34DE /* ARShowNetworkModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E0AEB7919B9EF57009F34DE /* ARShowNetworkModelTests.m */; }; + 5E0AEB7D19B9FAED009F34DE /* ARStubbedShowNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E0AEB7C19B9FAED009F34DE /* ARStubbedShowNetworkModel.m */; }; + 5E284609194A2E58007274AB /* ARFairGuideContainerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E284608194A2E58007274AB /* ARFairGuideContainerViewController.m */; }; + 5E50987D18F82FCF001AC704 /* AROfflineView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E50987C18F82FCF001AC704 /* AROfflineView.m */; }; + 5E6621DD19768F750064FC52 /* MapIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5E6621DC19768F750064FC52 /* MapIcon@2x.png */; }; + 5E71AFC7195C64C1000F6325 /* ARFairGuideContainerViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E71AFC6195C64C1000F6325 /* ARFairGuideContainerViewControllerTests.m */; }; + 5E9A782019068EDF00734E1B /* ARProfileViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E9A781F19068EDF00734E1B /* ARProfileViewControllerTests.m */; }; + 5E9A78231906BA3D00734E1B /* OCMArg+ClassChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E9A78221906BA3D00734E1B /* OCMArg+ClassChecker.m */; }; + 5EB33E74197EBDE200706EB1 /* ARParallaxHeaderViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EB33E73197EBDE200706EB1 /* ARParallaxHeaderViewControllerTests.m */; }; + 5EB33E77197EBFEB00706EB1 /* wide.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 5EB33E75197EBFEB00706EB1 /* wide.jpg */; }; + 5EB33E78197EBFEB00706EB1 /* square.png in Resources */ = {isa = PBXBuildFile; fileRef = 5EB33E76197EBFEB00706EB1 /* square.png */; }; + 5EBDC94E19792C3A0082C514 /* ARNavigationControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EBDC94D19792C3A0082C514 /* ARNavigationControllerTests.m */; }; + 5EBDC95119794D840082C514 /* ARSearchFieldButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EBDC95019794D840082C514 /* ARSearchFieldButtonTests.m */; }; + 5EBDC95319794DF10082C514 /* ARPendingOperationViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EBDC95219794DF10082C514 /* ARPendingOperationViewControllerTests.m */; }; + 5ECFA8E81907E26E000B92EA /* ARSwitchView+Artist.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ECFA8E71907E26E000B92EA /* ARSwitchView+Artist.m */; }; + 5ECFA8EA1907E9AB000B92EA /* ARSwitchViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ECFA8E91907E9AB000B92EA /* ARSwitchViewTests.m */; }; + 5ECFA8EF1907FC6C000B92EA /* ARSwitchView+FairGuide.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ECFA8EE1907FC6C000B92EA /* ARSwitchView+FairGuide.m */; }; + 5EDB120B197E691100E241F0 /* ARParallaxHeaderViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EDB120A197E691100E241F0 /* ARParallaxHeaderViewController.m */; }; + 5EE5DE14190167A400040B84 /* ARProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EE5DE13190167A400040B84 /* ARProfileViewController.m */; }; + 5EFE2BE41910FC81003B5EEA /* ARAppDelegate+Analytics.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EFE2BE31910FC81003B5EEA /* ARAppDelegate+Analytics.m */; }; + 5EFF52BA1976CA6C00E2A563 /* ARSearchFieldButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EFF52B91976CA6C00E2A563 /* ARSearchFieldButton.m */; }; + 5EFF52BE197916C800E2A563 /* ARPendingOperationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EFF52BD197916C800E2A563 /* ARPendingOperationViewController.m */; }; + 6001414817CA33C100612DB4 /* ARArtworkRelatedArtworksView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6001414717CA33C100612DB4 /* ARArtworkRelatedArtworksView.m */; }; + 600415C917C4ECE2003C7974 /* ArtsyAPI+RelatedModels.m in Sources */ = {isa = PBXBuildFile; fileRef = 600415C817C4ECE2003C7974 /* ArtsyAPI+RelatedModels.m */; }; + 600A734317DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.m in Sources */ = {isa = PBXBuildFile; fileRef = 600A734217DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.m */; }; + 600EE29E16B3003F002E9F9A /* ARNavigationButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 600EE29C16B3003F002E9F9A /* ARNavigationButton.m */; }; + 6012313418B153C500B7667F /* ARFairArtistViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6012313318B153C500B7667F /* ARFairArtistViewController.m */; }; + 6012313618B2E44A00B7667F /* MapButtonAction@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6012313518B2E44A00B7667F /* MapButtonAction@2x.png */; }; + 6016C190178C2B7F008EC8E7 /* AREmbeddedModelsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6016C18F178C2B7F008EC8E7 /* AREmbeddedModelsViewController.m */; }; + 6016C198178C2C33008EC8E7 /* ARArtworkMasonryModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 6016C197178C2C33008EC8E7 /* ARArtworkMasonryModule.m */; }; + 6016C199178C2C37008EC8E7 /* ARArtworkFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 6016C194178C2C06008EC8E7 /* ARArtworkFlowModule.m */; }; + 601C317816582BA30013E061 /* ARRouter.m in Sources */ = {isa = PBXBuildFile; fileRef = 601C317716582BA30013E061 /* ARRouter.m */; }; + 601F029817F3419400EB3E83 /* ARArtworkViewController+ButtonActions.m in Sources */ = {isa = PBXBuildFile; fileRef = 601F029717F3419400EB3E83 /* ARArtworkViewController+ButtonActions.m */; }; + 60215FD717CDF9FA000F3A62 /* ARSignUpActiveUserViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60215FD517CDF9FA000F3A62 /* ARSignUpActiveUserViewController.m */; }; + 60215FD817CDF9FA000F3A62 /* ARSignUpActiveUserViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 60215FD617CDF9FA000F3A62 /* ARSignUpActiveUserViewController.xib */; }; + 6021F97F17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 6021F97E17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.m */; }; + 60229B4E1683BE280072DC12 /* ARSpinner.m in Sources */ = {isa = PBXBuildFile; fileRef = 60229B4D1683BE280072DC12 /* ARSpinner.m */; }; + 60289229176BE98C00512977 /* ARViewInRoomViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60289228176BE98C00512977 /* ARViewInRoomViewController.m */; }; + 6029E9F51993D726002D42C3 /* ARAppSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6029E9F41993D726002D42C3 /* ARAppSearchViewController.m */; }; + 602BC089168E0C0E00069FDB /* ARReusableLoadingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 602BC088168E0C0D00069FDB /* ARReusableLoadingView.m */; }; + 60327DC91987AAD00075B399 /* ARTabContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DC81987AAD00075B399 /* ARTabContentView.m */; }; + 60327DCD1987AD240075B399 /* ARDispatchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DCC1987AD240075B399 /* ARDispatchManager.m */; }; + 60327DD01987B7940075B399 /* ARDeveloperOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DCF1987B7940075B399 /* ARDeveloperOptions.m */; }; + 60327DD21987BA830075B399 /* ARDeveloperOptionsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DD11987BA830075B399 /* ARDeveloperOptionsSpec.m */; }; + 60327DE11987FF490075B399 /* ARTopMenuNavigationDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DE01987FF490075B399 /* ARTopMenuNavigationDataSource.m */; }; + 60327DE9198933610075B399 /* ARTabContentViewSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DE3198933610075B399 /* ARTabContentViewSpec.m */; }; + 60327DEA198933610075B399 /* ARTestTopMenuNavigationDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DE5198933610075B399 /* ARTestTopMenuNavigationDataSource.m */; }; + 60327DEB198933610075B399 /* ARTopMenuNavigationDataSourceSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DE6198933610075B399 /* ARTopMenuNavigationDataSourceSpec.m */; }; + 60327DEC198933610075B399 /* ARTopMenuViewControllerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 60327DE7198933610075B399 /* ARTopMenuViewControllerSpec.m */; }; + 6034EB1D175F68350070478D /* SiteHeroUnit.m in Sources */ = {isa = PBXBuildFile; fileRef = 6034EB1C175F68350070478D /* SiteHeroUnit.m */; }; + 6034EB2F1760A9BE0070478D /* ARTopMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6034EB2B1760A9BD0070478D /* ARTopMenuViewController.m */; }; + 6036B5621760DC9100F1DD01 /* ARHeroUnitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6036B5611760DC9100F1DD01 /* ARHeroUnitViewController.m */; }; + 6036B5651760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 6036B5641760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.m */; }; + 6037442516D4227500AE7788 /* ARSwitchBoard.m in Sources */ = {isa = PBXBuildFile; fileRef = 6037442416D4227500AE7788 /* ARSwitchBoard.m */; }; + 60388703189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 60388702189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.m */; }; + 60388706189C103F00D3EEAA /* ARFairMapZoomManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 60388705189C103F00D3EEAA /* ARFairMapZoomManager.m */; }; + 603B3AEB1774A25700BA5BD3 /* ARNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 603B3AEA1774A0FA00BA5BD3 /* ARNavigationController.m */; }; + 603B55A117D64A1B00566935 /* ARArtworkPriceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 603B55A017D64A1B00566935 /* ARArtworkPriceView.m */; }; + 603D7C05195884C100ACA840 /* ARAuctionBidderStateLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 603D7C04195884C100ACA840 /* ARAuctionBidderStateLabel.m */; }; + 604166AD16C1D20900CFBD2F /* ArtsyAPI+Artworks.m in Sources */ = {isa = PBXBuildFile; fileRef = 604166AC16C1D20900CFBD2F /* ArtsyAPI+Artworks.m */; }; + 604166B116C1D47100CFBD2F /* ArtsyAPI+CurrentUserFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 604166B016C1D47000CFBD2F /* ArtsyAPI+CurrentUserFunctions.m */; }; + 60431FB818042A1E000118D7 /* ARAppNotificationsDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 60431FB718042A1E000118D7 /* ARAppNotificationsDelegate.m */; }; + 60431FBB18042E63000118D7 /* ARAppBackgroundFetchDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 60431FBA18042E63000118D7 /* ARAppBackgroundFetchDelegate.m */; }; + 60438F8A1782F8CC00C1B63B /* ArtsyAPI+Search.m in Sources */ = {isa = PBXBuildFile; fileRef = 60438F891782F8CC00C1B63B /* ArtsyAPI+Search.m */; }; + 6044CFC4179D64F500CE4132 /* Theme.json in Resources */ = {isa = PBXBuildFile; fileRef = 6044CFC3179D64F500CE4132 /* Theme.json */; }; + 6044CFC8179DD3C200CE4132 /* ARTheme+HeightAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 6044CFC7179DD3C200CE4132 /* ARTheme+HeightAdditions.m */; }; + 6044E545176E165600075B15 /* ARShowFeedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6044E544176E165600075B15 /* ARShowFeedViewController.m */; }; + 605002E317D8912300C090B8 /* Artwork_Icon_Share@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 605002E217D8912300C090B8 /* Artwork_Icon_Share@2x.png */; }; + 605002E917D9F5DC00C090B8 /* SmallMoreVerticalArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 605002E717D9F5DC00C090B8 /* SmallMoreVerticalArrow.png */; }; + 605002EA17D9F5DC00C090B8 /* SmallMoreVerticalArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 605002E817D9F5DC00C090B8 /* SmallMoreVerticalArrow@2x.png */; }; + 605B11B217CFD78400334196 /* AROnboardingWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 605B11B117CFD78400334196 /* AROnboardingWebViewController.m */; }; + 605B11B517CFE7CC00334196 /* ARTermsAndConditionsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 605B11B417CFE7CC00334196 /* ARTermsAndConditionsView.m */; }; + 60745DEA165802D9006CE156 /* ARUserManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 60745DE9165802D9006CE156 /* ARUserManager.m */; }; + 607D754A17C239E700CA1D41 /* ArtsyAPI+Following.m in Sources */ = {isa = PBXBuildFile; fileRef = 607D754917C239E700CA1D41 /* ArtsyAPI+Following.m */; }; + 607D756B17C3816600CA1D41 /* ArtsyAPI+ListCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = 607D756A17C3816600CA1D41 /* ArtsyAPI+ListCollection.m */; }; + 607D756E17C3873700CA1D41 /* ARFavoriteItemViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 607D756D17C3873700CA1D41 /* ARFavoriteItemViewCell.m */; }; + 607E2E7817C8C46100396120 /* ARArtworkViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 607E2E7717C8C46000396120 /* ARArtworkViewController.m */; }; + 607E2E7B17C8C9FE00396120 /* ARArtworkDetailView.m in Sources */ = {isa = PBXBuildFile; fileRef = 607E2E7A17C8C9FE00396120 /* ARArtworkDetailView.m */; }; + 607E2E7E17C8CA4D00396120 /* ARZoomArtworkImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 607E2E7D17C8CA4D00396120 /* ARZoomArtworkImageViewController.m */; }; + 607E2E8417C8CF6B00396120 /* ARArtworkPreviewActionsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 607E2E8317C8CF6B00396120 /* ARArtworkPreviewActionsView.m */; }; + 607E2E8717C8E87E00396120 /* ARArtworkPreviewImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 607E2E8617C8E87E00396120 /* ARArtworkPreviewImageView.m */; }; + 607E2E8A17C9121500396120 /* ARZoomImageTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = 607E2E8917C9121500396120 /* ARZoomImageTransition.m */; }; + 60838EAF17728C5D00869F6E /* ARHeartButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 60838EAE17728C5D00869F6E /* ARHeartButton.m */; }; + 60838EB51773547700869F6E /* ARViewInRoomTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = 60838EB41773547700869F6E /* ARViewInRoomTransition.m */; }; + 60889874175DEBC2008C9319 /* ARFeedSubclasses.m in Sources */ = {isa = PBXBuildFile; fileRef = 60889873175DEBC2008C9319 /* ARFeedSubclasses.m */; }; + 6088B75417D1DBAC00E4BB67 /* ARAnalyticsConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 6088B75317D1DBAC00E4BB67 /* ARAnalyticsConstants.m */; }; + 6088B75917D20A0A00E4BB67 /* ARSlideshowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 49F0C67E17B972F200721244 /* ARSlideshowView.m */; }; + 608920C3178C682A00989A10 /* ARItemThumbnailViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 608920C2178C682A00989A10 /* ARItemThumbnailViewCell.m */; }; + 608B2F5E1657D0500046956C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 608B2F5D1657D0500046956C /* main.m */; }; + 608B2F621657D1500046956C /* ARDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 608B2F611657D1500046956C /* ARDefaults.m */; }; + 608B2F6F1657D1D10046956C /* User.m in Sources */ = {isa = PBXBuildFile; fileRef = 608B2F6E1657D1D10046956C /* User.m */; }; + 608B706317D4A1C80088A56C /* ActionButton@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703117D4A1C70088A56C /* ActionButton@2x.png */; }; + 608B706917D4A1C80088A56C /* Heart_White@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703717D4A1C70088A56C /* Heart_White@2x.png */; }; + 608B706B17D4A1C80088A56C /* Heart_Black@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703917D4A1C70088A56C /* Heart_Black@2x.png */; }; + 608B706C17D4A1C80088A56C /* Artwork_Icon_Share.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703A17D4A1C70088A56C /* Artwork_Icon_Share.png */; }; + 608B706D17D4A1C80088A56C /* Artwork_Icon_VIR.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703B17D4A1C70088A56C /* Artwork_Icon_VIR.png */; }; + 608B706E17D4A1C80088A56C /* Artwork_Icon_VIR@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703C17D4A1C70088A56C /* Artwork_Icon_VIR@2x.png */; }; + 608B707017D4A1C80088A56C /* BackArrow_Highlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B703E17D4A1C70088A56C /* BackArrow_Highlighted@2x.png */; }; + 608B707217D4A1C80088A56C /* BackArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704017D4A1C70088A56C /* BackArrow@2x.png */; }; + 608B707917D4A1C80088A56C /* FollowCheckmark.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704717D4A1C70088A56C /* FollowCheckmark.png */; }; + 608B707A17D4A1C80088A56C /* FollowCheckmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704817D4A1C70088A56C /* FollowCheckmark@2x.png */; }; + 608B707B17D4A1C80088A56C /* FooterBackground@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704917D4A1C70088A56C /* FooterBackground@2x.png */; }; + 608B707E17D4A1C80088A56C /* MenuButtonBG@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704C17D4A1C70088A56C /* MenuButtonBG@2x.png */; }; + 608B708017D4A1C80088A56C /* MenuClose@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704E17D4A1C70088A56C /* MenuClose@2x.png */; }; + 608B708117D4A1C80088A56C /* MenuHamburger@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B704F17D4A1C70088A56C /* MenuHamburger@2x.png */; }; + 608B708317D4A1C80088A56C /* MoreArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B705117D4A1C70088A56C /* MoreArrow@2x.png */; }; + 608B708A17D4A1C80088A56C /* SettingsButton@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B705817D4A1C80088A56C /* SettingsButton@2x.png */; }; + 608B708B17D4A1C80088A56C /* SidebarButtonBG@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B705917D4A1C80088A56C /* SidebarButtonBG@2x.png */; }; + 608B708C17D4A1C80088A56C /* SidebarButtonHighlightBG@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B705A17D4A1C80088A56C /* SidebarButtonHighlightBG@2x.png */; }; + 608B709417D4A1C80088A56C /* CloseButtonLarge@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 608B706217D4A1C80088A56C /* CloseButtonLarge@2x.png */; }; + 608B709917D4F6520088A56C /* ARSearchTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 608B709817D4F6520088A56C /* ARSearchTableViewCell.m */; }; + 608EE3DB19954CEB001F4FE0 /* UIViewController+PresentWithFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 608EE3DA19954CEB001F4FE0 /* UIViewController+PresentWithFrame.m */; }; + 60903CC4175CE21A002AB800 /* AROptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CC3175CE21A002AB800 /* AROptions.m */; }; + 60903CCB175CE766002AB800 /* ARAdminSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CCA175CE766002AB800 /* ARAdminSettingsViewController.m */; }; + 60903CCF175CF088002AB800 /* ARGroupedTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CCE175CF088002AB800 /* ARGroupedTableViewCell.m */; }; + 60903CD2175CF095002AB800 /* ARAnimatedTickView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CD1175CF095002AB800 /* ARAnimatedTickView.m */; }; + 60903CD5175CF23F002AB800 /* ARTickedTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CD4175CF23F002AB800 /* ARTickedTableViewCell.m */; }; + 60903CD8175CF27D002AB800 /* ARAdminTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CD7175CF27D002AB800 /* ARAdminTableViewCell.m */; }; + 60903CDD175DE1C2002AB800 /* ARFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 60903CDC175DE1C2002AB800 /* ARFeed.m */; }; + 60935A441A69CFE700129CE1 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 601C3183165838590013E061 /* MobileCoreServices.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 60935A461A69CFEE00129CE1 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 60935A451A69CFEE00129CE1 /* WebKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 6099F8F9178DBF160004EF04 /* ARModernPartnerShowTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 6099F8F8178DBF160004EF04 /* ARModernPartnerShowTableViewCell.m */; }; + 6099F8FE178DE9400004EF04 /* ARTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 6099F8FD178DE9400004EF04 /* ARTheme.m */; }; + 6099F907178F24750004EF04 /* ARDefaultNavigationTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = 6099F906178F24750004EF04 /* ARDefaultNavigationTransition.m */; }; + 6099F90A17904F9B0004EF04 /* ARModelCollectionViewModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 6099F90917904F9B0004EF04 /* ARModelCollectionViewModule.m */; }; + 609A82F017A10C5C00AFDF13 /* ARNavigationTransitionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 609A82EF17A10C5C00AFDF13 /* ARNavigationTransitionController.m */; }; + 609A82F417A1117E00AFDF13 /* ARInquireForArtworkViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 609A82F317A1117E00AFDF13 /* ARInquireForArtworkViewController.m */; }; + 609A82FB17A1147800AFDF13 /* ARNavigationTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = 609A82FA17A1147800AFDF13 /* ARNavigationTransition.m */; }; + 609B3C091761F80C00953CB2 /* ARSiteHeroUnitView.m in Sources */ = {isa = PBXBuildFile; fileRef = 609B3C081761F80C00953CB2 /* ARSiteHeroUnitView.m */; }; + 60A0AD4A1797FD2E00E976B7 /* ARThemedFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 60A0AD491797FD2E00E976B7 /* ARThemedFactory.m */; }; + 60A224FC17CE040B00233CA1 /* AROnboardingTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = 60A224FB17CE040B00233CA1 /* AROnboardingTransition.m */; }; + 60A40E9118A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 60A40E9018A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.m */; }; + 60A49F601676559300B9B95D /* ARNetworkConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 60A49F5F1676559300B9B95D /* ARNetworkConstants.m */; }; + 60A612B8189673F4008FC19D /* ARGeneArtworksNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 60A612B7189673F4008FC19D /* ARGeneArtworksNetworkModel.m */; }; + 60AEC84716E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60AEC84616E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.m */; }; + 60B617CD1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B617CC1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.m */; }; + 60B617D31815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B617D21815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.m */; }; + 60B6F0F21662AADF007C9587 /* ARAppConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B6F0F11662AADF007C9587 /* ARAppConstants.m */; }; + 60B6F13916638785007C9587 /* ArtsyAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B6F13816638785007C9587 /* ArtsyAPI.m */; }; + 60B6F13E16638815007C9587 /* Artwork.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B6F13D16638815007C9587 /* Artwork.m */; }; + 60B7604617C9FBEA00073A14 /* ARArtworkActionsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B7604517C9FBEA00073A14 /* ARArtworkActionsView.m */; }; + 60B79B84182C346700945FFF /* ARQuicksilverViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B79B82182C346700945FFF /* ARQuicksilverViewController.m */; }; + 60B79B85182C346700945FFF /* ARQuicksilverViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 60B79B83182C346700945FFF /* ARQuicksilverViewController.xib */; }; + 60B79B89182C54CE00945FFF /* ARQuicksilverSearchBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 60B79B88182C54CE00945FFF /* ARQuicksilverSearchBar.m */; }; + 60BB9FE916695A8F001FA20A /* PhotoContentLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 49433300166920B3005AB483 /* PhotoContentLink.m */; }; + 60CEA77E19B61F8000CC3A91 /* ARArtistNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 60CEA77D19B61F8000CC3A91 /* ARArtistNetworkModel.m */; }; + 60CEA78119B6254800CC3A91 /* ARStubbedArtistNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 60CEA78019B6254800CC3A91 /* ARStubbedArtistNetworkModel.m */; }; + 60CF97FC17BE9303005ED59B /* ARShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60CF97FB17BE9303005ED59B /* ARShadowView.m */; }; + 60CF980817BF79CA005ED59B /* ARArtistBiographyViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60CF980717BF79CA005ED59B /* ARArtistBiographyViewController.m */; }; + 60D83DE8189EAB82001672E9 /* ARFairShowMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D83DE7189EAB82001672E9 /* ARFairShowMapper.m */; }; + 60D83DEE189EE679001672E9 /* ARFairGuideViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D83DED189EE679001672E9 /* ARFairGuideViewController.m */; }; + 60D8E63B18D256BB0040BEFD /* ARDemoSplashViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D8E63918D256BB0040BEFD /* ARDemoSplashViewController.m */; }; + 60D8E63C18D256BB0040BEFD /* ARDemoSplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 60D8E63A18D256BB0040BEFD /* ARDemoSplashViewController.xib */; }; + 60D90A0517C2109A0073D5B9 /* FeaturedLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D90A0417C2109A0073D5B9 /* FeaturedLink.m */; }; + 60D90A0817C2182F0073D5B9 /* ARExternalWebBrowserViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D90A0717C2182F0073D5B9 /* ARExternalWebBrowserViewController.m */; }; + 60D90A0B17C218930073D5B9 /* ARInternalMobileWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D90A0A17C218930073D5B9 /* ARInternalMobileWebViewController.m */; }; + 60DE2DE01677B2A600621540 /* ARFeedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A77240165ADC3C00BC6FD3 /* ARFeedViewController.m */; }; + 60DE2DE11677B30700621540 /* ARLoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 608B2F711657D2DF0046956C /* ARLoginViewController.m */; }; + 60E6447617BE424E004486B3 /* ARSwitchView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60E6447517BE424E004486B3 /* ARSwitchView.m */; }; + 60EFA6F516CAA8180094AD7C /* ArtsyAPI+Feed.m in Sources */ = {isa = PBXBuildFile; fileRef = 60EFA6F416CAA8180094AD7C /* ArtsyAPI+Feed.m */; }; + 60F1C50E17C0EC6A000938F7 /* ARBrowseViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60F1C50D17C0EC6A000938F7 /* ARBrowseViewController.m */; }; + 60F1C51217C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 60F1C51117C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.m */; }; + 60F1C51517C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 60F1C51417C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.m */; }; + 60F1C51817C0F3DE000938F7 /* ArtsyAPI+Browse.m in Sources */ = {isa = PBXBuildFile; fileRef = 60F1C51717C0F3DE000938F7 /* ArtsyAPI+Browse.m */; }; + 60F1C51B17C11303000938F7 /* ARGeneViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60F1C51A17C11303000938F7 /* ARGeneViewController.m */; }; + 60F8FFC1197E773E00DC3869 /* ARArtworkSetViewControllerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 60F8FFC0197E773E00DC3869 /* ARArtworkSetViewControllerSpec.m */; }; + 60FAEF43179843080031C88B /* ARFavoritesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60FAEF42179843080031C88B /* ARFavoritesViewController.m */; }; + 6C55DE07E86540899869A357 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + 7BAB70E261299573481071A9 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AABCA8DF484E65EF3CC11FA /* libPods.a */; }; + B30FEF5A181EEA47009E4EAD /* ARBidButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B30FEF59181EEA47009E4EAD /* ARBidButtonTests.m */; }; + B316E2F418170DF40086CCDB /* SaleArtwork.m in Sources */ = {isa = PBXBuildFile; fileRef = B316E2F318170DF40086CCDB /* SaleArtwork.m */; }; + B316E2F7181713110086CCDB /* Bidder.m in Sources */ = {isa = PBXBuildFile; fileRef = B316E2F6181713110086CCDB /* Bidder.m */; }; + B31DB35B17FA234C009B122B /* ARArtworkMetadataView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6089243217F617140023D1AC /* ARArtworkMetadataView.m */; }; + B375E5F418121EEA005FC680 /* Sale.m in Sources */ = {isa = PBXBuildFile; fileRef = B375E5F318121EEA005FC680 /* Sale.m */; }; + B375E5F71812207D005FC680 /* ArtsyAPI+Sales.m in Sources */ = {isa = PBXBuildFile; fileRef = B375E5F61812207D005FC680 /* ArtsyAPI+Sales.m */; }; + B3BD12D917F22370002CA230 /* fbp in Resources */ = {isa = PBXBuildFile; fileRef = B3BD12D717F22370002CA230 /* fbp */; }; + B3BD12DA17F22370002CA230 /* fbs in Resources */ = {isa = PBXBuildFile; fileRef = B3BD12D817F22370002CA230 /* fbs */; }; + B3DDE9C118313C7C0012819F /* ARArtworkAuctionPriceView.m in Sources */ = {isa = PBXBuildFile; fileRef = B3DDE9C018313C7C0012819F /* ARArtworkAuctionPriceView.m */; }; + B3DDE9C418313CFF0012819F /* ARArtworkPriceRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = B3DDE9C318313CFF0012819F /* ARArtworkPriceRowView.m */; }; + B3ECE8281819D1E6009F5C5B /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3ECE8271819D1E6009F5C5B /* XCTest.framework */; }; + B3ECE8291819D1E6009F5C5B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49BA7E051655ABE600C06572 /* Foundation.framework */; }; + B3ECE82A1819D1E6009F5C5B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49BA7E031655ABE600C06572 /* UIKit.framework */; }; + B3ECE8301819D1E6009F5C5B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3ECE82E1819D1E6009F5C5B /* InfoPlist.strings */; }; + B3ECE83C1819D1FD009F5C5B /* SaleArtworkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B3ECE83B1819D1FD009F5C5B /* SaleArtworkTests.m */; }; + B3EFC6391815B41C00F23540 /* BidderPosition.m in Sources */ = {isa = PBXBuildFile; fileRef = B3EFC6381815B41C00F23540 /* BidderPosition.m */; }; + CB11525B17C815210093D864 /* AROnboardingFollowableTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CB11525A17C815210093D864 /* AROnboardingFollowableTableViewCell.m */; }; + CB206F7117C3FA8F00A4FDC4 /* ARPriceRangeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB206F7017C3FA8F00A4FDC4 /* ARPriceRangeViewController.m */; }; + CB206F7417C569E800A4FDC4 /* ARPersonalizeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB206F7317C569E800A4FDC4 /* ARPersonalizeViewController.m */; }; + CB206F7717C5A5AD00A4FDC4 /* AROnboardingGeneTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB206F7617C5A5AD00A4FDC4 /* AROnboardingGeneTableController.m */; }; + CB206F7A17C5AEB400A4FDC4 /* AROnboardingArtistTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB206F7917C5AEB400A4FDC4 /* AROnboardingArtistTableController.m */; }; + CB260E8117C2C90900BF2012 /* ARCollectorStatusViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB260E8017C2C90900BF2012 /* ARCollectorStatusViewController.m */; }; + CB2C960417D3B4B500B36B44 /* ARFeedLinkUnitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB2C960317D3B4B500B36B44 /* ARFeedLinkUnitViewController.m */; }; + CB42B64F181092480069A801 /* ARCountdownView.m in Sources */ = {isa = PBXBuildFile; fileRef = CB42B64E181092480069A801 /* ARCountdownView.m */; }; + CB4D652717C80A9600390550 /* AROnboardingSearchField.m in Sources */ = {isa = PBXBuildFile; fileRef = CB4D652617C80A9600390550 /* AROnboardingSearchField.m */; }; + CB61F13117C2D83F003DB8A9 /* AROnboardingTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CB61F13017C2D83F003DB8A9 /* AROnboardingTableViewCell.m */; }; + CB73B48B17D2581400891305 /* SiteFeature.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73B48A17D2581400891305 /* SiteFeature.m */; }; + CB821E7918217AF500CC934E /* ARAuctionBannerView.m in Sources */ = {isa = PBXBuildFile; fileRef = CB821E7818217AF500CC934E /* ARAuctionBannerView.m */; }; + CB879D0C180746C900E2D8EC /* AuctionLot.m in Sources */ = {isa = PBXBuildFile; fileRef = CB879D0B180746C900E2D8EC /* AuctionLot.m */; }; + CB8D9D4417CEA7B900F3286B /* AROnboardingMoreInfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB8D9D4317CEA7B900F3286B /* AROnboardingMoreInfoViewController.m */; }; + CB9E244117CBC36F00773A9A /* ARAuthProviders.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9E244017CBC36F00773A9A /* ARAuthProviders.m */; }; + CBB25B3C17F36DDE00C31446 /* ARFeaturedArtworksViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB25B3B17F36DDE00C31446 /* ARFeaturedArtworksViewController.m */; }; + CBB469D0181F1F1200B5692B /* Bid.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB469CF181F1F1200B5692B /* Bid.m */; }; + CBE939F817FA336C00AD7DDD /* ARArtworkBlurbView.m in Sources */ = {isa = PBXBuildFile; fileRef = 601F029417F336AB00EB3E83 /* ARArtworkBlurbView.m */; }; + DCA78A9CCA74D94C34C35156 /* libPods-Artsy Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8D6BDCA48472E439B9D79CB /* libPods-Artsy Tests.a */; }; + E60673B319BE4E8C00EF05EB /* full_logo_white_small@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E60673B119BE4E8C00EF05EB /* full_logo_white_small@2x.png */; }; + E611847018D78068000FE4C9 /* ARMessageItemProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E611846F18D78068000FE4C9 /* ARMessageItemProviderTests.m */; }; + E611847218D7B4C4000FE4C9 /* ARSharingControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E611847118D7B4C4000FE4C9 /* ARSharingControllerTests.m */; }; + E612BC3F19D1B9DE00585CD6 /* ARFollowableButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E612BC3E19D1B9DE00585CD6 /* ARFollowableButtonTests.m */; }; + E61446AC195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E614469E195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.m */; }; + E61446AD195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E61446A0195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.m */; }; + E61446AF195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E61446A4195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.m */; }; + E61446B0195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E61446A7195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.m */; }; + E61446B1195A1CDC00BFB7C3 /* ARFollowableNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E61446A9195A1CDC00BFB7C3 /* ARFollowableNetworkModel.m */; }; + E61446B2195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E61446AB195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.m */; }; + E616B2481911664900D1CBC6 /* ARArtworkView.m in Sources */ = {isa = PBXBuildFile; fileRef = E616B2471911664900D1CBC6 /* ARArtworkView.m */; }; + E619969C19B9099400DB273C /* UIScrollView+HitTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E619969B19B9099400DB273C /* UIScrollView+HitTest.m */; }; + E61B703919904D9F00260D29 /* ARTopTapThroughTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = E61B703819904D9F00260D29 /* ARTopTapThroughTableView.m */; }; + E61E772619A5477300C55E14 /* ARExpectaExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = E61E772519A5477300C55E14 /* ARExpectaExtensions.m */; }; + E61E773919A7E8D500C55E14 /* ARGeneViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E61E773819A7E8D500C55E14 /* ARGeneViewControllerTests.m */; }; + E620C74819C746530064A0FF /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E620C74719C746530064A0FF /* Images.xcassets */; }; + E63C796C198811E400579C04 /* ARArtworkRelatedArtworksViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E63C796B198811E400579C04 /* ARArtworkRelatedArtworksViewTests.m */; }; + E649708918D7762F009DB0C4 /* ARImageItemProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E649708818D7762F009DB0C4 /* ARImageItemProviderTests.m */; }; + E6543CDB18AD795E00A6B9AF /* Parallax_Overlay_Bottom@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E6543CDA18AD795E00A6B9AF /* Parallax_Overlay_Bottom@2x.png */; }; + E655E4B8194B4BEF00F2B7DA /* ARArtworkFavoritesNetworkModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E655E4B7194B4BEF00F2B7DA /* ARArtworkFavoritesNetworkModelTests.m */; }; + E65BB51F1A3FB552004C4DB4 /* ARAppSearchViewControllerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = E65BB51E1A3FB552004C4DB4 /* ARAppSearchViewControllerSpec.m */; }; + E65BB5231A408A29004C4DB4 /* TextfieldClearButton.png in Resources */ = {isa = PBXBuildFile; fileRef = E65BB5221A408A29004C4DB4 /* TextfieldClearButton.png */; }; + E66492D318B7D26B00531B8F /* Parallax_Overlay_Top@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E66492D218B7D26B00531B8F /* Parallax_Overlay_Top@2x.png */; }; + E667F12218EC825D00503F50 /* ARHTTPRequestOperationLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = E667F12118EC825D00503F50 /* ARHTTPRequestOperationLogger.m */; }; + E667F12918EC82AB00503F50 /* ARLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = E667F12718EC82AB00503F50 /* ARLogger.m */; }; + E667F12C18EC889F00503F50 /* ARLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = E667F12B18EC889F00503F50 /* ARLogFormatter.m */; }; + E66A0A8319C2125400ECEB1A /* full_logo_white_large@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E66A0A8219C2125400ECEB1A /* full_logo_white_large@2x.png */; }; + E66EE4AF196460620081ED0C /* ARFavoriteItemModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E66EE4AE196460620081ED0C /* ARFavoriteItemModule.m */; }; + E6718310194A3D1E001A5566 /* ARHeroUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E671830F194A3D1E001A5566 /* ARHeroUnitTests.m */; }; + E6718316194A595F001A5566 /* ARTestContext.m in Sources */ = {isa = PBXBuildFile; fileRef = E6718315194A595F001A5566 /* ARTestContext.m */; }; + E67655261900660500F9A704 /* ARWhitespaceGobbler.m in Sources */ = {isa = PBXBuildFile; fileRef = E67655251900660500F9A704 /* ARWhitespaceGobbler.m */; }; + E676A62518EF5F6E00B9AF2C /* Artwork_v0.data in Resources */ = {isa = PBXBuildFile; fileRef = E676A62418EF5F6E00B9AF2C /* Artwork_v0.data */; }; + E676A62B18F3421600B9AF2C /* ARSeparatorViews.m in Sources */ = {isa = PBXBuildFile; fileRef = E676A62A18F3421600B9AF2C /* ARSeparatorViews.m */; }; + E676B2EB18D0CC330057B4E1 /* ARURLItemProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = E676B2EA18D0CC330057B4E1 /* ARURLItemProvider.m */; }; + E676B2EE18D11A9B0057B4E1 /* ARImageItemProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = E676B2ED18D11A9B0057B4E1 /* ARImageItemProvider.m */; }; + E676B2F218D224000057B4E1 /* ARMessageItemProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = E676B2F118D224000057B4E1 /* ARMessageItemProvider.m */; }; + E676B2F518D230A00057B4E1 /* ARURLItemProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E676B2F418D230A00057B4E1 /* ARURLItemProviderTests.m */; }; + E67DF2131A40A73C00C8495E /* ARSearchViewControllerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = E67DF2121A40A73C00C8495E /* ARSearchViewControllerSpec.m */; }; + E67E1DB219475642004252E0 /* ARAnimatedTickViewTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E67E1DB119475642004252E0 /* ARAnimatedTickViewTest.m */; }; + E691020019C9E17B0048149C /* SearchIcon_White@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E69101FF19C9E17B0048149C /* SearchIcon_White@2x.png */; }; + E693BF7C18A1941D00D464BC /* ARSwitchCell.m in Sources */ = {isa = PBXBuildFile; fileRef = E693BF7918A1941D00D464BC /* ARSwitchCell.m */; }; + E693BF7D18A1941D00D464BC /* ARTextInputCell.m in Sources */ = {isa = PBXBuildFile; fileRef = E693BF7B18A1941D00D464BC /* ARTextInputCell.m */; }; + E693BF8218A1942D00D464BC /* ARSwitchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E693BF7F18A1942D00D464BC /* ARSwitchCell.xib */; }; + E693BF8518A1949A00D464BC /* ARTextInputCellWithTitle.xib in Resources */ = {isa = PBXBuildFile; fileRef = E693BF8418A1949A00D464BC /* ARTextInputCellWithTitle.xib */; }; + E6A3500518AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = E6A3500418AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.m */; }; + E6AD6D4018E5C404005C8A3A /* ARArtworkInfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E6AD6D3F18E5C404005C8A3A /* ARArtworkInfoViewController.m */; }; + E6AD6D4218E610DA005C8A3A /* ArtsyAPI+ArtworksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6AD6D4118E610DA005C8A3A /* ArtsyAPI+ArtworksTests.m */; }; + E6B958EF188DB24200D75C86 /* ARViewTagConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = E6B958EE188DB24200D75C86 /* ARViewTagConstants.m */; }; + E6B958F1188DB6F800D75C86 /* ARArtworkViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6B958F0188DB6F800D75C86 /* ARArtworkViewControllerTests.m */; }; + E6B9FA7918A96BF500E961F9 /* ARBrowseFeaturedLinkInsetCellTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6B9FA7818A96BF500E961F9 /* ARBrowseFeaturedLinkInsetCellTests.m */; }; + E6B9FA7B18A97C0A00E961F9 /* stub.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6B9FA7A18A97C0A00E961F9 /* stub.jpg */; }; + E6BC2C3E197EDC950063ED3C /* ARBrowseViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6BC2C3D197EDC950063ED3C /* ARBrowseViewControllerTests.m */; }; + E6C0316018B291AB00137242 /* ARButtonWithCircularImage.m in Sources */ = {isa = PBXBuildFile; fileRef = E6C0315F18B291AB00137242 /* ARButtonWithCircularImage.m */; }; + E6C13AC11A23AA420050AB53 /* AROnboardingViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6C13AC01A23AA420050AB53 /* AROnboardingViewControllerTests.m */; }; + E6C13AC41A23AB160050AB53 /* ARPersonalizeWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E6C13AC31A23AB160050AB53 /* ARPersonalizeWebViewController.m */; }; + E6CD164A18ABE291001254B5 /* Image_Shadow_Overlay@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E6CD164918ABE291001254B5 /* Image_Shadow_Overlay@2x.png */; }; + E6E07D6D195DF5D800403D2B /* ARSwitchView+Favorites.m in Sources */ = {isa = PBXBuildFile; fileRef = E6E07D6C195DF5D800403D2B /* ARSwitchView+Favorites.m */; }; + E6EC246118F7069300C89192 /* PartnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6EC246018F7069300C89192 /* PartnerTests.m */; }; + E6F111A018A2A7CB00D33C3E /* ARBrowseFeaturedLinksCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6F1119F18A2A7CB00D33C3E /* ARBrowseFeaturedLinksCollectionViewTests.m */; }; + E6F466E818EF52A800A09AFA /* Artwork_v1.data in Resources */ = {isa = PBXBuildFile; fileRef = E6F466E718EF52A800A09AFA /* Artwork_v1.data */; }; + E6F84CFF1A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E6F84CFE1A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png */; }; + E6F84D001A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E6F84CFE1A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png */; }; + E6F84D021A0AB40500BF99A3 /* ARSignUpActiveUserViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E6F84D011A0AB40500BF99A3 /* ARSignUpActiveUserViewControllerTests.m */; }; + E6FD2F21187B0D77002AAFEB /* ARUserSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E6FD2F20187B0D76002AAFEB /* ARUserSettingsViewController.m */; }; + E6FFEAAF19C7925600A0D7DE /* onboard_1@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEA9F19C7925600A0D7DE /* onboard_1@2x~ipad.jpg */; }; + E6FFEAB019C7925600A0D7DE /* onboard_1@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA019C7925600A0D7DE /* onboard_1@2x~iphone.jpg */; }; + E6FFEAB119C7925600A0D7DE /* onboard_2@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA119C7925600A0D7DE /* onboard_2@2x~ipad.jpg */; }; + E6FFEAB219C7925600A0D7DE /* onboard_2@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA219C7925600A0D7DE /* onboard_2@2x~iphone.jpg */; }; + E6FFEAB319C7925600A0D7DE /* onboard_3@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA319C7925600A0D7DE /* onboard_3@2x~ipad.jpg */; }; + E6FFEAB419C7925600A0D7DE /* onboard_3@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA419C7925600A0D7DE /* onboard_3@2x~iphone.jpg */; }; + E6FFEAB519C7925600A0D7DE /* splash_1@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA519C7925600A0D7DE /* splash_1@2x~ipad.jpg */; }; + E6FFEAB619C7925600A0D7DE /* splash_1@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA619C7925600A0D7DE /* splash_1@2x~iphone.jpg */; }; + E6FFEAB719C7925600A0D7DE /* splash_2@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA719C7925600A0D7DE /* splash_2@2x~ipad.jpg */; }; + E6FFEAB819C7925600A0D7DE /* splash_2@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA819C7925600A0D7DE /* splash_2@2x~iphone.jpg */; }; + E6FFEAB919C7925600A0D7DE /* splash_3@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAA919C7925600A0D7DE /* splash_3@2x~ipad.jpg */; }; + E6FFEABA19C7925600A0D7DE /* splash_3@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAAA19C7925600A0D7DE /* splash_3@2x~iphone.jpg */; }; + E6FFEABB19C7925600A0D7DE /* splash_4@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAAB19C7925600A0D7DE /* splash_4@2x~ipad.jpg */; }; + E6FFEABC19C7925600A0D7DE /* splash_4@2x~iphone.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAAC19C7925600A0D7DE /* splash_4@2x~iphone.jpg */; }; + E6FFEABD19C7925600A0D7DE /* splash_5@2x~ipad.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E6FFEAAD19C7925600A0D7DE /* splash_5@2x~ipad.jpg */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B3ECE8341819D1E7009F5C5B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 49BA7DF61655ABE600C06572 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 49BA7DFE1655ABE600C06572; + remoteInfo = Artsy; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 062C201F16DD76C90095A7EC /* ARZoomView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARZoomView.h; sourceTree = ""; }; + 062C202016DD76C90095A7EC /* ARZoomView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARZoomView.m; sourceTree = ""; }; + 0631FA6E1705E77F000A5ED3 /* mail.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = mail.html; sourceTree = ""; }; + 064330E3170F526200FF6C41 /* ARArtistViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtistViewController.h; sourceTree = ""; }; + 064330E4170F526200FF6C41 /* ARArtistViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ARArtistViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 06E44EB8170235D8001B2EBF /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; + 1A25A9493C56A91CDFB95D0B /* Pods-Artsy Tests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Artsy Tests.beta.xcconfig"; path = "Pods/Target Support Files/Pods-Artsy Tests/Pods-Artsy Tests.beta.xcconfig"; sourceTree = ""; }; + 29773A6F17B9749800FC89B3 /* ARHasImageBaseURL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARHasImageBaseURL.h; sourceTree = ""; }; + 342F9005B8C4E89F9DA89E36 /* NSString+StringCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+StringCase.h"; sourceTree = ""; }; + 342F9018E314BCC99A929A6C /* UILabel+Typography.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UILabel+Typography.h"; sourceTree = ""; }; + 342F903047176176865D20CA /* NSString+StringCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+StringCase.m"; sourceTree = ""; }; + 342F9036F798770D9E0CC66C /* UIImageView+AsyncImageLoading.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImageView+AsyncImageLoading.h"; sourceTree = ""; }; + 342F9039177611EDF514D37E /* ARFollowableButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFollowableButton.m; sourceTree = ""; }; + 342F90BFE6268BEBC0E7BB2E /* ARValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARValueTransformer.m; sourceTree = ""; }; + 342F91073D92A49978A29DA6 /* ARImagePageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARImagePageViewController.h; sourceTree = ""; }; + 342F91131CCE3110E3E44E19 /* ARSplitStackView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSplitStackView.m; sourceTree = ""; }; + 342F9175329F0389E644C194 /* NSDate+DateRange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+DateRange.m"; sourceTree = ""; }; + 342F918CC4304843C82ED43C /* ARBidButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARBidButton.h; sourceTree = ""; }; + 342F919D0897DFE221EC5E73 /* ARFeedStatusIndicatorTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedStatusIndicatorTableViewCell.m; sourceTree = ""; }; + 342F91AA54E8417472868A61 /* UIViewController+FullScreenLoading.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+FullScreenLoading.h"; sourceTree = ""; }; + 342F924737155CB0C5A439F9 /* UIViewController+FullScreenLoading.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+FullScreenLoading.m"; sourceTree = ""; }; + 342F92647D726C0DC289B1F3 /* ARFairMapViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapViewControllerTests.m; sourceTree = ""; }; + 342F92656D83DA29BDA2094D /* ArtsyAPI+Shows.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Shows.h"; sourceTree = ""; }; + 342F927CDF62DAE64F3A1AD7 /* ARCollapsableTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARCollapsableTextView.m; sourceTree = ""; }; + 342F92B72CCFA5F1E35F0067 /* UIApplicationStateEnum.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIApplicationStateEnum.h; sourceTree = ""; }; + 342F92D1AEEE18F719695548 /* MTLModel+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MTLModel+JSON.m"; sourceTree = ""; }; + 342F92FE487AC8FCA43F5C74 /* Follow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Follow.m; sourceTree = ""; }; + 342F9300791CEBAEC9D58AD0 /* ArtsyAPI+Shows.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Shows.m"; sourceTree = ""; }; + 342F9316E861BD2C6EBFA31A /* ORStackViewArtsyCategoriesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ORStackViewArtsyCategoriesTests.m; sourceTree = ""; }; + 342F93309D1B60C53E2E0732 /* Artwork+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Artwork+Extensions.m"; sourceTree = ""; }; + 342F9331632309600346BD90 /* UIViewController+SimpleChildren.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+SimpleChildren.m"; sourceTree = ""; }; + 342F934FA11C379917B02017 /* Map.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Map.h; sourceTree = ""; }; + 342F937DAA05F0E703D7A020 /* ARActionButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARActionButtonsView.m; sourceTree = ""; }; + 342F939F8B8D9AC1A4773325 /* ARFairMapViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapViewController.m; sourceTree = ""; }; + 342F93E38025E2D2DA90C6DA /* MapFeature.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapFeature.m; sourceTree = ""; }; + 342F93F91FB4ACCDC29EB901 /* ARGeneArtworksNetworkModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARGeneArtworksNetworkModelTests.m; sourceTree = ""; }; + 342F940CACC66E85F5F48B96 /* ARPageSubTitleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPageSubTitleView.m; sourceTree = ""; }; + 342F94A8E1109ABC367CB751 /* ARStandardDateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARStandardDateFormatter.h; sourceTree = ""; }; + 342F94AB8EA7914F64F117AD /* UIDevice-Hardware.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIDevice-Hardware.h"; sourceTree = ""; }; + 342F94F249E33303787DC61E /* MTLModel+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MTLModel+JSON.h"; sourceTree = ""; }; + 342F951B04D7300332FF3DDB /* ARFairViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairViewController.m; sourceTree = ""; }; + 342F954D3ABCF82AAC1640D9 /* ARSharingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSharingController.h; sourceTree = ""; }; + 342F9559FD6EB9419E3CEE68 /* ARFairMapAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapAnnotationView.m; sourceTree = ""; }; + 342F955C47585DAE288DCC7F /* ARScrollNavigationChief.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARScrollNavigationChief.h; sourceTree = ""; }; + 342F95C4453C7D80AC5A9154 /* ARFeedStatusIndicatorTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedStatusIndicatorTableViewCell.h; sourceTree = ""; }; + 342F95F94C5759B9CA05AB59 /* Artwork+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Artwork+Extensions.h"; sourceTree = ""; }; + 342F96255EEC90B05A4FF213 /* ARFeedImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedImageLoader.h; sourceTree = ""; }; + 342F9629B03A1BEB6888BD4B /* ARBidButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBidButton.m; sourceTree = ""; }; + 342F965F76AA25C6C700FE1C /* ARNavigationButtonsViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationButtonsViewControllerTests.m; sourceTree = ""; }; + 342F966895F2D8BDFA7B39B1 /* ARFairShowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairShowViewController.m; sourceTree = ""; }; + 342F966C989CF0BE97711FB6 /* ARFeedImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedImageLoader.m; sourceTree = ""; }; + 342F9685CB5FB321D7FC7A2B /* NSString+StringSize.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+StringSize.h"; sourceTree = ""; }; + 342F969D222D3AAD67B93FCE /* ARTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTextView.m; sourceTree = ""; }; + 342F969E68B7F84A152937EF /* ARCollapsableTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARCollapsableTextView.h; sourceTree = ""; }; + 342F96AD6F2466A26A3152F8 /* ORStackView+ArtsyViews.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ORStackView+ArtsyViews.h"; sourceTree = ""; }; + 342F96B30465B8B7722DBD1D /* UIImage+ImageFromColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+ImageFromColor.h"; sourceTree = ""; }; + 342F96EF94FE8A32662F8BF1 /* ARTrialController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTrialController.m; sourceTree = ""; }; + 342F972963924EC91C9B5CFD /* ARFollowableButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFollowableButton.h; sourceTree = ""; }; + 342F9737E2E62CEC8B68D181 /* ARFileUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFileUtils.h; sourceTree = ""; }; + 342F976360A7DAA65AF7D755 /* MapPoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapPoint.m; sourceTree = ""; }; + 342F97A9F6D9D371703C2B19 /* ARNetworkErrorManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNetworkErrorManager.m; sourceTree = ""; }; + 342F97B49335330C4FD569B4 /* UIDevice-Hardware.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice-Hardware.m"; sourceTree = ""; }; + 342F97C153D8524B132D0927 /* NSDate+Util.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Util.m"; sourceTree = ""; }; + 342F97E1E35CFFE9767F07CD /* NSDate+DateRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+DateRange.h"; sourceTree = ""; }; + 342F984DED67FEAABB90AFC3 /* MTLModel+Dictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MTLModel+Dictionary.h"; sourceTree = ""; }; + 342F9860A7DDF0C11DC56572 /* Fair.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Fair.m; sourceTree = ""; }; + 342F987C1CACE95AAD292717 /* UIImage+ImageFromColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+ImageFromColor.m"; sourceTree = ""; }; + 342F98950528061D8AE370EA /* MTLModel+Dictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MTLModel+Dictionary.m"; sourceTree = ""; }; + 342F98A73E2E1162514B5A24 /* ARScrollNavigationChief.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARScrollNavigationChief.m; sourceTree = ""; }; + 342F9929E6FBF1C0AF1BADD7 /* UIFont+ArtsyFonts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+ArtsyFonts.m"; sourceTree = ""; }; + 342F992BB6F28B5BE6477E3F /* UIViewController+ARStateRestoration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+ARStateRestoration.m"; path = "Artsy/Classes/Categories/UIViewController+ARStateRestoration.m"; sourceTree = SOURCE_ROOT; }; + 342F994893B9660BEA23C18D /* ARFairViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairViewController.h; sourceTree = ""; }; + 342F995897B0B106E59A39A0 /* ARFairMapAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairMapAnnotationView.h; sourceTree = ""; }; + 342F9960EEF42063F7494D42 /* ARTrialController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTrialController.h; sourceTree = ""; }; + 342F997FDCA4513495F1E949 /* UIViewController+ARStateRestoration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+ARStateRestoration.h"; path = "Artsy/Classes/Categories/UIViewController+ARStateRestoration.h"; sourceTree = SOURCE_ROOT; }; + 342F9983D46712773FC71FC3 /* Fair.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Fair.h; sourceTree = ""; }; + 342F9A069BB0267F7BF6F260 /* UIView+HitTestExpansion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+HitTestExpansion.h"; sourceTree = ""; }; + 342F9A17225A44AC4BE26303 /* PartnerShowFairLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PartnerShowFairLocation.m; sourceTree = ""; }; + 342F9A7210CFDD1C15CFBE3C /* ARParallaxEffect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARParallaxEffect.m; sourceTree = ""; }; + 342F9AC158B5157E968BE7BE /* Follow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Follow.h; sourceTree = ""; }; + 342F9AC8BEBC76D655E34A8D /* ARFairMapViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairMapViewController.h; sourceTree = ""; }; + 342F9AF3B1FC9DB29D65F3D1 /* ActiveErrorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActiveErrorView.xib; sourceTree = ""; }; + 342F9B088114B9396356A2FC /* ARSplitStackView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSplitStackView.h; sourceTree = ""; }; + 342F9B2379536051BE35B6D4 /* ARValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARValueTransformer.h; sourceTree = ""; }; + 342F9B2B2ACEAEDFADE3E0B2 /* ARFairShowViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairShowViewController.h; sourceTree = ""; }; + 342F9BA51676376FB563BD00 /* ARActionButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARActionButtonsView.h; sourceTree = ""; }; + 342F9BBA54BBCF5FE29C8684 /* ARParallaxEffect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARParallaxEffect.h; sourceTree = ""; }; + 342F9C11DC281B1C4DC1FB8C /* Map.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Map.m; sourceTree = ""; }; + 342F9C3811C6F12C261F04D4 /* ARNavigationButtonsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationButtonsViewController.m; sourceTree = ""; }; + 342F9C3F2A3A8DD09B29D803 /* ARTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTextView.h; sourceTree = ""; }; + 342F9C6C79750887BDAC6499 /* UIFont+ArtsyFonts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+ArtsyFonts.h"; sourceTree = ""; }; + 342F9C73F3EBBD7348B5CB83 /* PartnerShowFairLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PartnerShowFairLocation.h; sourceTree = ""; }; + 342F9CDCE3E3D1674CA080AD /* UIApplicationStateEnum.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIApplicationStateEnum.m; sourceTree = ""; }; + 342F9D38472C06E2C065568E /* ARImagePageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARImagePageViewController.m; sourceTree = ""; }; + 342F9D82EE3BBB74F56DBC4B /* MapPoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapPoint.h; sourceTree = ""; }; + 342F9D994FE5E37F23DB781A /* ARNetworkErrorManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNetworkErrorManager.h; sourceTree = ""; }; + 342F9DA91638895F6A6A432C /* UIImageView+AsyncImageLoading.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImageView+AsyncImageLoading.m"; sourceTree = ""; }; + 342F9DC4018D17E7378DA607 /* ARSharingController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSharingController.m; sourceTree = ""; }; + 342F9DDBC48E0F833DBC76F4 /* NSString+StringSize.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+StringSize.m"; sourceTree = ""; }; + 342F9DE76CE6DE0D374BC703 /* UIViewController+SimpleChildren.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+SimpleChildren.h"; sourceTree = ""; }; + 342F9DED668071BB2B4C12F5 /* ARPageSubTitleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPageSubTitleView.h; sourceTree = ""; }; + 342F9DFFC4EBE280F4BDF6E8 /* NSDate+Util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Util.h"; sourceTree = ""; }; + 342F9E3AC3A1AD2AF62F353C /* ARNavigationButtonsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNavigationButtonsViewController.h; sourceTree = ""; }; + 342F9EF0AF8F712E38798AE8 /* MapFeature.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapFeature.h; sourceTree = ""; }; + 342F9F32AB53EF2503473315 /* ORStackView+ArtsyViews.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ORStackView+ArtsyViews.m"; sourceTree = ""; }; + 342F9F3C689C41D4B941B665 /* ARArtworkDetailViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkDetailViewTests.m; sourceTree = ""; }; + 342F9F3E8A0C15DD0DB437D1 /* ARStandardDateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARStandardDateFormatter.m; sourceTree = ""; }; + 342F9F4385F10D19BC94E456 /* UIView+HitTestExpansion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+HitTestExpansion.m"; sourceTree = ""; }; + 342F9F6EDBFE4624755933EB /* UILabel+Typography.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UILabel+Typography.m"; sourceTree = ""; }; + 342F9F73EE9B1DBFE4E88BA3 /* ARTiledImageDataSourceWithImageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTiledImageDataSourceWithImageTests.m; sourceTree = ""; }; + 342F9F752C66B13A43FD55DC /* StyledSubclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StyledSubclasses.h; sourceTree = ""; }; + 342F9FD434A78C36B76A85B6 /* ARFairViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairViewControllerTests.m; sourceTree = ""; }; + 342F9FEFF4A1B56EE307467B /* ARFileUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFileUtils.m; sourceTree = ""; }; + 3C11CD27189B07810060B26B /* ARAspectRatioImageViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAspectRatioImageViewTests.m; sourceTree = ""; }; + 3C1266F018BE6CC700B5AE72 /* ARFairGuideViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairGuideViewControllerTests.m; sourceTree = ""; }; + 3C205ACB1908041700B3C2B4 /* ARSearchResultsDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSearchResultsDataSource.h; sourceTree = ""; }; + 3C205ACC1908041700B3C2B4 /* ARSearchResultsDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSearchResultsDataSource.m; sourceTree = ""; }; + 3C205B58189000A5004280E0 /* ARAppNotificationsDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAppNotificationsDelegateTests.m; sourceTree = ""; }; + 3C22227318C6487C00B7CE3A /* User_v0.data */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = User_v0.data; path = TestData/User_v0.data; sourceTree = ""; }; + 3C22227518C64B4A00B7CE3A /* User_v1.data */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = User_v1.data; path = TestData/User_v1.data; sourceTree = ""; }; + 3C28D3DA18BE180B00C846EA /* ARNavigationButtonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationButtonTests.m; sourceTree = ""; }; + 3C2E6C5B192262A3009DAB28 /* ARRouterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARRouterTests.m; sourceTree = ""; }; + 3C33298B18AD3324006D28C0 /* ARFairSearchViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairSearchViewController.h; sourceTree = ""; }; + 3C33298C18AD3324006D28C0 /* ARFairSearchViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairSearchViewController.m; sourceTree = ""; }; + 3C33299118AD9399006D28C0 /* ARFairSearchViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairSearchViewControllerTests.m; sourceTree = ""; }; + 3C35CC79189FF05E00E3D8DE /* OrderedSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OrderedSet.h; sourceTree = ""; }; + 3C35CC7A189FF05E00E3D8DE /* OrderedSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OrderedSet.m; sourceTree = ""; }; + 3C35CC7C189FF14800E3D8DE /* OrderedSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OrderedSetTests.m; sourceTree = ""; }; + 3C35CC7E189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+OrderedSets.h"; sourceTree = ""; }; + 3C35CC7F189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+OrderedSets.m"; sourceTree = ""; }; + 3C3FEA8C188433F200E1A16F /* ARUserManager+Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ARUserManager+Stubs.h"; sourceTree = ""; }; + 3C3FEA8D1884346D00E1A16F /* ARUserManager+Stubs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARUserManager+Stubs.m"; sourceTree = ""; }; + 3C4877B2192A745400F40062 /* ARFairMapAnnotationCallOutViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapAnnotationCallOutViewTests.m; sourceTree = ""; }; + 3C48E20E1965AC640077A80B /* ARCustomEigenLabels.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARCustomEigenLabels.h; sourceTree = ""; }; + 3C48E20F1965AC640077A80B /* ARCustomEigenLabels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARCustomEigenLabels.m; sourceTree = ""; }; + 3C4AE96219094969009C0E8B /* SearchIcon_HeavyGrey@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SearchIcon_HeavyGrey@2x.png"; sourceTree = ""; }; + 3C4AE96319094969009C0E8B /* SearchIcon_LightGrey@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SearchIcon_LightGrey@2x.png"; sourceTree = ""; }; + 3C4AE96419094969009C0E8B /* SearchIcon_MediumGrey@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SearchIcon_MediumGrey@2x.png"; sourceTree = ""; }; + 3C4AE96519094969009C0E8B /* SearchThumb_HeavyGrey@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SearchThumb_HeavyGrey@2x.png"; sourceTree = ""; }; + 3C4AE96619094969009C0E8B /* SearchThumb_LightGrey@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SearchThumb_LightGrey@2x.png"; sourceTree = ""; }; + 3C4AE96D19094DAA009C0E8B /* MapAnnotation_Artsy@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Artsy@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Artsy@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE96E19094DAA009C0E8B /* MapAnnotation_CoatCheck@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_CoatCheck@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_CoatCheck@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE96F19094DAA009C0E8B /* MapAnnotation_Drink@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Drink@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Drink@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97019094DAA009C0E8B /* MapAnnotation_Food@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Food@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Food@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97119094DAA009C0E8B /* MapAnnotation_GenericEvent@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_GenericEvent@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_GenericEvent@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97219094DAA009C0E8B /* MapAnnotation_Highlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Highlighted@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Highlighted@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97319094DAA009C0E8B /* MapAnnotation_Info@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Info@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Info@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97419094DAA009C0E8B /* MapAnnotation_Installation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Installation@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Installation@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97519094DAA009C0E8B /* MapAnnotation_Lounge@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Lounge@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Lounge@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97619094DAA009C0E8B /* MapAnnotation_Restroom@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Restroom@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Restroom@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97719094DAA009C0E8B /* MapAnnotation_Saved@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Saved@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Saved@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97819094DAA009C0E8B /* MapAnnotation_Search@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Search@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Search@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97919094DAA009C0E8B /* MapAnnotation_Transport@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Transport@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Transport@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97A19094DAA009C0E8B /* MapAnnotation_Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_Default@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_Default@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE97B19094DAA009C0E8B /* MapAnnotation_VIP@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotation_VIP@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotation_VIP@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE98C19094F96009C0E8B /* ViewInRoom_Base@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ViewInRoom_Base@2x.png"; path = "Artsy/Resources/ViewInRoom/ViewInRoom_Base@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE98D19094F96009C0E8B /* ViewInRoom_BaseNoBench@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ViewInRoom_BaseNoBench@2x.png"; path = "Artsy/Resources/ViewInRoom/ViewInRoom_BaseNoBench@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE98E19094F96009C0E8B /* ViewInRoom_Bench@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ViewInRoom_Bench@2x.png"; path = "Artsy/Resources/ViewInRoom/ViewInRoom_Bench@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE98F19094F96009C0E8B /* ViewInRoom_Man_3@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ViewInRoom_Man_3@2x.png"; path = "Artsy/Resources/ViewInRoom/ViewInRoom_Man_3@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE99019094F96009C0E8B /* ViewInRoom_Wall_Right@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ViewInRoom_Wall_Right@2x.png"; path = "Artsy/Resources/ViewInRoom/ViewInRoom_Wall_Right@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE99119094F96009C0E8B /* ViewInRoom_Wall@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ViewInRoom_Wall@2x.png"; path = "Artsy/Resources/ViewInRoom/ViewInRoom_Wall@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE9A219096CED009C0E8B /* ARFairMapAnnotationCallOutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairMapAnnotationCallOutView.h; sourceTree = ""; }; + 3C4AE9A319096CED009C0E8B /* ARFairMapAnnotationCallOutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapAnnotationCallOutView.m; sourceTree = ""; }; + 3C4AE9A519098916009C0E8B /* MapAnnotationCallout_Anchor@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotationCallout_Anchor@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotationCallout_Anchor@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C4AE9AB1909C3A5009C0E8B /* MapAnnotationCallout_Arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotationCallout_Arrow@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotationCallout_Arrow@2x.png"; sourceTree = SOURCE_ROOT; }; + 3C5C7E3F18970E8B003823BB /* UIApplicationStateEnumTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIApplicationStateEnumTests.m; sourceTree = ""; }; + 3C6AA7C91885F38D00501F07 /* SaleArtwork+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SaleArtwork+Extensions.h"; sourceTree = ""; }; + 3C6AA7CA1885F38D00501F07 /* SaleArtwork+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SaleArtwork+Extensions.m"; sourceTree = ""; }; + 3C6AEE26188F228600DD98FC /* ARInternalMobileWebViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARInternalMobileWebViewControllerTests.m; sourceTree = ""; }; + 3C6AEE2C188F2D3E00DD98FC /* ARSwitchBoardTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSwitchBoardTests.m; sourceTree = ""; }; + 3C6BDCBC18E0AEF60028EF5D /* ArtsyAPI+PrivateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+PrivateTests.m"; sourceTree = ""; }; + 3C6BDCBE18E0B3E40028EF5D /* MutableNSURLResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MutableNSURLResponse.h; sourceTree = ""; }; + 3C6BDCBF18E0B3E40028EF5D /* MutableNSURLResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MutableNSURLResponse.m; sourceTree = ""; }; + 3C6BDCC218E0B8D50028EF5D /* ARInquireForArtworkViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARInquireForArtworkViewControllerTests.m; sourceTree = ""; }; + 3C6CB60418ABC7BB008DFE3B /* UserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserTests.m; sourceTree = ""; }; + 3C6CB60718ABD8D2008DFE3B /* ARPostsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPostsViewController.h; sourceTree = ""; }; + 3C6CB60818ABD8D2008DFE3B /* ARPostsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPostsViewController.m; sourceTree = ""; }; + 3C6CB60E18ABFB8D008DFE3B /* ARRelatedArtistsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARRelatedArtistsViewController.h; path = "../View Controllers/ARRelatedArtistsViewController.h"; sourceTree = ""; }; + 3C6CB60F18ABFB8D008DFE3B /* ARRelatedArtistsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARRelatedArtistsViewController.m; path = "../View Controllers/ARRelatedArtistsViewController.m"; sourceTree = ""; }; + 3C7294CB196C3E660073663D /* ARFairShowViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairShowViewControllerTests.m; sourceTree = ""; }; + 3C7880BA18B9081C00595E30 /* ARNotificationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNotificationView.h; sourceTree = ""; }; + 3C7880BB18B9081C00595E30 /* ARNotificationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNotificationView.m; sourceTree = ""; }; + 3C7A7FA618C6EDAB00E8D336 /* ArtworkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArtworkTests.m; sourceTree = ""; }; + 3C7ECF72189010CA004BC877 /* Extensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Extensions.h; sourceTree = ""; }; + 3C8A916518A299BF0038A5B2 /* ARFairSectionViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairSectionViewController.h; sourceTree = ""; }; + 3C8A916618A299BF0038A5B2 /* ARFairSectionViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairSectionViewController.m; sourceTree = ""; }; + 3C94B2D8192BF109008D04DF /* ARFairMapPreview.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairMapPreview.h; sourceTree = ""; }; + 3C94B2D9192BF109008D04DF /* ARFairMapPreview.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapPreview.m; sourceTree = ""; }; + 3C990C8618CF8E9000BF4C44 /* ARFairMapAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairMapAnnotation.h; sourceTree = ""; }; + 3C990C8718CF8E9000BF4C44 /* ARFairMapAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairMapAnnotation.m; sourceTree = ""; }; + 3C9F215D18B25D0D00D8898B /* ARSearchViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSearchViewController.h; sourceTree = ""; }; + 3C9F215E18B25D0D00D8898B /* ARSearchViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSearchViewController.m; sourceTree = ""; }; + 3CA0A17C18EF633900C361E5 /* ARArtistViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtistViewControllerTests.m; sourceTree = ""; }; + 3CA17D571901A4900010C9F5 /* SpectaDSL+Sleep.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SpectaDSL+Sleep.h"; sourceTree = ""; }; + 3CA17D581901A4900010C9F5 /* SpectaDSL+Sleep.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SpectaDSL+Sleep.m"; sourceTree = ""; }; + 3CA1E81E188465F0003C622D /* Sale+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Sale+Extensions.h"; sourceTree = ""; }; + 3CA1E81F188465F0003C622D /* Sale+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Sale+Extensions.m"; sourceTree = ""; }; + 3CA1E8211884663E003C622D /* Bid+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bid+Extensions.h"; sourceTree = ""; }; + 3CA1E8221884663E003C622D /* Bid+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Bid+Extensions.m"; sourceTree = ""; }; + 3CA1E82418846B3A003C622D /* SaleTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SaleTests.m; sourceTree = ""; }; + 3CA37E791910072C00B06E81 /* ARHeartStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARHeartStatus.h; sourceTree = ""; }; + 3CA37E7B1910217500B06E81 /* ARHeartButtonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARHeartButtonTests.m; sourceTree = ""; }; + 3CA55D8818BFF8F800B44CD3 /* MapFeatureTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapFeatureTests.m; sourceTree = ""; }; + 3CAA412D18D88F2E000EE867 /* UIViewController+InnermostTopViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+InnermostTopViewController.h"; path = "Artsy/Classes/Utils/UIViewController+InnermostTopViewController.h"; sourceTree = SOURCE_ROOT; }; + 3CAA412E18D88F2E000EE867 /* UIViewController+InnermostTopViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+InnermostTopViewController.m"; path = "Artsy/Classes/Utils/UIViewController+InnermostTopViewController.m"; sourceTree = SOURCE_ROOT; }; + 3CAA413118D8CC32000EE867 /* ARFairFavoritesNetworkModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairFavoritesNetworkModelTests.m; sourceTree = ""; }; + 3CAC639E190AA87A00B17325 /* MapAnnotationCallout_Partner@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "MapAnnotationCallout_Partner@2x.png"; path = "Artsy/Resources/MapAnnotations/MapAnnotationCallout_Partner@2x.png"; sourceTree = SOURCE_ROOT; }; + 3CACF2AB18F591E40054091E /* ARThemeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARThemeTests.m; sourceTree = ""; }; + 3CAED17B188026AC00840608 /* ARUserManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARUserManagerTests.m; sourceTree = ""; }; + 3CB37D95192246B500089A1D /* ArtsyAPI+ErrorHandlers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+ErrorHandlers.h"; sourceTree = ""; }; + 3CB37D96192246B500089A1D /* ArtsyAPI+ErrorHandlers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+ErrorHandlers.m"; sourceTree = ""; }; + 3CB37D981922483100089A1D /* ArtsyAPI+ErrorHandlers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+ErrorHandlers.m"; sourceTree = ""; }; + 3CB97D8E1887099E008C44FE /* ARLoginViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARLoginViewControllerTests.m; sourceTree = ""; }; + 3CB9A20C18F303B20056C72B /* ARArtworkActionsViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkActionsViewTests.m; sourceTree = ""; }; + 3CBB03A7192BA94C00689F89 /* ARFairArtistViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairArtistViewControllerTests.m; sourceTree = ""; }; + 3CCCC88B1899657C008015DD /* ARFairPostsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairPostsViewController.h; sourceTree = ""; }; + 3CCCC88C1899657C008015DD /* ARFairPostsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairPostsViewController.m; sourceTree = ""; }; + 3CCCC8931899676E008015DD /* ARFairPostsViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairPostsViewControllerTests.m; sourceTree = ""; }; + 3CCCC89918996DD4008015DD /* Post.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Post.h; sourceTree = ""; }; + 3CCCC89A18996DD4008015DD /* Post.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Post.m; sourceTree = ""; }; + 3CCCC89C18997948008015DD /* FairTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FairTests.m; sourceTree = ""; }; + 3CCCC8A01899B412008015DD /* ArtsyAPI+Fairs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Fairs.h"; sourceTree = ""; }; + 3CCCC8A11899B412008015DD /* ArtsyAPI+Fairs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Fairs.m"; sourceTree = ""; }; + 3CCCC8A31899B6F9008015DD /* FairOrganizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FairOrganizer.h; sourceTree = ""; }; + 3CCCC8A41899B6F9008015DD /* FairOrganizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FairOrganizer.m; sourceTree = ""; }; + 3CCE08A618A3C0A000AE4CC3 /* ProfileOwner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ProfileOwner.h; sourceTree = ""; }; + 3CD0BB8818EB0CDF00A59910 /* ARFavoritesViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFavoritesViewControllerTests.m; sourceTree = ""; }; + 3CD36797189A9B7A00285DF7 /* ARPostFeedItemLinkView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPostFeedItemLinkView.h; sourceTree = ""; }; + 3CD36798189A9B7A00285DF7 /* ARPostFeedItemLinkView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPostFeedItemLinkView.m; sourceTree = ""; }; + 3CD3679A189AC4F000285DF7 /* ARAspectRatioImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAspectRatioImageView.h; sourceTree = ""; }; + 3CD3679B189AC4F000285DF7 /* ARAspectRatioImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAspectRatioImageView.m; sourceTree = ""; }; + 3CD3679E189ADBEF00285DF7 /* ARPostFeedItemLinkViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPostFeedItemLinkViewTests.m; sourceTree = ""; }; + 3CE0DA2D18A12335000E537A /* Video.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Video.h; sourceTree = ""; }; + 3CE0DA2E18A12335000E537A /* Video.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Video.m; sourceTree = ""; }; + 3CE0DA3018A13604000E537A /* OHHTTPStubs+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OHHTTPStubs+JSON.h"; sourceTree = ""; }; + 3CE0DA3118A13604000E537A /* OHHTTPStubs+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OHHTTPStubs+JSON.m"; sourceTree = ""; }; + 3CE75A0A18B6367F00885355 /* ARValueTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARValueTransformerTests.m; sourceTree = ""; }; + 3CE7F34118A99E62002BA993 /* ARHeroUnitsNetworkModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARHeroUnitsNetworkModelTests.m; sourceTree = ""; }; + 3CE7F34318A99E6D002BA993 /* SiteHeroUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SiteHeroUnitTests.m; sourceTree = ""; }; + 3CEE0B5D18A16EA200FEA6E6 /* ArtsyAPI+Profiles.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Profiles.h"; sourceTree = ""; }; + 3CEE0B5E18A16EA200FEA6E6 /* ArtsyAPI+Profiles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Profiles.m"; sourceTree = ""; }; + 3CEE0B6018A16F6900FEA6E6 /* ArtsyAPI+Posts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Posts.h"; sourceTree = ""; }; + 3CEE0B6118A16F6900FEA6E6 /* ArtsyAPI+Posts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Posts.m"; sourceTree = ""; }; + 3CEE0B6318A16F7D00FEA6E6 /* ArtsyAPI+Artists.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Artists.h"; sourceTree = ""; }; + 3CEE0B6418A16F7D00FEA6E6 /* ArtsyAPI+Artists.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Artists.m"; sourceTree = ""; }; + 3CEE0B6618A16F8F00FEA6E6 /* ArtsyAPI+Genes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Genes.h"; sourceTree = ""; }; + 3CEE0B6718A16F8F00FEA6E6 /* ArtsyAPI+Genes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Genes.m"; sourceTree = ""; }; + 3CEE0B6918A18CDA00FEA6E6 /* ProfileTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProfileTests.m; sourceTree = ""; }; + 3CF0774418DC6585009E18E4 /* ARKonamiKeyboardView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARKonamiKeyboardView.h; sourceTree = ""; }; + 3CF0774518DC6585009E18E4 /* ARKonamiKeyboardView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARKonamiKeyboardView.m; sourceTree = ""; }; + 3CF144A218E3727F00B1A764 /* UIViewController+ScreenSize.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ScreenSize.h"; sourceTree = ""; }; + 3CF144A318E3727F00B1A764 /* UIViewController+ScreenSize.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+ScreenSize.m"; sourceTree = ""; }; + 3CF144A518E45F6C00B1A764 /* SystemTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SystemTime.h; sourceTree = ""; }; + 3CF144A618E45F6C00B1A764 /* SystemTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SystemTime.m; sourceTree = ""; }; + 3CF144A818E4607900B1A764 /* SystemTimeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SystemTimeTests.m; sourceTree = ""; }; + 3CF144AA18E460BE00B1A764 /* ArtsyAPI+SystemTimeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+SystemTimeTests.m"; sourceTree = ""; }; + 3CF144AC18E460EF00B1A764 /* ArtsyAPI+SystemTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+SystemTime.h"; sourceTree = ""; }; + 3CF144AD18E460EF00B1A764 /* ArtsyAPI+SystemTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+SystemTime.m"; sourceTree = ""; }; + 3CF144B218E47F9F00B1A764 /* ARSystemTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSystemTime.h; sourceTree = ""; }; + 3CF144B318E47F9F00B1A764 /* ARSystemTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSystemTime.m; sourceTree = ""; }; + 3CF144B518E4802400B1A764 /* ARSystemTimeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSystemTimeTests.m; sourceTree = ""; }; + 3CF144B718E9E00400B1A764 /* ARSignUpSplashViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSignUpSplashViewControllerTests.m; sourceTree = ""; }; + 3CFB078918EB417F00792024 /* ARSecureTextFieldWithPlaceholder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSecureTextFieldWithPlaceholder.h; sourceTree = ""; }; + 3CFB078A18EB417F00792024 /* ARSecureTextFieldWithPlaceholder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSecureTextFieldWithPlaceholder.m; sourceTree = ""; }; + 3CFB078F18EB585B00792024 /* ARTile+ASCII.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTile+ASCII.h"; sourceTree = ""; }; + 3CFB079018EB585B00792024 /* ARTile+ASCII.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARTile+ASCII.m"; sourceTree = ""; }; + 3CFBE32B18C3A3F400C781D0 /* ARNetworkErrorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNetworkErrorView.h; sourceTree = ""; }; + 3CFBE32C18C3A3F400C781D0 /* ARNetworkErrorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNetworkErrorView.m; sourceTree = ""; }; + 3CFBE33518C5848900C781D0 /* ARFileUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFileUtilsTests.m; sourceTree = ""; }; + 3E944480103FA2476C7049C2 /* Pods-Artsy Tests.demo.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Artsy Tests.demo.xcconfig"; path = "Pods/Target Support Files/Pods-Artsy Tests/Pods-Artsy Tests.demo.xcconfig"; sourceTree = ""; }; + 4917819D176A6B22001E751E /* ARArtworkSetViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARArtworkSetViewController.h; sourceTree = ""; }; + 4917819E176A6B22001E751E /* ARArtworkSetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARArtworkSetViewController.m; sourceTree = ""; }; + 491A4DE0168E4343003B2246 /* Gene.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Gene.h; sourceTree = ""; }; + 491A4DE1168E4343003B2246 /* Gene.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Gene.m; sourceTree = ""; }; + 4938742617BD512700724795 /* ARSignUpSplashViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSignUpSplashViewController.h; sourceTree = ""; }; + 4938742717BD512700724795 /* ARSignUpSplashViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSignUpSplashViewController.m; sourceTree = ""; }; + 4938743417BDB2CD00724795 /* ARCrossfadingImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARCrossfadingImageView.h; sourceTree = ""; }; + 4938743517BDB2CD00724795 /* ARCrossfadingImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARCrossfadingImageView.m; sourceTree = ""; }; + 49405AB117BEBAFF004F86D8 /* AROnboardingNavBarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingNavBarView.h; sourceTree = ""; }; + 49405AB217BEBAFF004F86D8 /* AROnboardingNavBarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingNavBarView.m; sourceTree = ""; }; + 49405AB417BEC87A004F86D8 /* ARSignupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSignupViewController.h; sourceTree = ""; }; + 49405AB517BEC87A004F86D8 /* ARSignupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSignupViewController.m; sourceTree = ""; }; + 494332FB16692010005AB483 /* VideoContentLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoContentLink.h; sourceTree = ""; }; + 494332FC16692010005AB483 /* VideoContentLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoContentLink.m; sourceTree = ""; }; + 494332FF166920B3005AB483 /* PhotoContentLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoContentLink.h; sourceTree = ""; }; + 49433300166920B3005AB483 /* PhotoContentLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoContentLink.m; sourceTree = ""; }; + 4943331A166947A3005AB483 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; + 49473F2A17BEDDA4004BF082 /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; + 49473F2C17BEDE1F004BF082 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; + 49473F2E17BEDFFD004BF082 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = System/Library/Frameworks/iAd.framework; sourceTree = SDKROOT; }; + 49473F3117C18772004BF082 /* ARSlideshowViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSlideshowViewController.h; sourceTree = ""; }; + 49473F3217C18772004BF082 /* ARSlideshowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSlideshowViewController.m; sourceTree = ""; }; + 49473F3417C1907F004BF082 /* ARCreateAccountViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARCreateAccountViewController.h; sourceTree = ""; }; + 49473F3517C1907F004BF082 /* ARCreateAccountViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARCreateAccountViewController.m; sourceTree = ""; }; + 49473F3717C192AE004BF082 /* ARTextFieldWithPlaceholder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARTextFieldWithPlaceholder.h; path = "../View Controllers/ARTextFieldWithPlaceholder.h"; sourceTree = ""; }; + 49473F3817C192AE004BF082 /* ARTextFieldWithPlaceholder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARTextFieldWithPlaceholder.m; path = "../View Controllers/ARTextFieldWithPlaceholder.m"; sourceTree = ""; }; + 494F16BC17BEDA8E00204F1D /* Accounts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accounts.framework; path = System/Library/Frameworks/Accounts.framework; sourceTree = SDKROOT; }; + 4953E8301668021D00A09726 /* Image.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Image.h; sourceTree = ""; }; + 4953E8311668021D00A09726 /* Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Image.m; sourceTree = ""; }; + 4953E8351668026000A09726 /* ARPostAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPostAttachment.h; sourceTree = ""; }; + 4953E8361668179500A09726 /* PostImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostImage.h; sourceTree = ""; }; + 4953E8371668179500A09726 /* PostImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostImage.m; sourceTree = ""; }; + 4953E83916681A4200A09726 /* ContentLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContentLink.h; sourceTree = ""; }; + 4953E83A16681A4200A09726 /* ContentLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContentLink.m; sourceTree = ""; }; + 4981FB46169F6A16004D9CDD /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = System/Library/Frameworks/CoreImage.framework; sourceTree = SDKROOT; }; + 499A580A165AEC39004B0E2F /* ARFollowArtistFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFollowArtistFeedItem.h; sourceTree = ""; }; + 499A580B165AEC39004B0E2F /* ARFollowArtistFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFollowArtistFeedItem.m; sourceTree = ""; }; + 499A5879166561E8004B0E2F /* ARFeedConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedConstants.h; sourceTree = ""; }; + 499A587A166561E8004B0E2F /* ARFeedConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedConstants.m; sourceTree = ""; }; + 499A588C16658CA9004B0E2F /* Partner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Partner.h; sourceTree = ""; }; + 499A588D16658CAA004B0E2F /* Partner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Partner.m; sourceTree = ""; }; + 499A5892166683AB004B0E2F /* Artist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Artist.h; sourceTree = ""; }; + 499A5893166683AB004B0E2F /* Artist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Artist.m; sourceTree = ""; }; + 499A58A316669CD4004B0E2F /* ARPostFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPostFeedItem.h; sourceTree = ""; }; + 499A58A416669CD5004B0E2F /* ARPostFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPostFeedItem.m; sourceTree = ""; }; + 499C8CEB1694D28700039D32 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + 499C8CEE1694D28C00039D32 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + 499C8CF01694D29100039D32 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + 499C8CF21694D29900039D32 /* OpenGLES.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGLES.framework; path = System/Library/Frameworks/OpenGLES.framework; sourceTree = SDKROOT; }; + 499C8CF41694D2D100039D32 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 49A76D0617592C96001D4B81 /* SearchResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SearchResult.h; sourceTree = ""; }; + 49A76D0717592C96001D4B81 /* SearchResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SearchResult.m; sourceTree = ""; }; + 49A76D0C17594E32001D4B81 /* Tag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Tag.h; sourceTree = ""; }; + 49A76D0D17594E32001D4B81 /* Tag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Tag.m; sourceTree = ""; }; + 49A7721F165ADB6E00BC6FD3 /* ARFeedTimeline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedTimeline.h; sourceTree = ""; }; + 49A77220165ADB6E00BC6FD3 /* ARFeedTimeline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedTimeline.m; sourceTree = ""; }; + 49A77223165ADB9300BC6FD3 /* ARFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedItem.h; sourceTree = ""; }; + 49A77224165ADB9300BC6FD3 /* ARFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedItem.m; sourceTree = ""; }; + 49A77225165ADB9300BC6FD3 /* ARFollowFairFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFollowFairFeedItem.h; sourceTree = ""; }; + 49A77226165ADB9300BC6FD3 /* ARFollowFairFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFollowFairFeedItem.m; sourceTree = ""; }; + 49A77227165ADB9300BC6FD3 /* ARPartnerShowFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPartnerShowFeedItem.h; sourceTree = ""; }; + 49A77228165ADB9300BC6FD3 /* ARPartnerShowFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPartnerShowFeedItem.m; sourceTree = ""; }; + 49A77229165ADB9300BC6FD3 /* ARPublishedArtworkSetFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPublishedArtworkSetFeedItem.h; sourceTree = ""; }; + 49A7722A165ADB9300BC6FD3 /* ARPublishedArtworkSetFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPublishedArtworkSetFeedItem.m; sourceTree = ""; }; + 49A7722B165ADB9300BC6FD3 /* ARRepostFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARRepostFeedItem.h; sourceTree = ""; }; + 49A7722C165ADB9300BC6FD3 /* ARRepostFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARRepostFeedItem.m; sourceTree = ""; }; + 49A7722D165ADB9300BC6FD3 /* ARSavedArtworkSetFeedItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSavedArtworkSetFeedItem.h; sourceTree = ""; }; + 49A7722E165ADB9300BC6FD3 /* ARSavedArtworkSetFeedItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSavedArtworkSetFeedItem.m; sourceTree = ""; }; + 49A7723F165ADC3C00BC6FD3 /* ARFeedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedViewController.h; sourceTree = ""; }; + 49A77240165ADC3C00BC6FD3 /* ARFeedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedViewController.m; sourceTree = ""; }; + 49BA7DFF1655ABE600C06572 /* Artsy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Artsy.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 49BA7E031655ABE600C06572 /* UIKit.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 49BA7E051655ABE600C06572 /* Foundation.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 49BA7E071655ABE600C06572 /* CoreGraphics.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 49BA7E0B1655ABE600C06572 /* Artsy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Artsy-Info.plist"; path = "../App/Artsy-Info.plist"; sourceTree = ""; }; + 49BA7E0D1655ABE600C06572 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 49BA7E111655ABE600C06572 /* Artsy-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Artsy-Prefix.pch"; path = "../App/Artsy-Prefix.pch"; sourceTree = ""; }; + 49BA7E121655ABE600C06572 /* ARAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARAppDelegate.h; sourceTree = ""; }; + 49BA7E131655ABE600C06572 /* ARAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARAppDelegate.m; sourceTree = ""; }; + 49EC62161778AF100020D648 /* PartnerShow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PartnerShow.h; sourceTree = ""; }; + 49EC62171778AF100020D648 /* PartnerShow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PartnerShow.m; sourceTree = ""; }; + 49EF164516C568EA00460BD7 /* Profile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Profile.h; sourceTree = ""; }; + 49EF164616C568EA00460BD7 /* Profile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Profile.m; sourceTree = ""; }; + 49F0C67917B9706000721244 /* AROnboardingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingViewController.h; sourceTree = ""; }; + 49F0C67A17B9706000721244 /* AROnboardingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingViewController.m; sourceTree = ""; }; + 49F0C67D17B972F200721244 /* ARSlideshowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSlideshowView.h; sourceTree = ""; }; + 49F0C67E17B972F200721244 /* ARSlideshowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSlideshowView.m; sourceTree = ""; }; + 540262C418A0FAFB00844AE1 /* ARButtonWithImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARButtonWithImage.h; path = "Table View Cells/ARButtonWithImage.h"; sourceTree = ""; }; + 540262C518A0FAFB00844AE1 /* ARButtonWithImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARButtonWithImage.m; path = "Table View Cells/ARButtonWithImage.m"; sourceTree = ""; }; + 54289FED18AA7F4E00681E49 /* UINavigationController_InnermostTopViewControllerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UINavigationController_InnermostTopViewControllerSpec.m; sourceTree = ""; }; + 5435192C18A8E9420060F31E /* UIView+OldSchoolSnapshots.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+OldSchoolSnapshots.h"; sourceTree = ""; }; + 5435192D18A8E9420060F31E /* UIView+OldSchoolSnapshots.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OldSchoolSnapshots.m"; sourceTree = ""; }; + 546778EC18A95642002C4C71 /* ARAppDelegate+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARAppDelegate+Testing.h"; sourceTree = ""; }; + 546778ED18A95642002C4C71 /* ARAppDelegate+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARAppDelegate+Testing.m"; sourceTree = ""; }; + 546A858017763349006D489B /* ARHeroUnitsNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARHeroUnitsNetworkModel.h; sourceTree = ""; }; + 546A858117763349006D489B /* ARHeroUnitsNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARHeroUnitsNetworkModel.m; sourceTree = ""; }; + 54AD031F18A4FCCE0055F2D2 /* ARMenuAwareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARMenuAwareViewController.h; path = "../View Controllers/ARMenuAwareViewController.h"; sourceTree = ""; }; + 54B7477318A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTopMenuViewController+DeveloperExtras.h"; sourceTree = ""; }; + 54B7477418A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARTopMenuViewController+DeveloperExtras.m"; sourceTree = ""; }; + 5E0AEB7619B9EA43009F34DE /* ARShowNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARShowNetworkModel.h; sourceTree = ""; }; + 5E0AEB7719B9EA43009F34DE /* ARShowNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARShowNetworkModel.m; sourceTree = ""; }; + 5E0AEB7919B9EF57009F34DE /* ARShowNetworkModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARShowNetworkModelTests.m; sourceTree = ""; }; + 5E0AEB7B19B9FAED009F34DE /* ARStubbedShowNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARStubbedShowNetworkModel.h; sourceTree = ""; }; + 5E0AEB7C19B9FAED009F34DE /* ARStubbedShowNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARStubbedShowNetworkModel.m; sourceTree = ""; }; + 5E284607194A2E58007274AB /* ARFairGuideContainerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairGuideContainerViewController.h; sourceTree = ""; }; + 5E284608194A2E58007274AB /* ARFairGuideContainerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairGuideContainerViewController.m; sourceTree = ""; }; + 5E50987B18F82FCF001AC704 /* AROfflineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROfflineView.h; sourceTree = ""; }; + 5E50987C18F82FCF001AC704 /* AROfflineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROfflineView.m; sourceTree = ""; }; + 5E6621DC19768F750064FC52 /* MapIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MapIcon@2x.png"; sourceTree = ""; }; + 5E71AF231912826B008B1426 /* ARPersonalizeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARPersonalizeViewController.h; sourceTree = ""; }; + 5E71AFC6195C64C1000F6325 /* ARFairGuideContainerViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairGuideContainerViewControllerTests.m; sourceTree = ""; }; + 5E9A781F19068EDF00734E1B /* ARProfileViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARProfileViewControllerTests.m; sourceTree = ""; }; + 5E9A78211906BA3D00734E1B /* OCMArg+ClassChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMArg+ClassChecker.h"; sourceTree = ""; }; + 5E9A78221906BA3D00734E1B /* OCMArg+ClassChecker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCMArg+ClassChecker.m"; sourceTree = ""; }; + 5EB33E73197EBDE200706EB1 /* ARParallaxHeaderViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARParallaxHeaderViewControllerTests.m; sourceTree = ""; }; + 5EB33E75197EBFEB00706EB1 /* wide.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = wide.jpg; sourceTree = ""; }; + 5EB33E76197EBFEB00706EB1 /* square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square.png; sourceTree = ""; }; + 5EBDC94D19792C3A0082C514 /* ARNavigationControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationControllerTests.m; sourceTree = ""; }; + 5EBDC95019794D840082C514 /* ARSearchFieldButtonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSearchFieldButtonTests.m; sourceTree = ""; }; + 5EBDC95219794DF10082C514 /* ARPendingOperationViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPendingOperationViewControllerTests.m; sourceTree = ""; }; + 5ECFA8E61907E26E000B92EA /* ARSwitchView+Artist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARSwitchView+Artist.h"; sourceTree = ""; }; + 5ECFA8E71907E26E000B92EA /* ARSwitchView+Artist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARSwitchView+Artist.m"; sourceTree = ""; }; + 5ECFA8E91907E9AB000B92EA /* ARSwitchViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSwitchViewTests.m; sourceTree = ""; }; + 5ECFA8ED1907FC6C000B92EA /* ARSwitchView+FairGuide.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARSwitchView+FairGuide.h"; sourceTree = ""; }; + 5ECFA8EE1907FC6C000B92EA /* ARSwitchView+FairGuide.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARSwitchView+FairGuide.m"; sourceTree = ""; }; + 5EDB1209197E691100E241F0 /* ARParallaxHeaderViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARParallaxHeaderViewController.h; sourceTree = ""; }; + 5EDB120A197E691100E241F0 /* ARParallaxHeaderViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARParallaxHeaderViewController.m; sourceTree = ""; }; + 5EE5DE12190167A400040B84 /* ARProfileViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARProfileViewController.h; sourceTree = ""; }; + 5EE5DE13190167A400040B84 /* ARProfileViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARProfileViewController.m; sourceTree = ""; }; + 5EE5DE1519019EFD00040B84 /* ARFairAwareObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARFairAwareObject.h; sourceTree = ""; }; + 5EFE2BE21910FC81003B5EEA /* ARAppDelegate+Analytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARAppDelegate+Analytics.h"; sourceTree = ""; }; + 5EFE2BE31910FC81003B5EEA /* ARAppDelegate+Analytics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARAppDelegate+Analytics.m"; sourceTree = ""; }; + 5EFF52B81976CA6C00E2A563 /* ARSearchFieldButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSearchFieldButton.h; sourceTree = ""; }; + 5EFF52B91976CA6C00E2A563 /* ARSearchFieldButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSearchFieldButton.m; sourceTree = ""; }; + 5EFF52BB1976D57C00E2A563 /* ARSearchViewController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ARSearchViewController+Private.h"; sourceTree = ""; }; + 5EFF52BC197916C800E2A563 /* ARPendingOperationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPendingOperationViewController.h; sourceTree = ""; }; + 5EFF52BD197916C800E2A563 /* ARPendingOperationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPendingOperationViewController.m; sourceTree = ""; }; + 6001414617CA33C100612DB4 /* ARArtworkRelatedArtworksView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkRelatedArtworksView.h; sourceTree = ""; }; + 6001414717CA33C100612DB4 /* ARArtworkRelatedArtworksView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkRelatedArtworksView.m; sourceTree = ""; }; + 600415C717C4ECE2003C7974 /* ArtsyAPI+RelatedModels.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+RelatedModels.h"; sourceTree = ""; }; + 600415C817C4ECE2003C7974 /* ArtsyAPI+RelatedModels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+RelatedModels.m"; sourceTree = ""; }; + 600A734117DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+DeviceTokens.h"; sourceTree = ""; }; + 600A734217DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+DeviceTokens.m"; sourceTree = ""; }; + 600EE29C16B3003F002E9F9A /* ARNavigationButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationButton.m; sourceTree = ""; }; + 600EE29D16B3003F002E9F9A /* ARNavigationButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNavigationButton.h; sourceTree = ""; }; + 6012313218B153C500B7667F /* ARFairArtistViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairArtistViewController.h; sourceTree = ""; }; + 6012313318B153C500B7667F /* ARFairArtistViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairArtistViewController.m; sourceTree = ""; }; + 6012313518B2E44A00B7667F /* MapButtonAction@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MapButtonAction@2x.png"; sourceTree = ""; }; + 6016C18E178C2B7F008EC8E7 /* AREmbeddedModelsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AREmbeddedModelsViewController.h; sourceTree = ""; }; + 6016C18F178C2B7F008EC8E7 /* AREmbeddedModelsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AREmbeddedModelsViewController.m; sourceTree = ""; }; + 6016C193178C2C06008EC8E7 /* ARArtworkFlowModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARArtworkFlowModule.h; sourceTree = ""; }; + 6016C194178C2C06008EC8E7 /* ARArtworkFlowModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARArtworkFlowModule.m; sourceTree = ""; }; + 6016C196178C2C33008EC8E7 /* ARArtworkMasonryModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkMasonryModule.h; sourceTree = ""; }; + 6016C197178C2C33008EC8E7 /* ARArtworkMasonryModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkMasonryModule.m; sourceTree = ""; }; + 601C317616582BA30013E061 /* ARRouter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARRouter.h; sourceTree = ""; }; + 601C317716582BA30013E061 /* ARRouter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARRouter.m; sourceTree = ""; }; + 601C3183165838590013E061 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 601C3185165838630013E061 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 601F029317F336AB00EB3E83 /* ARArtworkBlurbView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkBlurbView.h; sourceTree = ""; }; + 601F029417F336AB00EB3E83 /* ARArtworkBlurbView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkBlurbView.m; sourceTree = ""; }; + 601F029617F3419400EB3E83 /* ARArtworkViewController+ButtonActions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARArtworkViewController+ButtonActions.h"; sourceTree = ""; }; + 601F029717F3419400EB3E83 /* ARArtworkViewController+ButtonActions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARArtworkViewController+ButtonActions.m"; sourceTree = ""; }; + 60215FD417CDF9FA000F3A62 /* ARSignUpActiveUserViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSignUpActiveUserViewController.h; sourceTree = ""; }; + 60215FD517CDF9FA000F3A62 /* ARSignUpActiveUserViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSignUpActiveUserViewController.m; sourceTree = ""; }; + 60215FD617CDF9FA000F3A62 /* ARSignUpActiveUserViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ARSignUpActiveUserViewController.xib; sourceTree = ""; }; + 6021F97D17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkWithMetadataThumbnailCell.h; sourceTree = ""; }; + 6021F97E17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkWithMetadataThumbnailCell.m; sourceTree = ""; }; + 60229B4C1683BE280072DC12 /* ARSpinner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSpinner.h; sourceTree = ""; }; + 60229B4D1683BE280072DC12 /* ARSpinner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSpinner.m; sourceTree = ""; }; + 6025B42E1655FACA00845975 /* SenTestingKit.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 6025B4481655FBA900845975 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 60289227176BE98C00512977 /* ARViewInRoomViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARViewInRoomViewController.h; sourceTree = ""; }; + 60289228176BE98C00512977 /* ARViewInRoomViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARViewInRoomViewController.m; sourceTree = ""; }; + 6029E9F31993D726002D42C3 /* ARAppSearchViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAppSearchViewController.h; sourceTree = ""; }; + 6029E9F41993D726002D42C3 /* ARAppSearchViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAppSearchViewController.m; sourceTree = ""; }; + 602BC087168E0C0D00069FDB /* ARReusableLoadingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARReusableLoadingView.h; sourceTree = ""; }; + 602BC088168E0C0D00069FDB /* ARReusableLoadingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARReusableLoadingView.m; sourceTree = ""; }; + 602DCDAB16C6B6C1009EB10D /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + 602DCDAD16C6B74D009EB10D /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 602DCDAF16C6B752009EB10D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 602DCDB116C6B75D009EB10D /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; + 602DCDB316C6B7A5009EB10D /* libicucore.A.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.A.dylib; path = usr/lib/libicucore.A.dylib; sourceTree = SDKROOT; }; + 60327DC71987AAD00075B399 /* ARTabContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTabContentView.h; sourceTree = ""; }; + 60327DC81987AAD00075B399 /* ARTabContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTabContentView.m; sourceTree = ""; }; + 60327DCB1987AD240075B399 /* ARDispatchManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDispatchManager.h; sourceTree = ""; }; + 60327DCC1987AD240075B399 /* ARDispatchManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDispatchManager.m; sourceTree = ""; }; + 60327DCE1987B7940075B399 /* ARDeveloperOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDeveloperOptions.h; sourceTree = ""; }; + 60327DCF1987B7940075B399 /* ARDeveloperOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDeveloperOptions.m; sourceTree = ""; }; + 60327DD11987BA830075B399 /* ARDeveloperOptionsSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDeveloperOptionsSpec.m; sourceTree = ""; }; + 60327DDF1987FF490075B399 /* ARTopMenuNavigationDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTopMenuNavigationDataSource.h; sourceTree = ""; }; + 60327DE01987FF490075B399 /* ARTopMenuNavigationDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTopMenuNavigationDataSource.m; sourceTree = ""; }; + 60327DE3198933610075B399 /* ARTabContentViewSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTabContentViewSpec.m; sourceTree = ""; }; + 60327DE4198933610075B399 /* ARTestTopMenuNavigationDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTestTopMenuNavigationDataSource.h; sourceTree = ""; }; + 60327DE5198933610075B399 /* ARTestTopMenuNavigationDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTestTopMenuNavigationDataSource.m; sourceTree = ""; }; + 60327DE6198933610075B399 /* ARTopMenuNavigationDataSourceSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTopMenuNavigationDataSourceSpec.m; sourceTree = ""; }; + 60327DE7198933610075B399 /* ARTopMenuViewControllerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTopMenuViewControllerSpec.m; sourceTree = ""; }; + 6034EB1B175F68350070478D /* SiteHeroUnit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SiteHeroUnit.h; sourceTree = ""; }; + 6034EB1C175F68350070478D /* SiteHeroUnit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SiteHeroUnit.m; sourceTree = ""; }; + 6034EB2A1760A9BD0070478D /* ARTopMenuViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTopMenuViewController.h; sourceTree = ""; }; + 6034EB2B1760A9BD0070478D /* ARTopMenuViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTopMenuViewController.m; sourceTree = ""; }; + 6036B5601760DC9100F1DD01 /* ARHeroUnitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARHeroUnitViewController.h; sourceTree = ""; }; + 6036B5611760DC9100F1DD01 /* ARHeroUnitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARHeroUnitViewController.m; sourceTree = ""; }; + 6036B5631760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+SiteFunctions.h"; sourceTree = ""; }; + 6036B5641760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+SiteFunctions.m"; sourceTree = ""; }; + 6036B56617612CFA00F1DD01 /* ARNavigationContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARNavigationContainer.h; sourceTree = ""; }; + 6037442316D4227500AE7788 /* ARSwitchBoard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARSwitchBoard.h; path = Utils/ARSwitchBoard.h; sourceTree = ""; }; + 6037442416D4227500AE7788 /* ARSwitchBoard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARSwitchBoard.m; path = Utils/ARSwitchBoard.m; sourceTree = ""; }; + 60388701189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARTiledImageDataSourceWithImage.h; path = models/ARTiledImageDataSourceWithImage.h; sourceTree = ""; }; + 60388702189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARTiledImageDataSourceWithImage.m; path = models/ARTiledImageDataSourceWithImage.m; sourceTree = ""; }; + 60388704189C103F00D3EEAA /* ARFairMapZoomManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARFairMapZoomManager.h; path = models/ARFairMapZoomManager.h; sourceTree = ""; }; + 60388705189C103F00D3EEAA /* ARFairMapZoomManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARFairMapZoomManager.m; path = models/ARFairMapZoomManager.m; sourceTree = ""; }; + 603B3AE91774A0FA00BA5BD3 /* ARNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARNavigationController.h; path = "../View Controllers/ARNavigationController.h"; sourceTree = ""; }; + 603B3AEA1774A0FA00BA5BD3 /* ARNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ARNavigationController.m; path = "../View Controllers/ARNavigationController.m"; sourceTree = ""; }; + 603B559F17D64A1B00566935 /* ARArtworkPriceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkPriceView.h; sourceTree = ""; }; + 603B55A017D64A1B00566935 /* ARArtworkPriceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkPriceView.m; sourceTree = ""; }; + 603D7C03195884C100ACA840 /* ARAuctionBidderStateLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAuctionBidderStateLabel.h; sourceTree = ""; }; + 603D7C04195884C100ACA840 /* ARAuctionBidderStateLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAuctionBidderStateLabel.m; sourceTree = ""; }; + 604166AB16C1D20900CFBD2F /* ArtsyAPI+Artworks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Artworks.h"; sourceTree = ""; }; + 604166AC16C1D20900CFBD2F /* ArtsyAPI+Artworks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Artworks.m"; sourceTree = ""; }; + 604166AF16C1D46F00CFBD2F /* ArtsyAPI+CurrentUserFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+CurrentUserFunctions.h"; sourceTree = ""; }; + 604166B016C1D47000CFBD2F /* ArtsyAPI+CurrentUserFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+CurrentUserFunctions.m"; sourceTree = ""; }; + 604166B316C1D55000CFBD2F /* ArtsyAPI+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Private.h"; sourceTree = ""; }; + 60431FB618042A1E000118D7 /* ARAppNotificationsDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARAppNotificationsDelegate.h; path = App/ARAppNotificationsDelegate.h; sourceTree = ""; }; + 60431FB718042A1E000118D7 /* ARAppNotificationsDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARAppNotificationsDelegate.m; path = App/ARAppNotificationsDelegate.m; sourceTree = ""; }; + 60431FB918042E63000118D7 /* ARAppBackgroundFetchDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARAppBackgroundFetchDelegate.h; path = App/ARAppBackgroundFetchDelegate.h; sourceTree = ""; }; + 60431FBA18042E63000118D7 /* ARAppBackgroundFetchDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARAppBackgroundFetchDelegate.m; path = App/ARAppBackgroundFetchDelegate.m; sourceTree = ""; }; + 60438F881782F8CC00C1B63B /* ArtsyAPI+Search.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Search.h"; sourceTree = ""; }; + 60438F891782F8CC00C1B63B /* ArtsyAPI+Search.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Search.m"; sourceTree = ""; }; + 6044CFC3179D64F500CE4132 /* Theme.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Theme.json; sourceTree = ""; }; + 6044CFC6179DD3C200CE4132 /* ARTheme+HeightAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTheme+HeightAdditions.h"; sourceTree = ""; }; + 6044CFC7179DD3C200CE4132 /* ARTheme+HeightAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARTheme+HeightAdditions.m"; sourceTree = ""; }; + 6044E543176E165600075B15 /* ARShowFeedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARShowFeedViewController.h; sourceTree = ""; }; + 6044E544176E165600075B15 /* ARShowFeedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARShowFeedViewController.m; sourceTree = ""; }; + 60466F761A698B96004CAA93 /* BETA_CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = BETA_CHANGELOG.md; sourceTree = ""; }; + 605002E217D8912300C090B8 /* Artwork_Icon_Share@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Artwork_Icon_Share@2x.png"; sourceTree = ""; }; + 605002E717D9F5DC00C090B8 /* SmallMoreVerticalArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SmallMoreVerticalArrow.png; sourceTree = ""; }; + 605002E817D9F5DC00C090B8 /* SmallMoreVerticalArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SmallMoreVerticalArrow@2x.png"; sourceTree = ""; }; + 60567B2A167F8F9E009D205A /* ARFeedItems.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARFeedItems.h; sourceTree = ""; }; + 605B11B017CFD78400334196 /* AROnboardingWebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingWebViewController.h; sourceTree = ""; }; + 605B11B117CFD78400334196 /* AROnboardingWebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingWebViewController.m; sourceTree = ""; }; + 605B11B317CFE7CC00334196 /* ARTermsAndConditionsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTermsAndConditionsView.h; sourceTree = ""; }; + 605B11B417CFE7CC00334196 /* ARTermsAndConditionsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTermsAndConditionsView.m; sourceTree = ""; }; + 60745DE8165802D9006CE156 /* ARUserManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARUserManager.h; sourceTree = ""; }; + 60745DE9165802D9006CE156 /* ARUserManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARUserManager.m; sourceTree = ""; }; + 607861A1165E47470010FAA2 /* EXTConcreteProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = EXTConcreteProtocol.h; path = Artsy/Resources/EXTConcreteProtocol.h; sourceTree = ""; }; + 607861A2165E47470010FAA2 /* EXTConcreteProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = EXTConcreteProtocol.m; path = Artsy/Resources/EXTConcreteProtocol.m; sourceTree = ""; }; + 607D754817C239E700CA1D41 /* ArtsyAPI+Following.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Following.h"; sourceTree = ""; }; + 607D754917C239E700CA1D41 /* ArtsyAPI+Following.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Following.m"; sourceTree = ""; }; + 607D756917C3816600CA1D41 /* ArtsyAPI+ListCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+ListCollection.h"; sourceTree = ""; }; + 607D756A17C3816600CA1D41 /* ArtsyAPI+ListCollection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+ListCollection.m"; sourceTree = ""; }; + 607D756C17C3873700CA1D41 /* ARFavoriteItemViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFavoriteItemViewCell.h; sourceTree = ""; }; + 607D756D17C3873700CA1D41 /* ARFavoriteItemViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFavoriteItemViewCell.m; sourceTree = ""; }; + 607E2E7617C8C46000396120 /* ARArtworkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkViewController.h; sourceTree = ""; }; + 607E2E7717C8C46000396120 /* ARArtworkViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkViewController.m; sourceTree = ""; }; + 607E2E7917C8C9FE00396120 /* ARArtworkDetailView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARArtworkDetailView.h; path = "../View Controllers/ARArtworkDetailView.h"; sourceTree = ""; }; + 607E2E7A17C8C9FE00396120 /* ARArtworkDetailView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARArtworkDetailView.m; path = "../View Controllers/ARArtworkDetailView.m"; sourceTree = ""; }; + 607E2E7C17C8CA4D00396120 /* ARZoomArtworkImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARZoomArtworkImageViewController.h; sourceTree = ""; }; + 607E2E7D17C8CA4D00396120 /* ARZoomArtworkImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARZoomArtworkImageViewController.m; sourceTree = ""; }; + 607E2E8217C8CF6B00396120 /* ARArtworkPreviewActionsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARArtworkPreviewActionsView.h; path = "../View Controllers/ARArtworkPreviewActionsView.h"; sourceTree = ""; }; + 607E2E8317C8CF6B00396120 /* ARArtworkPreviewActionsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARArtworkPreviewActionsView.m; path = "../View Controllers/ARArtworkPreviewActionsView.m"; sourceTree = ""; }; + 607E2E8517C8E87E00396120 /* ARArtworkPreviewImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARArtworkPreviewImageView.h; path = "../View Controllers/ARArtworkPreviewImageView.h"; sourceTree = ""; }; + 607E2E8617C8E87E00396120 /* ARArtworkPreviewImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARArtworkPreviewImageView.m; path = "../View Controllers/ARArtworkPreviewImageView.m"; sourceTree = ""; }; + 607E2E8817C9121500396120 /* ARZoomImageTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARZoomImageTransition.h; sourceTree = ""; }; + 607E2E8917C9121500396120 /* ARZoomImageTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARZoomImageTransition.m; sourceTree = ""; }; + 60838EAD17728C5D00869F6E /* ARHeartButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARHeartButton.h; sourceTree = ""; }; + 60838EAE17728C5D00869F6E /* ARHeartButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARHeartButton.m; sourceTree = ""; }; + 60838EB31773547700869F6E /* ARViewInRoomTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARViewInRoomTransition.h; sourceTree = ""; }; + 60838EB41773547700869F6E /* ARViewInRoomTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARViewInRoomTransition.m; sourceTree = ""; }; + 60889872175DEBC2008C9319 /* ARFeedSubclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedSubclasses.h; sourceTree = ""; }; + 60889873175DEBC2008C9319 /* ARFeedSubclasses.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedSubclasses.m; sourceTree = ""; }; + 6088B75217D1DBAC00E4BB67 /* ARAnalyticsConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARAnalyticsConstants.h; path = Constants/ARAnalyticsConstants.h; sourceTree = ""; }; + 6088B75317D1DBAC00E4BB67 /* ARAnalyticsConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARAnalyticsConstants.m; path = Constants/ARAnalyticsConstants.m; sourceTree = ""; }; + 608920C1178C682A00989A10 /* ARItemThumbnailViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARItemThumbnailViewCell.h; sourceTree = ""; }; + 608920C2178C682A00989A10 /* ARItemThumbnailViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARItemThumbnailViewCell.m; sourceTree = ""; }; + 6089243117F617140023D1AC /* ARArtworkMetadataView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkMetadataView.h; sourceTree = ""; }; + 6089243217F617140023D1AC /* ARArtworkMetadataView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkMetadataView.m; sourceTree = ""; }; + 60892CEC16B9A849004C1B47 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; + 60892CEE16B9A871004C1B47 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; + 608B2F5D1657D0500046956C /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Artsy/App/main.m; sourceTree = SOURCE_ROOT; }; + 608B2F601657D1500046956C /* ARDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDefaults.h; sourceTree = ""; }; + 608B2F611657D1500046956C /* ARDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDefaults.m; sourceTree = ""; }; + 608B2F6C1657D1BB0046956C /* Models.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Models.h; sourceTree = ""; }; + 608B2F6D1657D1D10046956C /* User.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = User.h; sourceTree = ""; }; + 608B2F6E1657D1D10046956C /* User.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = User.m; sourceTree = ""; }; + 608B2F711657D2DF0046956C /* ARLoginViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARLoginViewController.m; sourceTree = ""; }; + 608B2F861657D5880046956C /* Categories.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Categories.h; sourceTree = ""; }; + 608B703117D4A1C70088A56C /* ActionButton@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ActionButton@2x.png"; sourceTree = ""; }; + 608B703717D4A1C70088A56C /* Heart_White@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Heart_White@2x.png"; sourceTree = ""; }; + 608B703917D4A1C70088A56C /* Heart_Black@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Heart_Black@2x.png"; sourceTree = ""; }; + 608B703A17D4A1C70088A56C /* Artwork_Icon_Share.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Artwork_Icon_Share.png; sourceTree = ""; }; + 608B703B17D4A1C70088A56C /* Artwork_Icon_VIR.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Artwork_Icon_VIR.png; sourceTree = ""; }; + 608B703C17D4A1C70088A56C /* Artwork_Icon_VIR@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Artwork_Icon_VIR@2x.png"; sourceTree = ""; }; + 608B703E17D4A1C70088A56C /* BackArrow_Highlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "BackArrow_Highlighted@2x.png"; sourceTree = ""; }; + 608B704017D4A1C70088A56C /* BackArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "BackArrow@2x.png"; sourceTree = ""; }; + 608B704717D4A1C70088A56C /* FollowCheckmark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FollowCheckmark.png; sourceTree = ""; }; + 608B704817D4A1C70088A56C /* FollowCheckmark@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "FollowCheckmark@2x.png"; sourceTree = ""; }; + 608B704917D4A1C70088A56C /* FooterBackground@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "FooterBackground@2x.png"; sourceTree = ""; }; + 608B704C17D4A1C70088A56C /* MenuButtonBG@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuButtonBG@2x.png"; sourceTree = ""; }; + 608B704E17D4A1C70088A56C /* MenuClose@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuClose@2x.png"; sourceTree = ""; }; + 608B704F17D4A1C70088A56C /* MenuHamburger@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuHamburger@2x.png"; sourceTree = ""; }; + 608B705117D4A1C70088A56C /* MoreArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MoreArrow@2x.png"; sourceTree = ""; }; + 608B705817D4A1C80088A56C /* SettingsButton@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SettingsButton@2x.png"; sourceTree = ""; }; + 608B705917D4A1C80088A56C /* SidebarButtonBG@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SidebarButtonBG@2x.png"; sourceTree = ""; }; + 608B705A17D4A1C80088A56C /* SidebarButtonHighlightBG@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SidebarButtonHighlightBG@2x.png"; sourceTree = ""; }; + 608B706217D4A1C80088A56C /* CloseButtonLarge@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "CloseButtonLarge@2x.png"; sourceTree = ""; }; + 608B709717D4F6520088A56C /* ARSearchTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSearchTableViewCell.h; sourceTree = ""; }; + 608B709817D4F6520088A56C /* ARSearchTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSearchTableViewCell.m; sourceTree = ""; }; + 608EE3D919954CEB001F4FE0 /* UIViewController+PresentWithFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+PresentWithFrame.h"; sourceTree = ""; }; + 608EE3DA19954CEB001F4FE0 /* UIViewController+PresentWithFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+PresentWithFrame.m"; sourceTree = ""; }; + 60903CC2175CE21A002AB800 /* AROptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AROptions.h; path = Utils/AROptions.h; sourceTree = ""; }; + 60903CC3175CE21A002AB800 /* AROptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AROptions.m; path = Utils/AROptions.m; sourceTree = ""; }; + 60903CC9175CE766002AB800 /* ARAdminSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAdminSettingsViewController.h; sourceTree = ""; }; + 60903CCA175CE766002AB800 /* ARAdminSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAdminSettingsViewController.m; sourceTree = ""; }; + 60903CCD175CF088002AB800 /* ARGroupedTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARGroupedTableViewCell.h; sourceTree = ""; }; + 60903CCE175CF088002AB800 /* ARGroupedTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARGroupedTableViewCell.m; sourceTree = ""; }; + 60903CD0175CF095002AB800 /* ARAnimatedTickView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAnimatedTickView.h; sourceTree = ""; }; + 60903CD1175CF095002AB800 /* ARAnimatedTickView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAnimatedTickView.m; sourceTree = ""; }; + 60903CD3175CF23F002AB800 /* ARTickedTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTickedTableViewCell.h; sourceTree = ""; }; + 60903CD4175CF23F002AB800 /* ARTickedTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTickedTableViewCell.m; sourceTree = ""; }; + 60903CD6175CF27D002AB800 /* ARAdminTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAdminTableViewCell.h; sourceTree = ""; }; + 60903CD7175CF27D002AB800 /* ARAdminTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAdminTableViewCell.m; sourceTree = ""; }; + 60903CDB175DE1C2002AB800 /* ARFeed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeed.h; sourceTree = ""; }; + 60903CDC175DE1C2002AB800 /* ARFeed.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeed.m; sourceTree = ""; }; + 60935A451A69CFEE00129CE1 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 6099F8F7178DBF160004EF04 /* ARModernPartnerShowTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARModernPartnerShowTableViewCell.h; path = "iPhone Feed Items/ARModernPartnerShowTableViewCell.h"; sourceTree = ""; }; + 6099F8F8178DBF160004EF04 /* ARModernPartnerShowTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARModernPartnerShowTableViewCell.m; path = "iPhone Feed Items/ARModernPartnerShowTableViewCell.m"; sourceTree = ""; }; + 6099F8FC178DE9400004EF04 /* ARTheme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTheme.h; sourceTree = ""; }; + 6099F8FD178DE9400004EF04 /* ARTheme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTheme.m; sourceTree = ""; }; + 6099F905178F24750004EF04 /* ARDefaultNavigationTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDefaultNavigationTransition.h; sourceTree = ""; }; + 6099F906178F24750004EF04 /* ARDefaultNavigationTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDefaultNavigationTransition.m; sourceTree = ""; }; + 6099F90817904F9B0004EF04 /* ARModelCollectionViewModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARModelCollectionViewModule.h; sourceTree = ""; }; + 6099F90917904F9B0004EF04 /* ARModelCollectionViewModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARModelCollectionViewModule.m; sourceTree = ""; }; + 609A82EE17A10C5C00AFDF13 /* ARNavigationTransitionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNavigationTransitionController.h; sourceTree = ""; }; + 609A82EF17A10C5C00AFDF13 /* ARNavigationTransitionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationTransitionController.m; sourceTree = ""; }; + 609A82F217A1117E00AFDF13 /* ARInquireForArtworkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARInquireForArtworkViewController.h; sourceTree = ""; }; + 609A82F317A1117E00AFDF13 /* ARInquireForArtworkViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ARInquireForArtworkViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 609A82F917A1147800AFDF13 /* ARNavigationTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNavigationTransition.h; sourceTree = ""; }; + 609A82FA17A1147800AFDF13 /* ARNavigationTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNavigationTransition.m; sourceTree = ""; }; + 609B3C071761F80C00953CB2 /* ARSiteHeroUnitView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSiteHeroUnitView.h; sourceTree = ""; }; + 609B3C081761F80C00953CB2 /* ARSiteHeroUnitView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSiteHeroUnitView.m; sourceTree = ""; }; + 60A0AD481797FD2E00E976B7 /* ARThemedFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARThemedFactory.h; sourceTree = ""; }; + 60A0AD491797FD2E00E976B7 /* ARThemedFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARThemedFactory.m; sourceTree = ""; }; + 60A224FA17CE040B00233CA1 /* AROnboardingTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingTransition.h; sourceTree = ""; }; + 60A224FB17CE040B00233CA1 /* AROnboardingTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingTransition.m; sourceTree = ""; }; + 60A224FD17CE0E1B00233CA1 /* AROnboardingViewControllers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AROnboardingViewControllers.h; sourceTree = ""; }; + 60A40E8F18A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARBrowseFeaturedLinkInsetCell.h; sourceTree = ""; }; + 60A40E9018A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBrowseFeaturedLinkInsetCell.m; sourceTree = ""; }; + 60A49F5E1676559200B9B95D /* ARNetworkConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARNetworkConstants.h; sourceTree = ""; }; + 60A49F5F1676559300B9B95D /* ARNetworkConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARNetworkConstants.m; sourceTree = ""; }; + 60A612B6189673F4008FC19D /* ARGeneArtworksNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARGeneArtworksNetworkModel.h; sourceTree = ""; }; + 60A612B7189673F4008FC19D /* ARGeneArtworksNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARGeneArtworksNetworkModel.m; sourceTree = ""; }; + 60A612B918969307008FC19D /* overview.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = overview.md; sourceTree = ""; }; + 60A7B1B017B26DCD003DC094 /* ARFeedHostItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARFeedHostItem.h; sourceTree = ""; }; + 60AEC84516E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkThumbnailMetadataView.h; sourceTree = ""; }; + 60AEC84616E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkThumbnailMetadataView.m; sourceTree = ""; }; + 60B617CB1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAuctionArtworkResultsViewController.h; sourceTree = ""; }; + 60B617CC1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAuctionArtworkResultsViewController.m; sourceTree = ""; }; + 60B617D11815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAuctionArtworkTableViewCell.h; sourceTree = ""; }; + 60B617D21815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAuctionArtworkTableViewCell.m; sourceTree = ""; }; + 60B6F0F01662AADF007C9587 /* ARAppConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAppConstants.h; sourceTree = ""; }; + 60B6F0F11662AADF007C9587 /* ARAppConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAppConstants.m; sourceTree = ""; }; + 60B6F13716638785007C9587 /* ArtsyAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArtsyAPI.h; sourceTree = ""; }; + 60B6F13816638785007C9587 /* ArtsyAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArtsyAPI.m; sourceTree = ""; }; + 60B6F13C16638815007C9587 /* Artwork.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Artwork.h; sourceTree = ""; }; + 60B6F13D16638815007C9587 /* Artwork.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Artwork.m; sourceTree = ""; }; + 60B7604417C9FBEA00073A14 /* ARArtworkActionsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkActionsView.h; sourceTree = ""; }; + 60B7604517C9FBEA00073A14 /* ARArtworkActionsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkActionsView.m; sourceTree = ""; }; + 60B79B81182C346700945FFF /* ARQuicksilverViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARQuicksilverViewController.h; sourceTree = ""; }; + 60B79B82182C346700945FFF /* ARQuicksilverViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARQuicksilverViewController.m; sourceTree = ""; }; + 60B79B83182C346700945FFF /* ARQuicksilverViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ARQuicksilverViewController.xib; sourceTree = ""; }; + 60B79B87182C54CE00945FFF /* ARQuicksilverSearchBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARQuicksilverSearchBar.h; path = quicksilver/ARQuicksilverSearchBar.h; sourceTree = ""; }; + 60B79B88182C54CE00945FFF /* ARQuicksilverSearchBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARQuicksilverSearchBar.m; path = quicksilver/ARQuicksilverSearchBar.m; sourceTree = ""; }; + 60BC390718182227003F34E7 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + 60C4BD6317B3B91800D79058 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = CHANGELOG.md; sourceTree = SOURCE_ROOT; }; + 60CAA3741782D87B00CA3DA8 /* ARContentViewControllers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARContentViewControllers.h; sourceTree = ""; }; + 60CEA77C19B61F8000CC3A91 /* ARArtistNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtistNetworkModel.h; sourceTree = ""; }; + 60CEA77D19B61F8000CC3A91 /* ARArtistNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtistNetworkModel.m; sourceTree = ""; }; + 60CEA77F19B6254800CC3A91 /* ARStubbedArtistNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARStubbedArtistNetworkModel.h; sourceTree = ""; }; + 60CEA78019B6254800CC3A91 /* ARStubbedArtistNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARStubbedArtistNetworkModel.m; sourceTree = ""; }; + 60CF97FA17BE9303005ED59B /* ARShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARShadowView.h; sourceTree = ""; }; + 60CF97FB17BE9303005ED59B /* ARShadowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARShadowView.m; sourceTree = ""; }; + 60CF980617BF79CA005ED59B /* ARArtistBiographyViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtistBiographyViewController.h; sourceTree = ""; }; + 60CF980717BF79CA005ED59B /* ARArtistBiographyViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtistBiographyViewController.m; sourceTree = ""; }; + 60D83DE6189EAB82001672E9 /* ARFairShowMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARFairShowMapper.h; path = models/ARFairShowMapper.h; sourceTree = ""; }; + 60D83DE7189EAB82001672E9 /* ARFairShowMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARFairShowMapper.m; path = models/ARFairShowMapper.m; sourceTree = ""; }; + 60D83DEC189EE679001672E9 /* ARFairGuideViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairGuideViewController.h; sourceTree = ""; }; + 60D83DED189EE679001672E9 /* ARFairGuideViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairGuideViewController.m; sourceTree = ""; }; + 60D8E63818D256BB0040BEFD /* ARDemoSplashViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDemoSplashViewController.h; sourceTree = ""; }; + 60D8E63918D256BB0040BEFD /* ARDemoSplashViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDemoSplashViewController.m; sourceTree = ""; }; + 60D8E63A18D256BB0040BEFD /* ARDemoSplashViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ARDemoSplashViewController.xib; sourceTree = ""; }; + 60D90A0317C2109A0073D5B9 /* FeaturedLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeaturedLink.h; sourceTree = ""; }; + 60D90A0417C2109A0073D5B9 /* FeaturedLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeaturedLink.m; sourceTree = ""; }; + 60D90A0617C2182F0073D5B9 /* ARExternalWebBrowserViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARExternalWebBrowserViewController.h; sourceTree = ""; }; + 60D90A0717C2182F0073D5B9 /* ARExternalWebBrowserViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARExternalWebBrowserViewController.m; sourceTree = ""; }; + 60D90A0917C218930073D5B9 /* ARInternalMobileWebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARInternalMobileWebViewController.h; sourceTree = ""; }; + 60D90A0A17C218930073D5B9 /* ARInternalMobileWebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARInternalMobileWebViewController.m; sourceTree = ""; }; + 60DE2DDE1677B26A00621540 /* ARLoginViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARLoginViewController.h; sourceTree = ""; }; + 60E6446F17BE219E004486B3 /* HACKS.md */ = {isa = PBXFileReference; lastKnownFileType = text; name = HACKS.md; path = ../HACKS.md; sourceTree = ""; }; + 60E6447417BE424E004486B3 /* ARSwitchView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSwitchView.h; sourceTree = ""; }; + 60E6447517BE424E004486B3 /* ARSwitchView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSwitchView.m; sourceTree = ""; }; + 60EEFE741658547C00F43F00 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + 60EFA6F316CAA8180094AD7C /* ArtsyAPI+Feed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Feed.h"; sourceTree = ""; }; + 60EFA6F416CAA8180094AD7C /* ArtsyAPI+Feed.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Feed.m"; sourceTree = ""; }; + 60F1C50C17C0EC6A000938F7 /* ARBrowseViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARBrowseViewController.h; path = "../View Controllers/ARBrowseViewController.h"; sourceTree = ""; }; + 60F1C50D17C0EC6A000938F7 /* ARBrowseViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARBrowseViewController.m; path = "../View Controllers/ARBrowseViewController.m"; sourceTree = ""; }; + 60F1C51017C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARBrowseFeaturedLinksCollectionView.h; sourceTree = ""; }; + 60F1C51117C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBrowseFeaturedLinksCollectionView.m; sourceTree = ""; usesTabs = 0; }; + 60F1C51317C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARBrowseFeaturedLinksCollectionViewCell.h; sourceTree = ""; }; + 60F1C51417C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBrowseFeaturedLinksCollectionViewCell.m; sourceTree = ""; }; + 60F1C51617C0F3DE000938F7 /* ArtsyAPI+Browse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Browse.h"; sourceTree = ""; }; + 60F1C51717C0F3DE000938F7 /* ArtsyAPI+Browse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Browse.m"; sourceTree = ""; }; + 60F1C51917C11303000938F7 /* ARGeneViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARGeneViewController.h; sourceTree = ""; }; + 60F1C51A17C11303000938F7 /* ARGeneViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARGeneViewController.m; sourceTree = ""; }; + 60F8FFC0197E773E00DC3869 /* ARArtworkSetViewControllerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkSetViewControllerSpec.m; sourceTree = ""; }; + 60FAEF41179843080031C88B /* ARFavoritesViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFavoritesViewController.h; sourceTree = ""; }; + 60FAEF42179843080031C88B /* ARFavoritesViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFavoritesViewController.m; sourceTree = ""; }; + 60FFE3D6188E91C50012B485 /* certs.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = certs.md; sourceTree = ""; }; + 60FFE3D7188E91C50012B485 /* deploy_to_app_store.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = deploy_to_app_store.md; sourceTree = ""; }; + 60FFE3D8188E91C50012B485 /* deploy_to_beta.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = deploy_to_beta.md; sourceTree = ""; }; + 60FFE3D9188E91C50012B485 /* getting_started.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = getting_started.md; sourceTree = ""; }; + 60FFE3DA188E91C50012B485 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = ""; }; + 60FFE3DB188E91C50012B485 /* troubleshooting.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = troubleshooting.md; sourceTree = ""; }; + 60FFE3DC188E91F20012B485 /* getting_confident.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = getting_confident.md; sourceTree = ""; }; + 60FFE3DD188E921A0012B485 /* eigen_tips.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = eigen_tips.md; sourceTree = ""; }; + 7AABCA8DF484E65EF3CC11FA /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B33890F5B4F8EFF536074A4 /* Pods.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.beta.xcconfig; path = "Pods/Target Support Files/Pods/Pods.beta.xcconfig"; sourceTree = ""; }; + 885DE1DBF1C1DB636BA9F61A /* Pods.store.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.store.xcconfig; path = "Pods/Target Support Files/Pods/Pods.store.xcconfig"; sourceTree = ""; }; + 8A6745B7A19EEDA119AEA48E /* Pods-Artsy Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Artsy Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Artsy Tests/Pods-Artsy Tests.debug.xcconfig"; sourceTree = ""; }; + 8E3DD2A89F196F5E5BFB9689 /* Pods-Artsy Tests.store.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Artsy Tests.store.xcconfig"; path = "Pods/Target Support Files/Pods-Artsy Tests/Pods-Artsy Tests.store.xcconfig"; sourceTree = ""; }; + A060C6085108B7738F5ADD02 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + B30FEF59181EEA47009E4EAD /* ARBidButtonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBidButtonTests.m; sourceTree = ""; }; + B316E2F218170DF40086CCDB /* SaleArtwork.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SaleArtwork.h; sourceTree = ""; }; + B316E2F318170DF40086CCDB /* SaleArtwork.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SaleArtwork.m; sourceTree = ""; }; + B316E2F5181713110086CCDB /* Bidder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Bidder.h; sourceTree = ""; }; + B316E2F6181713110086CCDB /* Bidder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Bidder.m; sourceTree = ""; }; + B375E5F218121EEA005FC680 /* Sale.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Sale.h; sourceTree = ""; }; + B375E5F318121EEA005FC680 /* Sale.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Sale.m; sourceTree = ""; }; + B375E5F51812207D005FC680 /* ArtsyAPI+Sales.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ArtsyAPI+Sales.h"; sourceTree = ""; }; + B375E5F61812207D005FC680 /* ArtsyAPI+Sales.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+Sales.m"; sourceTree = ""; }; + B3BD12D717F22370002CA230 /* fbp */ = {isa = PBXFileReference; lastKnownFileType = file; path = fbp; sourceTree = ""; }; + B3BD12D817F22370002CA230 /* fbs */ = {isa = PBXFileReference; lastKnownFileType = file; path = fbs; sourceTree = ""; }; + B3DDE9BF18313C7C0012819F /* ARArtworkAuctionPriceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkAuctionPriceView.h; sourceTree = ""; }; + B3DDE9C018313C7C0012819F /* ARArtworkAuctionPriceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkAuctionPriceView.m; sourceTree = ""; }; + B3DDE9C218313CFF0012819F /* ARArtworkPriceRowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkPriceRowView.h; sourceTree = ""; }; + B3DDE9C318313CFF0012819F /* ARArtworkPriceRowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkPriceRowView.m; sourceTree = ""; }; + B3ECE8261819D1E6009F5C5B /* Artsy Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Artsy Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + B3ECE8271819D1E6009F5C5B /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + B3ECE82D1819D1E6009F5C5B /* Artsy Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Artsy Tests-Info.plist"; sourceTree = ""; }; + B3ECE82F1819D1E6009F5C5B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + B3ECE8331819D1E6009F5C5B /* Artsy Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Artsy Tests-Prefix.pch"; sourceTree = ""; }; + B3ECE83B1819D1FD009F5C5B /* SaleArtworkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SaleArtworkTests.m; sourceTree = ""; }; + B3EFC6371815B41C00F23540 /* BidderPosition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BidderPosition.h; sourceTree = ""; }; + B3EFC6381815B41C00F23540 /* BidderPosition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BidderPosition.m; sourceTree = ""; }; + CB11525917C815210093D864 /* AROnboardingFollowableTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingFollowableTableViewCell.h; sourceTree = ""; }; + CB11525A17C815210093D864 /* AROnboardingFollowableTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingFollowableTableViewCell.m; sourceTree = ""; }; + CB206F6F17C3FA8F00A4FDC4 /* ARPriceRangeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPriceRangeViewController.h; sourceTree = ""; }; + CB206F7017C3FA8F00A4FDC4 /* ARPriceRangeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPriceRangeViewController.m; sourceTree = ""; }; + CB206F7317C569E800A4FDC4 /* ARPersonalizeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPersonalizeViewController.m; sourceTree = ""; }; + CB206F7517C5A5AD00A4FDC4 /* AROnboardingGeneTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingGeneTableController.h; sourceTree = ""; }; + CB206F7617C5A5AD00A4FDC4 /* AROnboardingGeneTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingGeneTableController.m; sourceTree = ""; }; + CB206F7817C5AEB400A4FDC4 /* AROnboardingArtistTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingArtistTableController.h; sourceTree = ""; }; + CB206F7917C5AEB400A4FDC4 /* AROnboardingArtistTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingArtistTableController.m; sourceTree = ""; }; + CB260E7F17C2C90900BF2012 /* ARCollectorStatusViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARCollectorStatusViewController.h; sourceTree = ""; }; + CB260E8017C2C90900BF2012 /* ARCollectorStatusViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARCollectorStatusViewController.m; sourceTree = ""; }; + CB2C960217D3B4B500B36B44 /* ARFeedLinkUnitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeedLinkUnitViewController.h; sourceTree = ""; }; + CB2C960317D3B4B500B36B44 /* ARFeedLinkUnitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeedLinkUnitViewController.m; sourceTree = ""; }; + CB355E8217CAB0B3002A798D /* ARFollowable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFollowable.h; sourceTree = ""; }; + CB42B64D181092480069A801 /* ARCountdownView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARCountdownView.h; sourceTree = ""; }; + CB42B64E181092480069A801 /* ARCountdownView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARCountdownView.m; sourceTree = ""; }; + CB4D652517C80A9600390550 /* AROnboardingSearchField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingSearchField.h; sourceTree = ""; }; + CB4D652617C80A9600390550 /* AROnboardingSearchField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingSearchField.m; sourceTree = ""; }; + CB61F12F17C2D83F003DB8A9 /* AROnboardingTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingTableViewCell.h; sourceTree = ""; }; + CB61F13017C2D83F003DB8A9 /* AROnboardingTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingTableViewCell.m; sourceTree = ""; }; + CB73B48917D2581400891305 /* SiteFeature.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SiteFeature.h; sourceTree = ""; }; + CB73B48A17D2581400891305 /* SiteFeature.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SiteFeature.m; sourceTree = ""; }; + CB821E7718217AF500CC934E /* ARAuctionBannerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAuctionBannerView.h; sourceTree = ""; }; + CB821E7818217AF500CC934E /* ARAuctionBannerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAuctionBannerView.m; sourceTree = ""; }; + CB879D0A180746C900E2D8EC /* AuctionLot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuctionLot.h; sourceTree = ""; }; + CB879D0B180746C900E2D8EC /* AuctionLot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AuctionLot.m; sourceTree = ""; }; + CB8D9D4217CEA7B900F3286B /* AROnboardingMoreInfoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AROnboardingMoreInfoViewController.h; sourceTree = ""; }; + CB8D9D4317CEA7B900F3286B /* AROnboardingMoreInfoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingMoreInfoViewController.m; sourceTree = ""; }; + CB9E243F17CBC36F00773A9A /* ARAuthProviders.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARAuthProviders.h; sourceTree = ""; }; + CB9E244017CBC36F00773A9A /* ARAuthProviders.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAuthProviders.m; sourceTree = ""; }; + CBB25B3A17F36DDE00C31446 /* ARFeaturedArtworksViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFeaturedArtworksViewController.h; sourceTree = ""; }; + CBB25B3B17F36DDE00C31446 /* ARFeaturedArtworksViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFeaturedArtworksViewController.m; sourceTree = ""; }; + CBB469CE181F1F1200B5692B /* Bid.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Bid.h; sourceTree = ""; }; + CBB469CF181F1F1200B5692B /* Bid.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Bid.m; sourceTree = ""; }; + E2C583DC5F5EBC24D3CC3B76 /* Pods.demo.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.demo.xcconfig; path = "Pods/Target Support Files/Pods/Pods.demo.xcconfig"; sourceTree = ""; }; + E60673B119BE4E8C00EF05EB /* full_logo_white_small@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "full_logo_white_small@2x.png"; sourceTree = ""; }; + E611846F18D78068000FE4C9 /* ARMessageItemProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARMessageItemProviderTests.m; sourceTree = ""; }; + E611847118D7B4C4000FE4C9 /* ARSharingControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSharingControllerTests.m; sourceTree = ""; }; + E612BC3E19D1B9DE00585CD6 /* ARFollowableButtonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFollowableButtonTests.m; sourceTree = ""; }; + E614469D195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtistFavoritesNetworkModel.h; sourceTree = ""; }; + E614469E195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtistFavoritesNetworkModel.m; sourceTree = ""; }; + E614469F195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkFavoritesNetworkModel.h; sourceTree = ""; }; + E61446A0195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkFavoritesNetworkModel.m; sourceTree = ""; }; + E61446A3195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFairFavoritesNetworkModel.h; sourceTree = ""; }; + E61446A4195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFairFavoritesNetworkModel.m; sourceTree = ""; }; + E61446A5195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARFairFavoritesNetworkModel+Private.h"; sourceTree = ""; }; + E61446A6195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFavoritesNetworkModel.h; sourceTree = ""; }; + E61446A7195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFavoritesNetworkModel.m; sourceTree = ""; }; + E61446A8195A1CDC00BFB7C3 /* ARFollowableNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFollowableNetworkModel.h; sourceTree = ""; }; + E61446A9195A1CDC00BFB7C3 /* ARFollowableNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFollowableNetworkModel.m; sourceTree = ""; }; + E61446AA195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARGeneFavoritesNetworkModel.h; sourceTree = ""; }; + E61446AB195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARGeneFavoritesNetworkModel.m; sourceTree = ""; }; + E616B2461911664900D1CBC6 /* ARArtworkView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkView.h; sourceTree = ""; }; + E616B2471911664900D1CBC6 /* ARArtworkView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkView.m; sourceTree = ""; }; + E619969A19B9099400DB273C /* UIScrollView+HitTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIScrollView+HitTest.h"; path = "Apple/UIScrollView+HitTest.h"; sourceTree = ""; }; + E619969B19B9099400DB273C /* UIScrollView+HitTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIScrollView+HitTest.m"; path = "Apple/UIScrollView+HitTest.m"; sourceTree = ""; }; + E61B703719904D9F00260D29 /* ARTopTapThroughTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTopTapThroughTableView.h; sourceTree = ""; }; + E61B703819904D9F00260D29 /* ARTopTapThroughTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTopTapThroughTableView.m; sourceTree = ""; }; + E61E772419A5477300C55E14 /* ARExpectaExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARExpectaExtensions.h; sourceTree = ""; }; + E61E772519A5477300C55E14 /* ARExpectaExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARExpectaExtensions.m; sourceTree = ""; }; + E61E773819A7E8D500C55E14 /* ARGeneViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARGeneViewControllerTests.m; sourceTree = ""; }; + E620C74719C746530064A0FF /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Artsy/Images.xcassets; sourceTree = SOURCE_ROOT; }; + E63C796B198811E400579C04 /* ARArtworkRelatedArtworksViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkRelatedArtworksViewTests.m; sourceTree = ""; }; + E649708818D7762F009DB0C4 /* ARImageItemProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARImageItemProviderTests.m; sourceTree = ""; }; + E6543CDA18AD795E00A6B9AF /* Parallax_Overlay_Bottom@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Parallax_Overlay_Bottom@2x.png"; sourceTree = ""; }; + E655E4B7194B4BEF00F2B7DA /* ARArtworkFavoritesNetworkModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkFavoritesNetworkModelTests.m; sourceTree = ""; }; + E65BB51E1A3FB552004C4DB4 /* ARAppSearchViewControllerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAppSearchViewControllerSpec.m; sourceTree = ""; }; + E65BB5221A408A29004C4DB4 /* TextfieldClearButton.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TextfieldClearButton.png; sourceTree = ""; }; + E66492D218B7D26B00531B8F /* Parallax_Overlay_Top@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Parallax_Overlay_Top@2x.png"; sourceTree = ""; }; + E667F12018EC825D00503F50 /* ARHTTPRequestOperationLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARHTTPRequestOperationLogger.h; path = Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.h; sourceTree = SOURCE_ROOT; }; + E667F12118EC825D00503F50 /* ARHTTPRequestOperationLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARHTTPRequestOperationLogger.m; path = Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.m; sourceTree = SOURCE_ROOT; }; + E667F12618EC82AB00503F50 /* ARLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARLogger.h; path = Logging/ARLogger.h; sourceTree = ""; }; + E667F12718EC82AB00503F50 /* ARLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARLogger.m; path = Logging/ARLogger.m; sourceTree = ""; }; + E667F12A18EC889F00503F50 /* ARLogFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARLogFormatter.h; path = Logging/ARLogFormatter.h; sourceTree = ""; }; + E667F12B18EC889F00503F50 /* ARLogFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARLogFormatter.m; path = Logging/ARLogFormatter.m; sourceTree = ""; }; + E66A0A8219C2125400ECEB1A /* full_logo_white_large@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "full_logo_white_large@2x.png"; sourceTree = ""; }; + E66EE4AD196460620081ED0C /* ARFavoriteItemModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFavoriteItemModule.h; sourceTree = ""; }; + E66EE4AE196460620081ED0C /* ARFavoriteItemModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFavoriteItemModule.m; sourceTree = ""; }; + E671830F194A3D1E001A5566 /* ARHeroUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARHeroUnitTests.m; sourceTree = ""; }; + E6718314194A595F001A5566 /* ARTestContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTestContext.h; sourceTree = ""; }; + E6718315194A595F001A5566 /* ARTestContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTestContext.m; sourceTree = ""; }; + E67655241900660500F9A704 /* ARWhitespaceGobbler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARWhitespaceGobbler.h; sourceTree = ""; }; + E67655251900660500F9A704 /* ARWhitespaceGobbler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARWhitespaceGobbler.m; sourceTree = ""; }; + E676A62418EF5F6E00B9AF2C /* Artwork_v0.data */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = Artwork_v0.data; sourceTree = ""; }; + E676A62918F3421600B9AF2C /* ARSeparatorViews.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSeparatorViews.h; sourceTree = ""; }; + E676A62A18F3421600B9AF2C /* ARSeparatorViews.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSeparatorViews.m; sourceTree = ""; }; + E676B2E918D0CC330057B4E1 /* ARURLItemProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARURLItemProvider.h; sourceTree = ""; }; + E676B2EA18D0CC330057B4E1 /* ARURLItemProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARURLItemProvider.m; sourceTree = ""; }; + E676B2EC18D11A9B0057B4E1 /* ARImageItemProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARImageItemProvider.h; sourceTree = ""; }; + E676B2ED18D11A9B0057B4E1 /* ARImageItemProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARImageItemProvider.m; sourceTree = ""; }; + E676B2EF18D207910057B4E1 /* ARShareableObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARShareableObject.h; sourceTree = ""; }; + E676B2F018D224000057B4E1 /* ARMessageItemProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARMessageItemProvider.h; sourceTree = ""; }; + E676B2F118D224000057B4E1 /* ARMessageItemProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARMessageItemProvider.m; sourceTree = ""; }; + E676B2F418D230A00057B4E1 /* ARURLItemProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARURLItemProviderTests.m; sourceTree = ""; }; + E67DF2121A40A73C00C8495E /* ARSearchViewControllerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSearchViewControllerSpec.m; sourceTree = ""; }; + E67E1DB119475642004252E0 /* ARAnimatedTickViewTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARAnimatedTickViewTest.m; sourceTree = ""; }; + E69101FF19C9E17B0048149C /* SearchIcon_White@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SearchIcon_White@2x.png"; sourceTree = ""; }; + E693BF7818A1941D00D464BC /* ARSwitchCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARSwitchCell.h; sourceTree = ""; }; + E693BF7918A1941D00D464BC /* ARSwitchCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSwitchCell.m; sourceTree = ""; }; + E693BF7A18A1941D00D464BC /* ARTextInputCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTextInputCell.h; sourceTree = ""; }; + E693BF7B18A1941D00D464BC /* ARTextInputCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTextInputCell.m; sourceTree = ""; }; + E693BF7F18A1942D00D464BC /* ARSwitchCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ARSwitchCell.xib; sourceTree = ""; }; + E693BF8418A1949A00D464BC /* ARTextInputCellWithTitle.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ARTextInputCellWithTitle.xib; sourceTree = ""; }; + E6A3500318AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSKeyedUnarchiver+ErrorLogging.h"; sourceTree = ""; }; + E6A3500418AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSKeyedUnarchiver+ErrorLogging.m"; sourceTree = ""; }; + E6AD6D3E18E5C404005C8A3A /* ARArtworkInfoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARArtworkInfoViewController.h; sourceTree = ""; }; + E6AD6D3F18E5C404005C8A3A /* ARArtworkInfoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkInfoViewController.m; sourceTree = ""; }; + E6AD6D4118E610DA005C8A3A /* ArtsyAPI+ArtworksTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ArtsyAPI+ArtworksTests.m"; sourceTree = ""; }; + E6B958ED188DB24200D75C86 /* ARViewTagConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARViewTagConstants.h; sourceTree = ""; }; + E6B958EE188DB24200D75C86 /* ARViewTagConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARViewTagConstants.m; sourceTree = ""; }; + E6B958F0188DB6F800D75C86 /* ARArtworkViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARArtworkViewControllerTests.m; sourceTree = ""; }; + E6B9FA7818A96BF500E961F9 /* ARBrowseFeaturedLinkInsetCellTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBrowseFeaturedLinkInsetCellTests.m; sourceTree = ""; }; + E6B9FA7A18A97C0A00E961F9 /* stub.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = stub.jpg; sourceTree = ""; }; + E6BC2C3D197EDC950063ED3C /* ARBrowseViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBrowseViewControllerTests.m; sourceTree = ""; }; + E6C0315E18B291AB00137242 /* ARButtonWithCircularImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARButtonWithCircularImage.h; sourceTree = ""; }; + E6C0315F18B291AB00137242 /* ARButtonWithCircularImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARButtonWithCircularImage.m; sourceTree = ""; }; + E6C13AC01A23AA420050AB53 /* AROnboardingViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AROnboardingViewControllerTests.m; sourceTree = ""; }; + E6C13AC21A23AB160050AB53 /* ARPersonalizeWebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARPersonalizeWebViewController.h; sourceTree = ""; }; + E6C13AC31A23AB160050AB53 /* ARPersonalizeWebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARPersonalizeWebViewController.m; sourceTree = ""; }; + E6CD164918ABE291001254B5 /* Image_Shadow_Overlay@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Image_Shadow_Overlay@2x.png"; sourceTree = ""; }; + E6E07D6B195DF5D800403D2B /* ARSwitchView+Favorites.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARSwitchView+Favorites.h"; sourceTree = ""; }; + E6E07D6C195DF5D800403D2B /* ARSwitchView+Favorites.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARSwitchView+Favorites.m"; sourceTree = ""; }; + E6EC246018F7069300C89192 /* PartnerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PartnerTests.m; sourceTree = ""; }; + E6F1119F18A2A7CB00D33C3E /* ARBrowseFeaturedLinksCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARBrowseFeaturedLinksCollectionViewTests.m; sourceTree = ""; }; + E6F466E718EF52A800A09AFA /* Artwork_v1.data */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = Artwork_v1.data; sourceTree = ""; }; + E6F84CFE1A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "CloseButtonLargeHighlighted@2x.png"; sourceTree = ""; }; + E6F84D011A0AB40500BF99A3 /* ARSignUpActiveUserViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARSignUpActiveUserViewControllerTests.m; sourceTree = ""; }; + E6FD2F1F187B0D76002AAFEB /* ARUserSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARUserSettingsViewController.h; path = ../Models/ARUserSettingsViewController.h; sourceTree = ""; }; + E6FD2F20187B0D76002AAFEB /* ARUserSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARUserSettingsViewController.m; path = ../Models/ARUserSettingsViewController.m; sourceTree = ""; }; + E6FFEA9F19C7925600A0D7DE /* onboard_1@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "onboard_1@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/onboard_1@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA019C7925600A0D7DE /* onboard_1@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "onboard_1@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/onboard_1@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA119C7925600A0D7DE /* onboard_2@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "onboard_2@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/onboard_2@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA219C7925600A0D7DE /* onboard_2@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "onboard_2@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/onboard_2@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA319C7925600A0D7DE /* onboard_3@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "onboard_3@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/onboard_3@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA419C7925600A0D7DE /* onboard_3@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "onboard_3@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/onboard_3@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA519C7925600A0D7DE /* splash_1@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_1@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/splash_1@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA619C7925600A0D7DE /* splash_1@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_1@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/splash_1@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA719C7925600A0D7DE /* splash_2@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_2@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/splash_2@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA819C7925600A0D7DE /* splash_2@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_2@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/splash_2@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAA919C7925600A0D7DE /* splash_3@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_3@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/splash_3@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAAA19C7925600A0D7DE /* splash_3@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_3@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/splash_3@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAAB19C7925600A0D7DE /* splash_4@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_4@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/splash_4@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAAC19C7925600A0D7DE /* splash_4@2x~iphone.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_4@2x~iphone.jpg"; path = "Artsy/Resources/onboarding/splash_4@2x~iphone.jpg"; sourceTree = SOURCE_ROOT; }; + E6FFEAAD19C7925600A0D7DE /* splash_5@2x~ipad.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = "splash_5@2x~ipad.jpg"; path = "Artsy/Resources/onboarding/splash_5@2x~ipad.jpg"; sourceTree = SOURCE_ROOT; }; + E8D6BDCA48472E439B9D79CB /* libPods-Artsy Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Artsy Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 49BA7DFC1655ABE600C06572 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 60935A461A69CFEE00129CE1 /* WebKit.framework in Frameworks */, + 60935A441A69CFE700129CE1 /* MobileCoreServices.framework in Frameworks */, + 7BAB70E261299573481071A9 /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3ECE8231819D1E6009F5C5B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B3ECE8281819D1E6009F5C5B /* XCTest.framework in Frameworks */, + B3ECE82A1819D1E6009F5C5B /* UIKit.framework in Frameworks */, + B3ECE8291819D1E6009F5C5B /* Foundation.framework in Frameworks */, + 6C55DE07E86540899869A357 /* (null) in Frameworks */, + DCA78A9CCA74D94C34C35156 /* libPods-Artsy Tests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 062C201A16DD76A30095A7EC /* Image */ = { + isa = PBXGroup; + children = ( + 3CFB078F18EB585B00792024 /* ARTile+ASCII.h */, + 3CFB079018EB585B00792024 /* ARTile+ASCII.m */, + 062C201F16DD76C90095A7EC /* ARZoomView.h */, + 062C202016DD76C90095A7EC /* ARZoomView.m */, + ); + name = Image; + sourceTree = ""; + }; + 342F90A2E677A86747A25FD8 /* Artsy */ = { + isa = PBXGroup; + children = ( + 342F9F32AB53EF2503473315 /* ORStackView+ArtsyViews.m */, + 342F96AD6F2466A26A3152F8 /* ORStackView+ArtsyViews.h */, + ); + path = Artsy; + sourceTree = ""; + }; + 342F91DD9B57A51A8352877C /* Util */ = { + isa = PBXGroup; + children = ( + 3C12144A18FD6AA9003CDC74 /* Sharing */, + E667F12318EC827B00503F50 /* Logging */, + 49473F3717C192AE004BF082 /* ARTextFieldWithPlaceholder.h */, + 49473F3817C192AE004BF082 /* ARTextFieldWithPlaceholder.m */, + 3CAA412D18D88F2E000EE867 /* UIViewController+InnermostTopViewController.h */, + 3CAA412E18D88F2E000EE867 /* UIViewController+InnermostTopViewController.m */, + 603B3AE91774A0FA00BA5BD3 /* ARNavigationController.h */, + 603B3AEA1774A0FA00BA5BD3 /* ARNavigationController.m */, + 54AD031F18A4FCCE0055F2D2 /* ARMenuAwareViewController.h */, + 60F1C50C17C0EC6A000938F7 /* ARBrowseViewController.h */, + 60F1C50D17C0EC6A000938F7 /* ARBrowseViewController.m */, + 342F9D63C266DC8B9A46A4F5 /* Emumerations */, + 342F949FC80594AFFBBE1017 /* Errors */, + 342F929D496BA3C99B4DB11A /* State Restoration */, + 342F9BBA54BBCF5FE29C8684 /* ARParallaxEffect.h */, + 342F9A7210CFDD1C15CFBE3C /* ARParallaxEffect.m */, + 342F955C47585DAE288DCC7F /* ARScrollNavigationChief.h */, + 342F98A73E2E1162514B5A24 /* ARScrollNavigationChief.m */, + 342F9737E2E62CEC8B68D181 /* ARFileUtils.h */, + 342F9FEFF4A1B56EE307467B /* ARFileUtils.m */, + 342F9B2379536051BE35B6D4 /* ARValueTransformer.h */, + 342F90BFE6268BEBC0E7BB2E /* ARValueTransformer.m */, + 342F94A8E1109ABC367CB751 /* ARStandardDateFormatter.h */, + 342F9F3E8A0C15DD0DB437D1 /* ARStandardDateFormatter.m */, + 342F96255EEC90B05A4FF213 /* ARFeedImageLoader.h */, + 342F966C989CF0BE97711FB6 /* ARFeedImageLoader.m */, + 342F9960EEF42063F7494D42 /* ARTrialController.h */, + 342F96EF94FE8A32662F8BF1 /* ARTrialController.m */, + 342F9B088114B9396356A2FC /* ARSplitStackView.h */, + 342F91131CCE3110E3E44E19 /* ARSplitStackView.m */, + 3CFB078918EB417F00792024 /* ARSecureTextFieldWithPlaceholder.h */, + 3CFB078A18EB417F00792024 /* ARSecureTextFieldWithPlaceholder.m */, + ); + name = Util; + path = Artsy/Classes/Utils; + sourceTree = SOURCE_ROOT; + }; + 342F929D496BA3C99B4DB11A /* State Restoration */ = { + isa = PBXGroup; + children = ( + 342F997FDCA4513495F1E949 /* UIViewController+ARStateRestoration.h */, + 342F992BB6F28B5BE6477E3F /* UIViewController+ARStateRestoration.m */, + ); + name = "State Restoration"; + sourceTree = ""; + }; + 342F93B0E900CB6BC98011DE /* Views */ = { + isa = PBXGroup; + children = ( + 342F995897B0B106E59A39A0 /* ARFairMapAnnotationView.h */, + 342F9559FD6EB9419E3CEE68 /* ARFairMapAnnotationView.m */, + 5EFF52B81976CA6C00E2A563 /* ARSearchFieldButton.h */, + 5EFF52B91976CA6C00E2A563 /* ARSearchFieldButton.m */, + 5EDB1209197E691100E241F0 /* ARParallaxHeaderViewController.h */, + 5EDB120A197E691100E241F0 /* ARParallaxHeaderViewController.m */, + ); + name = Views; + sourceTree = ""; + }; + 342F93BE04F58F4AD75A8FDD /* Mantle Extensions */ = { + isa = PBXGroup; + children = ( + 342F92D1AEEE18F719695548 /* MTLModel+JSON.m */, + 342F94F249E33303787DC61E /* MTLModel+JSON.h */, + 342F984DED67FEAABB90AFC3 /* MTLModel+Dictionary.h */, + 342F98950528061D8AE370EA /* MTLModel+Dictionary.m */, + ); + path = "Mantle Extensions"; + sourceTree = ""; + }; + 342F9453C931C592D4F8A2AA /* Fair */ = { + isa = PBXGroup; + children = ( + 342F9983D46712773FC71FC3 /* Fair.h */, + 342F9860A7DDF0C11DC56572 /* Fair.m */, + 3CCCC8A31899B6F9008015DD /* FairOrganizer.h */, + 3CCCC8A41899B6F9008015DD /* FairOrganizer.m */, + 342F976360A7DAA65AF7D755 /* MapPoint.m */, + 342F9D82EE3BBB74F56DBC4B /* MapPoint.h */, + 342F9C11DC281B1C4DC1FB8C /* Map.m */, + 342F934FA11C379917B02017 /* Map.h */, + 342F92FE487AC8FCA43F5C74 /* Follow.m */, + 342F9AC158B5157E968BE7BE /* Follow.h */, + ); + name = Fair; + sourceTree = ""; + }; + 342F949FC80594AFFBBE1017 /* Errors */ = { + isa = PBXGroup; + children = ( + 342F9D994FE5E37F23DB781A /* ARNetworkErrorManager.h */, + 342F97A9F6D9D371703C2B19 /* ARNetworkErrorManager.m */, + 342F9AF3B1FC9DB29D65F3D1 /* ActiveErrorView.xib */, + ); + name = Errors; + sourceTree = ""; + }; + 342F96EF894DC84D6564FA20 /* Collection */ = { + isa = PBXGroup; + children = ( + 342F93F91FB4ACCDC29EB901 /* ARGeneArtworksNetworkModelTests.m */, + ); + name = Collection; + sourceTree = ""; + }; + 342F99296B0CBCEF0CDA9ECA /* Network Models */ = { + isa = PBXGroup; + children = ( + E614469D195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.h */, + E614469E195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.m */, + E614469F195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.h */, + E61446A0195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.m */, + E61446A3195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.h */, + E61446A4195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.m */, + E61446A5195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel+Private.h */, + E61446A6195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.h */, + E61446A7195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.m */, + E61446A8195A1CDC00BFB7C3 /* ARFollowableNetworkModel.h */, + E61446A9195A1CDC00BFB7C3 /* ARFollowableNetworkModel.m */, + E61446AA195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.h */, + E61446AB195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.m */, + 60CEA77C19B61F8000CC3A91 /* ARArtistNetworkModel.h */, + 60CEA77D19B61F8000CC3A91 /* ARArtistNetworkModel.m */, + 5E0AEB7619B9EA43009F34DE /* ARShowNetworkModel.h */, + 5E0AEB7719B9EA43009F34DE /* ARShowNetworkModel.m */, + ); + path = "Network Models"; + sourceTree = ""; + }; + 342F9C2C3D87889B79459CED /* Apple */ = { + isa = PBXGroup; + children = ( + E619969919B9094F00DB273C /* Swizzles */, + 342F97E1E35CFFE9767F07CD /* NSDate+DateRange.h */, + 342F9175329F0389E644C194 /* NSDate+DateRange.m */, + 342F9DFFC4EBE280F4BDF6E8 /* NSDate+Util.h */, + 342F97C153D8524B132D0927 /* NSDate+Util.m */, + 342F9005B8C4E89F9DA89E36 /* NSString+StringCase.h */, + 342F903047176176865D20CA /* NSString+StringCase.m */, + 342F9685CB5FB321D7FC7A2B /* NSString+StringSize.h */, + 342F9DDBC48E0F833DBC76F4 /* NSString+StringSize.m */, + 342F9A069BB0267F7BF6F260 /* UIView+HitTestExpansion.h */, + 342F9F4385F10D19BC94E456 /* UIView+HitTestExpansion.m */, + 342F94AB8EA7914F64F117AD /* UIDevice-Hardware.h */, + 342F97B49335330C4FD569B4 /* UIDevice-Hardware.m */, + 342F9C6C79750887BDAC6499 /* UIFont+ArtsyFonts.h */, + 342F9929E6FBF1C0AF1BADD7 /* UIFont+ArtsyFonts.m */, + 342F96B30465B8B7722DBD1D /* UIImage+ImageFromColor.h */, + 342F987C1CACE95AAD292717 /* UIImage+ImageFromColor.m */, + 342F9036F798770D9E0CC66C /* UIImageView+AsyncImageLoading.h */, + 342F9DA91638895F6A6A432C /* UIImageView+AsyncImageLoading.m */, + 342F9018E314BCC99A929A6C /* UILabel+Typography.h */, + 342F9F6EDBFE4624755933EB /* UILabel+Typography.m */, + 5435192C18A8E9420060F31E /* UIView+OldSchoolSnapshots.h */, + 5435192D18A8E9420060F31E /* UIView+OldSchoolSnapshots.m */, + 342F91AA54E8417472868A61 /* UIViewController+FullScreenLoading.h */, + 342F924737155CB0C5A439F9 /* UIViewController+FullScreenLoading.m */, + 342F9DE76CE6DE0D374BC703 /* UIViewController+SimpleChildren.h */, + 342F9331632309600346BD90 /* UIViewController+SimpleChildren.m */, + E6A3500318AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.h */, + E6A3500418AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.m */, + 3CF144A218E3727F00B1A764 /* UIViewController+ScreenSize.h */, + 3CF144A318E3727F00B1A764 /* UIViewController+ScreenSize.m */, + ); + path = Apple; + sourceTree = ""; + }; + 342F9D35D55EDEA5EA203296 /* Styled Subclasses */ = { + isa = PBXGroup; + children = ( + E6B71BA11A129DA700BBD2E2 /* Buttons */, + 342F9F752C66B13A43FD55DC /* StyledSubclasses.h */, + 342F9C3F2A3A8DD09B29D803 /* ARTextView.h */, + 342F969D222D3AAD67B93FCE /* ARTextView.m */, + 342F969E68B7F84A152937EF /* ARCollapsableTextView.h */, + 342F927CDF62DAE64F3A1AD7 /* ARCollapsableTextView.m */, + ); + name = "Styled Subclasses"; + sourceTree = ""; + }; + 342F9D63C266DC8B9A46A4F5 /* Emumerations */ = { + isa = PBXGroup; + children = ( + 342F92B72CCFA5F1E35F0067 /* UIApplicationStateEnum.h */, + 342F9CDCE3E3D1674CA080AD /* UIApplicationStateEnum.m */, + ); + name = Emumerations; + sourceTree = ""; + }; + 342F9E0324CE65C4B8969EE4 /* Maps */ = { + isa = PBXGroup; + children = ( + 342F92647D726C0DC289B1F3 /* ARFairMapViewControllerTests.m */, + ); + name = Maps; + sourceTree = ""; + }; + 342F9FE0B50F629107FA5E5A /* Generics */ = { + isa = PBXGroup; + children = ( + 6099F90817904F9B0004EF04 /* ARModelCollectionViewModule.h */, + 6099F90917904F9B0004EF04 /* ARModelCollectionViewModule.m */, + 6016C18E178C2B7F008EC8E7 /* AREmbeddedModelsViewController.h */, + 6016C18F178C2B7F008EC8E7 /* AREmbeddedModelsViewController.m */, + 342F9C3811C6F12C261F04D4 /* ARNavigationButtonsViewController.m */, + 342F9E3AC3A1AD2AF62F353C /* ARNavigationButtonsViewController.h */, + 342F9D38472C06E2C065568E /* ARImagePageViewController.m */, + 342F91073D92A49978A29DA6 /* ARImagePageViewController.h */, + ); + name = Generics; + sourceTree = ""; + }; + 3C12144A18FD6AA9003CDC74 /* Sharing */ = { + isa = PBXGroup; + children = ( + E676B2F018D224000057B4E1 /* ARMessageItemProvider.h */, + E676B2F118D224000057B4E1 /* ARMessageItemProvider.m */, + E676B2E918D0CC330057B4E1 /* ARURLItemProvider.h */, + E676B2EA18D0CC330057B4E1 /* ARURLItemProvider.m */, + E676B2EC18D11A9B0057B4E1 /* ARImageItemProvider.h */, + E676B2ED18D11A9B0057B4E1 /* ARImageItemProvider.m */, + 342F954D3ABCF82AAC1640D9 /* ARSharingController.h */, + 342F9DC4018D17E7378DA607 /* ARSharingController.m */, + ); + name = Sharing; + sourceTree = ""; + }; + 3C205B5718900091004280E0 /* App Tests */ = { + isa = PBXGroup; + children = ( + 3C6AEE2C188F2D3E00DD98FC /* ARSwitchBoardTests.m */, + 3C205B58189000A5004280E0 /* ARAppNotificationsDelegateTests.m */, + 60327DD11987BA830075B399 /* ARDeveloperOptionsSpec.m */, + ); + name = "App Tests"; + sourceTree = ""; + }; + 3C22227018C647CC00B7CE3A /* Data */ = { + isa = PBXGroup; + children = ( + 3C22227518C64B4A00B7CE3A /* User_v1.data */, + 3C22227318C6487C00B7CE3A /* User_v0.data */, + E676A62418EF5F6E00B9AF2C /* Artwork_v0.data */, + E6F466E718EF52A800A09AFA /* Artwork_v1.data */, + ); + name = Data; + sourceTree = ""; + }; + 3C2E034A18DA7F640001B7E5 /* Image */ = { + isa = PBXGroup; + children = ( + 60388701189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.h */, + 60388702189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.m */, + ); + name = Image; + sourceTree = ""; + }; + 3C2E034B18DA7FD80001B7E5 /* Image */ = { + isa = PBXGroup; + children = ( + 342F9F73EE9B1DBFE4E88BA3 /* ARTiledImageDataSourceWithImageTests.m */, + ); + name = Image; + sourceTree = ""; + }; + 3C3FEA8B188433CB00E1A16F /* Extensions */ = { + isa = PBXGroup; + children = ( + 5E9A78211906BA3D00734E1B /* OCMArg+ClassChecker.h */, + 5E9A78221906BA3D00734E1B /* OCMArg+ClassChecker.m */, + 3C3FEA8C188433F200E1A16F /* ARUserManager+Stubs.h */, + 3C3FEA8D1884346D00E1A16F /* ARUserManager+Stubs.m */, + 3CA1E81E188465F0003C622D /* Sale+Extensions.h */, + 3CA1E81F188465F0003C622D /* Sale+Extensions.m */, + 3CA1E8211884663E003C622D /* Bid+Extensions.h */, + 3CA1E8221884663E003C622D /* Bid+Extensions.m */, + 3C6AA7C91885F38D00501F07 /* SaleArtwork+Extensions.h */, + 3C6AA7CA1885F38D00501F07 /* SaleArtwork+Extensions.m */, + 3C7ECF72189010CA004BC877 /* Extensions.h */, + 342F93309D1B60C53E2E0732 /* Artwork+Extensions.m */, + 342F95F94C5759B9CA05AB59 /* Artwork+Extensions.h */, + 3CE0DA3018A13604000E537A /* OHHTTPStubs+JSON.h */, + 3CE0DA3118A13604000E537A /* OHHTTPStubs+JSON.m */, + 3C6BDCBE18E0B3E40028EF5D /* MutableNSURLResponse.h */, + 3C6BDCBF18E0B3E40028EF5D /* MutableNSURLResponse.m */, + 3CA17D571901A4900010C9F5 /* SpectaDSL+Sleep.h */, + 3CA17D581901A4900010C9F5 /* SpectaDSL+Sleep.m */, + 608EE3D919954CEB001F4FE0 /* UIViewController+PresentWithFrame.h */, + 608EE3DA19954CEB001F4FE0 /* UIViewController+PresentWithFrame.m */, + E61E772419A5477300C55E14 /* ARExpectaExtensions.h */, + E61E772519A5477300C55E14 /* ARExpectaExtensions.m */, + ); + name = Extensions; + sourceTree = ""; + }; + 3C4AE96C19094D8F009C0E8B /* Map Annotations */ = { + isa = PBXGroup; + children = ( + 3C4AE9AB1909C3A5009C0E8B /* MapAnnotationCallout_Arrow@2x.png */, + 3C4AE9A519098916009C0E8B /* MapAnnotationCallout_Anchor@2x.png */, + 3C4AE96D19094DAA009C0E8B /* MapAnnotation_Artsy@2x.png */, + 3C4AE96E19094DAA009C0E8B /* MapAnnotation_CoatCheck@2x.png */, + 3C4AE96F19094DAA009C0E8B /* MapAnnotation_Drink@2x.png */, + 3C4AE97019094DAA009C0E8B /* MapAnnotation_Food@2x.png */, + 3C4AE97119094DAA009C0E8B /* MapAnnotation_GenericEvent@2x.png */, + 3C4AE97219094DAA009C0E8B /* MapAnnotation_Highlighted@2x.png */, + 3C4AE97319094DAA009C0E8B /* MapAnnotation_Info@2x.png */, + 3C4AE97419094DAA009C0E8B /* MapAnnotation_Installation@2x.png */, + 3C4AE97519094DAA009C0E8B /* MapAnnotation_Lounge@2x.png */, + 3C4AE97619094DAA009C0E8B /* MapAnnotation_Restroom@2x.png */, + 3C4AE97719094DAA009C0E8B /* MapAnnotation_Saved@2x.png */, + 3C4AE97819094DAA009C0E8B /* MapAnnotation_Search@2x.png */, + 3C4AE97919094DAA009C0E8B /* MapAnnotation_Transport@2x.png */, + 3C4AE97A19094DAA009C0E8B /* MapAnnotation_Default@2x.png */, + 3C4AE97B19094DAA009C0E8B /* MapAnnotation_VIP@2x.png */, + 3CAC639E190AA87A00B17325 /* MapAnnotationCallout_Partner@2x.png */, + ); + name = "Map Annotations"; + sourceTree = ""; + }; + 3C4AE98B19094F74009C0E8B /* ViewInRoom */ = { + isa = PBXGroup; + children = ( + 3C4AE98C19094F96009C0E8B /* ViewInRoom_Base@2x.png */, + 3C4AE98D19094F96009C0E8B /* ViewInRoom_BaseNoBench@2x.png */, + 3C4AE98E19094F96009C0E8B /* ViewInRoom_Bench@2x.png */, + 3C4AE98F19094F96009C0E8B /* ViewInRoom_Man_3@2x.png */, + 3C4AE99019094F96009C0E8B /* ViewInRoom_Wall_Right@2x.png */, + 3C4AE99119094F96009C0E8B /* ViewInRoom_Wall@2x.png */, + ); + name = ViewInRoom; + sourceTree = ""; + }; + 3C4C7D30E851A66B477B4498 /* Pods */ = { + isa = PBXGroup; + children = ( + A060C6085108B7738F5ADD02 /* Pods.debug.xcconfig */, + 7B33890F5B4F8EFF536074A4 /* Pods.beta.xcconfig */, + 885DE1DBF1C1DB636BA9F61A /* Pods.store.xcconfig */, + E2C583DC5F5EBC24D3CC3B76 /* Pods.demo.xcconfig */, + 8A6745B7A19EEDA119AEA48E /* Pods-Artsy Tests.debug.xcconfig */, + 1A25A9493C56A91CDFB95D0B /* Pods-Artsy Tests.beta.xcconfig */, + 8E3DD2A89F196F5E5BFB9689 /* Pods-Artsy Tests.store.xcconfig */, + 3E944480103FA2476C7049C2 /* Pods-Artsy Tests.demo.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 3C5C7E3D18970E73003823BB /* Util Tests */ = { + isa = PBXGroup; + children = ( + E676B2F318D2307D0057B4E1 /* Sharing Tests */, + 54289FED18AA7F4E00681E49 /* UINavigationController_InnermostTopViewControllerSpec.m */, + 3C5C7E3E18970E78003823BB /* Enumerations */, + ); + name = "Util Tests"; + sourceTree = ""; + }; + 3C5C7E3E18970E78003823BB /* Enumerations */ = { + isa = PBXGroup; + children = ( + 3C5C7E3F18970E8B003823BB /* UIApplicationStateEnumTests.m */, + ); + name = Enumerations; + sourceTree = ""; + }; + 3C6BDCC118E0B8C80028EF5D /* Contact */ = { + isa = PBXGroup; + children = ( + 3C6BDCC218E0B8D50028EF5D /* ARInquireForArtworkViewControllerTests.m */, + ); + name = Contact; + sourceTree = ""; + }; + 3C6CB60618ABD8C1008DFE3B /* Post */ = { + isa = PBXGroup; + children = ( + 3C6CB60718ABD8D2008DFE3B /* ARPostsViewController.h */, + 3C6CB60818ABD8D2008DFE3B /* ARPostsViewController.m */, + ); + name = Post; + sourceTree = ""; + }; + 3C7880B918B9080B00595E30 /* Notifications */ = { + isa = PBXGroup; + children = ( + 3C7880BA18B9081C00595E30 /* ARNotificationView.h */, + 3C7880BB18B9081C00595E30 /* ARNotificationView.m */, + ); + name = Notifications; + sourceTree = ""; + }; + 3C9F215C18B25CFA00D8898B /* Search */ = { + isa = PBXGroup; + children = ( + 3C9F215D18B25D0D00D8898B /* ARSearchViewController.h */, + 5EFF52BB1976D57C00E2A563 /* ARSearchViewController+Private.h */, + 3C9F215E18B25D0D00D8898B /* ARSearchViewController.m */, + 3C205ACB1908041700B3C2B4 /* ARSearchResultsDataSource.h */, + 3C205ACC1908041700B3C2B4 /* ARSearchResultsDataSource.m */, + ); + name = Search; + sourceTree = ""; + }; + 3CA0A17B18EF632800C361E5 /* Artist */ = { + isa = PBXGroup; + children = ( + 3CA0A17C18EF633900C361E5 /* ARArtistViewControllerTests.m */, + 60CEA77F19B6254800CC3A91 /* ARStubbedArtistNetworkModel.h */, + 60CEA78019B6254800CC3A91 /* ARStubbedArtistNetworkModel.m */, + 5E0AEB7B19B9FAED009F34DE /* ARStubbedShowNetworkModel.h */, + 5E0AEB7C19B9FAED009F34DE /* ARStubbedShowNetworkModel.m */, + ); + name = Artist; + sourceTree = ""; + }; + 3CA37E781910070600B06E81 /* Favorites */ = { + isa = PBXGroup; + children = ( + 3CA37E791910072C00B06E81 /* ARHeartStatus.h */, + ); + name = Favorites; + sourceTree = ""; + }; + 3CA4B73018B418F500256B33 /* Utils */ = { + isa = PBXGroup; + children = ( + ); + name = Utils; + sourceTree = ""; + }; + 3CAA413018D8CC21000EE867 /* Networking Models */ = { + isa = PBXGroup; + children = ( + 3CAA413118D8CC32000EE867 /* ARFairFavoritesNetworkModelTests.m */, + E655E4B7194B4BEF00F2B7DA /* ARArtworkFavoritesNetworkModelTests.m */, + 5E0AEB7919B9EF57009F34DE /* ARShowNetworkModelTests.m */, + ); + name = "Networking Models"; + sourceTree = ""; + }; + 3CACF2AA18F591D40054091E /* Theming Tests */ = { + isa = PBXGroup; + children = ( + 3CACF2AB18F591E40054091E /* ARThemeTests.m */, + ); + name = "Theming Tests"; + sourceTree = ""; + }; + 3CAED1791880268E00840608 /* Networking Tests */ = { + isa = PBXGroup; + children = ( + 3CAA413018D8CC21000EE867 /* Networking Models */, + 3CAED17A1880269E00840608 /* API Modules */, + 342F96EF894DC84D6564FA20 /* Collection */, + 3CF144B518E4802400B1A764 /* ARSystemTimeTests.m */, + 3C2E6C5B192262A3009DAB28 /* ARRouterTests.m */, + ); + name = "Networking Tests"; + sourceTree = ""; + }; + 3CAED17A1880269E00840608 /* API Modules */ = { + isa = PBXGroup; + children = ( + E6AD6D4118E610DA005C8A3A /* ArtsyAPI+ArtworksTests.m */, + 3CAED17B188026AC00840608 /* ARUserManagerTests.m */, + 3C6BDCBC18E0AEF60028EF5D /* ArtsyAPI+PrivateTests.m */, + 3CF144AA18E460BE00B1A764 /* ArtsyAPI+SystemTimeTests.m */, + 3CB37D981922483100089A1D /* ArtsyAPI+ErrorHandlers.m */, + ); + name = "API Modules"; + sourceTree = ""; + }; + 3CB97D8D18870979008C44FE /* View Controller Tests */ = { + isa = PBXGroup; + children = ( + E61E773A19A7E8DB00C55E14 /* Gene */, + E6BC2C3A197EDBA80063ED3C /* Browse */, + 603218C3195C92C2004E7A24 /* App Menu */, + E6718311194A3D31001A5566 /* Hero Unit */, + 3CA0A17B18EF632800C361E5 /* Artist */, + 3CD0BB8718EB0CCB00A59910 /* Favorites */, + 3C6BDCC118E0B8C80028EF5D /* Contact */, + 3C2E034B18DA7FD80001B7E5 /* Image */, + 3CE75A0918B6366F00885355 /* Util */, + 3CCCC8921899673C008015DD /* Embedded */, + 3CCCC8911899672E008015DD /* Web Browsing */, + 3CCCC890189966E3008015DD /* Login and Onboarding */, + 3CCCC88F189966CB008015DD /* Artwork */, + 3CCCC88E189966C0008015DD /* Fair */, + ); + name = "View Controller Tests"; + sourceTree = ""; + }; + 3CCCC88E189966C0008015DD /* Fair */ = { + isa = PBXGroup; + children = ( + 5EBDC94F19794D6C0082C514 /* Views */, + 3CA4B73018B418F500256B33 /* Utils */, + 342F9FD434A78C36B76A85B6 /* ARFairViewControllerTests.m */, + 5E9A781F19068EDF00734E1B /* ARProfileViewControllerTests.m */, + 3CCCC8931899676E008015DD /* ARFairPostsViewControllerTests.m */, + 342F9E0324CE65C4B8969EE4 /* Maps */, + 3C33299118AD9399006D28C0 /* ARFairSearchViewControllerTests.m */, + 3C1266F018BE6CC700B5AE72 /* ARFairGuideViewControllerTests.m */, + 3CBB03A7192BA94C00689F89 /* ARFairArtistViewControllerTests.m */, + 5E71AFC6195C64C1000F6325 /* ARFairGuideContainerViewControllerTests.m */, + 3C7294CB196C3E660073663D /* ARFairShowViewControllerTests.m */, + 5EBDC95219794DF10082C514 /* ARPendingOperationViewControllerTests.m */, + 5EB33E73197EBDE200706EB1 /* ARParallaxHeaderViewControllerTests.m */, + 5EB33E75197EBFEB00706EB1 /* wide.jpg */, + 5EB33E76197EBFEB00706EB1 /* square.png */, + ); + name = Fair; + sourceTree = ""; + }; + 3CCCC88F189966CB008015DD /* Artwork */ = { + isa = PBXGroup; + children = ( + E6B958F0188DB6F800D75C86 /* ARArtworkViewControllerTests.m */, + 60F8FFC0197E773E00DC3869 /* ARArtworkSetViewControllerSpec.m */, + ); + name = Artwork; + sourceTree = ""; + }; + 3CCCC890189966E3008015DD /* Login and Onboarding */ = { + isa = PBXGroup; + children = ( + E6C13AC01A23AA420050AB53 /* AROnboardingViewControllerTests.m */, + 3CB97D8E1887099E008C44FE /* ARLoginViewControllerTests.m */, + 3CF144B718E9E00400B1A764 /* ARSignUpSplashViewControllerTests.m */, + E6F84D011A0AB40500BF99A3 /* ARSignUpActiveUserViewControllerTests.m */, + ); + name = "Login and Onboarding"; + sourceTree = ""; + }; + 3CCCC8911899672E008015DD /* Web Browsing */ = { + isa = PBXGroup; + children = ( + 3C6AEE26188F228600DD98FC /* ARInternalMobileWebViewControllerTests.m */, + ); + name = "Web Browsing"; + sourceTree = ""; + }; + 3CCCC8921899673C008015DD /* Embedded */ = { + isa = PBXGroup; + children = ( + 342F965F76AA25C6C700FE1C /* ARNavigationButtonsViewControllerTests.m */, + ); + name = Embedded; + sourceTree = ""; + }; + 3CCCC89818996DC5008015DD /* Posts */ = { + isa = PBXGroup; + children = ( + 3CCCC89918996DD4008015DD /* Post.h */, + 3CCCC89A18996DD4008015DD /* Post.m */, + ); + name = Posts; + sourceTree = ""; + }; + 3CD0BB8718EB0CCB00A59910 /* Favorites */ = { + isa = PBXGroup; + children = ( + 3CD0BB8818EB0CDF00A59910 /* ARFavoritesViewControllerTests.m */, + ); + name = Favorites; + sourceTree = ""; + }; + 3CD36796189A9AD600285DF7 /* Post */ = { + isa = PBXGroup; + children = ( + 3CD36797189A9B7A00285DF7 /* ARPostFeedItemLinkView.h */, + 3CD36798189A9B7A00285DF7 /* ARPostFeedItemLinkView.m */, + ); + name = Post; + sourceTree = ""; + }; + 3CD3679D189AC4FF00285DF7 /* Images */ = { + isa = PBXGroup; + children = ( + 3CD3679A189AC4F000285DF7 /* ARAspectRatioImageView.h */, + 3CD3679B189AC4F000285DF7 /* ARAspectRatioImageView.m */, + ); + name = Images; + sourceTree = ""; + }; + 3CE0DA2C18A12326000E537A /* Media */ = { + isa = PBXGroup; + children = ( + 3CE0DA2D18A12335000E537A /* Video.h */, + 3CE0DA2E18A12335000E537A /* Video.m */, + ); + name = Media; + sourceTree = ""; + }; + 3CE75A0918B6366F00885355 /* Util */ = { + isa = PBXGroup; + children = ( + 3CE75A0A18B6367F00885355 /* ARValueTransformerTests.m */, + 3CFBE33518C5848900C781D0 /* ARFileUtilsTests.m */, + 5EBDC94D19792C3A0082C514 /* ARNavigationControllerTests.m */, + ); + name = Util; + sourceTree = ""; + }; + 3CE7F34018A99E4B002BA993 /* Feed Timeline Tests */ = { + isa = PBXGroup; + children = ( + 3CE7F34118A99E62002BA993 /* ARHeroUnitsNetworkModelTests.m */, + ); + name = "Feed Timeline Tests"; + sourceTree = ""; + }; + 4953E8341668022E00A09726 /* Protocols */ = { + isa = PBXGroup; + children = ( + 4953E8351668026000A09726 /* ARPostAttachment.h */, + 29773A6F17B9749800FC89B3 /* ARHasImageBaseURL.h */, + 3CCE08A618A3C0A000AE4CC3 /* ProfileOwner.h */, + ); + name = Protocols; + sourceTree = ""; + }; + 499A5805165AE24C004B0E2F /* Table View Cells */ = { + isa = PBXGroup; + children = ( + 499A5806165AE24C004B0E2F /* Feed Items */, + 60903CCC175CF074002AB800 /* AdminTableView */, + 342F919D0897DFE221EC5E73 /* ARFeedStatusIndicatorTableViewCell.m */, + 342F95C4453C7D80AC5A9154 /* ARFeedStatusIndicatorTableViewCell.h */, + 608B709717D4F6520088A56C /* ARSearchTableViewCell.h */, + 608B709817D4F6520088A56C /* ARSearchTableViewCell.m */, + 60B617D11815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.h */, + 60B617D21815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.m */, + E693BF7818A1941D00D464BC /* ARSwitchCell.h */, + E693BF7918A1941D00D464BC /* ARSwitchCell.m */, + E693BF7A18A1941D00D464BC /* ARTextInputCell.h */, + E693BF7B18A1941D00D464BC /* ARTextInputCell.m */, + ); + path = "Table View Cells"; + sourceTree = ""; + }; + 499A5806165AE24C004B0E2F /* Feed Items */ = { + isa = PBXGroup; + children = ( + 6099F8FA178DD7F90004EF04 /* Modern */, + ); + name = "Feed Items"; + path = "iPhone Feed Items"; + sourceTree = ""; + }; + 49A77222165ADB9300BC6FD3 /* Feed Items */ = { + isa = PBXGroup; + children = ( + 60567B2A167F8F9E009D205A /* ARFeedItems.h */, + 49A77223165ADB9300BC6FD3 /* ARFeedItem.h */, + 49A77224165ADB9300BC6FD3 /* ARFeedItem.m */, + 49A77225165ADB9300BC6FD3 /* ARFollowFairFeedItem.h */, + 49A77226165ADB9300BC6FD3 /* ARFollowFairFeedItem.m */, + 499A580A165AEC39004B0E2F /* ARFollowArtistFeedItem.h */, + 499A580B165AEC39004B0E2F /* ARFollowArtistFeedItem.m */, + 49A77227165ADB9300BC6FD3 /* ARPartnerShowFeedItem.h */, + 49A77228165ADB9300BC6FD3 /* ARPartnerShowFeedItem.m */, + 49A77229165ADB9300BC6FD3 /* ARPublishedArtworkSetFeedItem.h */, + 49A7722A165ADB9300BC6FD3 /* ARPublishedArtworkSetFeedItem.m */, + 49A7722B165ADB9300BC6FD3 /* ARRepostFeedItem.h */, + 49A7722C165ADB9300BC6FD3 /* ARRepostFeedItem.m */, + 49A7722D165ADB9300BC6FD3 /* ARSavedArtworkSetFeedItem.h */, + 49A7722E165ADB9300BC6FD3 /* ARSavedArtworkSetFeedItem.m */, + 499A58A316669CD4004B0E2F /* ARPostFeedItem.h */, + 499A58A416669CD5004B0E2F /* ARPostFeedItem.m */, + ); + path = "Feed Items"; + sourceTree = ""; + }; + 49A7723E165ADBF700BC6FD3 /* App */ = { + isa = PBXGroup; + children = ( + 60903CC2175CE21A002AB800 /* AROptions.h */, + 60903CC3175CE21A002AB800 /* AROptions.m */, + 608B2F5D1657D0500046956C /* main.m */, + 49BA7E111655ABE600C06572 /* Artsy-Prefix.pch */, + 49BA7E121655ABE600C06572 /* ARAppDelegate.h */, + 49BA7E131655ABE600C06572 /* ARAppDelegate.m */, + 5EFE2BE21910FC81003B5EEA /* ARAppDelegate+Analytics.h */, + 5EFE2BE31910FC81003B5EEA /* ARAppDelegate+Analytics.m */, + 6037442316D4227500AE7788 /* ARSwitchBoard.h */, + 6037442416D4227500AE7788 /* ARSwitchBoard.m */, + 6088B75217D1DBAC00E4BB67 /* ARAnalyticsConstants.h */, + 6088B75317D1DBAC00E4BB67 /* ARAnalyticsConstants.m */, + 60431FB618042A1E000118D7 /* ARAppNotificationsDelegate.h */, + 60431FB718042A1E000118D7 /* ARAppNotificationsDelegate.m */, + 60431FB918042E63000118D7 /* ARAppBackgroundFetchDelegate.h */, + 60431FBA18042E63000118D7 /* ARAppBackgroundFetchDelegate.m */, + ); + name = App; + sourceTree = ""; + }; + 49B32E11166D0AF9008BAB02 /* Collection View Cells */ = { + isa = PBXGroup; + children = ( + 608920C1178C682A00989A10 /* ARItemThumbnailViewCell.h */, + 608920C2178C682A00989A10 /* ARItemThumbnailViewCell.m */, + 6021F97D17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.h */, + 6021F97E17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.m */, + 60F1C51317C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.h */, + 60F1C51417C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.m */, + 607D756C17C3873700CA1D41 /* ARFavoriteItemViewCell.h */, + 607D756D17C3873700CA1D41 /* ARFavoriteItemViewCell.m */, + 60A40E8F18A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.h */, + 60A40E9018A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.m */, + ); + name = "Collection View Cells"; + sourceTree = ""; + }; + 49BA7DF41655ABE600C06572 = { + isa = PBXGroup; + children = ( + 60FFE3D5188E916C0012B485 /* Documentation */, + 49BA7E091655ABE600C06572 /* Artsy */, + B3ECE82B1819D1E6009F5C5B /* Artsy Tests */, + 49BA7E021655ABE600C06572 /* Frameworks */, + 49BA7E001655ABE600C06572 /* Products */, + 3C4C7D30E851A66B477B4498 /* Pods */, + ); + sourceTree = ""; + }; + 49BA7E001655ABE600C06572 /* Products */ = { + isa = PBXGroup; + children = ( + 49BA7DFF1655ABE600C06572 /* Artsy.app */, + B3ECE8261819D1E6009F5C5B /* Artsy Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 49BA7E021655ABE600C06572 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 60935A451A69CFEE00129CE1 /* WebKit.framework */, + 60892CEC16B9A849004C1B47 /* Accelerate.framework */, + 494F16BC17BEDA8E00204F1D /* Accounts.framework */, + 49473F2C17BEDE1F004BF082 /* AdSupport.framework */, + 499C8CF41694D2D100039D32 /* AssetsLibrary.framework */, + 499C8CEB1694D28700039D32 /* AVFoundation.framework */, + 602DCDAD16C6B74D009EB10D /* CFNetwork.framework */, + 602DCDB116C6B75D009EB10D /* CoreData.framework */, + 602DCDAB16C6B6C1009EB10D /* CoreFoundation.framework */, + 49BA7E071655ABE600C06572 /* CoreGraphics.framework */, + 4981FB46169F6A16004D9CDD /* CoreImage.framework */, + 499C8CEE1694D28C00039D32 /* CoreMedia.framework */, + 4943331A166947A3005AB483 /* CoreText.framework */, + 499C8CF01694D29100039D32 /* CoreVideo.framework */, + 607861A1165E47470010FAA2 /* EXTConcreteProtocol.h */, + 607861A2165E47470010FAA2 /* EXTConcreteProtocol.m */, + 49BA7E051655ABE600C06572 /* Foundation.framework */, + 49473F2E17BEDFFD004BF082 /* iAd.framework */, + 60892CEE16B9A871004C1B47 /* ImageIO.framework */, + 60BC390718182227003F34E7 /* JavaScriptCore.framework */, + 602DCDB316C6B7A5009EB10D /* libicucore.A.dylib */, + 06E44EB8170235D8001B2EBF /* MessageUI.framework */, + 601C3183165838590013E061 /* MobileCoreServices.framework */, + 499C8CF21694D29900039D32 /* OpenGLES.framework */, + 6025B4481655FBA900845975 /* QuartzCore.framework */, + 602DCDAF16C6B752009EB10D /* Security.framework */, + 6025B42E1655FACA00845975 /* SenTestingKit.framework */, + 49473F2A17BEDDA4004BF082 /* Social.framework */, + 601C3185165838630013E061 /* SystemConfiguration.framework */, + 49BA7E031655ABE600C06572 /* UIKit.framework */, + B3ECE8271819D1E6009F5C5B /* XCTest.framework */, + 7AABCA8DF484E65EF3CC11FA /* libPods.a */, + E8D6BDCA48472E439B9D79CB /* libPods-Artsy Tests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 49BA7E091655ABE600C06572 /* Artsy */ = { + isa = PBXGroup; + children = ( + 49A7723E165ADBF700BC6FD3 /* App */, + 608B2F5B1657CF600046956C /* App Resources */, + 608B2F7E1657D3B40046956C /* Categories */, + 608B2F5F1657D11F0046956C /* Constants */, + 608B2F6B1657D1A00046956C /* Models */, + 60838EB2177351DD00869F6E /* Navigation Transitions */, + 60F82FC21657FBEE0076AEAE /* Networking */, + 60438F801782E9A300C1B63B /* Protocols */, + 608B2F751657D3220046956C /* Resources */, + 49BA7E0A1655ABE600C06572 /* Supporting Files */, + 6099F8FB178DE91F0004EF04 /* Theming */, + 60327DCA1987AD140075B399 /* Tooling */, + 608B2F5A1657CED10046956C /* View Controllers */, + 608B2F821657D4780046956C /* Views */, + ); + name = Artsy; + path = Artsy/Classes; + sourceTree = ""; + }; + 49BA7E0A1655ABE600C06572 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + path = ../Resources; + sourceTree = ""; + }; + 49F0C67C17B972C900721244 /* Onboarding stages */ = { + isa = PBXGroup; + children = ( + 6088B75517D1E05200E4BB67 /* Views */, + 60059B3917CEA46700BE132D /* 5- Onboarding Personalization */, + 60059B1F17CE6DBE00BE132D /* 4- Onboarding New User */, + 60059B1D17CE6D8600BE132D /* 3 - Sign Up / Log In */, + 60059B1C17CE6D3F00BE132D /* 2 - Calls to Action */, + 60059B1B17CE6D3400BE132D /* 1 - Slideshow */, + ); + name = "Onboarding stages"; + sourceTree = ""; + }; + 49F45191176B9B2F0041A4B4 /* Artwork */ = { + isa = PBXGroup; + children = ( + 603D7C021958849C00ACA840 /* Auctions */, + E616B2461911664900D1CBC6 /* ARArtworkView.h */, + E616B2471911664900D1CBC6 /* ARArtworkView.m */, + CB821E7718217AF500CC934E /* ARAuctionBannerView.h */, + CB821E7818217AF500CC934E /* ARAuctionBannerView.m */, + 6089243117F617140023D1AC /* ARArtworkMetadataView.h */, + 6089243217F617140023D1AC /* ARArtworkMetadataView.m */, + 607E2E8217C8CF6B00396120 /* ARArtworkPreviewActionsView.h */, + 607E2E8317C8CF6B00396120 /* ARArtworkPreviewActionsView.m */, + 607E2E7917C8C9FE00396120 /* ARArtworkDetailView.h */, + 607E2E7A17C8C9FE00396120 /* ARArtworkDetailView.m */, + 607E2E8517C8E87E00396120 /* ARArtworkPreviewImageView.h */, + 607E2E8617C8E87E00396120 /* ARArtworkPreviewImageView.m */, + 60B7604417C9FBEA00073A14 /* ARArtworkActionsView.h */, + 60B7604517C9FBEA00073A14 /* ARArtworkActionsView.m */, + 6001414617CA33C100612DB4 /* ARArtworkRelatedArtworksView.h */, + 6001414717CA33C100612DB4 /* ARArtworkRelatedArtworksView.m */, + 603B559F17D64A1B00566935 /* ARArtworkPriceView.h */, + 603B55A017D64A1B00566935 /* ARArtworkPriceView.m */, + B3DDE9C218313CFF0012819F /* ARArtworkPriceRowView.h */, + B3DDE9C318313CFF0012819F /* ARArtworkPriceRowView.m */, + 601F029317F336AB00EB3E83 /* ARArtworkBlurbView.h */, + 601F029417F336AB00EB3E83 /* ARArtworkBlurbView.m */, + ); + name = Artwork; + sourceTree = ""; + }; + 5EBDC94F19794D6C0082C514 /* Views */ = { + isa = PBXGroup; + children = ( + 5EBDC95019794D840082C514 /* ARSearchFieldButtonTests.m */, + ); + name = Views; + sourceTree = ""; + }; + 5ECFA8EB1907FC4D000B92EA /* Fair */ = { + isa = PBXGroup; + children = ( + 3C4AE9A219096CED009C0E8B /* ARFairMapAnnotationCallOutView.h */, + 3C4AE9A319096CED009C0E8B /* ARFairMapAnnotationCallOutView.m */, + ); + name = Fair; + sourceTree = ""; + }; + 600249F0166CFCCE007E6E9A /* Core */ = { + isa = PBXGroup; + children = ( + CB2C960217D3B4B500B36B44 /* ARFeedLinkUnitViewController.h */, + CB2C960317D3B4B500B36B44 /* ARFeedLinkUnitViewController.m */, + 064330E3170F526200FF6C41 /* ARArtistViewController.h */, + 064330E4170F526200FF6C41 /* ARArtistViewController.m */, + 6036B5601760DC9100F1DD01 /* ARHeroUnitViewController.h */, + 6036B5611760DC9100F1DD01 /* ARHeroUnitViewController.m */, + 60289227176BE98C00512977 /* ARViewInRoomViewController.h */, + 60289228176BE98C00512977 /* ARViewInRoomViewController.m */, + 60CF980617BF79CA005ED59B /* ARArtistBiographyViewController.h */, + 60CF980717BF79CA005ED59B /* ARArtistBiographyViewController.m */, + ); + name = Core; + sourceTree = ""; + }; + 600249F1166CFCF5007E6E9A /* Feed VCs */ = { + isa = PBXGroup; + children = ( + CBB25B3A17F36DDE00C31446 /* ARFeaturedArtworksViewController.h */, + CBB25B3B17F36DDE00C31446 /* ARFeaturedArtworksViewController.m */, + 49A7723F165ADC3C00BC6FD3 /* ARFeedViewController.h */, + 49A77240165ADC3C00BC6FD3 /* ARFeedViewController.m */, + 6044E543176E165600075B15 /* ARShowFeedViewController.h */, + 6044E544176E165600075B15 /* ARShowFeedViewController.m */, + 3CF0774418DC6585009E18E4 /* ARKonamiKeyboardView.h */, + 3CF0774518DC6585009E18E4 /* ARKonamiKeyboardView.m */, + ); + name = "Feed VCs"; + sourceTree = ""; + }; + 600249F2166CFD08007E6E9A /* Login and Onboarding */ = { + isa = PBXGroup; + children = ( + 60A224FD17CE0E1B00233CA1 /* AROnboardingViewControllers.h */, + 49F0C67917B9706000721244 /* AROnboardingViewController.h */, + 49F0C67A17B9706000721244 /* AROnboardingViewController.m */, + 60A224FA17CE040B00233CA1 /* AROnboardingTransition.h */, + 60A224FB17CE040B00233CA1 /* AROnboardingTransition.m */, + 605B11B017CFD78400334196 /* AROnboardingWebViewController.h */, + 605B11B117CFD78400334196 /* AROnboardingWebViewController.m */, + E6C13AC21A23AB160050AB53 /* ARPersonalizeWebViewController.h */, + E6C13AC31A23AB160050AB53 /* ARPersonalizeWebViewController.m */, + 49F0C67C17B972C900721244 /* Onboarding stages */, + ); + name = "Login and Onboarding"; + sourceTree = ""; + }; + 600249F3166CFD13007E6E9A /* Menu */ = { + isa = PBXGroup; + children = ( + E6D2E911187E07530089728B /* Settings */, + ); + name = Menu; + sourceTree = ""; + }; + 60059B1B17CE6D3400BE132D /* 1 - Slideshow */ = { + isa = PBXGroup; + children = ( + 49473F3117C18772004BF082 /* ARSlideshowViewController.h */, + 49473F3217C18772004BF082 /* ARSlideshowViewController.m */, + 49F0C67E17B972F200721244 /* ARSlideshowView.m */, + 49F0C67D17B972F200721244 /* ARSlideshowView.h */, + ); + name = "1 - Slideshow"; + sourceTree = ""; + }; + 60059B1C17CE6D3F00BE132D /* 2 - Calls to Action */ = { + isa = PBXGroup; + children = ( + 60059B1E17CE6DB300BE132D /* Views */, + 60215FD417CDF9FA000F3A62 /* ARSignUpActiveUserViewController.h */, + 60215FD517CDF9FA000F3A62 /* ARSignUpActiveUserViewController.m */, + 60215FD617CDF9FA000F3A62 /* ARSignUpActiveUserViewController.xib */, + 49405AB417BEC87A004F86D8 /* ARSignupViewController.h */, + 49405AB517BEC87A004F86D8 /* ARSignupViewController.m */, + 4938742617BD512700724795 /* ARSignUpSplashViewController.h */, + 4938742717BD512700724795 /* ARSignUpSplashViewController.m */, + ); + name = "2 - Calls to Action"; + sourceTree = ""; + }; + 60059B1D17CE6D8600BE132D /* 3 - Sign Up / Log In */ = { + isa = PBXGroup; + children = ( + CB8D9D4217CEA7B900F3286B /* AROnboardingMoreInfoViewController.h */, + CB8D9D4317CEA7B900F3286B /* AROnboardingMoreInfoViewController.m */, + 49473F3417C1907F004BF082 /* ARCreateAccountViewController.h */, + 49473F3517C1907F004BF082 /* ARCreateAccountViewController.m */, + 60DE2DDE1677B26A00621540 /* ARLoginViewController.h */, + 608B2F711657D2DF0046956C /* ARLoginViewController.m */, + ); + name = "3 - Sign Up / Log In"; + sourceTree = ""; + }; + 60059B1E17CE6DB300BE132D /* Views */ = { + isa = PBXGroup; + children = ( + 605B11B317CFE7CC00334196 /* ARTermsAndConditionsView.h */, + 605B11B417CFE7CC00334196 /* ARTermsAndConditionsView.m */, + 4938743417BDB2CD00724795 /* ARCrossfadingImageView.h */, + 4938743517BDB2CD00724795 /* ARCrossfadingImageView.m */, + ); + name = Views; + sourceTree = ""; + }; + 60059B1F17CE6DBE00BE132D /* 4- Onboarding New User */ = { + isa = PBXGroup; + children = ( + CB260E7F17C2C90900BF2012 /* ARCollectorStatusViewController.h */, + CB260E8017C2C90900BF2012 /* ARCollectorStatusViewController.m */, + CB61F12F17C2D83F003DB8A9 /* AROnboardingTableViewCell.h */, + CB61F13017C2D83F003DB8A9 /* AROnboardingTableViewCell.m */, + CB206F6F17C3FA8F00A4FDC4 /* ARPriceRangeViewController.h */, + CB206F7017C3FA8F00A4FDC4 /* ARPriceRangeViewController.m */, + CB11525917C815210093D864 /* AROnboardingFollowableTableViewCell.h */, + CB11525A17C815210093D864 /* AROnboardingFollowableTableViewCell.m */, + ); + name = "4- Onboarding New User"; + sourceTree = ""; + }; + 60059B2017CE9CEE00BE132D /* Onboarding */ = { + isa = PBXGroup; + children = ( + E6FFEA9F19C7925600A0D7DE /* onboard_1@2x~ipad.jpg */, + E6FFEAA019C7925600A0D7DE /* onboard_1@2x~iphone.jpg */, + E6FFEAA119C7925600A0D7DE /* onboard_2@2x~ipad.jpg */, + E6FFEAA219C7925600A0D7DE /* onboard_2@2x~iphone.jpg */, + E6FFEAA319C7925600A0D7DE /* onboard_3@2x~ipad.jpg */, + E6FFEAA419C7925600A0D7DE /* onboard_3@2x~iphone.jpg */, + E6FFEAA519C7925600A0D7DE /* splash_1@2x~ipad.jpg */, + E6FFEAA619C7925600A0D7DE /* splash_1@2x~iphone.jpg */, + E6FFEAA719C7925600A0D7DE /* splash_2@2x~ipad.jpg */, + E6FFEAA819C7925600A0D7DE /* splash_2@2x~iphone.jpg */, + E6FFEAA919C7925600A0D7DE /* splash_3@2x~ipad.jpg */, + E6FFEAAA19C7925600A0D7DE /* splash_3@2x~iphone.jpg */, + E6FFEAAB19C7925600A0D7DE /* splash_4@2x~ipad.jpg */, + E6FFEAAC19C7925600A0D7DE /* splash_4@2x~iphone.jpg */, + E6FFEAAD19C7925600A0D7DE /* splash_5@2x~ipad.jpg */, + ); + name = Onboarding; + path = ../Resources/Onboarding; + sourceTree = ""; + }; + 60059B3917CEA46700BE132D /* 5- Onboarding Personalization */ = { + isa = PBXGroup; + children = ( + CB206F7817C5AEB400A4FDC4 /* AROnboardingArtistTableController.h */, + CB206F7917C5AEB400A4FDC4 /* AROnboardingArtistTableController.m */, + CB206F7517C5A5AD00A4FDC4 /* AROnboardingGeneTableController.h */, + CB206F7617C5A5AD00A4FDC4 /* AROnboardingGeneTableController.m */, + 5E71AF231912826B008B1426 /* ARPersonalizeViewController.h */, + CB206F7317C569E800A4FDC4 /* ARPersonalizeViewController.m */, + ); + name = "5- Onboarding Personalization"; + sourceTree = ""; + }; + 6008A58F16D8155C007E8E26 /* Utilities */ = { + isa = PBXGroup; + children = ( + 609B3C071761F80C00953CB2 /* ARSiteHeroUnitView.h */, + 609B3C081761F80C00953CB2 /* ARSiteHeroUnitView.m */, + 342F9BA51676376FB563BD00 /* ARActionButtonsView.h */, + 342F937DAA05F0E703D7A020 /* ARActionButtonsView.m */, + 3C48E20E1965AC640077A80B /* ARCustomEigenLabels.h */, + 3C48E20F1965AC640077A80B /* ARCustomEigenLabels.m */, + 3CFBE32B18C3A3F400C781D0 /* ARNetworkErrorView.h */, + 3CFBE32C18C3A3F400C781D0 /* ARNetworkErrorView.m */, + 5E50987B18F82FCF001AC704 /* AROfflineView.h */, + 5E50987C18F82FCF001AC704 /* AROfflineView.m */, + 60AEC84516E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.h */, + 60AEC84616E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.m */, + 602BC087168E0C0D00069FDB /* ARReusableLoadingView.h */, + 602BC088168E0C0D00069FDB /* ARReusableLoadingView.m */, + E676A62918F3421600B9AF2C /* ARSeparatorViews.h */, + E676A62A18F3421600B9AF2C /* ARSeparatorViews.m */, + 60CF97FA17BE9303005ED59B /* ARShadowView.h */, + 60CF97FB17BE9303005ED59B /* ARShadowView.m */, + 60229B4C1683BE280072DC12 /* ARSpinner.h */, + 60229B4D1683BE280072DC12 /* ARSpinner.m */, + 60E6447417BE424E004486B3 /* ARSwitchView.h */, + 60E6447517BE424E004486B3 /* ARSwitchView.m */, + 60327DC71987AAD00075B399 /* ARTabContentView.h */, + 60327DC81987AAD00075B399 /* ARTabContentView.m */, + E67655241900660500F9A704 /* ARWhitespaceGobbler.h */, + E67655251900660500F9A704 /* ARWhitespaceGobbler.m */, + E61B703719904D9F00260D29 /* ARTopTapThroughTableView.h */, + E61B703819904D9F00260D29 /* ARTopTapThroughTableView.m */, + 062C201A16DD76A30095A7EC /* Image */, + ); + name = Utilities; + sourceTree = ""; + }; + 6008A59016D815A4007E8E26 /* Pages */ = { + isa = PBXGroup; + children = ( + 342F940CACC66E85F5F48B96 /* ARPageSubTitleView.m */, + 342F9DED668071BB2B4C12F5 /* ARPageSubTitleView.h */, + ); + name = Pages; + sourceTree = ""; + }; + 6008A59116D815E5007E8E26 /* Debug */ = { + isa = PBXGroup; + children = ( + 60B79B86182C549900945FFF /* Quicksilver */, + ); + name = Debug; + sourceTree = ""; + }; + 6016C18D178C2B46008EC8E7 /* Embedded */ = { + isa = PBXGroup; + children = ( + E66EE4AC196460530081ED0C /* Favorites */, + 6016C191178C2BD9008EC8E7 /* Artworks */, + 342F9FE0B50F629107FA5E5A /* Generics */, + ); + name = Embedded; + sourceTree = ""; + }; + 6016C191178C2BD9008EC8E7 /* Artworks */ = { + isa = PBXGroup; + children = ( + 6016C192178C2BE2008EC8E7 /* Modules */, + ); + name = Artworks; + sourceTree = ""; + }; + 6016C192178C2BE2008EC8E7 /* Modules */ = { + isa = PBXGroup; + children = ( + 6016C196178C2C33008EC8E7 /* ARArtworkMasonryModule.h */, + 6016C197178C2C33008EC8E7 /* ARArtworkMasonryModule.m */, + 6016C193178C2C06008EC8E7 /* ARArtworkFlowModule.h */, + 6016C194178C2C06008EC8E7 /* ARArtworkFlowModule.m */, + ); + name = Modules; + sourceTree = ""; + }; + 603218C3195C92C2004E7A24 /* App Menu */ = { + isa = PBXGroup; + children = ( + 60327DE3198933610075B399 /* ARTabContentViewSpec.m */, + 60327DE4198933610075B399 /* ARTestTopMenuNavigationDataSource.h */, + 60327DE5198933610075B399 /* ARTestTopMenuNavigationDataSource.m */, + 60327DE6198933610075B399 /* ARTopMenuNavigationDataSourceSpec.m */, + 60327DE7198933610075B399 /* ARTopMenuViewControllerSpec.m */, + E65BB51E1A3FB552004C4DB4 /* ARAppSearchViewControllerSpec.m */, + E67DF2121A40A73C00C8495E /* ARSearchViewControllerSpec.m */, + ); + name = "App Menu"; + sourceTree = ""; + }; + 60327DCA1987AD140075B399 /* Tooling */ = { + isa = PBXGroup; + children = ( + 60327DCB1987AD240075B399 /* ARDispatchManager.h */, + 60327DCC1987AD240075B399 /* ARDispatchManager.m */, + 60327DCE1987B7940075B399 /* ARDeveloperOptions.h */, + 60327DCF1987B7940075B399 /* ARDeveloperOptions.m */, + ); + path = Tooling; + sourceTree = ""; + }; + 6034EB261760A1150070478D /* App Navigation */ = { + isa = PBXGroup; + children = ( + 6029E9F31993D726002D42C3 /* ARAppSearchViewController.h */, + 6029E9F41993D726002D42C3 /* ARAppSearchViewController.m */, + 60327DDF1987FF490075B399 /* ARTopMenuNavigationDataSource.h */, + 60327DE01987FF490075B399 /* ARTopMenuNavigationDataSource.m */, + 6036B56617612CFA00F1DD01 /* ARNavigationContainer.h */, + 60CAA3741782D87B00CA3DA8 /* ARContentViewControllers.h */, + 6034EB2A1760A9BD0070478D /* ARTopMenuViewController.h */, + 6034EB2B1760A9BD0070478D /* ARTopMenuViewController.m */, + 54B7477318A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.h */, + 54B7477418A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.m */, + ); + name = "App Navigation"; + sourceTree = ""; + }; + 60388700189C0ED600D3EEAA /* Utils */ = { + isa = PBXGroup; + children = ( + 60388704189C103F00D3EEAA /* ARFairMapZoomManager.h */, + 60388705189C103F00D3EEAA /* ARFairMapZoomManager.m */, + 60D83DE6189EAB82001672E9 /* ARFairShowMapper.h */, + 60D83DE7189EAB82001672E9 /* ARFairShowMapper.m */, + ); + name = Utils; + sourceTree = ""; + }; + 603D7C021958849C00ACA840 /* Auctions */ = { + isa = PBXGroup; + children = ( + CB42B64D181092480069A801 /* ARCountdownView.h */, + CB42B64E181092480069A801 /* ARCountdownView.m */, + B3DDE9BF18313C7C0012819F /* ARArtworkAuctionPriceView.h */, + B3DDE9C018313C7C0012819F /* ARArtworkAuctionPriceView.m */, + 603D7C03195884C100ACA840 /* ARAuctionBidderStateLabel.h */, + 603D7C04195884C100ACA840 /* ARAuctionBidderStateLabel.m */, + ); + name = Auctions; + sourceTree = ""; + }; + 60438F801782E9A300C1B63B /* Protocols */ = { + isa = PBXGroup; + children = ( + 60A7B1B017B26DCD003DC094 /* ARFeedHostItem.h */, + CB355E8217CAB0B3002A798D /* ARFollowable.h */, + E676B2EF18D207910057B4E1 /* ARShareableObject.h */, + 5EE5DE1519019EFD00040B84 /* ARFairAwareObject.h */, + ); + path = Protocols; + sourceTree = ""; + }; + 60438F811782ED4A00C1B63B /* Social */ = { + isa = PBXGroup; + children = ( + 49EF164516C568EA00460BD7 /* Profile.h */, + 49EF164616C568EA00460BD7 /* Profile.m */, + 608B2F6D1657D1D10046956C /* User.h */, + 608B2F6E1657D1D10046956C /* User.m */, + ); + name = Social; + sourceTree = ""; + }; + 60438F821782ED6000C1B63B /* Artwork Metadata */ = { + isa = PBXGroup; + children = ( + 49A76D0C17594E32001D4B81 /* Tag.h */, + 49A76D0D17594E32001D4B81 /* Tag.m */, + 491A4DE0168E4343003B2246 /* Gene.h */, + CB879D0A180746C900E2D8EC /* AuctionLot.h */, + CB879D0B180746C900E2D8EC /* AuctionLot.m */, + 491A4DE1168E4343003B2246 /* Gene.m */, + 4953E8301668021D00A09726 /* Image.h */, + 4953E8311668021D00A09726 /* Image.m */, + ); + name = "Artwork Metadata"; + sourceTree = ""; + }; + 60438F831782ED6A00C1B63B /* Application */ = { + isa = PBXGroup; + children = ( + 6034EB1B175F68350070478D /* SiteHeroUnit.h */, + 6034EB1C175F68350070478D /* SiteHeroUnit.m */, + CB73B48917D2581400891305 /* SiteFeature.h */, + CB73B48A17D2581400891305 /* SiteFeature.m */, + 49A76D0617592C96001D4B81 /* SearchResult.h */, + 49A76D0717592C96001D4B81 /* SearchResult.m */, + 60D90A0317C2109A0073D5B9 /* FeaturedLink.h */, + 60D90A0417C2109A0073D5B9 /* FeaturedLink.m */, + 3C35CC79189FF05E00E3D8DE /* OrderedSet.h */, + 3C35CC7A189FF05E00E3D8DE /* OrderedSet.m */, + 3CF144A518E45F6C00B1A764 /* SystemTime.h */, + 3CF144A618E45F6C00B1A764 /* SystemTime.m */, + ); + name = Application; + sourceTree = ""; + }; + 60438F841782ED8600C1B63B /* Partner Metadata */ = { + isa = PBXGroup; + children = ( + 499A588C16658CA9004B0E2F /* Partner.h */, + 499A588D16658CAA004B0E2F /* Partner.m */, + 49EC62161778AF100020D648 /* PartnerShow.h */, + 49EC62171778AF100020D648 /* PartnerShow.m */, + 499A5892166683AB004B0E2F /* Artist.h */, + 499A5893166683AB004B0E2F /* Artist.m */, + 60B6F13C16638815007C9587 /* Artwork.h */, + 60B6F13D16638815007C9587 /* Artwork.m */, + 342F9A17225A44AC4BE26303 /* PartnerShowFairLocation.m */, + 342F9C73F3EBBD7348B5CB83 /* PartnerShowFairLocation.h */, + 342F93E38025E2D2DA90C6DA /* MapFeature.m */, + 342F9EF0AF8F712E38798AE8 /* MapFeature.h */, + ); + name = "Partner Metadata"; + sourceTree = ""; + }; + 6044CFC5179DD38F00CE4132 /* Extensions */ = { + isa = PBXGroup; + children = ( + 6044CFC6179DD3C200CE4132 /* ARTheme+HeightAdditions.h */, + 6044CFC7179DD3C200CE4132 /* ARTheme+HeightAdditions.m */, + ); + name = Extensions; + sourceTree = ""; + }; + 607D6974181A97F0009462DD /* ArtworkSources */ = { + isa = PBXGroup; + children = ( + 60A612B6189673F4008FC19D /* ARGeneArtworksNetworkModel.h */, + 60A612B7189673F4008FC19D /* ARGeneArtworksNetworkModel.m */, + ); + name = ArtworkSources; + sourceTree = ""; + }; + 607E2E7517C8C44000396120 /* Artwork */ = { + isa = PBXGroup; + children = ( + E6AD6D3E18E5C404005C8A3A /* ARArtworkInfoViewController.h */, + E6AD6D3F18E5C404005C8A3A /* ARArtworkInfoViewController.m */, + 4917819D176A6B22001E751E /* ARArtworkSetViewController.h */, + 4917819E176A6B22001E751E /* ARArtworkSetViewController.m */, + 607E2E7C17C8CA4D00396120 /* ARZoomArtworkImageViewController.h */, + 607E2E7D17C8CA4D00396120 /* ARZoomArtworkImageViewController.m */, + 607E2E7617C8C46000396120 /* ARArtworkViewController.h */, + 607E2E7717C8C46000396120 /* ARArtworkViewController.m */, + 601F029617F3419400EB3E83 /* ARArtworkViewController+ButtonActions.h */, + 601F029717F3419400EB3E83 /* ARArtworkViewController+ButtonActions.m */, + ); + name = Artwork; + sourceTree = ""; + }; + 60838EB2177351DD00869F6E /* Navigation Transitions */ = { + isa = PBXGroup; + children = ( + 609A82EE17A10C5C00AFDF13 /* ARNavigationTransitionController.h */, + 609A82EF17A10C5C00AFDF13 /* ARNavigationTransitionController.m */, + 609A82F917A1147800AFDF13 /* ARNavigationTransition.h */, + 609A82FA17A1147800AFDF13 /* ARNavigationTransition.m */, + 6099F905178F24750004EF04 /* ARDefaultNavigationTransition.h */, + 6099F906178F24750004EF04 /* ARDefaultNavigationTransition.m */, + 60838EB31773547700869F6E /* ARViewInRoomTransition.h */, + 60838EB41773547700869F6E /* ARViewInRoomTransition.m */, + 607E2E8817C9121500396120 /* ARZoomImageTransition.h */, + 607E2E8917C9121500396120 /* ARZoomImageTransition.m */, + ); + name = "Navigation Transitions"; + path = "Utils/Navigation Animations"; + sourceTree = ""; + }; + 6088B75517D1E05200E4BB67 /* Views */ = { + isa = PBXGroup; + children = ( + CB4D652517C80A9600390550 /* AROnboardingSearchField.h */, + CB4D652617C80A9600390550 /* AROnboardingSearchField.m */, + 49405AB117BEBAFF004F86D8 /* AROnboardingNavBarView.h */, + 49405AB217BEBAFF004F86D8 /* AROnboardingNavBarView.m */, + ); + name = Views; + sourceTree = ""; + }; + 608B2F5A1657CED10046956C /* View Controllers */ = { + isa = PBXGroup; + children = ( + 3C2E034A18DA7F640001B7E5 /* Image */, + 60D8E63718D256A30040BEFD /* Demo */, + 342F91DD9B57A51A8352877C /* Util */, + 3C9F215C18B25CFA00D8898B /* Search */, + 3C6CB60618ABD8C1008DFE3B /* Post */, + 60FFE3DE188E97FE0012B485 /* Fair */, + 607E2E7517C8C44000396120 /* Artwork */, + 60B617CA181598F400EBDC51 /* Auction Results */, + 609A82F117A1114600AFDF13 /* Contact */, + 60FAEF40179842D90031C88B /* Favorites */, + 6016C18D178C2B46008EC8E7 /* Embedded */, + 6034EB261760A1150070478D /* App Navigation */, + 60903CC5175CE588002AB800 /* Admin */, + 6008A59116D815E5007E8E26 /* Debug */, + 600249F3166CFD13007E6E9A /* Menu */, + 600249F2166CFD08007E6E9A /* Login and Onboarding */, + 600249F1166CFCF5007E6E9A /* Feed VCs */, + 60B9D00F17834A35006E498A /* Web Browsing */, + 600249F0166CFCCE007E6E9A /* Core */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 608B2F5B1657CF600046956C /* App Resources */ = { + isa = PBXGroup; + children = ( + 49BA7E0B1655ABE600C06572 /* Artsy-Info.plist */, + 49BA7E0C1655ABE600C06572 /* InfoPlist.strings */, + ); + name = "App Resources"; + sourceTree = ""; + }; + 608B2F5F1657D11F0046956C /* Constants */ = { + isa = PBXGroup; + children = ( + 60EEFE741658547C00F43F00 /* Constants.h */, + 608B2F601657D1500046956C /* ARDefaults.h */, + 608B2F611657D1500046956C /* ARDefaults.m */, + 60B6F0F01662AADF007C9587 /* ARAppConstants.h */, + 60B6F0F11662AADF007C9587 /* ARAppConstants.m */, + 499A5879166561E8004B0E2F /* ARFeedConstants.h */, + 499A587A166561E8004B0E2F /* ARFeedConstants.m */, + E6B958ED188DB24200D75C86 /* ARViewTagConstants.h */, + E6B958EE188DB24200D75C86 /* ARViewTagConstants.m */, + ); + path = Constants; + sourceTree = ""; + }; + 608B2F6B1657D1A00046956C /* Models */ = { + isa = PBXGroup; + children = ( + 608B2F6C1657D1BB0046956C /* Models.h */, + 60B6F14E16639607007C9587 /* API Models */, + 4953E8341668022E00A09726 /* Protocols */, + 60903CD9175DE136002AB800 /* Feed Item Models */, + 49A77222165ADB9300BC6FD3 /* Feed Items */, + 60903CDA175DE18F002AB800 /* Feed Timelines */, + ); + path = Models; + sourceTree = ""; + }; + 608B2F751657D3220046956C /* Resources */ = { + isa = PBXGroup; + children = ( + E620C74719C746530064A0FF /* Images.xcassets */, + 3C4AE98B19094F74009C0E8B /* ViewInRoom */, + 3C4AE96C19094D8F009C0E8B /* Map Annotations */, + 608B703017D4A10F0088A56C /* Assets */, + 60059B2017CE9CEE00BE132D /* Onboarding */, + 0631FA6E1705E77F000A5ED3 /* mail.html */, + ); + name = Resources; + sourceTree = ""; + }; + 608B2F7E1657D3B40046956C /* Categories */ = { + isa = PBXGroup; + children = ( + 608B2F861657D5880046956C /* Categories.h */, + 342F9C2C3D87889B79459CED /* Apple */, + 342F90A2E677A86747A25FD8 /* Artsy */, + ); + path = Categories; + sourceTree = ""; + }; + 608B2F821657D4780046956C /* Views */ = { + isa = PBXGroup; + children = ( + 5ECFA8EB1907FC4D000B92EA /* Fair */, + 3C7880B918B9080B00595E30 /* Notifications */, + 3CD3679D189AC4FF00285DF7 /* Images */, + 3CD36796189A9AD600285DF7 /* Post */, + 60E6447017BE41A0004486B3 /* Artist */, + 49F45191176B9B2F0041A4B4 /* Artwork */, + 342F9D35D55EDEA5EA203296 /* Styled Subclasses */, + 60E5D8881694ED0B00BDC57E /* SwitchViews */, + 60F1C50F17C0EDA7000938F7 /* Collection Views */, + 49B32E11166D0AF9008BAB02 /* Collection View Cells */, + 499A5805165AE24C004B0E2F /* Table View Cells */, + 6008A59016D815A4007E8E26 /* Pages */, + 6008A58F16D8155C007E8E26 /* Utilities */, + ); + path = Views; + sourceTree = ""; + }; + 608B703017D4A10F0088A56C /* Assets */ = { + isa = PBXGroup; + children = ( + 608B703117D4A1C70088A56C /* ActionButton@2x.png */, + 608B703A17D4A1C70088A56C /* Artwork_Icon_Share.png */, + 605002E217D8912300C090B8 /* Artwork_Icon_Share@2x.png */, + 608B703B17D4A1C70088A56C /* Artwork_Icon_VIR.png */, + 608B703C17D4A1C70088A56C /* Artwork_Icon_VIR@2x.png */, + 608B704017D4A1C70088A56C /* BackArrow@2x.png */, + 608B703E17D4A1C70088A56C /* BackArrow_Highlighted@2x.png */, + 608B706217D4A1C80088A56C /* CloseButtonLarge@2x.png */, + E6F84CFE1A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png */, + B3BD12D717F22370002CA230 /* fbp */, + B3BD12D817F22370002CA230 /* fbs */, + 608B704717D4A1C70088A56C /* FollowCheckmark.png */, + 608B704817D4A1C70088A56C /* FollowCheckmark@2x.png */, + 608B704917D4A1C70088A56C /* FooterBackground@2x.png */, + E66A0A8219C2125400ECEB1A /* full_logo_white_large@2x.png */, + E60673B119BE4E8C00EF05EB /* full_logo_white_small@2x.png */, + 608B703917D4A1C70088A56C /* Heart_Black@2x.png */, + 608B703717D4A1C70088A56C /* Heart_White@2x.png */, + E6CD164918ABE291001254B5 /* Image_Shadow_Overlay@2x.png */, + 6012313518B2E44A00B7667F /* MapButtonAction@2x.png */, + 5E6621DC19768F750064FC52 /* MapIcon@2x.png */, + 608B704C17D4A1C70088A56C /* MenuButtonBG@2x.png */, + 608B704E17D4A1C70088A56C /* MenuClose@2x.png */, + 608B704F17D4A1C70088A56C /* MenuHamburger@2x.png */, + 608B705117D4A1C70088A56C /* MoreArrow@2x.png */, + E6543CDA18AD795E00A6B9AF /* Parallax_Overlay_Bottom@2x.png */, + E66492D218B7D26B00531B8F /* Parallax_Overlay_Top@2x.png */, + E69101FF19C9E17B0048149C /* SearchIcon_White@2x.png */, + 3C4AE96319094969009C0E8B /* SearchIcon_LightGrey@2x.png */, + 3C4AE96419094969009C0E8B /* SearchIcon_MediumGrey@2x.png */, + 3C4AE96219094969009C0E8B /* SearchIcon_HeavyGrey@2x.png */, + 3C4AE96519094969009C0E8B /* SearchThumb_HeavyGrey@2x.png */, + 3C4AE96619094969009C0E8B /* SearchThumb_LightGrey@2x.png */, + 608B705817D4A1C80088A56C /* SettingsButton@2x.png */, + 608B705917D4A1C80088A56C /* SidebarButtonBG@2x.png */, + 608B705A17D4A1C80088A56C /* SidebarButtonHighlightBG@2x.png */, + 605002E717D9F5DC00C090B8 /* SmallMoreVerticalArrow.png */, + 605002E817D9F5DC00C090B8 /* SmallMoreVerticalArrow@2x.png */, + E65BB5221A408A29004C4DB4 /* TextfieldClearButton.png */, + ); + name = Assets; + path = ../Resources/ImageAssets; + sourceTree = ""; + }; + 60903CC5175CE588002AB800 /* Admin */ = { + isa = PBXGroup; + children = ( + 60903CC9175CE766002AB800 /* ARAdminSettingsViewController.h */, + 60903CCA175CE766002AB800 /* ARAdminSettingsViewController.m */, + ); + name = Admin; + sourceTree = ""; + }; + 60903CCC175CF074002AB800 /* AdminTableView */ = { + isa = PBXGroup; + children = ( + 60903CD6175CF27D002AB800 /* ARAdminTableViewCell.h */, + 60903CD7175CF27D002AB800 /* ARAdminTableViewCell.m */, + 60903CD3175CF23F002AB800 /* ARTickedTableViewCell.h */, + 60903CD4175CF23F002AB800 /* ARTickedTableViewCell.m */, + 60903CD0175CF095002AB800 /* ARAnimatedTickView.h */, + 60903CD1175CF095002AB800 /* ARAnimatedTickView.m */, + 60903CCD175CF088002AB800 /* ARGroupedTableViewCell.h */, + 60903CCE175CF088002AB800 /* ARGroupedTableViewCell.m */, + ); + name = AdminTableView; + sourceTree = ""; + }; + 60903CD9175DE136002AB800 /* Feed Item Models */ = { + isa = PBXGroup; + children = ( + 4953E83916681A4200A09726 /* ContentLink.h */, + 4953E83A16681A4200A09726 /* ContentLink.m */, + 4953E8361668179500A09726 /* PostImage.h */, + 4953E8371668179500A09726 /* PostImage.m */, + 494332FB16692010005AB483 /* VideoContentLink.h */, + 494332FC16692010005AB483 /* VideoContentLink.m */, + 494332FF166920B3005AB483 /* PhotoContentLink.h */, + 49433300166920B3005AB483 /* PhotoContentLink.m */, + ); + name = "Feed Item Models"; + sourceTree = ""; + }; + 60903CDA175DE18F002AB800 /* Feed Timelines */ = { + isa = PBXGroup; + children = ( + 49A7721F165ADB6E00BC6FD3 /* ARFeedTimeline.h */, + 49A77220165ADB6E00BC6FD3 /* ARFeedTimeline.m */, + 60903CDB175DE1C2002AB800 /* ARFeed.h */, + 60903CDC175DE1C2002AB800 /* ARFeed.m */, + 546A858017763349006D489B /* ARHeroUnitsNetworkModel.h */, + 546A858117763349006D489B /* ARHeroUnitsNetworkModel.m */, + 60889872175DEBC2008C9319 /* ARFeedSubclasses.h */, + 60889873175DEBC2008C9319 /* ARFeedSubclasses.m */, + ); + name = "Feed Timelines"; + sourceTree = ""; + }; + 6099F8FA178DD7F90004EF04 /* Modern */ = { + isa = PBXGroup; + children = ( + 6099F8F7178DBF160004EF04 /* ARModernPartnerShowTableViewCell.h */, + 6099F8F8178DBF160004EF04 /* ARModernPartnerShowTableViewCell.m */, + ); + name = Modern; + path = ..; + sourceTree = ""; + }; + 6099F8FB178DE91F0004EF04 /* Theming */ = { + isa = PBXGroup; + children = ( + 6044CFC5179DD38F00CE4132 /* Extensions */, + 6044CFC3179D64F500CE4132 /* Theme.json */, + 6099F8FC178DE9400004EF04 /* ARTheme.h */, + 6099F8FD178DE9400004EF04 /* ARTheme.m */, + 60A0AD481797FD2E00E976B7 /* ARThemedFactory.h */, + 60A0AD491797FD2E00E976B7 /* ARThemedFactory.m */, + ); + path = Theming; + sourceTree = ""; + }; + 609A82F117A1114600AFDF13 /* Contact */ = { + isa = PBXGroup; + children = ( + 609A82F217A1117E00AFDF13 /* ARInquireForArtworkViewController.h */, + 609A82F317A1117E00AFDF13 /* ARInquireForArtworkViewController.m */, + ); + name = Contact; + sourceTree = ""; + }; + 60B617CA181598F400EBDC51 /* Auction Results */ = { + isa = PBXGroup; + children = ( + 60B617CB1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.h */, + 60B617CC1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.m */, + ); + name = "Auction Results"; + sourceTree = ""; + }; + 60B6F13B16638797007C9587 /* API Modules */ = { + isa = PBXGroup; + children = ( + 604166B316C1D55000CFBD2F /* ArtsyAPI+Private.h */, + 60745DE8165802D9006CE156 /* ARUserManager.h */, + 60745DE9165802D9006CE156 /* ARUserManager.m */, + 604166AB16C1D20900CFBD2F /* ArtsyAPI+Artworks.h */, + 604166AC16C1D20900CFBD2F /* ArtsyAPI+Artworks.m */, + 604166AF16C1D46F00CFBD2F /* ArtsyAPI+CurrentUserFunctions.h */, + 604166B016C1D47000CFBD2F /* ArtsyAPI+CurrentUserFunctions.m */, + 60EFA6F316CAA8180094AD7C /* ArtsyAPI+Feed.h */, + 60EFA6F416CAA8180094AD7C /* ArtsyAPI+Feed.m */, + 6036B5631760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.h */, + 6036B5641760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.m */, + 60438F881782F8CC00C1B63B /* ArtsyAPI+Search.h */, + 60438F891782F8CC00C1B63B /* ArtsyAPI+Search.m */, + 60F1C51617C0F3DE000938F7 /* ArtsyAPI+Browse.h */, + 60F1C51717C0F3DE000938F7 /* ArtsyAPI+Browse.m */, + 607D754817C239E700CA1D41 /* ArtsyAPI+Following.h */, + 607D754917C239E700CA1D41 /* ArtsyAPI+Following.m */, + 607D756917C3816600CA1D41 /* ArtsyAPI+ListCollection.h */, + 607D756A17C3816600CA1D41 /* ArtsyAPI+ListCollection.m */, + 600415C717C4ECE2003C7974 /* ArtsyAPI+RelatedModels.h */, + 600415C817C4ECE2003C7974 /* ArtsyAPI+RelatedModels.m */, + 600A734117DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.h */, + 600A734217DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.m */, + B375E5F51812207D005FC680 /* ArtsyAPI+Sales.h */, + B375E5F61812207D005FC680 /* ArtsyAPI+Sales.m */, + 3CCCC8A01899B412008015DD /* ArtsyAPI+Fairs.h */, + 3CCCC8A11899B412008015DD /* ArtsyAPI+Fairs.m */, + 3C35CC7E189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.h */, + 3C35CC7F189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.m */, + 3CEE0B5D18A16EA200FEA6E6 /* ArtsyAPI+Profiles.h */, + 3CEE0B5E18A16EA200FEA6E6 /* ArtsyAPI+Profiles.m */, + 3CEE0B6018A16F6900FEA6E6 /* ArtsyAPI+Posts.h */, + 3CEE0B6118A16F6900FEA6E6 /* ArtsyAPI+Posts.m */, + 3CEE0B6318A16F7D00FEA6E6 /* ArtsyAPI+Artists.h */, + 3CEE0B6418A16F7D00FEA6E6 /* ArtsyAPI+Artists.m */, + 3CEE0B6618A16F8F00FEA6E6 /* ArtsyAPI+Genes.h */, + 3CEE0B6718A16F8F00FEA6E6 /* ArtsyAPI+Genes.m */, + 342F9300791CEBAEC9D58AD0 /* ArtsyAPI+Shows.m */, + 342F92656D83DA29BDA2094D /* ArtsyAPI+Shows.h */, + 3CF144AC18E460EF00B1A764 /* ArtsyAPI+SystemTime.h */, + 3CF144AD18E460EF00B1A764 /* ArtsyAPI+SystemTime.m */, + 3CB37D95192246B500089A1D /* ArtsyAPI+ErrorHandlers.h */, + 3CB37D96192246B500089A1D /* ArtsyAPI+ErrorHandlers.m */, + ); + name = "API Modules"; + sourceTree = ""; + }; + 60B6F14E16639607007C9587 /* API Models */ = { + isa = PBXGroup; + children = ( + 3CA37E781910070600B06E81 /* Favorites */, + 3CE0DA2C18A12326000E537A /* Media */, + 3CCCC89818996DC5008015DD /* Posts */, + CBB469CD181F1EED00B5692B /* Auctions */, + 60438F841782ED8600C1B63B /* Partner Metadata */, + 60438F831782ED6A00C1B63B /* Application */, + 60438F821782ED6000C1B63B /* Artwork Metadata */, + 60438F811782ED4A00C1B63B /* Social */, + 342F9453C931C592D4F8A2AA /* Fair */, + ); + name = "API Models"; + sourceTree = ""; + }; + 60B79B86182C549900945FFF /* Quicksilver */ = { + isa = PBXGroup; + children = ( + 60B79B81182C346700945FFF /* ARQuicksilverViewController.h */, + 60B79B82182C346700945FFF /* ARQuicksilverViewController.m */, + 60B79B83182C346700945FFF /* ARQuicksilverViewController.xib */, + 60B79B87182C54CE00945FFF /* ARQuicksilverSearchBar.h */, + 60B79B88182C54CE00945FFF /* ARQuicksilverSearchBar.m */, + ); + name = Quicksilver; + sourceTree = ""; + }; + 60B9D00F17834A35006E498A /* Web Browsing */ = { + isa = PBXGroup; + children = ( + 60D90A0617C2182F0073D5B9 /* ARExternalWebBrowserViewController.h */, + 60D90A0717C2182F0073D5B9 /* ARExternalWebBrowserViewController.m */, + 60D90A0917C218930073D5B9 /* ARInternalMobileWebViewController.h */, + 60D90A0A17C218930073D5B9 /* ARInternalMobileWebViewController.m */, + ); + name = "Web Browsing"; + sourceTree = ""; + }; + 60D83DEF189EE9BF001672E9 /* Guides */ = { + isa = PBXGroup; + children = ( + 5E284607194A2E58007274AB /* ARFairGuideContainerViewController.h */, + 5E284608194A2E58007274AB /* ARFairGuideContainerViewController.m */, + 3C94B2D8192BF109008D04DF /* ARFairMapPreview.h */, + 3C94B2D9192BF109008D04DF /* ARFairMapPreview.m */, + 60D83DEC189EE679001672E9 /* ARFairGuideViewController.h */, + 60D83DED189EE679001672E9 /* ARFairGuideViewController.m */, + 342F939F8B8D9AC1A4773325 /* ARFairMapViewController.m */, + 342F9AC8BEBC76D655E34A8D /* ARFairMapViewController.h */, + 3C990C8618CF8E9000BF4C44 /* ARFairMapAnnotation.h */, + 3C990C8718CF8E9000BF4C44 /* ARFairMapAnnotation.m */, + ); + name = Guides; + sourceTree = ""; + }; + 60D8E63718D256A30040BEFD /* Demo */ = { + isa = PBXGroup; + children = ( + 60D8E63818D256BB0040BEFD /* ARDemoSplashViewController.h */, + 60D8E63918D256BB0040BEFD /* ARDemoSplashViewController.m */, + 60D8E63A18D256BB0040BEFD /* ARDemoSplashViewController.xib */, + ); + name = Demo; + sourceTree = ""; + }; + 60E5D8881694ED0B00BDC57E /* SwitchViews */ = { + isa = PBXGroup; + children = ( + E6E07D6B195DF5D800403D2B /* ARSwitchView+Favorites.h */, + E6E07D6C195DF5D800403D2B /* ARSwitchView+Favorites.m */, + 5ECFA8E61907E26E000B92EA /* ARSwitchView+Artist.h */, + 5ECFA8E71907E26E000B92EA /* ARSwitchView+Artist.m */, + 5ECFA8ED1907FC6C000B92EA /* ARSwitchView+FairGuide.h */, + 5ECFA8EE1907FC6C000B92EA /* ARSwitchView+FairGuide.m */, + ); + name = SwitchViews; + sourceTree = ""; + }; + 60E6447017BE41A0004486B3 /* Artist */ = { + isa = PBXGroup; + children = ( + 3C6CB60F18ABFB8D008DFE3B /* ARRelatedArtistsViewController.m */, + 3C6CB60E18ABFB8D008DFE3B /* ARRelatedArtistsViewController.h */, + ); + name = Artist; + sourceTree = ""; + }; + 60F1C50F17C0EDA7000938F7 /* Collection Views */ = { + isa = PBXGroup; + children = ( + 60F1C51017C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.h */, + 60F1C51117C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.m */, + 60F1C51917C11303000938F7 /* ARGeneViewController.h */, + 60F1C51A17C11303000938F7 /* ARGeneViewController.m */, + ); + name = "Collection Views"; + sourceTree = ""; + }; + 60F82FC21657FBEE0076AEAE /* Networking */ = { + isa = PBXGroup; + children = ( + 607D6974181A97F0009462DD /* ArtworkSources */, + 60A49F5E1676559200B9B95D /* ARNetworkConstants.h */, + 60A49F5F1676559300B9B95D /* ARNetworkConstants.m */, + 601C317616582BA30013E061 /* ARRouter.h */, + 601C317716582BA30013E061 /* ARRouter.m */, + 60B6F13716638785007C9587 /* ArtsyAPI.h */, + CB9E243F17CBC36F00773A9A /* ARAuthProviders.h */, + CB9E244017CBC36F00773A9A /* ARAuthProviders.m */, + 60B6F13816638785007C9587 /* ArtsyAPI.m */, + 60B6F13B16638797007C9587 /* API Modules */, + 342F99296B0CBCEF0CDA9ECA /* Network Models */, + 342F93BE04F58F4AD75A8FDD /* Mantle Extensions */, + 3CF144B218E47F9F00B1A764 /* ARSystemTime.h */, + 3CF144B318E47F9F00B1A764 /* ARSystemTime.m */, + ); + path = Networking; + sourceTree = ""; + }; + 60FAEF40179842D90031C88B /* Favorites */ = { + isa = PBXGroup; + children = ( + 60FAEF41179843080031C88B /* ARFavoritesViewController.h */, + 60FAEF42179843080031C88B /* ARFavoritesViewController.m */, + ); + name = Favorites; + sourceTree = ""; + }; + 60FFE3D5188E916C0012B485 /* Documentation */ = { + isa = PBXGroup; + children = ( + 60FFE3D6188E91C50012B485 /* certs.md */, + 60FFE3D7188E91C50012B485 /* deploy_to_app_store.md */, + 60FFE3D8188E91C50012B485 /* deploy_to_beta.md */, + 60FFE3D9188E91C50012B485 /* getting_started.md */, + 60FFE3DC188E91F20012B485 /* getting_confident.md */, + 60FFE3DD188E921A0012B485 /* eigen_tips.md */, + 60FFE3DA188E91C50012B485 /* README.md */, + 60FFE3DB188E91C50012B485 /* troubleshooting.md */, + 60E6446F17BE219E004486B3 /* HACKS.md */, + 60466F761A698B96004CAA93 /* BETA_CHANGELOG.md */, + 60C4BD6317B3B91800D79058 /* CHANGELOG.md */, + 60A612B918969307008FC19D /* overview.md */, + ); + name = Documentation; + path = docs; + sourceTree = ""; + }; + 60FFE3DE188E97FE0012B485 /* Fair */ = { + isa = PBXGroup; + children = ( + 60D83DEF189EE9BF001672E9 /* Guides */, + 342F994893B9660BEA23C18D /* ARFairViewController.h */, + 342F951B04D7300332FF3DDB /* ARFairViewController.m */, + 5EE5DE12190167A400040B84 /* ARProfileViewController.h */, + 5EE5DE13190167A400040B84 /* ARProfileViewController.m */, + 3CCCC88B1899657C008015DD /* ARFairPostsViewController.h */, + 3CCCC88C1899657C008015DD /* ARFairPostsViewController.m */, + 60388700189C0ED600D3EEAA /* Utils */, + 3C8A916518A299BF0038A5B2 /* ARFairSectionViewController.h */, + 3C8A916618A299BF0038A5B2 /* ARFairSectionViewController.m */, + 6012313218B153C500B7667F /* ARFairArtistViewController.h */, + 6012313318B153C500B7667F /* ARFairArtistViewController.m */, + 342F93B0E900CB6BC98011DE /* Views */, + 3C33298B18AD3324006D28C0 /* ARFairSearchViewController.h */, + 3C33298C18AD3324006D28C0 /* ARFairSearchViewController.m */, + 342F966895F2D8BDFA7B39B1 /* ARFairShowViewController.m */, + 342F9B2B2ACEAEDFADE3E0B2 /* ARFairShowViewController.h */, + 5EFF52BC197916C800E2A563 /* ARPendingOperationViewController.h */, + 5EFF52BD197916C800E2A563 /* ARPendingOperationViewController.m */, + ); + name = Fair; + sourceTree = ""; + }; + B31BBE0F181EF9F7001C623C /* View Tests */ = { + isa = PBXGroup; + children = ( + E6B71BA21A129F1C00BBD2E2 /* Buttons */, + E67E1DB119475642004252E0 /* ARAnimatedTickViewTest.m */, + 3CB9A20C18F303B20056C72B /* ARArtworkActionsViewTests.m */, + 342F9F3C689C41D4B941B665 /* ARArtworkDetailViewTests.m */, + E63C796B198811E400579C04 /* ARArtworkRelatedArtworksViewTests.m */, + 3C11CD27189B07810060B26B /* ARAspectRatioImageViewTests.m */, + E6B9FA7818A96BF500E961F9 /* ARBrowseFeaturedLinkInsetCellTests.m */, + E6F1119F18A2A7CB00D33C3E /* ARBrowseFeaturedLinksCollectionViewTests.m */, + 3C4877B2192A745400F40062 /* ARFairMapAnnotationCallOutViewTests.m */, + 3CD3679E189ADBEF00285DF7 /* ARPostFeedItemLinkViewTests.m */, + 5ECFA8E91907E9AB000B92EA /* ARSwitchViewTests.m */, + 342F9316E861BD2C6EBFA31A /* ORStackViewArtsyCategoriesTests.m */, + ); + name = "View Tests"; + sourceTree = ""; + }; + B31BBE10181EFA08001C623C /* Model Tests */ = { + isa = PBXGroup; + children = ( + 3C22227018C647CC00B7CE3A /* Data */, + 3CE7F34018A99E4B002BA993 /* Feed Timeline Tests */, + 3C7A7FA618C6EDAB00E8D336 /* ArtworkTests.m */, + 3CCCC89C18997948008015DD /* FairTests.m */, + 3CA55D8818BFF8F800B44CD3 /* MapFeatureTests.m */, + 3C35CC7C189FF14800E3D8DE /* OrderedSetTests.m */, + 3CEE0B6918A18CDA00FEA6E6 /* ProfileTests.m */, + B3ECE83B1819D1FD009F5C5B /* SaleArtworkTests.m */, + 3CA1E82418846B3A003C622D /* SaleTests.m */, + 3CE7F34318A99E6D002BA993 /* SiteHeroUnitTests.m */, + 3CF144A818E4607900B1A764 /* SystemTimeTests.m */, + 3C6CB60418ABC7BB008DFE3B /* UserTests.m */, + E6EC246018F7069300C89192 /* PartnerTests.m */, + ); + name = "Model Tests"; + sourceTree = ""; + }; + B3ECE82B1819D1E6009F5C5B /* Artsy Tests */ = { + isa = PBXGroup; + children = ( + 3C205B5718900091004280E0 /* App Tests */, + 3C3FEA8B188433CB00E1A16F /* Extensions */, + B31BBE10181EFA08001C623C /* Model Tests */, + 3CAED1791880268E00840608 /* Networking Tests */, + B3ECE82C1819D1E6009F5C5B /* Supporting Files */, + 3CACF2AA18F591D40054091E /* Theming Tests */, + 3C5C7E3D18970E73003823BB /* Util Tests */, + 3CB97D8D18870979008C44FE /* View Controller Tests */, + B31BBE0F181EF9F7001C623C /* View Tests */, + ); + path = "Artsy Tests"; + sourceTree = ""; + }; + B3ECE82C1819D1E6009F5C5B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E6718314194A595F001A5566 /* ARTestContext.h */, + E6718315194A595F001A5566 /* ARTestContext.m */, + 546778EC18A95642002C4C71 /* ARAppDelegate+Testing.h */, + 546778ED18A95642002C4C71 /* ARAppDelegate+Testing.m */, + E6B9FA7A18A97C0A00E961F9 /* stub.jpg */, + B3ECE82D1819D1E6009F5C5B /* Artsy Tests-Info.plist */, + B3ECE82E1819D1E6009F5C5B /* InfoPlist.strings */, + B3ECE8331819D1E6009F5C5B /* Artsy Tests-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CBB469CD181F1EED00B5692B /* Auctions */ = { + isa = PBXGroup; + children = ( + B375E5F218121EEA005FC680 /* Sale.h */, + B375E5F318121EEA005FC680 /* Sale.m */, + B3EFC6371815B41C00F23540 /* BidderPosition.h */, + CBB469CE181F1F1200B5692B /* Bid.h */, + CBB469CF181F1F1200B5692B /* Bid.m */, + B3EFC6381815B41C00F23540 /* BidderPosition.m */, + B316E2F218170DF40086CCDB /* SaleArtwork.h */, + B316E2F318170DF40086CCDB /* SaleArtwork.m */, + B316E2F5181713110086CCDB /* Bidder.h */, + B316E2F6181713110086CCDB /* Bidder.m */, + ); + name = Auctions; + sourceTree = ""; + }; + E619969919B9094F00DB273C /* Swizzles */ = { + isa = PBXGroup; + children = ( + E619969A19B9099400DB273C /* UIScrollView+HitTest.h */, + E619969B19B9099400DB273C /* UIScrollView+HitTest.m */, + ); + name = Swizzles; + path = ..; + sourceTree = ""; + }; + E61E773A19A7E8DB00C55E14 /* Gene */ = { + isa = PBXGroup; + children = ( + E61E773819A7E8D500C55E14 /* ARGeneViewControllerTests.m */, + ); + name = Gene; + sourceTree = ""; + }; + E667F12318EC827B00503F50 /* Logging */ = { + isa = PBXGroup; + children = ( + E667F12618EC82AB00503F50 /* ARLogger.h */, + E667F12718EC82AB00503F50 /* ARLogger.m */, + E667F12018EC825D00503F50 /* ARHTTPRequestOperationLogger.h */, + E667F12118EC825D00503F50 /* ARHTTPRequestOperationLogger.m */, + E667F12A18EC889F00503F50 /* ARLogFormatter.h */, + E667F12B18EC889F00503F50 /* ARLogFormatter.m */, + ); + name = Logging; + sourceTree = ""; + }; + E66EE4AC196460530081ED0C /* Favorites */ = { + isa = PBXGroup; + children = ( + E66EE4AD196460620081ED0C /* ARFavoriteItemModule.h */, + E66EE4AE196460620081ED0C /* ARFavoriteItemModule.m */, + ); + name = Favorites; + sourceTree = ""; + }; + E6718311194A3D31001A5566 /* Hero Unit */ = { + isa = PBXGroup; + children = ( + E671830F194A3D1E001A5566 /* ARHeroUnitTests.m */, + ); + name = "Hero Unit"; + sourceTree = ""; + }; + E676B2F318D2307D0057B4E1 /* Sharing Tests */ = { + isa = PBXGroup; + children = ( + E676B2F418D230A00057B4E1 /* ARURLItemProviderTests.m */, + E649708818D7762F009DB0C4 /* ARImageItemProviderTests.m */, + E611846F18D78068000FE4C9 /* ARMessageItemProviderTests.m */, + E611847118D7B4C4000FE4C9 /* ARSharingControllerTests.m */, + ); + name = "Sharing Tests"; + sourceTree = ""; + }; + E6B71BA11A129DA700BBD2E2 /* Buttons */ = { + isa = PBXGroup; + children = ( + 60838EAD17728C5D00869F6E /* ARHeartButton.h */, + 60838EAE17728C5D00869F6E /* ARHeartButton.m */, + E6C0315E18B291AB00137242 /* ARButtonWithCircularImage.h */, + E6C0315F18B291AB00137242 /* ARButtonWithCircularImage.m */, + 342F918CC4304843C82ED43C /* ARBidButton.h */, + 342F9629B03A1BEB6888BD4B /* ARBidButton.m */, + 342F9039177611EDF514D37E /* ARFollowableButton.m */, + 342F972963924EC91C9B5CFD /* ARFollowableButton.h */, + 600EE29C16B3003F002E9F9A /* ARNavigationButton.m */, + 600EE29D16B3003F002E9F9A /* ARNavigationButton.h */, + 540262C418A0FAFB00844AE1 /* ARButtonWithImage.h */, + 540262C518A0FAFB00844AE1 /* ARButtonWithImage.m */, + ); + name = Buttons; + sourceTree = ""; + }; + E6B71BA21A129F1C00BBD2E2 /* Buttons */ = { + isa = PBXGroup; + children = ( + B30FEF59181EEA47009E4EAD /* ARBidButtonTests.m */, + E612BC3E19D1B9DE00585CD6 /* ARFollowableButtonTests.m */, + 3CA37E7B1910217500B06E81 /* ARHeartButtonTests.m */, + 3C28D3DA18BE180B00C846EA /* ARNavigationButtonTests.m */, + ); + name = Buttons; + sourceTree = ""; + }; + E6BC2C3A197EDBA80063ED3C /* Browse */ = { + isa = PBXGroup; + children = ( + E6BC2C3D197EDC950063ED3C /* ARBrowseViewControllerTests.m */, + ); + name = Browse; + sourceTree = ""; + }; + E6D2E911187E07530089728B /* Settings */ = { + isa = PBXGroup; + children = ( + E6D2E915187E09CE0089728B /* FormCellNibs */, + E6FD2F1F187B0D76002AAFEB /* ARUserSettingsViewController.h */, + E6FD2F20187B0D76002AAFEB /* ARUserSettingsViewController.m */, + ); + name = Settings; + sourceTree = ""; + }; + E6D2E915187E09CE0089728B /* FormCellNibs */ = { + isa = PBXGroup; + children = ( + E693BF8418A1949A00D464BC /* ARTextInputCellWithTitle.xib */, + E693BF7F18A1942D00D464BC /* ARSwitchCell.xib */, + ); + name = FormCellNibs; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 49BA7DFE1655ABE600C06572 /* Artsy */ = { + isa = PBXNativeTarget; + buildConfigurationList = 49BA7E231655ABE600C06572 /* Build configuration list for PBXNativeTarget "Artsy" */; + buildPhases = ( + 1614211B3CFCBD9C8A9E2169 /* Check Pods Manifest.lock */, + 49BA7DFB1655ABE600C06572 /* Sources */, + 49BA7DFC1655ABE600C06572 /* Frameworks */, + 49BA7DFD1655ABE600C06572 /* Resources */, + 1A2FC4ED23AE843DFE3A8B1B /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Artsy; + productName = Artsy; + productReference = 49BA7DFF1655ABE600C06572 /* Artsy.app */; + productType = "com.apple.product-type.application"; + }; + B3ECE8251819D1E6009F5C5B /* Artsy Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B3ECE8361819D1E7009F5C5B /* Build configuration list for PBXNativeTarget "Artsy Tests" */; + buildPhases = ( + 36DA7C0A4A7F53AF3E07FB0A /* Check Pods Manifest.lock */, + B3ECE8221819D1E6009F5C5B /* Sources */, + B3ECE8231819D1E6009F5C5B /* Frameworks */, + B3ECE8241819D1E6009F5C5B /* Resources */, + 3CB52256809EEA89C7536751 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + B3ECE8351819D1E7009F5C5B /* PBXTargetDependency */, + ); + name = "Artsy Tests"; + productName = "Artsy Tests"; + productReference = B3ECE8261819D1E6009F5C5B /* Artsy Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 49BA7DF61655ABE600C06572 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = AR; + LastUpgradeCheck = 0510; + ORGANIZATIONNAME = Artsy; + TargetAttributes = { + 49BA7DFE1655ABE600C06572 = { + DevelopmentTeam = 23KMWZ572J; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + com.apple.InterAppAudio = { + enabled = 0; + }; + com.apple.iCloud = { + enabled = 0; + }; + }; + }; + B3ECE8251819D1E6009F5C5B = { + TestTargetID = 49BA7DFE1655ABE600C06572; + }; + }; + }; + buildConfigurationList = 49BA7DF91655ABE600C06572 /* Build configuration list for PBXProject "Artsy" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 49BA7DF41655ABE600C06572; + productRefGroup = 49BA7E001655ABE600C06572 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 49BA7DFE1655ABE600C06572 /* Artsy */, + B3ECE8251819D1E6009F5C5B /* Artsy Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 49BA7DFD1655ABE600C06572 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C4AE98319094DAA009C0E8B /* MapAnnotation_Installation@2x.png in Resources */, + E6FFEAB319C7925600A0D7DE /* onboard_3@2x~ipad.jpg in Resources */, + B3BD12D917F22370002CA230 /* fbp in Resources */, + 608B706D17D4A1C80088A56C /* Artwork_Icon_VIR.png in Resources */, + 608B707B17D4A1C80088A56C /* FooterBackground@2x.png in Resources */, + 3C4AE99319094F96009C0E8B /* ViewInRoom_BaseNoBench@2x.png in Resources */, + E6FFEAB019C7925600A0D7DE /* onboard_1@2x~iphone.jpg in Resources */, + 3C4AE96A19094969009C0E8B /* SearchThumb_HeavyGrey@2x.png in Resources */, + E6FFEAB219C7925600A0D7DE /* onboard_2@2x~iphone.jpg in Resources */, + 608B707E17D4A1C80088A56C /* MenuButtonBG@2x.png in Resources */, + 3C4AE96919094969009C0E8B /* SearchIcon_MediumGrey@2x.png in Resources */, + 3C4AE98419094DAA009C0E8B /* MapAnnotation_Lounge@2x.png in Resources */, + 6044CFC4179D64F500CE4132 /* Theme.json in Resources */, + 608B708B17D4A1C80088A56C /* SidebarButtonBG@2x.png in Resources */, + E6FFEAB919C7925600A0D7DE /* splash_3@2x~ipad.jpg in Resources */, + E6F84CFF1A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png in Resources */, + E6FFEABB19C7925600A0D7DE /* splash_4@2x~ipad.jpg in Resources */, + 3C4AE97D19094DAA009C0E8B /* MapAnnotation_CoatCheck@2x.png in Resources */, + 3C4AE97C19094DAA009C0E8B /* MapAnnotation_Artsy@2x.png in Resources */, + 608B707917D4A1C80088A56C /* FollowCheckmark.png in Resources */, + E6FFEAB119C7925600A0D7DE /* onboard_2@2x~ipad.jpg in Resources */, + E6FFEABA19C7925600A0D7DE /* splash_3@2x~iphone.jpg in Resources */, + 3C4AE98119094DAA009C0E8B /* MapAnnotation_Highlighted@2x.png in Resources */, + 605002E317D8912300C090B8 /* Artwork_Icon_Share@2x.png in Resources */, + E6FFEABD19C7925600A0D7DE /* splash_5@2x~ipad.jpg in Resources */, + 3C4AE96819094969009C0E8B /* SearchIcon_LightGrey@2x.png in Resources */, + 608B706C17D4A1C80088A56C /* Artwork_Icon_Share.png in Resources */, + E66A0A8319C2125400ECEB1A /* full_logo_white_large@2x.png in Resources */, + E6FFEAB519C7925600A0D7DE /* splash_1@2x~ipad.jpg in Resources */, + 6012313618B2E44A00B7667F /* MapButtonAction@2x.png in Resources */, + 608B706317D4A1C80088A56C /* ActionButton@2x.png in Resources */, + 60215FD817CDF9FA000F3A62 /* ARSignUpActiveUserViewController.xib in Resources */, + 49BA7E0E1655ABE600C06572 /* InfoPlist.strings in Resources */, + 3C4AE99419094F96009C0E8B /* ViewInRoom_Bench@2x.png in Resources */, + 608B708117D4A1C80088A56C /* MenuHamburger@2x.png in Resources */, + 3C4AE99219094F96009C0E8B /* ViewInRoom_Base@2x.png in Resources */, + 3C4AE9AC1909C3A5009C0E8B /* MapAnnotationCallout_Arrow@2x.png in Resources */, + 608B708C17D4A1C80088A56C /* SidebarButtonHighlightBG@2x.png in Resources */, + B3BD12DA17F22370002CA230 /* fbs in Resources */, + 3C4AE98819094DAA009C0E8B /* MapAnnotation_Transport@2x.png in Resources */, + E6543CDB18AD795E00A6B9AF /* Parallax_Overlay_Bottom@2x.png in Resources */, + 5E6621DD19768F750064FC52 /* MapIcon@2x.png in Resources */, + E60673B319BE4E8C00EF05EB /* full_logo_white_small@2x.png in Resources */, + 608B707017D4A1C80088A56C /* BackArrow_Highlighted@2x.png in Resources */, + 3C4AE98719094DAA009C0E8B /* MapAnnotation_Search@2x.png in Resources */, + 3C4AE98519094DAA009C0E8B /* MapAnnotation_Restroom@2x.png in Resources */, + 3C4AE97F19094DAA009C0E8B /* MapAnnotation_Food@2x.png in Resources */, + E620C74819C746530064A0FF /* Images.xcassets in Resources */, + 3CAC639F190AA87A00B17325 /* MapAnnotationCallout_Partner@2x.png in Resources */, + E66492D318B7D26B00531B8F /* Parallax_Overlay_Top@2x.png in Resources */, + E693BF8518A1949A00D464BC /* ARTextInputCellWithTitle.xib in Resources */, + 3C4AE97E19094DAA009C0E8B /* MapAnnotation_Drink@2x.png in Resources */, + 608B708A17D4A1C80088A56C /* SettingsButton@2x.png in Resources */, + 3C4AE99719094F96009C0E8B /* ViewInRoom_Wall@2x.png in Resources */, + 60B79B85182C346700945FFF /* ARQuicksilverViewController.xib in Resources */, + 3C4AE98919094DAA009C0E8B /* MapAnnotation_Default@2x.png in Resources */, + 3C4AE99519094F96009C0E8B /* ViewInRoom_Man_3@2x.png in Resources */, + 608B706B17D4A1C80088A56C /* Heart_Black@2x.png in Resources */, + 3C4AE96719094969009C0E8B /* SearchIcon_HeavyGrey@2x.png in Resources */, + 3C4AE96B19094969009C0E8B /* SearchThumb_LightGrey@2x.png in Resources */, + 608B706E17D4A1C80088A56C /* Artwork_Icon_VIR@2x.png in Resources */, + E6FFEAB619C7925600A0D7DE /* splash_1@2x~iphone.jpg in Resources */, + 605002E917D9F5DC00C090B8 /* SmallMoreVerticalArrow.png in Resources */, + 605002EA17D9F5DC00C090B8 /* SmallMoreVerticalArrow@2x.png in Resources */, + E6CD164A18ABE291001254B5 /* Image_Shadow_Overlay@2x.png in Resources */, + 608B707217D4A1C80088A56C /* BackArrow@2x.png in Resources */, + 0631FA6F1705E77F000A5ED3 /* mail.html in Resources */, + 3C4AE98A19094DAA009C0E8B /* MapAnnotation_VIP@2x.png in Resources */, + 3C4AE98019094DAA009C0E8B /* MapAnnotation_GenericEvent@2x.png in Resources */, + E6FFEAB419C7925600A0D7DE /* onboard_3@2x~iphone.jpg in Resources */, + 3C4AE98619094DAA009C0E8B /* MapAnnotation_Saved@2x.png in Resources */, + E693BF8218A1942D00D464BC /* ARSwitchCell.xib in Resources */, + E6FFEAB719C7925600A0D7DE /* splash_2@2x~ipad.jpg in Resources */, + 608B706917D4A1C80088A56C /* Heart_White@2x.png in Resources */, + E6FFEABC19C7925600A0D7DE /* splash_4@2x~iphone.jpg in Resources */, + 3C4AE99619094F96009C0E8B /* ViewInRoom_Wall_Right@2x.png in Resources */, + 608B707A17D4A1C80088A56C /* FollowCheckmark@2x.png in Resources */, + 608B709417D4A1C80088A56C /* CloseButtonLarge@2x.png in Resources */, + 608B708017D4A1C80088A56C /* MenuClose@2x.png in Resources */, + 60D8E63C18D256BB0040BEFD /* ARDemoSplashViewController.xib in Resources */, + 3C4AE98219094DAA009C0E8B /* MapAnnotation_Info@2x.png in Resources */, + E6FFEAB819C7925600A0D7DE /* splash_2@2x~iphone.jpg in Resources */, + 608B708317D4A1C80088A56C /* MoreArrow@2x.png in Resources */, + 3C4AE9A619098916009C0E8B /* MapAnnotationCallout_Anchor@2x.png in Resources */, + E691020019C9E17B0048149C /* SearchIcon_White@2x.png in Resources */, + 342F9A63FF3B06029E405043 /* ActiveErrorView.xib in Resources */, + E6FFEAAF19C7925600A0D7DE /* onboard_1@2x~ipad.jpg in Resources */, + E65BB5231A408A29004C4DB4 /* TextfieldClearButton.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3ECE8241819D1E6009F5C5B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6F84D001A0A920600BF99A3 /* CloseButtonLargeHighlighted@2x.png in Resources */, + 3C22227418C6487C00B7CE3A /* User_v0.data in Resources */, + 3C22227618C64B4A00B7CE3A /* User_v1.data in Resources */, + 5EB33E77197EBFEB00706EB1 /* wide.jpg in Resources */, + E676A62518EF5F6E00B9AF2C /* Artwork_v0.data in Resources */, + B3ECE8301819D1E6009F5C5B /* InfoPlist.strings in Resources */, + E6B9FA7B18A97C0A00E961F9 /* stub.jpg in Resources */, + 5EB33E78197EBFEB00706EB1 /* square.png in Resources */, + E6F466E818EF52A800A09AFA /* Artwork_v1.data in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1614211B3CFCBD9C8A9E2169 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 1A2FC4ED23AE843DFE3A8B1B /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 36DA7C0A4A7F53AF3E07FB0A /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 3CB52256809EEA89C7536751 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Artsy Tests/Pods-Artsy Tests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 49BA7DFB1655ABE600C06572 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CBE939F817FA336C00AD7DDD /* ARArtworkBlurbView.m in Sources */, + 6088B75917D20A0A00E4BB67 /* ARSlideshowView.m in Sources */, + 49405AB317BEBAFF004F86D8 /* AROnboardingNavBarView.m in Sources */, + 49F45188176A71B50041A4B4 /* ARArtworkSetViewController.m in Sources */, + 49473F3317C18772004BF082 /* ARSlideshowViewController.m in Sources */, + B3EFC6391815B41C00F23540 /* BidderPosition.m in Sources */, + 60A40E9118A052BE003E9F5E /* ARBrowseFeaturedLinkInsetCell.m in Sources */, + 60BB9FE916695A8F001FA20A /* PhotoContentLink.m in Sources */, + 49BA7E141655ABE600C06572 /* ARAppDelegate.m in Sources */, + B375E5F418121EEA005FC680 /* Sale.m in Sources */, + 608B2F5E1657D0500046956C /* main.m in Sources */, + 608B2F621657D1500046956C /* ARDefaults.m in Sources */, + 600415C917C4ECE2003C7974 /* ArtsyAPI+RelatedModels.m in Sources */, + 3CF144A718E45F6C00B1A764 /* SystemTime.m in Sources */, + 608B2F6F1657D1D10046956C /* User.m in Sources */, + CBB25B3C17F36DDE00C31446 /* ARFeaturedArtworksViewController.m in Sources */, + 60D83DEE189EE679001672E9 /* ARFairGuideViewController.m in Sources */, + 60B617D31815BD3B00EBDC51 /* ARAuctionArtworkTableViewCell.m in Sources */, + 6044CFC8179DD3C200CE4132 /* ARTheme+HeightAdditions.m in Sources */, + 60745DEA165802D9006CE156 /* ARUserManager.m in Sources */, + 60CF980817BF79CA005ED59B /* ARArtistBiographyViewController.m in Sources */, + B375E5F71812207D005FC680 /* ArtsyAPI+Sales.m in Sources */, + 6012313418B153C500B7667F /* ARFairArtistViewController.m in Sources */, + 601C317816582BA30013E061 /* ARRouter.m in Sources */, + 60A224FC17CE040B00233CA1 /* AROnboardingTransition.m in Sources */, + 6088B75417D1DBAC00E4BB67 /* ARAnalyticsConstants.m in Sources */, + 3CF144AE18E460EF00B1A764 /* ArtsyAPI+SystemTime.m in Sources */, + 49A77221165ADB6E00BC6FD3 /* ARFeedTimeline.m in Sources */, + 49A77235165ADB9400BC6FD3 /* ARFeedItem.m in Sources */, + 3CCCC88D1899657C008015DD /* ARFairPostsViewController.m in Sources */, + 49A77236165ADB9400BC6FD3 /* ARFollowFairFeedItem.m in Sources */, + 49A77237165ADB9400BC6FD3 /* ARPartnerShowFeedItem.m in Sources */, + 49A77238165ADB9400BC6FD3 /* ARPublishedArtworkSetFeedItem.m in Sources */, + 49A77239165ADB9400BC6FD3 /* ARRepostFeedItem.m in Sources */, + 608B709917D4F6520088A56C /* ARSearchTableViewCell.m in Sources */, + 49A7723A165ADB9400BC6FD3 /* ARSavedArtworkSetFeedItem.m in Sources */, + 3CEE0B6518A16F7D00FEA6E6 /* ArtsyAPI+Artists.m in Sources */, + 607D756B17C3816600CA1D41 /* ArtsyAPI+ListCollection.m in Sources */, + 3C7880BC18B9081C00595E30 /* ARNotificationView.m in Sources */, + B3DDE9C118313C7C0012819F /* ARArtworkAuctionPriceView.m in Sources */, + 54B7477518A397170050E6C7 /* ARTopMenuViewController+DeveloperExtras.m in Sources */, + 60B6F0F21662AADF007C9587 /* ARAppConstants.m in Sources */, + 60B79B89182C54CE00945FFF /* ARQuicksilverSearchBar.m in Sources */, + 49EC62181778AF100020D648 /* PartnerShow.m in Sources */, + E61446AF195A1CDC00BFB7C3 /* ARFairFavoritesNetworkModel.m in Sources */, + 60B6F13916638785007C9587 /* ArtsyAPI.m in Sources */, + 60B6F13E16638815007C9587 /* Artwork.m in Sources */, + 49473F3617C1907F004BF082 /* ARCreateAccountViewController.m in Sources */, + 6029E9F51993D726002D42C3 /* ARAppSearchViewController.m in Sources */, + 49473F3917C192AE004BF082 /* ARTextFieldWithPlaceholder.m in Sources */, + 3CCCC8A51899B6F9008015DD /* FairOrganizer.m in Sources */, + 60D90A0817C2182F0073D5B9 /* ARExternalWebBrowserViewController.m in Sources */, + E61B703919904D9F00260D29 /* ARTopTapThroughTableView.m in Sources */, + 499A587B166561E9004B0E2F /* ARFeedConstants.m in Sources */, + CB879D0C180746C900E2D8EC /* AuctionLot.m in Sources */, + 499A588E16658CAA004B0E2F /* Partner.m in Sources */, + 5EFE2BE41910FC81003B5EEA /* ARAppDelegate+Analytics.m in Sources */, + 3C8A916718A299BF0038A5B2 /* ARFairSectionViewController.m in Sources */, + 60838EAF17728C5D00869F6E /* ARHeartButton.m in Sources */, + 499A5894166683AC004B0E2F /* Artist.m in Sources */, + 5EE5DE14190167A400040B84 /* ARProfileViewController.m in Sources */, + E667F12218EC825D00503F50 /* ARHTTPRequestOperationLogger.m in Sources */, + 60327DD01987B7940075B399 /* ARDeveloperOptions.m in Sources */, + 60327DE11987FF490075B399 /* ARTopMenuNavigationDataSource.m in Sources */, + CBB469D0181F1F1200B5692B /* Bid.m in Sources */, + 499A58A516669CD6004B0E2F /* ARPostFeedItem.m in Sources */, + 3CAA412F18D88F2E000EE867 /* UIViewController+InnermostTopViewController.m in Sources */, + 60B79B84182C346700945FFF /* ARQuicksilverViewController.m in Sources */, + CB206F7717C5A5AD00A4FDC4 /* AROnboardingGeneTableController.m in Sources */, + 3CEE0B6218A16F6900FEA6E6 /* ArtsyAPI+Posts.m in Sources */, + 3CE0DA2F18A12335000E537A /* Video.m in Sources */, + 60B617CD1815991E00EBDC51 /* ARAuctionArtworkResultsViewController.m in Sources */, + 4953E8321668021D00A09726 /* Image.m in Sources */, + 60D90A0517C2109A0073D5B9 /* FeaturedLink.m in Sources */, + 60F1C50E17C0EC6A000938F7 /* ARBrowseViewController.m in Sources */, + E61446B2195A1CDC00BFB7C3 /* ARGeneFavoritesNetworkModel.m in Sources */, + 4953E8381668179500A09726 /* PostImage.m in Sources */, + CB42B64F181092480069A801 /* ARCountdownView.m in Sources */, + 4953E83B16681A4200A09726 /* ContentLink.m in Sources */, + 494332FD16692010005AB483 /* VideoContentLink.m in Sources */, + E676B2EB18D0CC330057B4E1 /* ARURLItemProvider.m in Sources */, + E6FD2F21187B0D77002AAFEB /* ARUserSettingsViewController.m in Sources */, + 60A49F601676559300B9B95D /* ARNetworkConstants.m in Sources */, + E61446B0195A1CDC00BFB7C3 /* ARFavoritesNetworkModel.m in Sources */, + 60431FB818042A1E000118D7 /* ARAppNotificationsDelegate.m in Sources */, + 5E0AEB7819B9EA43009F34DE /* ARShowNetworkModel.m in Sources */, + E616B2481911664900D1CBC6 /* ARArtworkView.m in Sources */, + CB61F13117C2D83F003DB8A9 /* AROnboardingTableViewCell.m in Sources */, + E676A62B18F3421600B9AF2C /* ARSeparatorViews.m in Sources */, + 607E2E7E17C8CA4D00396120 /* ARZoomArtworkImageViewController.m in Sources */, + 60DE2DE01677B2A600621540 /* ARFeedViewController.m in Sources */, + 60431FBB18042E63000118D7 /* ARAppBackgroundFetchDelegate.m in Sources */, + 6016C199178C2C37008EC8E7 /* ARArtworkFlowModule.m in Sources */, + 6044E545176E165600075B15 /* ARShowFeedViewController.m in Sources */, + 60DE2DE11677B30700621540 /* ARLoginViewController.m in Sources */, + 600A734317DE3F3F00E21233 /* ArtsyAPI+DeviceTokens.m in Sources */, + B31DB35B17FA234C009B122B /* ARArtworkMetadataView.m in Sources */, + 5435192E18A8E9420060F31E /* UIView+OldSchoolSnapshots.m in Sources */, + CB2C960417D3B4B500B36B44 /* ARFeedLinkUnitViewController.m in Sources */, + 609A82F417A1117E00AFDF13 /* ARInquireForArtworkViewController.m in Sources */, + CB206F7117C3FA8F00A4FDC4 /* ARPriceRangeViewController.m in Sources */, + 60229B4E1683BE280072DC12 /* ARSpinner.m in Sources */, + 491A4DE2168E4343003B2246 /* Gene.m in Sources */, + 602BC089168E0C0E00069FDB /* ARReusableLoadingView.m in Sources */, + 6099F8FE178DE9400004EF04 /* ARTheme.m in Sources */, + 6099F907178F24750004EF04 /* ARDefaultNavigationTransition.m in Sources */, + CB206F7A17C5AEB400A4FDC4 /* AROnboardingArtistTableController.m in Sources */, + 3CB37D97192246B500089A1D /* ArtsyAPI+ErrorHandlers.m in Sources */, + 6099F8F9178DBF160004EF04 /* ARModernPartnerShowTableViewCell.m in Sources */, + E676B2EE18D11A9B0057B4E1 /* ARImageItemProvider.m in Sources */, + 3CFBE32D18C3A3F400C781D0 /* ARNetworkErrorView.m in Sources */, + 3CF0774618DC6585009E18E4 /* ARKonamiKeyboardView.m in Sources */, + 60838EB51773547700869F6E /* ARViewInRoomTransition.m in Sources */, + 5ECFA8E81907E26E000B92EA /* ARSwitchView+Artist.m in Sources */, + 601F029817F3419400EB3E83 /* ARArtworkViewController+ButtonActions.m in Sources */, + E619969C19B9099400DB273C /* UIScrollView+HitTest.m in Sources */, + B3DDE9C418313CFF0012819F /* ARArtworkPriceRowView.m in Sources */, + 60E6447617BE424E004486B3 /* ARSwitchView.m in Sources */, + 3CF144A418E3727F00B1A764 /* UIViewController+ScreenSize.m in Sources */, + 603B55A117D64A1B00566935 /* ARArtworkPriceView.m in Sources */, + 6001414817CA33C100612DB4 /* ARArtworkRelatedArtworksView.m in Sources */, + 607D754A17C239E700CA1D41 /* ArtsyAPI+Following.m in Sources */, + E67655261900660500F9A704 /* ARWhitespaceGobbler.m in Sources */, + 60388703189C0F0B00D3EEAA /* ARTiledImageDataSourceWithImage.m in Sources */, + CB9E244117CBC36F00773A9A /* ARAuthProviders.m in Sources */, + E6AD6D4018E5C404005C8A3A /* ARArtworkInfoViewController.m in Sources */, + 607D756E17C3873700CA1D41 /* ARFavoriteItemViewCell.m in Sources */, + 540262C618A0FAFB00844AE1 /* ARButtonWithImage.m in Sources */, + 609A82FB17A1147800AFDF13 /* ARNavigationTransition.m in Sources */, + CB8D9D4417CEA7B900F3286B /* AROnboardingMoreInfoViewController.m in Sources */, + 5EDB120B197E691100E241F0 /* ARParallaxHeaderViewController.m in Sources */, + 60215FD717CDF9FA000F3A62 /* ARSignUpActiveUserViewController.m in Sources */, + 600EE29E16B3003F002E9F9A /* ARNavigationButton.m in Sources */, + 5EFF52BE197916C800E2A563 /* ARPendingOperationViewController.m in Sources */, + 6021F97F17B2B27400318185 /* ARArtworkWithMetadataThumbnailCell.m in Sources */, + 3CFB079118EB585B00792024 /* ARTile+ASCII.m in Sources */, + 3CFB078B18EB417F00792024 /* ARSecureTextFieldWithPlaceholder.m in Sources */, + 607E2E7B17C8C9FE00396120 /* ARArtworkDetailView.m in Sources */, + CB206F7417C569E800A4FDC4 /* ARPersonalizeViewController.m in Sources */, + 3CF144B418E47F9F00B1A764 /* ARSystemTime.m in Sources */, + 342F9C27685AF340FF34282B /* ARPageSubTitleView.m in Sources */, + 604166AD16C1D20900CFBD2F /* ArtsyAPI+Artworks.m in Sources */, + E66EE4AF196460620081ED0C /* ARFavoriteItemModule.m in Sources */, + 605B11B517CFE7CC00334196 /* ARTermsAndConditionsView.m in Sources */, + 604166B116C1D47100CFBD2F /* ArtsyAPI+CurrentUserFunctions.m in Sources */, + 603B3AEB1774A25700BA5BD3 /* ARNavigationController.m in Sources */, + 49EF164716C568EA00460BD7 /* Profile.m in Sources */, + 49405AB617BEC87A004F86D8 /* ARSignupViewController.m in Sources */, + 60EFA6F516CAA8180094AD7C /* ArtsyAPI+Feed.m in Sources */, + 3C35CC80189FF20D00E3D8DE /* ArtsyAPI+OrderedSets.m in Sources */, + CB260E8117C2C90900BF2012 /* ARCollectorStatusViewController.m in Sources */, + 5EFF52BA1976CA6C00E2A563 /* ARSearchFieldButton.m in Sources */, + 60FAEF43179843080031C88B /* ARFavoritesViewController.m in Sources */, + 3CD3679C189AC4F000285DF7 /* ARAspectRatioImageView.m in Sources */, + 607E2E8417C8CF6B00396120 /* ARArtworkPreviewActionsView.m in Sources */, + B316E2F418170DF40086CCDB /* SaleArtwork.m in Sources */, + 6037442516D4227500AE7788 /* ARSwitchBoard.m in Sources */, + 3C4AE9A419096CED009C0E8B /* ARFairMapAnnotationCallOutView.m in Sources */, + 60D83DE8189EAB82001672E9 /* ARFairShowMapper.m in Sources */, + 607E2E8A17C9121500396120 /* ARZoomImageTransition.m in Sources */, + 6016C198178C2C33008EC8E7 /* ARArtworkMasonryModule.m in Sources */, + 60CF97FC17BE9303005ED59B /* ARShadowView.m in Sources */, + 60F1C51817C0F3DE000938F7 /* ArtsyAPI+Browse.m in Sources */, + 062C202116DD76C90095A7EC /* ARZoomView.m in Sources */, + E6A3500518AAEBFF0075398F /* NSKeyedUnarchiver+ErrorLogging.m in Sources */, + 3CCCC89B18996DD4008015DD /* Post.m in Sources */, + 3CEE0B6818A16F9000FEA6E6 /* ArtsyAPI+Genes.m in Sources */, + 60F1C51B17C11303000938F7 /* ARGeneViewController.m in Sources */, + 3CD36799189A9B7A00285DF7 /* ARPostFeedItemLinkView.m in Sources */, + 342F95E4121D5C70F42DF9A2 /* ARFeedStatusIndicatorTableViewCell.m in Sources */, + 60A612B8189673F4008FC19D /* ARGeneArtworksNetworkModel.m in Sources */, + 60D8E63B18D256BB0040BEFD /* ARDemoSplashViewController.m in Sources */, + 3C48E2101965AC640077A80B /* ARCustomEigenLabels.m in Sources */, + 60AEC84716E0FB4D00514CB5 /* ARArtworkThumbnailMetadataView.m in Sources */, + 3C205ACD1908041700B3C2B4 /* ARSearchResultsDataSource.m in Sources */, + 4938742917BD512700724795 /* ARSignUpSplashViewController.m in Sources */, + 60327DCD1987AD240075B399 /* ARDispatchManager.m in Sources */, + 60B7604617C9FBEA00073A14 /* ARArtworkActionsView.m in Sources */, + 4938743617BDB2CD00724795 /* ARCrossfadingImageView.m in Sources */, + E6B958EF188DB24200D75C86 /* ARViewTagConstants.m in Sources */, + 60D90A0B17C218930073D5B9 /* ARInternalMobileWebViewController.m in Sources */, + 609A82F017A10C5C00AFDF13 /* ARNavigationTransitionController.m in Sources */, + 60F1C51217C0EDDB000938F7 /* ARBrowseFeaturedLinksCollectionView.m in Sources */, + CB4D652717C80A9600390550 /* AROnboardingSearchField.m in Sources */, + 607E2E7817C8C46100396120 /* ARArtworkViewController.m in Sources */, + 3CEE0B5F18A16EA200FEA6E6 /* ArtsyAPI+Profiles.m in Sources */, + 60438F8A1782F8CC00C1B63B /* ArtsyAPI+Search.m in Sources */, + 3C990C8818CF8E9000BF4C44 /* ARFairMapAnnotation.m in Sources */, + 064330E5170F526300FF6C41 /* ARArtistViewController.m in Sources */, + 49A76D0817592C96001D4B81 /* SearchResult.m in Sources */, + 608920C3178C682A00989A10 /* ARItemThumbnailViewCell.m in Sources */, + 49A76D0E17594E32001D4B81 /* Tag.m in Sources */, + 607E2E8717C8E87E00396120 /* ARArtworkPreviewImageView.m in Sources */, + E61446AC195A1CDC00BFB7C3 /* ARArtistFavoritesNetworkModel.m in Sources */, + 3C6CB61018ABFB8D008DFE3B /* ARRelatedArtistsViewController.m in Sources */, + 3C35CC7B189FF05E00E3D8DE /* OrderedSet.m in Sources */, + 60903CC4175CE21A002AB800 /* AROptions.m in Sources */, + 60903CCB175CE766002AB800 /* ARAdminSettingsViewController.m in Sources */, + 60F1C51517C0EFD7000938F7 /* ARBrowseFeaturedLinksCollectionViewCell.m in Sources */, + 60903CCF175CF088002AB800 /* ARGroupedTableViewCell.m in Sources */, + 49F0C67B17B9706000721244 /* AROnboardingViewController.m in Sources */, + CB11525B17C815210093D864 /* AROnboardingFollowableTableViewCell.m in Sources */, + 60903CD2175CF095002AB800 /* ARAnimatedTickView.m in Sources */, + 546A858217763349006D489B /* ARHeroUnitsNetworkModel.m in Sources */, + 5E50987D18F82FCF001AC704 /* AROfflineView.m in Sources */, + E6C0316018B291AB00137242 /* ARButtonWithCircularImage.m in Sources */, + 60903CD5175CF23F002AB800 /* ARTickedTableViewCell.m in Sources */, + E61446AD195A1CDC00BFB7C3 /* ARArtworkFavoritesNetworkModel.m in Sources */, + 60903CD8175CF27D002AB800 /* ARAdminTableViewCell.m in Sources */, + 60903CDD175DE1C2002AB800 /* ARFeed.m in Sources */, + 60CEA77E19B61F8000CC3A91 /* ARArtistNetworkModel.m in Sources */, + 603D7C05195884C100ACA840 /* ARAuctionBidderStateLabel.m in Sources */, + 60289229176BE98C00512977 /* ARViewInRoomViewController.m in Sources */, + 6016C190178C2B7F008EC8E7 /* AREmbeddedModelsViewController.m in Sources */, + CB821E7918217AF500CC934E /* ARAuctionBannerView.m in Sources */, + 60889874175DEBC2008C9319 /* ARFeedSubclasses.m in Sources */, + 6034EB1D175F68350070478D /* SiteHeroUnit.m in Sources */, + 6034EB2F1760A9BE0070478D /* ARTopMenuViewController.m in Sources */, + 60A0AD4A1797FD2E00E976B7 /* ARThemedFactory.m in Sources */, + 6036B5621760DC9100F1DD01 /* ARHeroUnitViewController.m in Sources */, + B316E2F7181713110086CCDB /* Bidder.m in Sources */, + E693BF7C18A1941D00D464BC /* ARSwitchCell.m in Sources */, + 6036B5651760E22E00F1DD01 /* ArtsyAPI+SiteFunctions.m in Sources */, + CB73B48B17D2581400891305 /* SiteFeature.m in Sources */, + 6099F90A17904F9B0004EF04 /* ARModelCollectionViewModule.m in Sources */, + 609B3C091761F80C00953CB2 /* ARSiteHeroUnitView.m in Sources */, + 605B11B217CFD78400334196 /* AROnboardingWebViewController.m in Sources */, + 342F971CCE09F6261F8460AF /* ARFairViewController.m in Sources */, + 342F9CDD0CDC753B394F3CFC /* Fair.m in Sources */, + 342F9D3F000632CC30AC5260 /* ARNavigationButtonsViewController.m in Sources */, + 342F9D9296B47F66184FA760 /* NSDate+DateRange.m in Sources */, + 3C33298D18AD3324006D28C0 /* ARFairSearchViewController.m in Sources */, + 3C9F215F18B25D0D00D8898B /* ARSearchViewController.m in Sources */, + E61446B1195A1CDC00BFB7C3 /* ARFollowableNetworkModel.m in Sources */, + 342F99C0F0921C78A384B0E6 /* UIFont+ArtsyFonts.m in Sources */, + 3CCCC8A21899B412008015DD /* ArtsyAPI+Fairs.m in Sources */, + E667F12C18EC889F00503F50 /* ARLogFormatter.m in Sources */, + 342F961978DD1D771928472A /* UIDevice-Hardware.m in Sources */, + 342F90AEE6950A52210D2DC2 /* UILabel+Typography.m in Sources */, + 3C6CB60918ABD8D2008DFE3B /* ARPostsViewController.m in Sources */, + 342F96505F255C0F443B5EAA /* UIViewController+SimpleChildren.m in Sources */, + 342F902302C1CF0FD342AB93 /* NSDate+Util.m in Sources */, + 342F9A60D7F0C30310535232 /* UIImage+ImageFromColor.m in Sources */, + 342F9A43E9B4295EF2CE07E2 /* UIViewController+FullScreenLoading.m in Sources */, + 342F91B952D170DCF5A0CDA4 /* UIImageView+AsyncImageLoading.m in Sources */, + E667F12918EC82AB00503F50 /* ARLogger.m in Sources */, + 3C94B2DA192BF109008D04DF /* ARFairMapPreview.m in Sources */, + E6C13AC41A23AB160050AB53 /* ARPersonalizeWebViewController.m in Sources */, + 342F9F54666BFDFF200E31AE /* UIView+HitTestExpansion.m in Sources */, + 342F9BBE60676CEC2AD66C5A /* NSString+StringCase.m in Sources */, + 342F9CEA884D36A98BE6BA96 /* NSString+StringSize.m in Sources */, + 342F979DA15866BB8A70CC23 /* ORStackView+ArtsyViews.m in Sources */, + E693BF7D18A1941D00D464BC /* ARTextInputCell.m in Sources */, + 342F9667FC693B47498549FE /* UIApplicationStateEnum.m in Sources */, + 342F9D1673764FE7442062AC /* ARNetworkErrorManager.m in Sources */, + 342F92A94769B065A53C6735 /* UIViewController+ARStateRestoration.m in Sources */, + E6E07D6D195DF5D800403D2B /* ARSwitchView+Favorites.m in Sources */, + 60327DC91987AAD00075B399 /* ARTabContentView.m in Sources */, + 60388706189C103F00D3EEAA /* ARFairMapZoomManager.m in Sources */, + 342F9272F2C534C1DFE2EB95 /* ARParallaxEffect.m in Sources */, + 342F995E23C13C290DEFF23D /* ARScrollNavigationChief.m in Sources */, + 342F9BDEE699AB7F9EA34E9F /* ARFileUtils.m in Sources */, + 342F9841DEB316C8E4E3C68B /* ARValueTransformer.m in Sources */, + 5ECFA8EF1907FC6C000B92EA /* ARSwitchView+FairGuide.m in Sources */, + E676B2F218D224000057B4E1 /* ARMessageItemProvider.m in Sources */, + 342F95D6D6422286C9F5A33A /* ARStandardDateFormatter.m in Sources */, + 342F9061CC93426E7F8579A2 /* ARFeedImageLoader.m in Sources */, + 342F906FD605E75158D32876 /* ARSharingController.m in Sources */, + 342F9DDFA581974195A708D3 /* ARTrialController.m in Sources */, + 342F91E3124D3E5EA18AB301 /* ARSplitStackView.m in Sources */, + 342F9D9D248E4024524174E8 /* ARFairMapViewController.m in Sources */, + 342F92E8397ACCA0D2EFF1CB /* MapPoint.m in Sources */, + 342F90FACF8AABA704AF5FC1 /* PartnerShowFairLocation.m in Sources */, + 342F9F5789AD65AD09205030 /* MapFeature.m in Sources */, + 342F97CF4D0BD118671622A0 /* ARFairMapAnnotationView.m in Sources */, + 342F95DB4E709752C0EC1FE2 /* Map.m in Sources */, + 342F94077E27A45531E7A76F /* ARBidButton.m in Sources */, + 342F9CB876CC63CF8E8C656F /* ARTextView.m in Sources */, + 342F917E6CFBD49D6F43873D /* ARCollapsableTextView.m in Sources */, + 342F97971D28FDC703392765 /* MTLModel+JSON.m in Sources */, + 342F949383329F78F589BE53 /* MTLModel+Dictionary.m in Sources */, + 342F996B2CF23951168534AD /* Follow.m in Sources */, + 342F9308EE5B3487AE62276D /* ARFairShowViewController.m in Sources */, + 342F953528DFA7BCEAC0552A /* ArtsyAPI+Shows.m in Sources */, + 342F9C00DD759E6DEA845426 /* ARImagePageViewController.m in Sources */, + 5E284609194A2E58007274AB /* ARFairGuideContainerViewController.m in Sources */, + 342F9CF7231DFEF359B2D829 /* ARFollowableButton.m in Sources */, + 342F9654C7AF4AEF8C21BC11 /* ARActionButtonsView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3ECE8221819D1E6009F5C5B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6B9FA7918A96BF500E961F9 /* ARBrowseFeaturedLinkInsetCellTests.m in Sources */, + 3CB97D8F1887099E008C44FE /* ARLoginViewControllerTests.m in Sources */, + 3C6AA7CB1885F38D00501F07 /* SaleArtwork+Extensions.m in Sources */, + 60F8FFC1197E773E00DC3869 /* ARArtworkSetViewControllerSpec.m in Sources */, + 5EBDC95119794D840082C514 /* ARSearchFieldButtonTests.m in Sources */, + 5E0AEB7A19B9EF57009F34DE /* ARShowNetworkModelTests.m in Sources */, + 3CCCC89D18997948008015DD /* FairTests.m in Sources */, + 3CF144A918E4607900B1A764 /* SystemTimeTests.m in Sources */, + E649708918D7762F009DB0C4 /* ARImageItemProviderTests.m in Sources */, + E61E772619A5477300C55E14 /* ARExpectaExtensions.m in Sources */, + 3CB9A20D18F303B20056C72B /* ARArtworkActionsViewTests.m in Sources */, + 3C4877B3192A745400F40062 /* ARFairMapAnnotationCallOutViewTests.m in Sources */, + 3C3FEA8E1884346D00E1A16F /* ARUserManager+Stubs.m in Sources */, + 60327DE9198933610075B399 /* ARTabContentViewSpec.m in Sources */, + B30FEF5A181EEA47009E4EAD /* ARBidButtonTests.m in Sources */, + 5EBDC95319794DF10082C514 /* ARPendingOperationViewControllerTests.m in Sources */, + 3CACF2AC18F591E40054091E /* ARThemeTests.m in Sources */, + 60327DEB198933610075B399 /* ARTopMenuNavigationDataSourceSpec.m in Sources */, + 5EB33E74197EBDE200706EB1 /* ARParallaxHeaderViewControllerTests.m in Sources */, + E6F84D021A0AB40500BF99A3 /* ARSignUpActiveUserViewControllerTests.m in Sources */, + 3C6BDCC318E0B8D50028EF5D /* ARInquireForArtworkViewControllerTests.m in Sources */, + 3C205B59189000A5004280E0 /* ARAppNotificationsDelegateTests.m in Sources */, + E655E4B8194B4BEF00F2B7DA /* ARArtworkFavoritesNetworkModelTests.m in Sources */, + E67DF2131A40A73C00C8495E /* ARSearchViewControllerSpec.m in Sources */, + 3C6BDCBD18E0AEF60028EF5D /* ArtsyAPI+PrivateTests.m in Sources */, + 3CB37D991922483100089A1D /* ArtsyAPI+ErrorHandlers.m in Sources */, + E6C13AC11A23AA420050AB53 /* AROnboardingViewControllerTests.m in Sources */, + E6B958F1188DB6F800D75C86 /* ARArtworkViewControllerTests.m in Sources */, + E63C796C198811E400579C04 /* ARArtworkRelatedArtworksViewTests.m in Sources */, + 3C11CD28189B07810060B26B /* ARAspectRatioImageViewTests.m in Sources */, + 3CF144AB18E460BE00B1A764 /* ArtsyAPI+SystemTimeTests.m in Sources */, + 3C28D3DB18BE180B00C846EA /* ARNavigationButtonTests.m in Sources */, + 3C6AEE27188F228600DD98FC /* ARInternalMobileWebViewControllerTests.m in Sources */, + 3C6AEE2D188F2D3E00DD98FC /* ARSwitchBoardTests.m in Sources */, + E6718316194A595F001A5566 /* ARTestContext.m in Sources */, + 3CE0DA3218A13604000E537A /* OHHTTPStubs+JSON.m in Sources */, + 60327DEC198933610075B399 /* ARTopMenuViewControllerSpec.m in Sources */, + 3CA37E7C1910217500B06E81 /* ARHeartButtonTests.m in Sources */, + 3CEE0B6A18A18CDA00FEA6E6 /* ProfileTests.m in Sources */, + E65BB51F1A3FB552004C4DB4 /* ARAppSearchViewControllerSpec.m in Sources */, + 3CE7F34418A99E6D002BA993 /* SiteHeroUnitTests.m in Sources */, + 5E9A78231906BA3D00734E1B /* OCMArg+ClassChecker.m in Sources */, + 3CA17D591901A4900010C9F5 /* SpectaDSL+Sleep.m in Sources */, + 3CE75A0B18B6367F00885355 /* ARValueTransformerTests.m in Sources */, + 3CA1E820188465F0003C622D /* Sale+Extensions.m in Sources */, + E61E773919A7E8D500C55E14 /* ARGeneViewControllerTests.m in Sources */, + E611847018D78068000FE4C9 /* ARMessageItemProviderTests.m in Sources */, + E612BC3F19D1B9DE00585CD6 /* ARFollowableButtonTests.m in Sources */, + B3ECE83C1819D1FD009F5C5B /* SaleArtworkTests.m in Sources */, + 3C2E6C5C192262A3009DAB28 /* ARRouterTests.m in Sources */, + 5E0AEB7D19B9FAED009F34DE /* ARStubbedShowNetworkModel.m in Sources */, + 3C6BDCC018E0B3E40028EF5D /* MutableNSURLResponse.m in Sources */, + 546778EE18A95642002C4C71 /* ARAppDelegate+Testing.m in Sources */, + 3CAA413218D8CC32000EE867 /* ARFairFavoritesNetworkModelTests.m in Sources */, + 60327DEA198933610075B399 /* ARTestTopMenuNavigationDataSource.m in Sources */, + 3CA1E82518846B3A003C622D /* SaleTests.m in Sources */, + 60CEA78119B6254800CC3A91 /* ARStubbedArtistNetworkModel.m in Sources */, + 5E9A782019068EDF00734E1B /* ARProfileViewControllerTests.m in Sources */, + E6EC246118F7069300C89192 /* PartnerTests.m in Sources */, + 3CD3679F189ADBEF00285DF7 /* ARPostFeedItemLinkViewTests.m in Sources */, + 3CA55D8918BFF8F800B44CD3 /* MapFeatureTests.m in Sources */, + 3C6CB60518ABC7BB008DFE3B /* UserTests.m in Sources */, + 3CA1E8231884663E003C622D /* Bid+Extensions.m in Sources */, + E6718310194A3D1E001A5566 /* ARHeroUnitTests.m in Sources */, + 3CAED17C188026AC00840608 /* ARUserManagerTests.m in Sources */, + 3C5C7E4018970E8B003823BB /* UIApplicationStateEnumTests.m in Sources */, + 5EBDC94E19792C3A0082C514 /* ARNavigationControllerTests.m in Sources */, + 3CFBE33618C5848900C781D0 /* ARFileUtilsTests.m in Sources */, + 60327DD21987BA830075B399 /* ARDeveloperOptionsSpec.m in Sources */, + 3C7294CC196C3E660073663D /* ARFairShowViewControllerTests.m in Sources */, + E6BC2C3E197EDC950063ED3C /* ARBrowseViewControllerTests.m in Sources */, + 3CBB03A8192BA94C00689F89 /* ARFairArtistViewControllerTests.m in Sources */, + 3CF144B618E4802400B1A764 /* ARSystemTimeTests.m in Sources */, + E6F111A018A2A7CB00D33C3E /* ARBrowseFeaturedLinksCollectionViewTests.m in Sources */, + 3C7A7FA718C6EDAB00E8D336 /* ArtworkTests.m in Sources */, + E67E1DB219475642004252E0 /* ARAnimatedTickViewTest.m in Sources */, + 3CE7F34218A99E62002BA993 /* ARHeroUnitsNetworkModelTests.m in Sources */, + E676B2F518D230A00057B4E1 /* ARURLItemProviderTests.m in Sources */, + 342F92546490FE42643B3060 /* ARFairViewControllerTests.m in Sources */, + 3C35CC7D189FF14800E3D8DE /* OrderedSetTests.m in Sources */, + 5ECFA8EA1907E9AB000B92EA /* ARSwitchViewTests.m in Sources */, + 3CD0BB8918EB0CDF00A59910 /* ARFavoritesViewControllerTests.m in Sources */, + 342F9FBB86D02FABC8AA9339 /* ARNavigationButtonsViewControllerTests.m in Sources */, + E6AD6D4218E610DA005C8A3A /* ArtsyAPI+ArtworksTests.m in Sources */, + 3CF144B818E9E00400B1A764 /* ARSignUpSplashViewControllerTests.m in Sources */, + 3C33299218AD9399006D28C0 /* ARFairSearchViewControllerTests.m in Sources */, + 3CCCC8941899676E008015DD /* ARFairPostsViewControllerTests.m in Sources */, + 3C1266F118BE6CC700B5AE72 /* ARFairGuideViewControllerTests.m in Sources */, + 5E71AFC7195C64C1000F6325 /* ARFairGuideContainerViewControllerTests.m in Sources */, + 54289FEE18AA7F4E00681E49 /* UINavigationController_InnermostTopViewControllerSpec.m in Sources */, + 342F98EAAAAA4C3264E90CD2 /* ARGeneArtworksNetworkModelTests.m in Sources */, + E611847218D7B4C4000FE4C9 /* ARSharingControllerTests.m in Sources */, + 3CA0A17D18EF633900C361E5 /* ARArtistViewControllerTests.m in Sources */, + 608EE3DB19954CEB001F4FE0 /* UIViewController+PresentWithFrame.m in Sources */, + 342F9E0892E99A2F66657964 /* Artwork+Extensions.m in Sources */, + 342F9715E899AC7D24E70734 /* ORStackViewArtsyCategoriesTests.m in Sources */, + 342F925E34CAC3E5DB8E5F52 /* ARFairMapViewControllerTests.m in Sources */, + 342F99FA4B25031ABC0613DA /* ARTiledImageDataSourceWithImageTests.m in Sources */, + 342F9276A48EA339833228D8 /* ARArtworkDetailViewTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B3ECE8351819D1E7009F5C5B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 49BA7DFE1655ABE600C06572 /* Artsy */; + targetProxy = B3ECE8341819D1E7009F5C5B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 49BA7E0C1655ABE600C06572 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 49BA7E0D1655ABE600C06572 /* en */, + ); + name = InfoPlist.strings; + path = ..; + sourceTree = ""; + }; + B3ECE82E1819D1E6009F5C5B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + B3ECE82F1819D1E6009F5C5B /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 49BA7E211655ABE600C06572 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DAR_SHOW_ALL_DEBUG=1"; + PROVISIONING_PROFILE = "b82e2154-c0ac-43d2-ac9d-3d72c3f83610"; + RUN_CLANG_STATIC_ANALYZER = NO; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 49BA7E221655ABE600C06572 /* Beta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = "${inherited}"; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + PROVISIONING_PROFILE = "11c30f45-73c9-41d5-b84c-6349065af307"; + RUN_CLANG_STATIC_ANALYZER = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Beta; + }; + 49BA7E241655ABE600C06572 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A060C6085108B7738F5ADD02 /* Pods.debug.xcconfig */; + buildSettings = { + APPLICATION_BUNDLE_IDENTIFIER = net.artsy.artsy.dev; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + ASSETCATALOG_WARNINGS = NO; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + ); + GCC_INCREASE_PRECOMPILED_HEADER_SHARING = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy/App/Artsy-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = NO; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = NO; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_SHADOW = NO; + GCC_WARN_STRICT_SELECTOR_MATCH = NO; + GCC_WARN_UNDECLARED_SELECTOR = NO; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = NO; + INFOPLIST_FILE = "Artsy/App/Artsy-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = Artsy; + PROVISIONING_PROFILE = "c1818180-549a-4ee6-a1cf-2b6c08e0a1d1"; + RUN_CLANG_STATIC_ANALYZER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_AFTER_BUILD = YES; + VALID_ARCHS = "$(ARCHS_STANDARD)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 49BA7E251655ABE600C06572 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7B33890F5B4F8EFF536074A4 /* Pods.beta.xcconfig */; + buildSettings = { + APPLICATION_BUNDLE_IDENTIFIER = net.artsy.artsy.beta; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + ASSETCATALOG_WARNINGS = NO; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + ); + GCC_INCREASE_PRECOMPILED_HEADER_SHARING = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy/App/Artsy-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = NO; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = NO; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_SHADOW = NO; + GCC_WARN_STRICT_SELECTOR_MATCH = NO; + GCC_WARN_UNDECLARED_SELECTOR = NO; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = NO; + INFOPLIST_FILE = "Artsy/App/Artsy-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = Artsy; + RUN_CLANG_STATIC_ANALYZER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_AFTER_BUILD = YES; + VALID_ARCHS = "$(ARCHS_STANDARD)"; + WRAPPER_EXTENSION = app; + }; + name = Beta; + }; + 60AF3D0218D33C80008F3CC0 /* Demo */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = "${inherited}"; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + PROVISIONING_PROFILE = "a947500d-6376-41ca-b4f7-12ef34f2459f"; + RUN_CLANG_STATIC_ANALYZER = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Demo; + }; + 60AF3D0318D33C80008F3CC0 /* Demo */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E2C583DC5F5EBC24D3CC3B76 /* Pods.demo.xcconfig */; + buildSettings = { + APPLICATION_BUNDLE_IDENTIFIER = net.artsy.artsy.demo; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + ASSETCATALOG_WARNINGS = NO; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + ); + GCC_INCREASE_PRECOMPILED_HEADER_SHARING = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy/App/Artsy-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "STORE=1", + "DEMO_MODE=1", + ); + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = NO; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = NO; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_SHADOW = NO; + GCC_WARN_STRICT_SELECTOR_MATCH = NO; + GCC_WARN_UNDECLARED_SELECTOR = NO; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = NO; + INFOPLIST_FILE = "Artsy/App/Artsy-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = Artsy; + RUN_CLANG_STATIC_ANALYZER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_AFTER_BUILD = YES; + VALID_ARCHS = "$(ARCHS_STANDARD)"; + WRAPPER_EXTENSION = app; + }; + name = Demo; + }; + 60AF3D0418D33C80008F3CC0 /* Demo */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3E944480103FA2476C7049C2 /* Pods-Artsy Tests.demo.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Artsy.app/Artsy"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy Tests/Artsy Tests-Prefix.pch"; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Artsy Tests/Artsy Tests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + TEST_HOST = "$(BUNDLE_LOADER)"; + VALIDATE_PRODUCT = NO; + WRAPPER_EXTENSION = xctest; + }; + name = Demo; + }; + B34D39FE17DFD99600308CAB /* Store */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = "${inherited}"; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + PROVISIONING_PROFILE = "049e1a3a-52f3-41da-b341-ae36edc7fe17"; + RUN_CLANG_STATIC_ANALYZER = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Store; + }; + B34D39FF17DFD99600308CAB /* Store */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 885DE1DBF1C1DB636BA9F61A /* Pods.store.xcconfig */; + buildSettings = { + APPLICATION_BUNDLE_IDENTIFIER = net.artsy.artsy; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + ASSETCATALOG_WARNINGS = NO; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + ); + GCC_INCREASE_PRECOMPILED_HEADER_SHARING = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy/App/Artsy-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "STORE=1", + ); + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = NO; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = NO; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_SHADOW = NO; + GCC_WARN_STRICT_SELECTOR_MATCH = NO; + GCC_WARN_UNDECLARED_SELECTOR = NO; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = NO; + INFOPLIST_FILE = "Artsy/App/Artsy-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = Artsy; + RUN_CLANG_STATIC_ANALYZER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_AFTER_BUILD = YES; + VALID_ARCHS = "$(ARCHS_STANDARD)"; + WRAPPER_EXTENSION = app; + }; + name = Store; + }; + B3ECE8371819D1E7009F5C5B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8A6745B7A19EEDA119AEA48E /* Pods-Artsy Tests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Artsy.app/Artsy"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy Tests/Artsy Tests-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/Artsy\\ Tests/ReferenceImages\\\"\"", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + INFOPLIST_FILE = "Artsy Tests/Artsy Tests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + B3ECE8391819D1E7009F5C5B /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1A25A9493C56A91CDFB95D0B /* Pods-Artsy Tests.beta.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Artsy.app/Artsy"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy Tests/Artsy Tests-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Artsy Tests/Artsy Tests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + TEST_HOST = "$(BUNDLE_LOADER)"; + VALIDATE_PRODUCT = NO; + WRAPPER_EXTENSION = xctest; + }; + name = Beta; + }; + B3ECE83A1819D1E7009F5C5B /* Store */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8E3DD2A89F196F5E5BFB9689 /* Pods-Artsy Tests.store.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Artsy.app/Artsy"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Artsy Tests/Artsy Tests-Prefix.pch"; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Artsy Tests/Artsy Tests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + TEST_HOST = "$(BUNDLE_LOADER)"; + VALIDATE_PRODUCT = NO; + WRAPPER_EXTENSION = xctest; + }; + name = Store; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 49BA7DF91655ABE600C06572 /* Build configuration list for PBXProject "Artsy" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 49BA7E211655ABE600C06572 /* Debug */, + 49BA7E221655ABE600C06572 /* Beta */, + B34D39FE17DFD99600308CAB /* Store */, + 60AF3D0218D33C80008F3CC0 /* Demo */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Beta; + }; + 49BA7E231655ABE600C06572 /* Build configuration list for PBXNativeTarget "Artsy" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 49BA7E241655ABE600C06572 /* Debug */, + 49BA7E251655ABE600C06572 /* Beta */, + B34D39FF17DFD99600308CAB /* Store */, + 60AF3D0318D33C80008F3CC0 /* Demo */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Beta; + }; + B3ECE8361819D1E7009F5C5B /* Build configuration list for PBXNativeTarget "Artsy Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3ECE8371819D1E7009F5C5B /* Debug */, + B3ECE8391819D1E7009F5C5B /* Beta */, + B3ECE83A1819D1E7009F5C5B /* Store */, + 60AF3D0418D33C80008F3CC0 /* Demo */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Beta; + }; +/* End XCConfigurationList section */ + }; + rootObject = 49BA7DF61655ABE600C06572 /* Project object */; +} diff --git a/Artsy.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Artsy.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..a7cf77502ce --- /dev/null +++ b/Artsy.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Artsy.xcworkspace/contents.xcworkspacedata b/Artsy.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..5d2b8faf164 --- /dev/null +++ b/Artsy.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Artsy.xcworkspace/xcshareddata/Artsy.xccheckout b/Artsy.xcworkspace/xcshareddata/Artsy.xccheckout new file mode 100644 index 00000000000..a3910168df8 --- /dev/null +++ b/Artsy.xcworkspace/xcshareddata/Artsy.xccheckout @@ -0,0 +1,125 @@ + + + + + IDESourceControlProjectFavoriteDictionaryKey + + IDESourceControlProjectIdentifier + EF742A98-E127-40BF-B21F-DB4EE9116803 + IDESourceControlProjectName + Artsy + IDESourceControlProjectOriginsDictionary + + 0B1CB3A2D81D8FD5BBD2FD17F27A95AB88A2DEE2 + https://github.com/orta/eigen.git + 1E01CC43-5AFE-43BF-95A0-B3371CC7E3C3 + file:///Users/orta/Library/Caches/CocoaPods/GitHub/b13734b3f7530db77cfbf57ab7084df18dcb2cac/ + 29BA1618-B064-41B4-87B3-380E6E0F669A + file:///Users/orta/Library/Caches/CocoaPods/GitHub/004a3cc1761b10f3f82a090cc26cc2c2ce1d838c/ + 50E2DA14808978A91345AF54368A077CA57D1CE5 + https://github.com/1aurabrown/Artsy-UIButtons.git + 845859A7-FA92-405A-8322-857295701E99 + file:///Users/orta/Library/Caches/CocoaPods/GitHub/24df5a2085de21d8fad23d8c62031457c0bf59f1/ + A0BF4531-3219-4B64-8B43-1EDF74C43CA0 + file:///Users/orta/Library/Caches/CocoaPods/GitHub/a7f129229d47b74a225e1e2ed0bc604f35d71ac7/ + B8053E78-5929-4A95-A657-903D63D4CC60 + file:///Users/orta/Library/Caches/CocoaPods/GitHub/028c8a87e1617d65f3454a8c77b6e761087587df/ + FD8ED308-808D-4971-A0B2-DE6907733005 + file:///Users/orta/Library/Caches/CocoaPods/GitHub/2e6cb1306425d8124977abb073411d99d18e3051/ + + IDESourceControlProjectPath + Artsy.xcworkspace + IDESourceControlProjectRelativeInstallPathDictionary + + 0B1CB3A2D81D8FD5BBD2FD17F27A95AB88A2DEE2 + .. + 1E01CC43-5AFE-43BF-95A0-B3371CC7E3C3 + ../Pods/JLRoutes + 29BA1618-B064-41B4-87B3-380E6E0F669A + ../Pods/ARGenericTableViewController + 50E2DA14808978A91345AF54368A077CA57D1CE5 + ../../Artsy-UIButtons + 845859A7-FA92-405A-8322-857295701E99 + ../Pods/TSMiniWebBrowser + A0BF4531-3219-4B64-8B43-1EDF74C43CA0 + ../Pods/AFNetworking + B8053E78-5929-4A95-A657-903D63D4CC60 + ../Pods/NAMapKit + FD8ED308-808D-4971-A0B2-DE6907733005 + ../Pods/FODFormKit + + IDESourceControlProjectURL + https://github.com/orta/eigen.git + IDESourceControlProjectVersion + 111 + IDESourceControlProjectWCCIdentifier + 0B1CB3A2D81D8FD5BBD2FD17F27A95AB88A2DEE2 + IDESourceControlProjectWCConfigurations + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + A0BF4531-3219-4B64-8B43-1EDF74C43CA0 + IDESourceControlWCCName + AFNetworking + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + 29BA1618-B064-41B4-87B3-380E6E0F669A + IDESourceControlWCCName + ARGenericTableViewController + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + 50E2DA14808978A91345AF54368A077CA57D1CE5 + IDESourceControlWCCName + Artsy-UIButtons + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + 0B1CB3A2D81D8FD5BBD2FD17F27A95AB88A2DEE2 + IDESourceControlWCCName + eigen + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + FD8ED308-808D-4971-A0B2-DE6907733005 + IDESourceControlWCCName + FODFormKit + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + 1E01CC43-5AFE-43BF-95A0-B3371CC7E3C3 + IDESourceControlWCCName + JLRoutes + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + B8053E78-5929-4A95-A657-903D63D4CC60 + IDESourceControlWCCName + NAMapKit + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + 845859A7-FA92-405A-8322-857295701E99 + IDESourceControlWCCName + TSMiniWebBrowser + + + + diff --git a/Artsy/App/Artsy-Info.plist b/Artsy/App/Artsy-Info.plist new file mode 100644 index 00000000000..6464214cff6 --- /dev/null +++ b/Artsy/App/Artsy-Info.plist @@ -0,0 +1,138 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Artsy + CFBundleDocumentTypes + + + CFBundleTypeIconFiles + + CFBundleTypeName + Link + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + net.artsy.link + + + + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${APPLICATION_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.7.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + fb308278682573501 + + + + CFBundleURLName + net.artsy.artsy + CFBundleURLSchemes + + artsy + + + + CFBundleTypeRole + Viewer + CFBundleURLSchemes + + applewebdata + + + + CFBundleVersion + 2015.01.05 + FacebookAppID + 308278682573501 + FacebookDisplayName + Artsy + GITCommitRev + c419a33 + GITCommitSha + c419a33b2834bac57daaff33f52b52416d53037b + GITRemoteOriginURL + https://github.com/1aurabrown/eigen.git + LSRequiresIPhoneOS + + UIAppFonts + + AGaramondPro-BoldItalic.otf + AGaramondPro-Bold.otf + AGaramondPro-Italic.otf + AGaramondPro-Regular.otf + ITC_Avant_Garde_Gothic__Demi.ttf + AGaramondPro-Semibold.otf + + UIBackgroundModes + + remote-notification + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleDefault + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UIViewControllerBasedStatusBarAppearance + + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Link + UTTypeIdentifier + net.artsy.link + UTTypeTagSpecification + + public.filename-extension + Artsy + + + + + diff --git a/Artsy/App/Artsy-Prefix.pch b/Artsy/App/Artsy-Prefix.pch new file mode 100644 index 00000000000..243c05bd523 --- /dev/null +++ b/Artsy/App/Artsy-Prefix.pch @@ -0,0 +1,79 @@ +// +// Artsy-Prefix.pch +// Artsy +// +// Created by Orta Therox on 07/31/2013. +// Copyright (c) 2013 Artsy. All rights reserved. +// + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 4.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import + #import + #import + + #import "Constants.h" + + #import + #import + #import + #import + #import + #import + #import + #import + #import + #import + #import + #import + + #import "ArtsyAPI.h" + #import "Models.h" + #import "Categories.h" + #import "StyledSubclasses.h" + #import "AROptions.h" + #import "ARTheme.h" + #import "ARSwitchBoard.h" + #import "ARNetworkErrorManager.h" + #import "ARTrialController.h" + #import "ARTopMenuViewController.h" + #import "ARScrollNavigationChief.h" + #import "ARSystemTime.h" + #import "NSString+ObjectiveSugar.h" + #import "ARDispatchManager.h" + #import "ARDeveloperOptions.h" + + #import + #import + + #ifdef DEBUG + static const int ddLogLevel = LOG_LEVEL_VERBOSE; + static const int httpLogLevel = AFLoggerLevelInfo; + #else + static const int ddLogLevel = LOG_LEVEL_WARN; + static const int httpLogLevel = AFLoggerLevelError; + #endif + + #ifdef DEMO_MODE + static const BOOL ARIsRunningInDemoMode = YES; + #else + static const BOOL ARIsRunningInDemoMode = NO; + #endif + + #import "ARLogger.h" + +#endif + +#ifndef __FEATURES +#define __FEATURES + +// define here all the features you want on +//#define STUBBED_FEED + +#endif \ No newline at end of file diff --git a/Artsy/App/main.m b/Artsy/App/main.m new file mode 100644 index 00000000000..0e7a05967f3 --- /dev/null +++ b/Artsy/App/main.m @@ -0,0 +1,11 @@ +#import "ARAppDelegate.h" +#import + +int main(int argc, char *argv[]) { + + @autoreleasepool { + return UIApplicationMain(argc, argv, + NSStringFromClass([ORKeyboardReactingApplication class]), + NSStringFromClass([JSDecoupledAppDelegate class])); + } +} diff --git a/Artsy/Classes/ARAppDelegate+Analytics.h b/Artsy/Classes/ARAppDelegate+Analytics.h new file mode 100644 index 00000000000..bc1aaeae94b --- /dev/null +++ b/Artsy/Classes/ARAppDelegate+Analytics.h @@ -0,0 +1,7 @@ +#import "ARAppDelegate.h" + +@interface ARAppDelegate (Analytics) + +- (void)setupAnalytics; + +@end diff --git a/Artsy/Classes/ARAppDelegate+Analytics.m b/Artsy/Classes/ARAppDelegate+Analytics.m new file mode 100644 index 00000000000..05c1df4e4f8 --- /dev/null +++ b/Artsy/Classes/ARAppDelegate+Analytics.m @@ -0,0 +1,1099 @@ +#import "ARAppDelegate+Analytics.h" +#import +#import +#import "ARAnalyticsConstants.h" +#import + +#import "ARUserManager.h" + +// View Controllers +#import "ARFairGuideViewController.h" +#import "ARFairArtistViewController.h" +#import "ARFairShowViewController.h" +#import "ARProfileViewController.h" +#import "ARAppSearchViewController.h" +#import "AROnboardingGeneTableController.h" +#import "ARInquireForArtworkViewController.h" +#import "ARArtworkViewController.h" +#import "AROnboardingArtistTableController.h" +#import "ARLoginViewController.h" +#import "ARSignUpActiveUserViewController.h" +#import "ARSignupViewController.h" +#import "ARGeneViewController.h" +#import "ARPersonalizeViewController.h" +#import "ARViewInRoomViewController.h" +#import "AROnboardingMoreInfoViewController.h" +#import "ARArtistViewController.h" +#import "ARFairViewController.h" +#import "ARFairSearchViewController.h" +#import "ARCollectorStatusViewController.h" +#import "ARSharingController.h" + +// Views +#import "ARHeartButton.h" +#import "ARFairMapAnnotationCallOutView.h" +#import "ARSiteHeroUnitView.h" +#import "ARButtonWithImage.h" + +// Models +#import "ARFairFavoritesNetworkModel+Private.h" +#import + +@implementation ARAppDelegate (Analytics) + +- (void)setupAnalytics +{ + ArtsyKeys *keys = [[ArtsyKeys alloc] init]; + NSString *mixpanelToken = keys.mixpanelProductionAPIClientKey; + + NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; + if ([bundleID containsString:@".dev"]) { + mixpanelToken = keys.mixpanelDevAPIClientKey; + + } else if ([bundleID containsString:@".beta"]) { + mixpanelToken = keys.mixpanelStagingAPIClientKey; + + } else if ([bundleID containsString:@".demo"]) { + mixpanelToken = keys.mixpanelInStoreAPIClientKey; + } + + ARAppDelegate *appDelegate = [ARAppDelegate sharedInstance]; + +#if DEBUG + [[BITHockeyManager sharedHockeyManager] setDisableUpdateManager:YES]; + [[BITHockeyManager sharedHockeyManager] setDisableCrashManager: YES]; +#endif + + ARAnalyticsEventPropertiesBlock fairAndProfileIDBlock = ^NSDictionary*(ARFairGuideViewController *controller, RACTuple *_) { + return @{ + @"profile_id" : controller.fair.organizer.profileID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + }; + }; + + ARAnalyticsEventShouldFireBlock heartedShouldFireBlock = ^BOOL(id controller, RACTuple *parameters) { + ARHeartButton *sender = parameters.first; + return sender.isHearted; + }; + + ARAnalyticsEventShouldFireBlock unheartedShouldFireBlock = ^BOOL(id controller, RACTuple *parameters) { + return !heartedShouldFireBlock(controller, parameters); + }; + + [ARAnalytics setupWithAnalytics: + @{ + ARHockeyAppBetaID:@"306e66bde3cb91a2043f2606cf335700", + ARHockeyAppLiveID:@"d7bceb80c6fa1e83e787b3919c749311", + ARMixpanelToken:mixpanelToken + } configuration: + @{ + ARAnalyticsTrackedEvents: + @[ + @{ + ARAnalyticsClass: ARFairGuideViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsFairGuideView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: fairAndProfileIDBlock + }, + ] + }, + @{ + ARAnalyticsClass: ARFairArtistViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsFairLeaveFromArtist, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(addArtistOnArtsyButton)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairArtistViewController *controller, RACTuple *_) { + return @{ + @"artist_id" : controller.artist.artistID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairLeaveFromArtist, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(addArtistOnArtsyButton)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairArtistViewController *controller, RACTuple *_) { + return @{ + @"followed": controller.isFollowingArtist ? @"yes" : @"no", + @"artist_id" : controller.artist.artistID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairMapButtonTapped, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(mapButtonTapped:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairArtistViewController *controller, RACTuple *_) { + FairOrganizer *organizer = controller.fair.organizer; + return @{ + @"artist_id" : controller.artist.artistID ?: @"", + @"profile_id" : organizer.fairOrganizerID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"" + }; + } + }, + ] + }, + @{ + ARAnalyticsClass: ARFairShowViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsPartnerShowView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairShowViewController *controller, RACTuple *_) { + return controller.dictionaryForAnalytics; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairMapButtonTapped, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(handleMapButtonPress:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairShowViewController *controller, RACTuple *_) { + return controller.dictionaryForAnalytics; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsProfileFollow, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowShow:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairShowViewController *controller, RACTuple *_) { + return @{ + @"followed": controller.isFollowing ? @"yes" : @"no", + @"profile_id" : controller.show.partner.profileID ?: @"", + @"partner_show_id" : controller.show.showID ?: @"", + @"partner_id" : controller.show.partner.partnerID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + }; + } + }, + ] + }, + @{ + ARAnalyticsClass: ARHeartButton.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsHearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(setHearted:animated:)), + ARAnalyticsShouldFire: ^BOOL(ARHeartButton *button, RACTuple *_) { + return button.hearted; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsUnhearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(setHearted:animated:)), + ARAnalyticsShouldFire: ^BOOL(ARHeartButton *button, RACTuple *_) { + return !button.hearted; + } + }, + ] + }, + @{ + ARAnalyticsClass: ARProfileViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsProfileView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loadMartsyView)), + ARAnalyticsEventProperties: ^NSDictionary*(ARProfileViewController *controller, RACTuple *_) { + return @{ + @"profile_id" : controller.profileID ?: @"", + @"user_id" : [[ARUserManager sharedManager] currentUser].userID ?: @"" + }; + } + } + ] + }, + @{ + ARAnalyticsClass: ARFairFavoritesNetworkModel.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsFairGuidePartnerShowSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(handleShowButtonPress:fair:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairFavoritesNetworkModel *model, RACTuple *parameters) { + PartnerShow *show = parameters[0]; + Fair *fair = parameters[1]; + return @{ + @"profile_id" : fair.organizer.profileID ?: @"", + @"fair_id" : fair.fairID ?: @"", + @"partner_show_id" : show.showID ?: @"", + @"partner_id" : show.partner.partnerID ?: @"" + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairGuideArtworkSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(handleArtworkButtonPress:fair:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairFavoritesNetworkModel *model, RACTuple *parameters) { + Artwork *artwork = parameters[0]; + Fair *fair = parameters[1]; + return @{ + @"profile_id" : fair.organizer.profileID ?: @"", + @"fair_id" : fair.fairID ?: @"", + @"artwork_id" : artwork.artworkID ?: @"", + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairGuideArtistSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(handleArtistButtonPress:fair:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairFavoritesNetworkModel *model, RACTuple *parameters) { + Artist *artist = parameters[0]; + Fair *fair = parameters[1]; + return @{ + @"profile_id" : fair.organizer.profileID ?: @"", + @"fair_id" : fair.fairID ?: @"", + @"artist_id" : artist.artistID ?: @"", + }; + } + } + ] + }, + @{ + ARAnalyticsClass: ARAppSearchViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsSearchItemSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(selectedResult:ofType:fromQuery:)), + ARAnalyticsShouldFire: ^BOOL(ARAppSearchViewController *controller, RACTuple *parameters) { + NSString *type = parameters[1]; + return type != nil; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARAppSearchViewController *controller, RACTuple *parameters) { + SearchResult *result = parameters[0]; + NSString *type = parameters[1]; + NSString *query = parameters[2]; + + return @{ + type : result.modelID ?: @"", + @"query" : query ?: @"", + }; + } + } + ] + }, + @{ + ARAnalyticsClass: AROnboardingGeneTableController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsArtworkFavorite, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tableView:didSelectRowAtIndexPath:)), + ARAnalyticsEventProperties: ^NSDictionary*(AROnboardingGeneTableController *controller, RACTuple *parameters) { + NSIndexPath *indexPath = parameters[1]; + Gene *gene = [controller.genes objectAtIndex:indexPath.row]; + return @{ + @"followed": gene.followed? @"yes" : @"no", + @"gene_id" : gene.geneID ?: @"", + }; + } + } + ] + }, + @{ + ARAnalyticsClass: ARArtworkViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsStartedInquiry, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedContactRepresentative:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtworkViewController *controller, RACTuple *parameters) { + return @{ + @"context" : ARAnalyticsInquiryContextSpecialist, + @"fair_id" : controller.fair.fairID ?: @"", + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedInquiry, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedContactGallery:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtworkViewController *controller, RACTuple *parameters) { + return @{ + @"context" : ARAnalyticsInquiryContextGallery, + @"fair_id" : controller.fair.fairID ?: @"", + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsArtworkFavorite, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedArtworkFavorite:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtworkViewController *controller, RACTuple *parameters){ + ARHeartButton *sender = parameters.first; + return @{ + @"followed": sender.isHearted? @"yes" : @"no", + @"artwork_id" : controller.artwork.artworkID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsHearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedArtworkFavorite:)), + ARAnalyticsShouldFire: heartedShouldFireBlock, + }, + @{ + ARAnalyticsEventName: ARAnalyticsUnhearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedArtworkFavorite:)), + ARAnalyticsShouldFire: unheartedShouldFireBlock, + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairMapButtonTapped, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedArtworkViewInMap:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtworkViewController *controller, RACTuple *_){ + return @{@"artwork_id" : controller.artwork.artworkID ?: @""}; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsAuctionBidTapped, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(bidCompelted:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtworkViewController *controller, RACTuple *parameters){ + SaleArtwork *saleArtwork = parameters.first; + return @{ + @"artwork_id" : controller.artwork.artworkID ?: @"", + @"sale_id" : saleArtwork.auction.saleID ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsArtworkView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtworkViewController *controller, RACTuple *parameters){ + return @{ + @"artwork_id" : controller.artwork.artworkID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARInquireForArtworkViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsCancelledInquiry, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(cancelButtonTapped:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARInquireForArtworkViewController *controller, RACTuple *_) { + return @{ + @"fair_id" : controller.fair.fairID ?: @"", + }; + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsSubmittedInquiry, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(inquiryCompleted:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARInquireForArtworkViewController *controller, RACTuple *_) { + return @{ + @"body_length": controller.body ?: @"", + @"type": (controller.state == ARInquireStatePartner) ? @"partner" : @"representative", + @"artwork_id": controller.artwork.artworkID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + @"inquiry_url": controller.inquiryURLRepresentation ?: @"", + @"referring_url": appDelegate.referralURLRepresentation ?: @"", + @"landing_url": appDelegate.landingURLRepresentation ?: @"", + }; + } + }, + ] + }, + @{ + ARAnalyticsClass: AROnboardingArtistTableController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsArtistFollow, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(unfollowArtist:)), + ARAnalyticsEventProperties:^NSDictionary*(AROnboardingArtistTableController *controller, RACTuple *parameters) { + Artist *artist = parameters[0]; + return @{ + @"followed": @"no", + @"artist_id" : artist.artistID?: @"", + }; + } + } + ] + }, + @{ + ARAnalyticsClass: ARLoginViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignIn, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(twitter:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextTwitter }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsUserSignedIn, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loggedInWithType:user:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *parameters){ + NSNumber *typeNumber = parameters.first; + + ARLoginViewControllerLoginType type = typeNumber.integerValue; + + NSString *context = @""; + + switch (type) { + case ARLoginViewControllerLoginTypeTwitter: + context = ARAnalyticsUserContextTwitter; + break; + case ARLoginViewControllerLoginTypeFacebook: + context = ARAnalyticsUserContextFacebook; + break; + case ARLoginViewControllerLoginTypeEmail: + context = ARAnalyticsUserContextFacebook; + break; + } + + return @{ @"context" : context }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsSignInTwitter, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loggedInWithType:user:)), + ARAnalyticsShouldFire: ^BOOL(ARLoginViewController *controller, RACTuple *parameters) { + ARLoginViewControllerLoginType type = [parameters.first integerValue]; + return type == ARLoginViewControllerLoginTypeTwitter; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *parameters){ + User *currentUser = parameters[1]; + return @{ @"user_id": currentUser.userID ?: @"" }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsSignInFacebook, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loggedInWithType:user:)), + ARAnalyticsShouldFire: ^BOOL(ARLoginViewController *controller, RACTuple *parameters) { + ARLoginViewControllerLoginType type = [parameters.first integerValue]; + return type == ARLoginViewControllerLoginTypeFacebook; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *parameters){ + User *currentUser = parameters[1]; + return @{ @"user_id": currentUser.userID ?: @"" }; + }, + }, + @{ + ARAnalyticsEventName: @"Log in", + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loggedInWithType:user:)), + ARAnalyticsShouldFire: ^BOOL(ARLoginViewController *controller, RACTuple *parameters) { + ARLoginViewControllerLoginType type = [parameters.first integerValue]; + return type == ARLoginViewControllerLoginTypeEmail; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *parameters){ + User *currentUser = parameters[1]; + return @{ @"user_id": currentUser.userID ?: @"" }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsSignInError, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(failedToLoginToTwitter)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *parameters){ + return @{ @"context" : ARAnalyticsUserContextTwitter }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignIn, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(fb:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextFacebook }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignIn, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loginWithUsername:andPassword:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextEmail }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsSignInError, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(authenticationFailure)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextEmail }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsSignInError, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(networkFailure:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextEmail }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsSignInError, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(failedToLoginToFacebook)), + ARAnalyticsEventProperties: ^NSDictionary*(ARLoginViewController *controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextFacebook }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARSignUpActiveUserViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsDismissedActiveUserSignUp, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(goBackToApp:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARSignUpActiveUserViewController *controller, RACTuple *_){ + return @{ @"context" : [ARTrialController stringForTrialContext:controller.trialContext] }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARNavigationController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsShowMenu, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(showMenu:)), + } + ] + }, + @{ + ARAnalyticsClass: ARFairMapAnnotationCallOutView.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsFairMapPartnerShowTapped, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tapped:)), + ARAnalyticsShouldFire: ^BOOL(ARFairMapAnnotationCallOutView *view, RACTuple *parameters) { + return [view.annotation.representedObject isKindOfClass:[PartnerShow class]]; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARFairMapAnnotationCallOutView *view, RACTuple *_){ + PartnerShow *partnerShow = view.annotation.representedObject; + return @{ + @"fair_id" : partnerShow.fair.fairID ?: @"", + @"profile_id" : partnerShow.fair.organizer.profileID ?: @"", + @"partner_show_id" : partnerShow.showID ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairMapPartnerShowTapped, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tapped:)), + ARAnalyticsShouldFire: ^BOOL(ARFairMapAnnotationCallOutView *view, RACTuple *parameters) { + return ![view.annotation.representedObject isKindOfClass:[PartnerShow class]] && view.annotation.href; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARFairMapAnnotationCallOutView *view, RACTuple *_){ + return @{ + @"fair_id" : view.fair.fairID ?: @"", + @"profile_id" : view.fair.organizer.profileID ?: @"", + @"href" : view.annotation.href ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARSignupViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignup, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(twitter:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextTwitter }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignup, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(email:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextEmail }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignup, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(fb:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextFacebook }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARGeneViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsGeneView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARGeneViewController *controller, RACTuple *_){ + return @{ + @"gene_id" : controller.gene.geneID ?: @"", + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsGeneFollow, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowingGene:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARGeneViewController *controller, RACTuple *parameters){ + ARHeartButton *sender = parameters.first; + return @{ + @"followed": sender.isHearted? @"yes" : @"no", + @"gene_id" : controller.gene.geneID ?: @"", + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsHearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowingGene:)), + ARAnalyticsShouldFire: ^BOOL (ARGeneViewController *controller, RACTuple *parameters) { + ARHeartButton *sender = parameters.first; + return sender.hearted; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsUnhearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowingGene:)), + ARAnalyticsShouldFire: ^BOOL (ARGeneViewController *controller, RACTuple *parameters) { + ARHeartButton *sender = parameters.first; + return sender.hearted; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARInternalMobileWebViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsOpenedArtsyGravityURL, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(webView:shouldStartLoadWithRequest:navigationType:)), + ARAnalyticsShouldFire: ^BOOL (ARInternalMobileWebViewController *controller, RACTuple *parameters) { + NSURLRequest *request = parameters[1]; + return [request.URL.absoluteString containsString:@"stop_microgravity_redirect"]; + }, + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *parameters){ + NSURLRequest *request = parameters[1]; + return @{ @"url" : request.URL.absoluteString ?: @"" }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARPersonalizeViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingCompletedPersonalize, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(continueTapped:)), + ARAnalyticsShouldFire: ^BOOL (ARPersonalizeViewController *controller, RACTuple *parameters) { + return controller.followedThisSession || controller.geneController.numberOfFollowedGenes; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARPersonalizeViewController *controller, RACTuple *_){ + return @{ + @"artist_count": @(controller.followedThisSession), + @"gene_count": @(controller.geneController.numberOfFollowedGenes) + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingSkippedPersonalize, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(continueTapped:)), + ARAnalyticsShouldFire: ^BOOL (ARPersonalizeViewController *controller, RACTuple *parameters) { + return !(controller.followedThisSession || controller.geneController.numberOfFollowedGenes); + } + }, + @{ + ARAnalyticsEventName: ARAnalyticsArtistFollow, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(searchToggleFollowStatusForArtist:atIndexPath:)), + ARAnalyticsShouldFire: ^BOOL (ARPersonalizeViewController *controller, RACTuple *parameters) { + Artist *artist = parameters.first; + return [controller.artistController hasArtist:artist]; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARPersonalizeViewController *controller, RACTuple *parameters){ + Artist *artist = parameters.first; + return @{ + @"followed": @"no", + @"artist_id" : artist.artistID, + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsArtistFollow, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(searchToggleFollowStatusForArtist:atIndexPath:)), + ARAnalyticsShouldFire: ^BOOL (ARPersonalizeViewController *controller, RACTuple *parameters) { + Artist *artist = parameters.first; + return !([controller.artistController hasArtist:artist]); + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARPersonalizeViewController *controller, RACTuple *parameters){ + Artist *artist = parameters.first; + return @{ + @"followed": @"yes", + @"artist_id" : artist.artistID, + }; + }, + }, + ] + }, + @{ + ARAnalyticsClass: ARSignUpSplashViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsTappedLogIn, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(logIn:)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsTappedSignUp, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(signUp:)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedTrial, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(startTrial)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + NSInteger threshold = [[NSUserDefaults standardUserDefaults] integerForKey:AROnboardingPromptThresholdDefault]; + return @{ + @"tap_threshold" : @(threshold) + }; + }, + }, + ] + }, + @{ + ARAnalyticsClass: ARViewInRoomViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsArtworkViewInRoom, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARViewInRoomViewController *controller, RACTuple *_){ + return @{ + @"via_rotation" : @(controller.popOnRotation), + @"artwork" : controller.artwork.artworkID ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: AROnboardingMoreInfoViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsAmendingDetails, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(initForFacebookWithToken:email:name:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextFacebook }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsAmendingDetails, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(initForTwitterWithToken:andSecret:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextTwitter }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsUserAlreadyExistedAtSignUp, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(userAlreadyExistsForLoginType:)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsCompletedSignUp, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(loginCompletedForLoginType:skipAhead:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *parameters){ + NSString *context = @""; + + AROnboardingMoreInfoViewControllerLoginType type = [parameters.first integerValue]; + + switch (type) { + case AROnboardingMoreInfoViewControllerLoginTypeFacebook: + context = ARAnalyticsUserContextFacebook; + break; + case AROnboardingMoreInfoViewControllerLoginTypeTwitter: + context = ARAnalyticsUserContextTwitter; + break; + } + + return @{ @"context" : context }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARViewInRoomViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsArtworkViewInRoom, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARViewInRoomViewController *controller, RACTuple *_){ + return @{ + @"via_rotation" : @(controller.popOnRotation), + @"artwork" : controller.artwork.artworkID ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARSiteHeroUnitView.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsTappedHeroUnit, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(tappedView:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARSiteHeroUnitView *view, RACTuple *_){ + return @{ + @"url" : view.linkAddress ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARArtistViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsArtistView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidLoad)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtistViewController *controller, RACTuple *_){ + return @{ + @"artist_id" : controller.artist.artistID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsArtistFollow, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowingArtist:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtistViewController *controller, RACTuple *parameters){ + ARHeartButton *sender = parameters.first; + return @{ + @"followed": sender.isHearted? @"yes" : @"no", + @"artist_id" : controller.artist.artistID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsHearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowingArtist:)), + ARAnalyticsShouldFire: heartedShouldFireBlock, + }, + @{ + ARAnalyticsEventName: ARAnalyticsUnhearted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(toggleFollowingArtist:)), + ARAnalyticsShouldFire: unheartedShouldFireBlock, + }, + @{ + ARAnalyticsEventName: ARAnalyticsArtistTappedForSale, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(forSaleOnlyArtworksTapped)), + ARAnalyticsEventProperties: ^NSDictionary*(ARArtistViewController *controller, RACTuple *_){ + return @{ + @"artist_id" : controller.artist.artistID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARFairViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsProfileView, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(viewDidAppear:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairViewController *controller, RACTuple *_){ + return @{ + @"profile_id" : controller.fairProfile.profileID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairFeaturedLinkSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(didSelectFeaturedLink:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairViewController *controller, RACTuple *parameters){ + FeaturedLink *featuredLink = parameters.first; + return @{ + @"profile_id" : controller.fair.organizer.profileID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + @"url" : featuredLink.href ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairPostSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(didSelectPost:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairViewController *controller, RACTuple *parameters){ + NSString *postURL = parameters.first; + return @{ + @"profile_id" : controller.fair.organizer.profileID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + @"url" : postURL ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsFairFeaturedLinkSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(buttonPressed:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARFairViewController *controller, RACTuple *parameters){ + ARButtonWithImage *button = parameters.first; + return @{ + @"profile_id" : controller.fair.organizer.profileID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + @"url" : button.targetURL ?: @"" + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARFairSearchViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsSearchItemSelected, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(selectedResult:ofType:fromQuery:)), + ARAnalyticsShouldFire: ^BOOL(ARFairSearchViewController *controller, RACTuple *parameters) { + NSString *type = parameters[1]; + return type != nil; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARFairSearchViewController *controller, RACTuple *parameters){ + SearchResult *result = parameters[0]; + NSString *type = parameters[1]; + NSString *query = parameters[2]; + return @{ + type ?: @"" : result.modelID ?: @"", + @"fair_id" : controller.fair.fairID ?: @"", + @"query" : query.length > 0 ? query : @"", + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: AROnboardingViewController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingStarted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(signupDone)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingStartedCollectorLevel, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(collectorLevel)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingStartedPersonalize, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(personalize)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingCompletedPersonalize, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(personalizeDone)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingStartedPriceRange, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(priceRange)), + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingCompletedPriceRange, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(setPriceRangeDone:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *parameters){ + NSInteger range = [parameters.first integerValue]; + NSString *stringRange = [NSString stringWithFormat:@"%@", @(range)]; + return @{ @"price_range" : stringRange }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingCompleted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(dismissOnboardingWithVoidAnimation:)), + ARAnalyticsEventProperties: ^NSDictionary*(AROnboardingViewController *controller, RACTuple *_){ + return @{ + @"configuration" : [controller onboardingConfigurationString] + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignup, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(signUpWithFacebook)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextFacebook }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsStartedSignup, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(signUpWithTwitter)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *_){ + return @{ @"context" : ARAnalyticsUserContextTwitter }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsOnboardingCompletedCollectorLevel, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(collectorLevelDone:)), + ARAnalyticsEventProperties: ^NSDictionary*(id controller, RACTuple *parameters){ + ARCollectorLevel level = [parameters.first integerValue]; + NSString *collectorLevel = [ARCollectorStatusViewController stringFromCollectorLevel:level]; + return @{ + @"collector_level" : collectorLevel + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARTrialController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsShowTrialSplash, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(presentTrialWithContext:fromTarget:selector:)), + ARAnalyticsEventProperties: ^NSDictionary*(ARTrialController *controller, RACTuple *parameters){ + enum ARTrialContext context = [parameters.first integerValue]; + return @{ + @"context" : [ARTrialController stringForTrialContext:context], + @"tap_threshold" : @(controller.threshold) + }; + }, + } + ] + }, + @{ + ARAnalyticsClass: ARSharingController.class, + ARAnalyticsDetails: @[ + @{ + ARAnalyticsEventName: ARAnalyticsShareStarted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(presentActivityViewController)), + ARAnalyticsEventProperties: ^NSDictionary*(ARSharingController *controller, RACTuple *_){ + NSString *itemType = [NSStringFromClass([controller.object class]) + stringByReplacingOccurrencesOfString:@"partner" + withString:@""].lowercaseString; + NSString *itemId = [controller objectID]; + return @{ + @"context" : itemType ?: @"", + @"id" : itemId ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsShareCompleted, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(handleActivityCompletion:completed:)), + ARAnalyticsShouldFire: ^BOOL(ARSharingController *controller, RACTuple *parameters) { + BOOL completed = [parameters[1] boolValue]; + return completed; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARSharingController * controller, RACTuple *parameters){ + NSString *itemType = [NSStringFromClass([controller.object class]) + stringByReplacingOccurrencesOfString:@"partner" + withString:@""].lowercaseString; + NSString *itemId = [controller objectID]; + NSString *activityType = parameters.first; + return @{ + @"context" : itemType ?: @"", + @"id" : itemId ?: @"", + @"service" : activityType ?: @"" + }; + }, + }, + @{ + ARAnalyticsEventName: ARAnalyticsShareCancelled, + ARAnalyticsSelectorName: NSStringFromSelector(@selector(handleActivityCompletion:completed:)), + ARAnalyticsShouldFire: ^BOOL(ARSharingController *controller, RACTuple *parameters) { + BOOL completed = [parameters[1] boolValue]; + return !completed; + }, + ARAnalyticsEventProperties: ^NSDictionary*(ARSharingController * controller, RACTuple *parameters){ + NSString *itemType = [NSStringFromClass([controller.object class]) + stringByReplacingOccurrencesOfString:@"partner" + withString:@""].lowercaseString; + NSString *itemId = [controller objectID]; + return @{ + @"context" : itemType ?: @"", + @"id" : itemId ?: @"" + }; + }, + } + ] + }, + ] + }]; + + [ARUserManager identifyAnalyticsUser]; + [ARAnalytics incrementUserProperty:ARAnalyticsAppUsageCountProperty byInt:1]; +} + +@end diff --git a/Artsy/Classes/ARAppDelegate.h b/Artsy/Classes/ARAppDelegate.h new file mode 100644 index 00000000000..1060aa88edc --- /dev/null +++ b/Artsy/Classes/ARAppDelegate.h @@ -0,0 +1,19 @@ +#import +#import "AROnboardingViewController.h" +#import "ARTrialController.h" + +@interface ARAppDelegate : UIResponder + ++ (ARAppDelegate *)sharedInstance; + +@property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) UIViewController *viewController; + +@property (strong, nonatomic, readonly) NSString *referralURLRepresentation; +@property (strong, nonatomic, readonly) NSString *landingURLRepresentation; + +- (void)showTrialOnboardingWithState:(enum ARInitialOnboardingState)state andContext:(enum ARTrialContext)context; + +- (void)finishOnboardingAnimated:(BOOL)animated; + +@end diff --git a/Artsy/Classes/ARAppDelegate.m b/Artsy/Classes/ARAppDelegate.m new file mode 100644 index 00000000000..67ef15c81a3 --- /dev/null +++ b/Artsy/Classes/ARAppDelegate.m @@ -0,0 +1,295 @@ +#ifdef STORE +#define ADMIN_MENU_ENABLED 0 +#else +#define ADMIN_MENU_ENABLED 1 +#endif + +#import +#import +#import +#import +#import "ARAnalyticsConstants.h" + +#import "ARAppDelegate.h" +#import "ARAppDelegate+Analytics.h" +#import "ARUserManager.h" + +#import "ARAdminSettingsViewController.h" +#import "ARQuicksilverViewController.h" +#import "ARRouter.h" +#import "UIViewController+ARStateRestoration.h" +#import "ARNetworkConstants.h" +#import "ArtsyAPI+Private.h" +#import "ARFileUtils.h" +#import "FBSettings.h" +#import "FBAppCall.h" +#import + +#if ADMIN_MENU_ENABLED +#import +#import +#endif + +// demo +#import "ARDemoSplashViewController.h" +#import "ARShowFeedViewController.h" + +@interface ARAppDelegate() +@property (strong, nonatomic, readwrite) NSString *referralURLRepresentation; +@property (strong, nonatomic, readwrite) NSString *landingURLRepresentation; +@end + +@implementation ARAppDelegate + +static ARAppDelegate *_sharedInstance = nil; + ++ (void)load +{ + id delegate = [[self alloc] init]; + [JSDecoupledAppDelegate sharedAppDelegate].appStateDelegate = delegate; + [JSDecoupledAppDelegate sharedAppDelegate].URLResourceOpeningDelegate = delegate; +} + ++ (ARAppDelegate *)sharedInstance +{ + return _sharedInstance; +} + +// These methods are swizzled during unit tests. See ARAppDelegate(Testing). + +- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + _sharedInstance = self; + + if (ARIsRunningInDemoMode) { + [self resetUserDefaults]; + } + + [ARDefaults setup]; + [ARRouter setup]; + + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.viewController = [ARTopMenuViewController sharedController]; + + [self.viewController setupRestorationIdentifierAndClass]; + + [self setupAdminTools]; + + [self setupAnalytics]; + [self setupRatingTool]; + [self countNumberOfRuns]; + + self.window.rootViewController = self.viewController; + + [self.window makeKeyAndVisible]; + + return YES; +} + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + _landingURLRepresentation = self.landingURLRepresentation ?: @"http://artsy.net"; + + [[ARLogger sharedLogger] startLogging]; + [FBSettings setDefaultAppID:[ArtsyKeys new].artsyFacebookAppID]; + + if (ARIsRunningInDemoMode) { + + [self.viewController presentViewController:[[ARDemoSplashViewController alloc] init] animated:NO completion:nil]; + [self performSelector:@selector(finishDemoSplash) withObject:nil afterDelay:1]; + + } else if(![[ARUserManager sharedManager] hasExistingAccount]) { + + [self fetchSiteFeatures]; + [self showTrialOnboardingWithState:ARInitialOnboardingStateSlideShow andContext:ARTrialContextNotTrial]; + } + + ARShowFeedViewController *topVC = (id)ARTopMenuViewController.sharedController.rootNavigationController.topViewController; + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + + // Sync clock with server + [ARSystemTime sync]; + + // Start doing the network calls to grab the feed + [topVC refreshFeedItems]; + + }]; + + return YES; +} + +- (void)finishDemoSplash +{ + [self.viewController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)finishOnboardingAnimated:(BOOL)animated +{ + [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade]; + [[ARTopMenuViewController sharedController] moveToInAppAnimated:animated]; +} + +- (void)showTrialOnboardingWithState:(enum ARInitialOnboardingState)state andContext:(enum ARTrialContext)context +{ + AROnboardingViewController *onboardVC = [[AROnboardingViewController alloc] initWithState:state]; + onboardVC.trialContext = context; + onboardVC.modalPresentationStyle = UIModalPresentationOverFullScreen; + [self.viewController presentViewController:onboardVC animated:NO completion:nil]; +} + +- (void)setupAdminTools +{ +#if ADMIN_MENU_ENABLED + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(rageShakeNotificationRecieved) name:DHCSHakeNotificationName object:nil]; + + if ([AROptions boolForOption:AROptionsUseVCR]) { + NSURL *url = [NSURL fileURLWithPath:[ARFileUtils cachesPathWithFolder:@"vcr" filename:@"eigen.json"]]; + [VCR loadCassetteWithContentsOfURL:url]; + [VCR start]; + } + + [ORKeyboardReactingApplication registerForCallbackOnKeyDown:ORTildeKey :^{ + [self rageShakeNotificationRecieved]; + }]; + + [ORKeyboardReactingApplication registerForCallbackOnKeyDown:ORSpaceKey :^{ + [self showQuicksilver]; + }]; + + [ORKeyboardReactingApplication registerForCallbackOnKeyDown:ORDeleteKey :^{ + [ARTopMenuViewController.sharedController.rootNavigationController popViewControllerAnimated:YES]; + }]; + +#endif +} + +- (void)setupRatingTool +{ + [iRate sharedInstance].promptForNewVersionIfUserRated = NO; + [iRate sharedInstance].verboseLogging = NO; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation +{ + _referralURLRepresentation = sourceApplication; + _landingURLRepresentation = [url absoluteString]; + + // Twitter SSO + NSString *fbScheme = [@"fb" stringByAppendingString:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"FacebookAppID"]]; + if ([[url absoluteString] hasPrefix:ARTwitterCallbackPath]) { + NSNotification *notification = nil; + notification = [NSNotification notificationWithName:kAFApplicationLaunchedWithURLNotification + object:nil + userInfo:@{ kAFApplicationLaunchOptionsURLKey:url }]; + + [[NSNotificationCenter defaultCenter] postNotification:notification]; + return YES; + + // Facebook + } else if ([[url scheme] isEqualToString:fbScheme]) { + // Call FBAppCall's handleOpenURL:sourceApplication to handle Facebook app responses + BOOL wasHandled = [FBAppCall handleOpenURL:url sourceApplication:sourceApplication]; + + // You can add your app-specific url handling code here if needed + + return wasHandled; + } else if ([url isFileURL]) { + // AirDrop receipt + NSData *fileData = [NSData dataWithContentsOfURL:url]; + NSDictionary *data = [NSJSONSerialization JSONObjectWithData:fileData options:0 error:nil]; + NSString *urlString = [data valueForKey:@"url"]; + + if (urlString) { + _landingURLRepresentation = urlString; + + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadURL:[NSURL URLWithString:urlString]]; + if (viewController) { + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + } + } + } else { + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadURL:url]; + if (viewController) { + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + } + } + + return YES; + +} + +- (void)rageShakeNotificationRecieved +{ + UINavigationController *navigationController = ARTopMenuViewController.sharedController.rootNavigationController; + + if (![navigationController.topViewController isKindOfClass:ARAdminSettingsViewController.class]) { + ARAdminSettingsViewController *adminSettings = [[ARAdminSettingsViewController alloc] initWithStyle:UITableViewStyleGrouped]; + [navigationController pushViewController:adminSettings animated:YES]; + } +} + +- (void)showQuicksilver +{ + UINavigationController *navigationController = ARTopMenuViewController.sharedController.rootNavigationController; + + // As this is hooked up to return everywhere, it shouldn't be able to + // call itself when it's just finished showing + NSInteger count = navigationController.viewControllers.count; + + if (count > 1) { + id oldVC = navigationController.viewControllers[count -2]; + if ([oldVC isKindOfClass:[ARQuicksilverViewController class]]) { + return; + } + } + + ARQuicksilverViewController *adminSettings = [[ARQuicksilverViewController alloc] init]; + [navigationController pushViewController:adminSettings animated:YES]; + +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + [ARTrialController extendTrial]; + [ARAnalytics startTimingEvent:ARAnalyticsTimePerSession]; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + [ARAnalytics finishTimingEvent:ARAnalyticsTimePerSession]; +} + +- (void)fetchSiteFeatures +{ + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + [ArtsyAPI getSiteFeatures:^(NSArray *features) { + [ARDefaults setOnboardingDefaults:features]; + + } failure:^(NSError *error) { + ARErrorLog(@"Couldn't get site features. Error %@", error.localizedDescription); + }]; + }]; +} + +-(void)countNumberOfRuns +{ + NSInteger numberOfRuns = [[NSUserDefaults standardUserDefaults] integerForKey:ARAnalyticsAppUsageCountProperty] + 1; + if (numberOfRuns == 1) { + [ARAnalytics event:ARAnalyticsFreshInstall]; + } + + [[NSUserDefaults standardUserDefaults] setInteger:numberOfRuns forKey:ARAnalyticsAppUsageCountProperty]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)resetUserDefaults +{ + [[ARUserManager sharedManager] logout]; +} + +@end diff --git a/Artsy/Classes/App/ARAppBackgroundFetchDelegate.h b/Artsy/Classes/App/ARAppBackgroundFetchDelegate.h new file mode 100644 index 00000000000..1c58df30d65 --- /dev/null +++ b/Artsy/Classes/App/ARAppBackgroundFetchDelegate.h @@ -0,0 +1,7 @@ +#import + +@interface ARAppBackgroundFetchDelegate : NSObject + ++ (NSString *)pathForDownloadedShowFeed; + +@end diff --git a/Artsy/Classes/App/ARAppBackgroundFetchDelegate.m b/Artsy/Classes/App/ARAppBackgroundFetchDelegate.m new file mode 100644 index 00000000000..b2ca17f6939 --- /dev/null +++ b/Artsy/Classes/App/ARAppBackgroundFetchDelegate.m @@ -0,0 +1,48 @@ +#import "ARAppBackgroundFetchDelegate.h" +#import "ARFileUtils.h" + +@implementation ARAppBackgroundFetchDelegate + ++ (void)load +{ + //[JSDecoupledAppDelegate sharedAppDelegate].backgroundFetchDelegate = [[self alloc] init]; +} + ++ (NSString *)pathForDownloadedShowFeed +{ + return [ARFileUtils cachesPathWithFolder:@"PartnerShows" filename:@"feed.dict"]; +} + +- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler +{ + ARActionLog(@"Fetching show feed in the background."); + + [ArtsyAPI getFeedResultsForShowsWithCursor:nil pageSize:10 success:^(NSDictionary *JSON) { + if ([JSON isKindOfClass:[NSDictionary class]]) { + + NSString *path = [self.class pathForDownloadedShowFeed]; + [NSKeyedArchiver archiveRootObject:JSON toFile:path]; + + ARActionLog(@"Downloaded show feed in the background"); + + if (completionHandler) { + completionHandler(UIBackgroundFetchResultNewData); + } + + return; + } + + ARErrorLog(@"Error feed is in an unexpected format"); + if (completionHandler) { + completionHandler(UIBackgroundFetchResultFailed); + } + + } failure:^(NSError *error) { + ARErrorLog(@"Error downloading feed from the background : %@", error.localizedDescription); + if (completionHandler) { + completionHandler(UIBackgroundFetchResultFailed); + } + }]; +} + +@end diff --git a/Artsy/Classes/App/ARAppNotificationsDelegate.h b/Artsy/Classes/App/ARAppNotificationsDelegate.h new file mode 100644 index 00000000000..382b91312ec --- /dev/null +++ b/Artsy/Classes/App/ARAppNotificationsDelegate.h @@ -0,0 +1,5 @@ +#import + +@interface ARAppNotificationsDelegate : NSObject +- (void)registerForDeviceNotificationsOnce; +@end diff --git a/Artsy/Classes/App/ARAppNotificationsDelegate.m b/Artsy/Classes/App/ARAppNotificationsDelegate.m new file mode 100644 index 00000000000..11ac78ac54d --- /dev/null +++ b/Artsy/Classes/App/ARAppNotificationsDelegate.m @@ -0,0 +1,110 @@ +#import +#import "ARAppNotificationsDelegate.h" +#import "ARAnalyticsConstants.h" +#import "UIApplicationStateEnum.h" +#import "ARNotificationView.h" +#import + +@implementation ARAppNotificationsDelegate + ++ (void)load +{ + [JSDecoupledAppDelegate sharedAppDelegate].remoteNotificationsDelegate = [[self alloc] init]; +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ +#if (TARGET_IPHONE_SIMULATOR == 0) + ARErrorLog(@"Error registering for remote notifications: %@", error.localizedDescription); +#endif +} + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + ARActionLog(@"Got device notification token: %@", deviceToken); + Mixpanel *mixpanel = [Mixpanel sharedInstance]; + [mixpanel.people addPushDeviceToken:deviceToken]; + + [ARAnalytics setUserProperty:ARAnalyticsEnabledNotificationsProperty toValue:@"true"]; + + if (![[NSUserDefaults standardUserDefaults] boolForKey:ARHasSubmittedDeviceTokenDefault]) { + [ArtsyAPI setAPNTokenForCurrentDevice:deviceToken success:^(id response) { + ARActionLog(@"Pushed device token to Artsy's servers"); + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:ARHasSubmittedDeviceTokenDefault]; + } failure:^(NSError *error) { + ARErrorLog(@"Couldn't push the device token to Artsy, error: %@", error.localizedDescription); + }]; + } +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +{ + UIApplication *app = [UIApplication sharedApplication]; + NSString *uiApplicationState = [UIApplicationStateEnum toString:app.applicationState]; + + ARActionLog(@"Incoming notification in the %@ application state: %@", uiApplicationState, userInfo); + + NSMutableDictionary *notificationInfo = [[NSMutableDictionary alloc] initWithDictionary:userInfo]; + [notificationInfo setObject:uiApplicationState forKey:@"UIApplicationState"]; + [ARAnalytics event:ARAnalyticsNotificationReceived withProperties:notificationInfo]; + + NSString *message = [[userInfo objectForKey:@"aps"] objectForKey:@"alert"]; + NSString *url = userInfo[@"url"]; + + if (!message) { + message = url; + } + + if (app.applicationState == UIApplicationStateActive && message) { + // app is in the foreground + [ARNotificationView showNoticeInView:[self findVisibleWindow] + title:message + hideAfter:0 + response: ^{ + if (url) { + [ARAnalytics event:ARAnalyticsNotificationTapped withProperties:notificationInfo]; + + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:url]; + if (viewController) { + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + } + } + } + ]; + } else { + // app was brought from the background after a user clicked on the notification + [ARAnalytics event:ARAnalyticsNotificationTapped withProperties:notificationInfo]; + if (url) { + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:url]; + + if (viewController) { + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + } + } + } +} + +-(void)registerForDeviceNotificationsOnce +{ + static dispatch_once_t once; + dispatch_once(&once, ^ { + if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerForRemoteNotifications)]) { + [[UIApplication sharedApplication] registerForRemoteNotifications]; + } else { + UIRemoteNotificationType allTypes = (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert); + [[UIApplication sharedApplication] registerForRemoteNotificationTypes:allTypes]; + } + }); +} + +- (UIWindow *)findVisibleWindow { + NSArray *windows = [[UIApplication sharedApplication] windows]; + for (UIWindow *window in [windows reverseObjectEnumerator]) { + if (!window.hidden) { + return window; + } + } + return NULL; +} + +@end diff --git a/Artsy/Classes/AppIcon_120.png b/Artsy/Classes/AppIcon_120.png new file mode 100644 index 00000000000..faa605d76fc Binary files /dev/null and b/Artsy/Classes/AppIcon_120.png differ diff --git a/Artsy/Classes/AppIcon_58.png b/Artsy/Classes/AppIcon_58.png new file mode 100644 index 00000000000..6d10651e032 Binary files /dev/null and b/Artsy/Classes/AppIcon_58.png differ diff --git a/Artsy/Classes/AppIcon_80.png b/Artsy/Classes/AppIcon_80.png new file mode 100644 index 00000000000..99890f503c0 Binary files /dev/null and b/Artsy/Classes/AppIcon_80.png differ diff --git a/Artsy/Classes/Categories/Apple/NSDate+DateRange.h b/Artsy/Classes/Categories/Apple/NSDate+DateRange.h new file mode 100644 index 00000000000..eeeaeb5fa0e --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSDate+DateRange.h @@ -0,0 +1,7 @@ +#import + +@interface NSDate (DateRange) + +- (NSString *)ausstellungsdauerToDate:(NSDate *)endDate; + +@end diff --git a/Artsy/Classes/Categories/Apple/NSDate+DateRange.m b/Artsy/Classes/Categories/Apple/NSDate+DateRange.m new file mode 100644 index 00000000000..b249040b87f --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSDate+DateRange.m @@ -0,0 +1,85 @@ +#import "NSDate+DateRange.h" + +static NSDateFormatter *ARMonthFormatter; + +@implementation NSDate (DateRange) + + +- (NSString *)ausstellungsdauerToDate:(NSDate *)endDate +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ARMonthFormatter = [[NSDateFormatter alloc] init]; + [ARMonthFormatter setDateFormat:@"MMM"]; + }); + + // This function will return a string that shows the time that the show is/was open, e.g. "July 2nd - 12th, 2011" + // If you can figure a better name for the function, I'd love to hear it, no-one could come up with it on #irtsy + // it turned out the word did exist in German. Thanks Leonard / Jessica ./ + + NSString *dateString = nil; + NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + NSInteger desiredComponents = (NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit); + NSDateComponents *startsComponents = [gregorian components:desiredComponents fromDate:self]; + NSDateComponents *endsComponents = [gregorian components:desiredComponents fromDate:endDate]; + + + NSInteger thisYear = [gregorian components:NSYearCalendarUnit fromDate:[ARSystemTime date]].year; + BOOL shouldShowYear = endsComponents.year != thisYear; + + // Same month - "July 2nd - 12th, 2011" + if (endsComponents.month == startsComponents.month && endsComponents.year == startsComponents.year) { + dateString = [NSString stringWithFormat:@"%@ %@%@ - %@%@", + [ARMonthFormatter stringFromDate:self], + @(startsComponents.day), + [self ordinalForDay:startsComponents.day], + @(endsComponents.day), + [self ordinalForDay:endsComponents.day]]; + + if (shouldShowYear) { + dateString = [NSString stringWithFormat:@"%@, %@", dateString, @(endsComponents.year)]; + } + + // Same year - "June 12th - August 20th, 2012" + } else if (endsComponents.year == startsComponents.year) { + dateString = [NSString stringWithFormat:@"%@ %@%@ - %@ %@%@", + [ARMonthFormatter stringFromDate:self], + @(startsComponents.day), + [self ordinalForDay:startsComponents.day], + [ARMonthFormatter stringFromDate:endDate], + @(endsComponents.day), + [self ordinalForDay:endsComponents.day]]; + if (shouldShowYear) { + dateString = [NSString stringWithFormat:@"%@, %@",dateString, @(endsComponents.year)]; + } + + // Different year - "January 12th, 2011 - April 19th, 2014" + } else { + dateString = [NSString stringWithFormat:@"%@ %@, %@ - %@ %@, %@", + [ARMonthFormatter stringFromDate:self], + @(startsComponents.day), + @(startsComponents.year), + [ARMonthFormatter stringFromDate:endDate], + @(endsComponents.day), + @(endsComponents.year)]; + } + + return dateString; +} + +// returns the bit after the number e.g. st, nd, th +- (NSString *)ordinalForDay:(NSInteger)integer +{ + switch (integer) { + case 0: + return @""; + case 1: + return @"st"; + case 2: + return @"nd"; + default: + return @"th"; + } +} + +@end diff --git a/Artsy/Classes/Categories/Apple/NSDate+Util.h b/Artsy/Classes/Categories/Apple/NSDate+Util.h new file mode 100644 index 00000000000..ec8cf77a520 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSDate+Util.h @@ -0,0 +1,8 @@ +#import + +// TODO: Util is a bad name category name, plus this is in the networking stack so can it be dealt with another way? + +@interface NSDate (Util) +- (NSDate *)GMTDate; +- (NSString *)relativeDate; +@end diff --git a/Artsy/Classes/Categories/Apple/NSDate+Util.m b/Artsy/Classes/Categories/Apple/NSDate+Util.m new file mode 100644 index 00000000000..0bd3e4df89e --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSDate+Util.m @@ -0,0 +1,42 @@ +#import "NSDate+Util.h" + +@implementation NSDate (Util) + +- (NSDate *)GMTDate { + NSTimeInterval timeZoneOffset = [[NSTimeZone defaultTimeZone] secondsFromGMT]; + NSTimeInterval gmtTimeInterval = [self timeIntervalSinceReferenceDate] - timeZoneOffset; + return [NSDate dateWithTimeIntervalSinceReferenceDate:gmtTimeInterval]; +} + +// Adapted from http://digdog.tumblr.com/post/254073803/relative-date-for-nsdate +- (NSString *)relativeDate { + NSCalendar *calendar = [NSCalendar currentCalendar]; + + NSUInteger unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSWeekCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; + + NSDateComponents *components = [calendar components:unitFlags fromDate:self toDate:[ARSystemTime date] options:0]; + + NSArray *selectorNames = [NSArray arrayWithObjects:@"year", @"month", @"week", @"day", @"hour", @"minute", @"second", nil]; + + for (NSString *selectorName in selectorNames) { + SEL currentSelector = NSSelectorFromString(selectorName); + NSMethodSignature *currentSignature = [NSDateComponents instanceMethodSignatureForSelector:currentSelector]; + NSInvocation *currentInvocation = [NSInvocation invocationWithMethodSignature:currentSignature]; + + [currentInvocation setTarget:components]; + [currentInvocation setSelector:currentSelector]; + [currentInvocation invoke]; + + NSInteger relativeNumber; + [currentInvocation getReturnValue:&relativeNumber]; + + if (relativeNumber) { + NSString *plural = relativeNumber > 1 ? @"s" : @""; + return [NSString stringWithFormat:@"%@ %@%@ ago", @(relativeNumber), selectorName, plural]; + } + } + + return @"Now"; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/NSKeyedUnarchiver+ErrorLogging.h b/Artsy/Classes/Categories/Apple/NSKeyedUnarchiver+ErrorLogging.h new file mode 100644 index 00000000000..246f1344462 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSKeyedUnarchiver+ErrorLogging.h @@ -0,0 +1,5 @@ +#import + +@interface NSKeyedUnarchiver (ErrorLogging) ++ (id)unarchiveObjectWithFile:(NSString *)path exceptionBlock:(id (^)(NSException *exception))exceptionBlock; +@end diff --git a/Artsy/Classes/Categories/Apple/NSKeyedUnarchiver+ErrorLogging.m b/Artsy/Classes/Categories/Apple/NSKeyedUnarchiver+ErrorLogging.m new file mode 100644 index 00000000000..bef810469d3 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSKeyedUnarchiver+ErrorLogging.m @@ -0,0 +1,14 @@ +#import "NSKeyedUnarchiver+ErrorLogging.h" + +@implementation NSKeyedUnarchiver (ErrorLogging) + ++ (id)unarchiveObjectWithFile:(NSString *)path exceptionBlock:(id (^)(NSException *))exceptionBlock{ + @try { + return [NSKeyedUnarchiver unarchiveObjectWithFile:path]; + } + @catch (NSException *exception) { + return exceptionBlock(exception); + } +} + +@end diff --git a/Artsy/Classes/Categories/Apple/NSString+StringCase.h b/Artsy/Classes/Categories/Apple/NSString+StringCase.h new file mode 100644 index 00000000000..b5aa6689256 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSString+StringCase.h @@ -0,0 +1,9 @@ +#import + +@interface NSString (StringCase) + ++ (NSString *)humanReadableStringFromClass:(Class)klass; + +- (NSString *)fromCamelCaseToDashed; + +@end diff --git a/Artsy/Classes/Categories/Apple/NSString+StringCase.m b/Artsy/Classes/Categories/Apple/NSString+StringCase.m new file mode 100644 index 00000000000..03535b8bde3 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSString+StringCase.m @@ -0,0 +1,55 @@ +// http://stackoverflow.com/questions/1918972/camelcase-to-underscores-and-back-in-objective-c + +#import "NSString+StringCase.h" + +@implementation NSString (StringCase) + ++ (NSString *)humanReadableStringFromClass:(Class)klass +{ + NSString *klassString = NSStringFromClass(klass); + klassString = [klassString stringByReplacingOccurrencesOfString:@"AR" withString:@""]; + klassString = [klassString stringByReplacingOccurrencesOfString:@"ViewController" withString:@""]; + klassString = [klassString stringByReplacingOccurrencesOfString:@"Modern" withString:@""]; + + // The function below doesn't deal with starting caps + klassString = [klassString stringByReplacingCharactersInRange:NSMakeRange(0,1) + withString:[[klassString substringToIndex:1] lowercaseString]]; + + return [klassString fromCamelCaseToDashed]; +} + +- (NSString *)fromCamelCaseToDashed +{ + + NSScanner *scanner = [NSScanner scannerWithString:self]; + scanner.caseSensitive = YES; + + NSString *builder = [NSString string]; + NSString *buffer = nil; + NSUInteger lastScanLocation = 0; + + while ([scanner isAtEnd] == NO) { + + if ([scanner scanCharactersFromSet:[NSCharacterSet lowercaseLetterCharacterSet] intoString:&buffer]) { + + builder = [builder stringByAppendingString:buffer]; + + if ([scanner scanCharactersFromSet:[NSCharacterSet uppercaseLetterCharacterSet] intoString:&buffer]) { + + builder = [builder stringByAppendingString:@"_"]; + builder = [builder stringByAppendingString:[buffer lowercaseString]]; + } + } + + // If the scanner location has not moved, there's a problem somewhere. + if (lastScanLocation == scanner.scanLocation) { + return nil; + } + + lastScanLocation = scanner.scanLocation; + } + + return builder; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/NSString+StringSize.h b/Artsy/Classes/Categories/Apple/NSString+StringSize.h new file mode 100644 index 00000000000..b6fcc16a9c5 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSString+StringSize.h @@ -0,0 +1,10 @@ +#import + +@interface NSString (StringSize) + +/// This is to emulate the old deprecated API for getting a string size using the +/// new API. + +- (CGSize)ar_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size; + +@end diff --git a/Artsy/Classes/Categories/Apple/NSString+StringSize.m b/Artsy/Classes/Categories/Apple/NSString+StringSize.m new file mode 100644 index 00000000000..a261de1de5a --- /dev/null +++ b/Artsy/Classes/Categories/Apple/NSString+StringSize.m @@ -0,0 +1,12 @@ +@implementation NSString (StringSize) + +- (CGSize)ar_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size +{ + return [self boundingRectWithSize:size + options:NSLineBreakByWordWrapping | NSStringDrawingUsesLineFragmentOrigin + attributes:@{ NSFontAttributeName: font } + context:nil].size; + +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIDevice-Hardware.h b/Artsy/Classes/Categories/Apple/UIDevice-Hardware.h new file mode 100644 index 00000000000..0b35b42a6a7 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIDevice-Hardware.h @@ -0,0 +1,108 @@ +#import + +#define IFPGA_NAMESTRING @"iFPGA" + +#define IPHONE_1G_NAMESTRING @"iPhone 1G" +#define IPHONE_3G_NAMESTRING @"iPhone 3G" +#define IPHONE_3GS_NAMESTRING @"iPhone 3GS" +#define IPHONE_4_NAMESTRING @"iPhone 4" +#define IPHONE_4S_NAMESTRING @"iPhone 4S" +#define IPHONE_5_NAMESTRING @"iPhone 5" +#define IPHONE_UNKNOWN_NAMESTRING @"Unknown iPhone" + +#define IPOD_1G_NAMESTRING @"iPod touch 1G" +#define IPOD_2G_NAMESTRING @"iPod touch 2G" +#define IPOD_3G_NAMESTRING @"iPod touch 3G" +#define IPOD_4G_NAMESTRING @"iPod touch 4G" +#define IPOD_UNKNOWN_NAMESTRING @"Unknown iPod" + +#define IPAD_1G_NAMESTRING @"iPad 1G" +#define IPAD_2G_NAMESTRING @"iPad 2G" +#define IPAD_3G_NAMESTRING @"iPad 3G" +#define IPAD_4G_NAMESTRING @"iPad 4G" +#define IPAD_UNKNOWN_NAMESTRING @"Unknown iPad" + +#define APPLETV_2G_NAMESTRING @"Apple TV 2G" +#define APPLETV_3G_NAMESTRING @"Apple TV 3G" +#define APPLETV_4G_NAMESTRING @"Apple TV 4G" +#define APPLETV_UNKNOWN_NAMESTRING @"Unknown Apple TV" + +#define IOS_FAMILY_UNKNOWN_DEVICE @"Unknown iOS device" + +#define SIMULATOR_NAMESTRING @"iPhone Simulator" +#define SIMULATOR_IPHONE_NAMESTRING @"iPhone Simulator" +#define SIMULATOR_IPAD_NAMESTRING @"iPad Simulator" +#define SIMULATOR_APPLETV_NAMESTRING @"Apple TV Simulator" // :) + +typedef enum { + UIDeviceUnknown, + + UIDeviceSimulator, + UIDeviceSimulatoriPhone, + UIDeviceSimulatoriPad, + UIDeviceSimulatorAppleTV, + + UIDevice1GiPhone, + UIDevice3GiPhone, + UIDevice3GSiPhone, + UIDevice4iPhone, + UIDevice4SiPhone, + UIDevice5iPhone, + + UIDevice1GiPod, + UIDevice2GiPod, + UIDevice3GiPod, + UIDevice4GiPod, + + UIDevice1GiPad, + UIDevice2GiPad, + UIDevice3GiPad, + UIDevice4GiPad, + + UIDeviceAppleTV2, + UIDeviceAppleTV3, + UIDeviceAppleTV4, + + UIDeviceUnknowniPhone, + UIDeviceUnknowniPod, + UIDeviceUnknowniPad, + UIDeviceUnknownAppleTV, + UIDeviceIFPGA, + +} UIDevicePlatform; + +typedef enum { + UIDeviceFamilyiPhone, + UIDeviceFamilyiPod, + UIDeviceFamilyiPad, + UIDeviceFamilyAppleTV, + UIDeviceFamilyUnknown, + +} UIDeviceFamily; + +@interface UIDevice (Hardware) +- (NSString *) platform; +- (NSString *) hwmodel; +- (NSUInteger) platformType; +- (NSString *) platformString; + +- (NSUInteger) cpuFrequency; +- (NSUInteger) busFrequency; +- (NSUInteger) cpuCount; +- (NSUInteger) totalMemory; +- (NSUInteger) userMemory; + +- (NSNumber *) totalDiskSpace; +- (NSNumber *) freeDiskSpace; + +- (NSString *) macaddress; + +- (BOOL) hasRetinaDisplay; +- (UIDeviceFamily) deviceFamily; + +/// Orta ++ (BOOL)isPad; ++ (BOOL)isPhone; ++ (BOOL)isRunningUnitTests; +- (NSString *)deviceFamilyString; +@end \ No newline at end of file diff --git a/Artsy/Classes/Categories/Apple/UIDevice-Hardware.m b/Artsy/Classes/Categories/Apple/UIDevice-Hardware.m new file mode 100644 index 00000000000..f31e545b997 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIDevice-Hardware.m @@ -0,0 +1,340 @@ +// Thanks to Emanuele Vulcano, Kevin Ballard/Eridius, Ryandjohnson, Matt Brown, etc. + +#include +#include +#include + +@implementation UIDevice (Hardware) +/* + Platforms + + iFPGA -> ?? + + iPhone1,1 -> iPhone 1G, M68 + iPhone1,2 -> iPhone 3G, N82 + iPhone2,1 -> iPhone 3GS, N88 + iPhone3,1 -> iPhone 4/AT&T, N89 + iPhone3,2 -> iPhone 4/Other Carrier?, ?? + iPhone3,3 -> iPhone 4/Verizon, TBD + iPhone4,1 -> (iPhone 4S/GSM), TBD + iPhone4,2 -> (iPhone 4S/CDMA), TBD + iPhone4,3 -> (iPhone 4S/???) + iPhone5,1 -> iPhone Next Gen, TBD + iPhone5,1 -> iPhone Next Gen, TBD + iPhone5,1 -> iPhone Next Gen, TBD + + iPod1,1 -> iPod touch 1G, N45 + iPod2,1 -> iPod touch 2G, N72 + iPod2,2 -> Unknown, ?? + iPod3,1 -> iPod touch 3G, N18 + iPod4,1 -> iPod touch 4G, N80 + + // Thanks NSForge + iPad1,1 -> iPad 1G, WiFi and 3G, K48 + iPad2,1 -> iPad 2G, WiFi, K93 + iPad2,2 -> iPad 2G, GSM 3G, K94 + iPad2,3 -> iPad 2G, CDMA 3G, K95 + iPad3,1 -> (iPad 3G, WiFi) + iPad3,2 -> (iPad 3G, GSM) + iPad3,3 -> (iPad 3G, CDMA) + iPad4,1 -> (iPad 4G, WiFi) + iPad4,2 -> (iPad 4G, GSM) + iPad4,3 -> (iPad 4G, CDMA) + + AppleTV2,1 -> AppleTV 2, K66 + AppleTV3,1 -> AppleTV 3, ?? + + i386, x86_64 -> iPhone Simulator +*/ + + +#pragma mark sysctlbyname utils +- (NSString *) getSysInfoByName:(char *)typeSpecifier +{ + size_t size; + sysctlbyname(typeSpecifier, NULL, &size, NULL, 0); + + char *answer = malloc(size); + sysctlbyname(typeSpecifier, answer, &size, NULL, 0); + + NSString *results = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; + + free(answer); + return results; +} + +- (NSString *) platform +{ + return [self getSysInfoByName:"hw.machine"]; +} + + +// Thanks, Tom Harrington (Atomicbird) +- (NSString *) hwmodel +{ + return [self getSysInfoByName:"hw.model"]; +} + +#pragma mark sysctl utils +- (NSUInteger) getSysInfo: (uint) typeSpecifier +{ + size_t size = sizeof(int); + int results; + int mib[2] = {CTL_HW, typeSpecifier}; + sysctl(mib, 2, &results, &size, NULL, 0); + return (NSUInteger) results; +} + +- (NSUInteger) cpuFrequency +{ + return [self getSysInfo:HW_CPU_FREQ]; +} + +- (NSUInteger) busFrequency +{ + return [self getSysInfo:HW_BUS_FREQ]; +} + +- (NSUInteger) cpuCount +{ + return [self getSysInfo:HW_NCPU]; +} + +- (NSUInteger) totalMemory +{ + return [self getSysInfo:HW_PHYSMEM]; +} + +- (NSUInteger) userMemory +{ + return [self getSysInfo:HW_USERMEM]; +} + +- (NSUInteger) maxSocketBufferSize +{ + return [self getSysInfo:KIPC_MAXSOCKBUF]; +} + +#pragma mark file system -- Thanks Joachim Bean! + +/* + extern NSString *NSFileSystemSize; + extern NSString *NSFileSystemFreeSize; + extern NSString *NSFileSystemNodes; + extern NSString *NSFileSystemFreeNodes; + extern NSString *NSFileSystemNumber; +*/ + +- (NSNumber *) totalDiskSpace +{ + NSDictionary *fattributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:nil]; + return [fattributes objectForKey:NSFileSystemSize]; +} + +- (NSNumber *) freeDiskSpace +{ + NSDictionary *fattributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:nil]; + return [fattributes objectForKey:NSFileSystemFreeSize]; +} + +#pragma mark platform type and name utils +- (NSUInteger) platformType +{ + NSString *platform = [self platform]; + + // The ever mysterious iFPGA + if ([platform isEqualToString:@"iFPGA"]) return UIDeviceIFPGA; + + // iPhone + if ([platform isEqualToString:@"iPhone1,1"]) return UIDevice1GiPhone; + if ([platform isEqualToString:@"iPhone1,2"]) return UIDevice3GiPhone; + if ([platform hasPrefix:@"iPhone2"]) return UIDevice3GSiPhone; + if ([platform hasPrefix:@"iPhone3"]) return UIDevice4iPhone; + if ([platform hasPrefix:@"iPhone4"]) return UIDevice4SiPhone; + if ([platform hasPrefix:@"iPhone5"]) return UIDevice5iPhone; + + // iPod + if ([platform hasPrefix:@"iPod1"]) return UIDevice1GiPod; + if ([platform hasPrefix:@"iPod2"]) return UIDevice2GiPod; + if ([platform hasPrefix:@"iPod3"]) return UIDevice3GiPod; + if ([platform hasPrefix:@"iPod4"]) return UIDevice4GiPod; + + // iPad + if ([platform hasPrefix:@"iPad1"]) return UIDevice1GiPad; + if ([platform hasPrefix:@"iPad2"]) return UIDevice2GiPad; + if ([platform hasPrefix:@"iPad3"]) return UIDevice3GiPad; + if ([platform hasPrefix:@"iPad4"]) return UIDevice4GiPad; + + // Apple TV + if ([platform hasPrefix:@"AppleTV2"]) return UIDeviceAppleTV2; + if ([platform hasPrefix:@"AppleTV3"]) return UIDeviceAppleTV3; + + if ([platform hasPrefix:@"iPhone"]) return UIDeviceUnknowniPhone; + if ([platform hasPrefix:@"iPod"]) return UIDeviceUnknowniPod; + if ([platform hasPrefix:@"iPad"]) return UIDeviceUnknowniPad; + if ([platform hasPrefix:@"AppleTV"]) return UIDeviceUnknownAppleTV; + + // Simulator thanks Jordan Breeding + if ([platform hasSuffix:@"86"] || [platform isEqual:@"x86_64"]) + { + BOOL smallerScreen = [[UIScreen mainScreen] bounds].size.width < 768; + return smallerScreen ? UIDeviceSimulatoriPhone : UIDeviceSimulatoriPad; + } + + return UIDeviceUnknown; +} + +- (NSString *) platformString +{ + switch ([self platformType]) + { + case UIDevice1GiPhone: return IPHONE_1G_NAMESTRING; + case UIDevice3GiPhone: return IPHONE_3G_NAMESTRING; + case UIDevice3GSiPhone: return IPHONE_3GS_NAMESTRING; + case UIDevice4iPhone: return IPHONE_4_NAMESTRING; + case UIDevice4SiPhone: return IPHONE_4S_NAMESTRING; + case UIDevice5iPhone: return IPHONE_5_NAMESTRING; + case UIDeviceUnknowniPhone: return IPHONE_UNKNOWN_NAMESTRING; + + case UIDevice1GiPod: return IPOD_1G_NAMESTRING; + case UIDevice2GiPod: return IPOD_2G_NAMESTRING; + case UIDevice3GiPod: return IPOD_3G_NAMESTRING; + case UIDevice4GiPod: return IPOD_4G_NAMESTRING; + case UIDeviceUnknowniPod: return IPOD_UNKNOWN_NAMESTRING; + + case UIDevice1GiPad : return IPAD_1G_NAMESTRING; + case UIDevice2GiPad : return IPAD_2G_NAMESTRING; + case UIDevice3GiPad : return IPAD_3G_NAMESTRING; + case UIDevice4GiPad : return IPAD_4G_NAMESTRING; + case UIDeviceUnknowniPad : return IPAD_UNKNOWN_NAMESTRING; + + case UIDeviceAppleTV2 : return APPLETV_2G_NAMESTRING; + case UIDeviceAppleTV3 : return APPLETV_3G_NAMESTRING; + case UIDeviceAppleTV4 : return APPLETV_4G_NAMESTRING; + case UIDeviceUnknownAppleTV: return APPLETV_UNKNOWN_NAMESTRING; + + case UIDeviceSimulator: return SIMULATOR_NAMESTRING; + case UIDeviceSimulatoriPhone: return SIMULATOR_IPHONE_NAMESTRING; + case UIDeviceSimulatoriPad: return SIMULATOR_IPAD_NAMESTRING; + case UIDeviceSimulatorAppleTV: return SIMULATOR_APPLETV_NAMESTRING; + + case UIDeviceIFPGA: return IFPGA_NAMESTRING; + + default: return IOS_FAMILY_UNKNOWN_DEVICE; + } +} + +- (BOOL) hasRetinaDisplay +{ + return ([UIScreen mainScreen].scale == 2.0f); +} + +- (UIDeviceFamily) deviceFamily +{ + NSString *platform = [self platform]; + if ([platform hasPrefix:@"iPhone"]) { return UIDeviceFamilyiPhone; } + if ([platform hasPrefix:@"iPod"]) { return UIDeviceFamilyiPod; } + if ([platform hasPrefix:@"iPad"]) { return UIDeviceFamilyiPad; } + if ([platform hasPrefix:@"AppleTV"]) { return UIDeviceFamilyAppleTV; } + return UIDeviceFamilyUnknown; +} + +#pragma mark MAC addy +// Return the local MAC addy +// Courtesy of FreeBSD hackers email list +// Accidentally munged during previous update. Fixed thanks to mlamb. +- (NSString *) macaddress +{ + int mib[6]; + size_t len; + char *buf; + unsigned char *ptr; + struct if_msghdr *ifm; + struct sockaddr_dl *sdl; + + mib[0] = CTL_NET; + mib[1] = AF_ROUTE; + mib[2] = 0; + mib[3] = AF_LINK; + mib[4] = NET_RT_IFLIST; + + if ((mib[5] = if_nametoindex("en0")) == 0) { + printf("Error: if_nametoindex error\n"); + return NULL; + } + + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + printf("Error: sysctl, take 1\n"); + return NULL; + } + + if ((buf = malloc(len)) == NULL) { + printf("Error: Memory allocation error\n"); + return NULL; + } + + if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { + printf("Error: sysctl, take 2\n"); + free(buf); // Thanks, Remy "Psy" Demerest + return NULL; + } + + ifm = (struct if_msghdr *)buf; + sdl = (struct sockaddr_dl *)(ifm + 1); + ptr = (unsigned char *)LLADDR(sdl); + NSString *outstring = [NSString stringWithFormat:@"%02X:%02X:%02X:%02X:%02X:%02X", *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5)]; + + free(buf); + return outstring; +} + +// Illicit Bluetooth check -- cannot be used in App Store +/* +Class btclass = NSClassFromString(@"GKBluetoothSupport"); +if ([btclass respondsToSelector:@selector(bluetoothStatus)]) +{ + printf("BTStatus %d\n", ((int)[btclass performSelector:@selector(bluetoothStatus)] & 1) != 0); + bluetooth = ((int)[btclass performSelector:@selector(bluetoothStatus)] & 1) != 0; + printf("Bluetooth %s enabled\n", bluetooth ? "is" : "isn't"); +} +*/ + +/// Orta + ++ (BOOL)isPad +{ + return UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPhone; +} + ++ (BOOL)isPhone +{ + return UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone; +} + +- (NSString *) deviceFamilyString +{ + NSString *platform = [self platform]; + if ([platform hasPrefix:@"iPhone"]) { return @"iPhone"; } + if ([platform hasPrefix:@"iPod"]) { return @"iPod"; } + if ([platform hasPrefix:@"iPad"]) { return @"iPad"; } + if ([platform hasPrefix:@"AppleTV"]) { return @"AppleTV"; } + return @"Unknown"; +} + +// Thanks https://github.com/pietbrauer + +static BOOL ARRunningUnitTests = NO; + ++ (BOOL)isRunningUnitTests +{ + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + NSString *XCInjectBundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"XCInjectBundle"]; + + ARRunningUnitTests = [XCInjectBundle hasSuffix:@".xctest"]; + }); + + return ARRunningUnitTests; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIFont+ArtsyFonts.h b/Artsy/Classes/Categories/Apple/UIFont+ArtsyFonts.h new file mode 100644 index 00000000000..20a7a7014be --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIFont+ArtsyFonts.h @@ -0,0 +1,9 @@ +@interface UIFont (ArtsyFonts) ++ (UIFont *)serifBoldItalicFontWithSize:(CGFloat)size; ++ (UIFont *)serifBoldFontWithSize:(CGFloat)size; ++ (UIFont *)serifSemiBoldFontWithSize:(CGFloat)size; ++ (UIFont *)serifFontWithSize:(CGFloat)size; ++ (UIFont *)serifItalicFontWithSize:(CGFloat)size; ++ (UIFont *)sansSerifFontWithSize:(CGFloat)size; ++ (UIFont *)smallCapsSerifFontWithSize:(CGFloat)size; +@end diff --git a/Artsy/Classes/Categories/Apple/UIFont+ArtsyFonts.m b/Artsy/Classes/Categories/Apple/UIFont+ArtsyFonts.m new file mode 100644 index 00000000000..69b6f68aab1 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIFont+ArtsyFonts.m @@ -0,0 +1,48 @@ +@implementation UIFont (ArtsyFonts) + ++ (UIFont *)serifBoldItalicFontWithSize:(CGFloat)size +{ + return [UIFont fontWithName:@"AGaramondPro-BoldItalic" size:size]; +} + ++ (UIFont *)serifBoldFontWithSize:(CGFloat)size +{ + return [UIFont fontWithName:@"AGaramondPro-Bold" size:size]; +} + ++ (UIFont *)serifSemiBoldFontWithSize:(CGFloat)size +{ + return [UIFont fontWithName:@"AGaramondPro-Semibold" size:size]; +} + ++ (UIFont *)serifFontWithSize:(CGFloat)size +{ + return [UIFont fontWithName:@"AGaramondPro-Regular" size:size]; +} + ++ (UIFont *)serifItalicFontWithSize:(CGFloat)size +{ + return [UIFont fontWithName:@"AGaramondPro-Italic" size:size]; +} + ++ (UIFont *)sansSerifFontWithSize:(CGFloat)size +{ + return [UIFont fontWithName:@"AvantGardeGothicITCW01Dm" size:size]; +} + ++ (UIFont *)smallCapsSerifFontWithSize:(CGFloat)size +{ + NSArray *fontFeatureSettings = @[ @{ UIFontFeatureTypeIdentifierKey: @(38), + UIFontFeatureSelectorIdentifierKey : @(1) } ]; + + NSDictionary *fontAttributes = @{ UIFontDescriptorFeatureSettingsAttribute: fontFeatureSettings , + UIFontDescriptorNameAttribute: @"AGaramondPro-Regular", + UIFontDescriptorSizeAttribute: @(size)} ; + + UIFontDescriptor *fontDescriptor = [ [UIFontDescriptor alloc] initWithFontAttributes: fontAttributes ]; + + return [UIFont fontWithDescriptor:fontDescriptor size:size]; + +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIImage+ImageFromColor.h b/Artsy/Classes/Categories/Apple/UIImage+ImageFromColor.h new file mode 100644 index 00000000000..223c5fb2dac --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIImage+ImageFromColor.h @@ -0,0 +1,5 @@ +#import + +@interface UIImage (ImageWithColor) ++ (UIImage *)imageFromColor:(UIColor *)color; +@end diff --git a/Artsy/Classes/Categories/Apple/UIImage+ImageFromColor.m b/Artsy/Classes/Categories/Apple/UIImage+ImageFromColor.m new file mode 100644 index 00000000000..dde891d0d44 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIImage+ImageFromColor.m @@ -0,0 +1,33 @@ +static NSCache *imageCache; + +@implementation UIImage (ImageWithColor) + +// creates a 1x1 UIImage with a color and caches it +// derived from http://stackoverflow.com/questions/2808888/is-it-even-possible-to-change-a-uibuttons-background-color + ++ (UIImage *)imageFromColor:(UIColor *)color +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + imageCache = [[NSCache alloc] init]; + }); + + UIImage *image = [imageCache objectForKey:color]; + if (image) { + return image; + } + + CGRect rect = CGRectMake(0, 0, 1, 1); + UIGraphicsBeginImageContext(rect.size); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, [color CGColor]); + CGContextFillRect(context, rect); + + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + [imageCache setObject:image forKey:color]; + return image; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIImageView+AsyncImageLoading.h b/Artsy/Classes/Categories/Apple/UIImageView+AsyncImageLoading.h new file mode 100644 index 00000000000..a8d7f415c95 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIImageView+AsyncImageLoading.h @@ -0,0 +1,13 @@ +/// @class Allow us to easily switch out the image downloading process + +#import + +@interface UIImageView (AsyncImageLoading) + +- (void)ar_setImageWithURL:(NSURL *)url; + +- (void)ar_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completed; + +- (void)ar_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; + +@end diff --git a/Artsy/Classes/Categories/Apple/UIImageView+AsyncImageLoading.m b/Artsy/Classes/Categories/Apple/UIImageView+AsyncImageLoading.m new file mode 100644 index 00000000000..49e4aa3f811 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIImageView+AsyncImageLoading.m @@ -0,0 +1,27 @@ +#import + +@implementation UIImageView (AsyncImageLoading) + +- (void)ar_setImageWithURL:(NSURL *)url +{ + UIImage *placeholder = [UIImage imageFromColor:[UIColor artsyLightGrey]]; + [self sd_setImageWithURL:url placeholderImage:placeholder]; +} + +- (void)ar_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completed +{ + UIImage *placeholder = [UIImage imageFromColor:[UIColor artsyLightGrey]]; + [self sd_setImageWithURL:url placeholderImage:placeholder completed:completed]; +} + +- (void)ar_setImageWithURL:(NSURL *)url + placeholderImage:(UIImage *)placeholder +{ + if (!placeholder) { + placeholder = [UIImage imageFromColor:[UIColor artsyLightGrey]]; + } + + [self sd_setImageWithURL:url placeholderImage:placeholder]; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UILabel+Typography.h b/Artsy/Classes/Categories/Apple/UILabel+Typography.h new file mode 100644 index 00000000000..248fe47af51 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UILabel+Typography.h @@ -0,0 +1,9 @@ +@interface UILabel (Typography) + +// Like marmite and peanut-butter, some things are nice on their own, but they just don't mix. +// For the moment, you can have one or the other. + +- (void)setText:(NSString *)text withLineHeight:(CGFloat)lineHeight; +- (void)setText:(NSString *)text withLetterSpacing:(CGFloat)letterSpacing; + +@end diff --git a/Artsy/Classes/Categories/Apple/UILabel+Typography.m b/Artsy/Classes/Categories/Apple/UILabel+Typography.m new file mode 100644 index 00000000000..1f1b957558f --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UILabel+Typography.m @@ -0,0 +1,24 @@ +@implementation UILabel (Typography) + +- (void)setText:(NSString *)text withLineHeight:(CGFloat)lineHeight +{ + if (!text) { return; } + NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:text]; + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setLineSpacing:lineHeight]; + [paragraphStyle setAlignment:self.textAlignment]; + + [attr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [text length])]; + self.attributedText = attr; +} + +- (void)setText:(NSString *)text withLetterSpacing:(CGFloat)letterSpacing +{ + if (!text) { return; } + NSMutableAttributedString *attributedText = nil; + attributedText = [[NSMutableAttributedString alloc] initWithString:text]; + [attributedText addAttribute:NSKernAttributeName value:@(letterSpacing) range:NSMakeRange(0, text.length)]; + self.attributedText = attributedText; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIScrollView+HitTest.h b/Artsy/Classes/Categories/Apple/UIScrollView+HitTest.h new file mode 100644 index 00000000000..663f97e7486 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIScrollView+HitTest.h @@ -0,0 +1,5 @@ +#import + +@interface UIScrollView (HitTest) + +@end diff --git a/Artsy/Classes/Categories/Apple/UIScrollView+HitTest.m b/Artsy/Classes/Categories/Apple/UIScrollView+HitTest.m new file mode 100644 index 00000000000..199066e12ae --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIScrollView+HitTest.m @@ -0,0 +1,32 @@ +#import + +@implementation UIScrollView (HitTest) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL old = @selector(hitTest:withEvent:); + SEL new = @selector(swizzled_hitTest:withEvent:); + Class class = [self class]; + Method oldMethod = class_getInstanceMethod(class, old); + Method newMethod = class_getInstanceMethod(class, new); + + if (class_addMethod(class, old, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { + class_replaceMethod(class, new, method_getImplementation(oldMethod), method_getTypeEncoding(oldMethod)); + } else { + method_exchangeImplementations(oldMethod, newMethod); + } + }); +} + +- (UIView *)swizzled_hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + if (self.isDragging || self.isDecelerating) { + return self; + } + + return [self swizzled_hitTest:point withEvent:event]; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIView+HitTestExpansion.h b/Artsy/Classes/Categories/Apple/UIView+HitTestExpansion.h new file mode 100644 index 00000000000..cba830e2c97 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIView+HitTestExpansion.h @@ -0,0 +1,11 @@ +#import + +@interface UIView (HitTestExpansion) + +/// Allow the tap detection range to be larger than the frame of the button. +- (void)ar_extendHitTestSizeByWidth:(CGFloat)width andHeight:(CGFloat)height; + +/// Check how big the tap frame would be +- (void)ar_visuallyExtendHitTestSizeByWidth:(CGFloat)width andHeight:(CGFloat)height; + +@end diff --git a/Artsy/Classes/Categories/Apple/UIView+HitTestExpansion.m b/Artsy/Classes/Categories/Apple/UIView+HitTestExpansion.m new file mode 100644 index 00000000000..8806959746f --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIView+HitTestExpansion.m @@ -0,0 +1,93 @@ +// Thanks! http://stackoverflow.com/questions/808503/uibutton-making-the-hit-area-larger-than-the-default-hit-area + +#import "UIView+HitTestExpansion.h" +#import + +@implementation UIView (HitTestExpansion) + +static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets"; +static BOOL ARHasSwizzledSetFrame; + +- (void)ar_extendHitTestSizeByWidth:(CGFloat)width andHeight:(CGFloat)height +{ + // As they are stored as a UIEdgeInset and we're dealing with extending + // we invert the height & width to make the API make sense + + UIEdgeInsets insets = UIEdgeInsetsMake(-height, -width, -height, -width); + NSValue *value = [NSValue value:&insets withObjCType:@encode(UIEdgeInsets)]; + objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (UIEdgeInsets)hitTestEdgeInsets +{ + NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS); + if(value) { + UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; return edgeInsets; + }else { + return UIEdgeInsetsZero; + } +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + CGRect relativeFrame = self.bounds; + CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets); + + return CGRectContainsPoint(hitFrame, point); +} + + +// Support showing what it would look like at runtime +// We need to ensure that setting the frame resizes correctly +// Thus swizzling only when at least one view uses the visually method + +- (void)ar_visuallyExtendHitTestSizeByWidth:(CGFloat)width andHeight:(CGFloat)height +{ + UIEdgeInsets highlightInsets = UIEdgeInsetsMake(-height, -width, -height, -width); + NSInteger highlightViewTag = 232323; + + UIView *highlightView = [self viewWithTag:highlightViewTag]; + if (!highlightView) { + highlightView = [[UIView alloc] init]; + highlightView.backgroundColor = [[UIColor purpleColor] colorWithAlphaComponent:0.5]; + highlightView.tag = 232323; + highlightView.userInteractionEnabled = NO; + self.clipsToBounds = NO; + [self addSubview:highlightView]; + } + + highlightView.frame = UIEdgeInsetsInsetRect(self.bounds, highlightInsets); + + [self ar_extendHitTestSizeByWidth:width andHeight:height]; + + if (!ARHasSwizzledSetFrame){ + ARHasSwizzledSetFrame = YES; + + SEL setFrame = @selector(layoutSubviews); + SEL newSetFrame = @selector(swizzledLayoutSubviews); + + Method originalMethod = class_getInstanceMethod(self.class, setFrame); + Method overrideMethod = class_getInstanceMethod(self.class, newSetFrame); + if (class_addMethod(self.class, setFrame, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) { + class_replaceMethod(self.class, newSetFrame, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, overrideMethod); + } + } +} + +- (void)swizzledLayoutSubviews +{ + + [self swizzledLayoutSubviews]; + + NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS); + if (value) { + UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; + [self ar_visuallyExtendHitTestSizeByWidth:-edgeInsets.left andHeight:-edgeInsets.top]; + } + +} + + +@end diff --git a/Artsy/Classes/Categories/Apple/UIView+OldSchoolSnapshots.h b/Artsy/Classes/Categories/Apple/UIView+OldSchoolSnapshots.h new file mode 100644 index 00000000000..d41ceb9dabc --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIView+OldSchoolSnapshots.h @@ -0,0 +1,7 @@ +#import + +@interface UIView (OldSchoolSnapshots) + +- (UIView *)ar_snapshot; + +@end diff --git a/Artsy/Classes/Categories/Apple/UIView+OldSchoolSnapshots.m b/Artsy/Classes/Categories/Apple/UIView+OldSchoolSnapshots.m new file mode 100644 index 00000000000..edf43ca02f2 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIView+OldSchoolSnapshots.m @@ -0,0 +1,17 @@ +#import "UIView+OldSchoolSnapshots.h" + +@implementation UIView (OldSchoolSnapshots) + +- (UIView *)ar_snapshot +{ + UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0); + + CGContextRef context = UIGraphicsGetCurrentContext(); + [self.layer renderInContext:context]; + UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return [[UIImageView alloc] initWithImage:snapshotImage]; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIViewController+FullScreenLoading.h b/Artsy/Classes/Categories/Apple/UIViewController+FullScreenLoading.h new file mode 100644 index 00000000000..6d0f94fdf02 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIViewController+FullScreenLoading.h @@ -0,0 +1,10 @@ +@interface UIViewController (FullScreenLoading) + +/// Show a full screen loading indicator, fades in a view +/// at the top of the view heirarchy +- (void)ar_presentIndeterminateLoadingIndicatorAnimated:(BOOL)animated; + +/// Removes the full screen indicator if it exists +- (void)ar_removeIndeterminateLoadingIndicatorAnimated:(BOOL)animated; + +@end diff --git a/Artsy/Classes/Categories/Apple/UIViewController+FullScreenLoading.m b/Artsy/Classes/Categories/Apple/UIViewController+FullScreenLoading.m new file mode 100644 index 00000000000..efd9547b640 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIViewController+FullScreenLoading.m @@ -0,0 +1,44 @@ +#import "UIViewController+FullScreenLoading.h" +#import "ARReusableLoadingView.h" +#import + +@implementation UIViewController (FullScreenLoading) + +static const NSString *AR_LOADING_VIEW = @"ARLoadingView"; + +- (void)ar_presentIndeterminateLoadingIndicatorAnimated:(BOOL)animated +{ + ARReusableLoadingView *loadingView = objc_getAssociatedObject(self, &AR_LOADING_VIEW); + if (loadingView) { return; } + + loadingView = [[ARReusableLoadingView alloc] init]; + if ([AROptions boolForOption:AROptionsLoadingScreenAlpha]) { + loadingView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.3]; + } + + [self.view addSubview:loadingView]; + [loadingView startIndeterminateAnimated:animated completion:nil]; + + [loadingView alignCenterWithView:self.view]; + [loadingView constrainWidthToView:self.view predicate:@""]; + [loadingView constrainHeightToView:self.view predicate:@""]; + + objc_setAssociatedObject(self, &AR_LOADING_VIEW, loadingView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)ar_removeIndeterminateLoadingIndicatorAnimated:(BOOL)animated +{ + ARReusableLoadingView *loadingView = objc_getAssociatedObject(self, &AR_LOADING_VIEW); + + // Don't run an animation if it's not needed + if (!loadingView) { return; } + + // Nil the associated object + objc_setAssociatedObject(self, &AR_LOADING_VIEW, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + [loadingView stopIndeterminateAnimated:animated completion:^(BOOL finished) { + [loadingView removeFromSuperview]; + }]; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIViewController+ScreenSize.h b/Artsy/Classes/Categories/Apple/UIViewController+ScreenSize.h new file mode 100644 index 00000000000..4ae1901f1b4 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIViewController+ScreenSize.h @@ -0,0 +1,11 @@ +#import + +@interface UIViewController (ScreenSize) +/** + * Returns whether the current view controller has been rendered on a small device, + * such as an iPhone4 (480px tall) vs. a tall device, such as an iPhone5 (568px tall). + * + * @return YES if the screen is small, NO otherwise + */ +- (BOOL)smallScreen; +@end diff --git a/Artsy/Classes/Categories/Apple/UIViewController+ScreenSize.m b/Artsy/Classes/Categories/Apple/UIViewController+ScreenSize.m new file mode 100644 index 00000000000..31e7977e90a --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIViewController+ScreenSize.m @@ -0,0 +1,10 @@ +#import "UIViewController+ScreenSize.h" + +@implementation UIViewController (ScreenSize) + +- (BOOL)smallScreen +{ + return CGRectGetHeight(self.view.bounds) <= 480; +} + +@end diff --git a/Artsy/Classes/Categories/Apple/UIViewController+SimpleChildren.h b/Artsy/Classes/Categories/Apple/UIViewController+SimpleChildren.h new file mode 100644 index 00000000000..433b8b75c45 --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIViewController+SimpleChildren.h @@ -0,0 +1,28 @@ +#import + +@interface UIViewController (SimpleChildren) + +/// Add a childVC to another controller, deals with the normal +/// View Controller containment methods. + +- (void)ar_addChildViewController:(UIViewController *)controller atFrame:(CGRect)frame; + +/// Allows you to add a childViewController inside a view in your hierarchy and will deal +/// the normal view controller containment methods. + +- (void)ar_addChildViewController:(UIViewController *)controller inView:(UIView *)view atFrame:(CGRect)frame; + +/// For Auto-Layout child view controllers. The other methods aren't deprecated yet but you shouldn't be using them. +- (void)ar_addModernChildViewController:(UIViewController *)controller; + +/// For Auto Layout, adds the childVC but allows you to place the view inside another view +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view; + +/// These methods use the various insertSubview methods to let you control the ordering of your subviews. +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view atIndex:(NSInteger)index; +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view belowSubview:(UIView *)subview; +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view aboveSubview:(UIView *)subview; + +/// Remove a child View Controller and removes from superview +- (void)ar_removeChildViewController:(UIViewController *)controller; +@end diff --git a/Artsy/Classes/Categories/Apple/UIViewController+SimpleChildren.m b/Artsy/Classes/Categories/Apple/UIViewController+SimpleChildren.m new file mode 100644 index 00000000000..cca2895d73e --- /dev/null +++ b/Artsy/Classes/Categories/Apple/UIViewController+SimpleChildren.m @@ -0,0 +1,62 @@ +#import "UIViewController+SimpleChildren.h" + +@implementation UIViewController (SimpleChildren) + +- (void)ar_addChildViewController:(UIViewController *)controller atFrame:(CGRect)frame { + [self ar_addChildViewController:controller inView:self.view atFrame:frame]; +} + +- (void)ar_addChildViewController:(UIViewController *)controller inView:(UIView *)view atFrame:(CGRect)frame { + [controller willMoveToParentViewController:self]; + [self addChildViewController:controller]; + + controller.view.frame = frame; + [view addSubview:controller.view]; + + [controller didMoveToParentViewController:self]; +} + +- (void)ar_addModernChildViewController:(UIViewController *)controller +{ + [self ar_addModernChildViewController:controller intoView:self.view]; +} + +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view aboveSubview:(UIView *)subview +{ + [controller willMoveToParentViewController:self]; + [self addChildViewController:controller]; + [view insertSubview:controller.view aboveSubview:subview]; + [controller didMoveToParentViewController:self]; +} + +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view belowSubview:(UIView *)subview +{ + [controller willMoveToParentViewController:self]; + [self addChildViewController:controller]; + [view insertSubview:controller.view belowSubview:subview]; + [controller didMoveToParentViewController:self]; +} + +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view atIndex:(NSInteger)index +{ + [controller willMoveToParentViewController:self]; + [self addChildViewController:controller]; + [view insertSubview:controller.view atIndex:index]; + [controller didMoveToParentViewController:self]; +} + +- (void)ar_addModernChildViewController:(UIViewController *)controller intoView:(UIView *)view +{ + [controller willMoveToParentViewController:self]; + [self addChildViewController:controller]; + [view addSubview:controller.view]; + [controller didMoveToParentViewController:self]; +} + +- (void)ar_removeChildViewController:(UIViewController *)controller { + [controller willMoveToParentViewController:nil]; + [controller removeFromParentViewController]; + [controller.view removeFromSuperview]; +} + +@end diff --git a/Artsy/Classes/Categories/Artsy/ORStackView+ArtsyViews.h b/Artsy/Classes/Categories/Artsy/ORStackView+ArtsyViews.h new file mode 100644 index 00000000000..992b937f212 --- /dev/null +++ b/Artsy/Classes/Categories/Artsy/ORStackView+ArtsyViews.h @@ -0,0 +1,30 @@ +#import + +@interface ORStackView (ArtsyViews) + +/// Use the consistently styled page title which is has +/// a larger letter spacing. + +- (UILabel *)addPageTitleWithString:(NSString *)title; +- (UILabel *)addPageTitleWithString:(NSString *)title tag:(NSInteger)tag; + +/// An alternative page title style +- (void)addSerifPageTitle:(NSString *)title subtitle:(NSString *)subtitle; +- (void)addSerifPageTitle:(NSString *)title subtitle:(NSString *)subtitle tag:(NSInteger)tag; + +/// Use a consistent subtitle +- (UILabel *)addPageSubtitleWithString:(NSString *)title; +- (UILabel *)addPageSubtitleWithString:(NSString *)title tag:(NSInteger)tag; + +/// Use a consistent seperator +- (UIView *)addGenericSeparatorWithSideMargin:(NSString *)sideMargin; +- (UIView *)addGenericSeparatorWithSideMargin:(NSString *)sideMargin tag:(NSInteger)tag; + +/// Sometimes there should be a whitespace +- (UIView *)addWhiteSpaceWithHeight:(NSString *)height; +- (UIView *)addWhiteSpaceWithHeight:(NSString *)height tag:(NSInteger)tag; + +/// Ensure scrolling behavior by adding a whitespace gobbler and a full height constraint. +- (UIView *)ensureScrollingWithHeight:(CGFloat)height; +- (UIView *)ensureScrollingWithHeight:(CGFloat)height tag:(NSInteger)tag; +@end diff --git a/Artsy/Classes/Categories/Artsy/ORStackView+ArtsyViews.m b/Artsy/Classes/Categories/Artsy/ORStackView+ArtsyViews.m new file mode 100644 index 00000000000..9da9540b823 --- /dev/null +++ b/Artsy/Classes/Categories/Artsy/ORStackView+ArtsyViews.m @@ -0,0 +1,97 @@ +#import "ORStackView+ArtsyViews.h" +#import "ARWhitespaceGobbler.h" + +@implementation ORStackView (ArtsyViews) + +- (UILabel *)addPageTitleWithString:(NSString *)title +{ + return [self addPageTitleWithString:title tag:0]; +} + +- (UILabel *)addPageTitleWithString:(NSString *)title tag:(NSInteger)tag +{ + UILabel *titleLabel = [[ARSansSerifHeaderLabel alloc] init]; + titleLabel.tag = tag; + [titleLabel setText:title]; + [self addSubview:titleLabel withTopMargin:[UIDevice isPad] ? @"50" : @"20" sideMargin:@"120"]; + return titleLabel; +} + +- (UILabel *)addPageSubtitleWithString:(NSString *)title +{ + return [self addPageSubtitleWithString:title tag:0]; +} + +- (UILabel *)addPageSubtitleWithString:(NSString *)title tag:(NSInteger)tag +{ + UILabel *featuredTitle = [ARThemedFactory labelForViewSubHeaders]; + featuredTitle.text = title.uppercaseString; + featuredTitle.tag = tag; + [self addSubview:featuredTitle withTopMargin:@"44" sideMargin:@"40"]; + return featuredTitle; +} + +- (void)addSerifPageTitle:(NSString *)title subtitle:(NSString *)subtitle +{ + return [self addSerifPageTitle:title subtitle:subtitle tag:0]; +} + +- (void)addSerifPageTitle:(NSString *)title subtitle:(NSString *)subtitle tag:(NSInteger)tag +{ + UILabel *titleLabel = [ARThemedFactory labelForSerifHeaders]; + titleLabel.text = title; + titleLabel.tag = tag; + [self addSubview:titleLabel withTopMargin:@"20" sideMargin:@"80"]; + + UILabel *subtitleLabel = [ARThemedFactory labelForSerifSubHeaders]; + subtitleLabel.text = subtitle; + subtitleLabel.tag = tag + 1; + subtitleLabel.font = [UIFont serifFontWithSize:16]; + subtitleLabel.textColor = [UIColor blackColor]; + [self addSubview:subtitleLabel withTopMargin:@"8" sideMargin:@"48"]; +} + +- (UIView *)addGenericSeparatorWithSideMargin:(NSString *)sideMargin +{ + return [self addGenericSeparatorWithSideMargin:sideMargin tag:0]; +} + +- (UIView *)addGenericSeparatorWithSideMargin:(NSString *)sideMargin tag:(NSInteger)tag +{ + ARSeparatorView *separator = [[ARSeparatorView alloc] init]; + separator.tag = tag; + [self addSubview:separator withTopMargin:@"12" sideMargin:sideMargin]; + return separator; +} + +- (UIView *)addWhiteSpaceWithHeight:(NSString *)height +{ + return [self addWhiteSpaceWithHeight:height tag:0]; +} + +- (UIView *)addWhiteSpaceWithHeight:(NSString *)height tag:(NSInteger)tag +{ + UIView *gap = [[UIView alloc] init]; + gap.tag = tag; + gap.backgroundColor = [UIColor whiteColor]; + [self addSubview:gap withTopMargin:height sideMargin:nil]; + return gap; +} + +- (UIView *)ensureScrollingWithHeight:(CGFloat)height +{ + return [self ensureScrollingWithHeight:height tag:0]; +} + +- (UIView *)ensureScrollingWithHeight:(CGFloat)height tag:(NSInteger)tag +{ + NSString *heightConstraint = [NSString stringWithFormat:@">=%.0f@800", height]; + [self constrainHeight:heightConstraint]; + + ARWhitespaceGobbler *whitespaceGobbler = [[ARWhitespaceGobbler alloc] init]; + whitespaceGobbler.tag = tag; + [self addSubview:whitespaceGobbler withTopMargin:nil sideMargin:nil]; + return whitespaceGobbler; +} + +@end diff --git a/Artsy/Classes/Categories/Categories.h b/Artsy/Classes/Categories/Categories.h new file mode 100644 index 00000000000..2d6b7207e46 --- /dev/null +++ b/Artsy/Classes/Categories/Categories.h @@ -0,0 +1,14 @@ +#import "UIFont+ArtsyFonts.h" +#import "UIColor+ArtsyColors.h" +#import "UIColor+DebugColours.h" + +#import "UIImage+ImageFromColor.h" +#import "UIImageView+AsyncImageLoading.h" +#import "UILabel+Typography.h" + +#import "NSString+StringSize.h" +#import "UIDevice-Hardware.h" +#import "UIScrollView+HitTest.h" + +#import "MTLModel+JSON.h" +#import "MTLModel+Dictionary.h" diff --git a/Artsy/Classes/Categories/UIViewController+ARStateRestoration.h b/Artsy/Classes/Categories/UIViewController+ARStateRestoration.h new file mode 100644 index 00000000000..cb866072d3d --- /dev/null +++ b/Artsy/Classes/Categories/UIViewController+ARStateRestoration.h @@ -0,0 +1,10 @@ +#import + +@interface UIViewController (ARStateRestoration) + +/// Automatic setup of the restoration identifier and class +/// based on the class of the View Controller instance + +- (void)setupRestorationIdentifierAndClass; + +@end diff --git a/Artsy/Classes/Categories/UIViewController+ARStateRestoration.m b/Artsy/Classes/Categories/UIViewController+ARStateRestoration.m new file mode 100644 index 00000000000..35569c0d16c --- /dev/null +++ b/Artsy/Classes/Categories/UIViewController+ARStateRestoration.m @@ -0,0 +1,15 @@ +#import "UIViewController+ARStateRestoration.h" + +@implementation UIViewController (ARStateRestoration) + +- (void)setupRestorationIdentifierAndClass +{ + self.restorationClass = [self class]; + + NSString *classString = NSStringFromClass([self class]); + NSString *restorationIdentifier = [NSString stringWithFormat:@"%@RID", classString]; + self.restorationIdentifier = restorationIdentifier; +} + +@end + diff --git a/Artsy/Classes/Constants/ARAnalyticsConstants.h b/Artsy/Classes/Constants/ARAnalyticsConstants.h new file mode 100644 index 00000000000..93b2d796161 --- /dev/null +++ b/Artsy/Classes/Constants/ARAnalyticsConstants.h @@ -0,0 +1,158 @@ +// User properties + +extern NSString *const ARAnalyticsAppUsageCountProperty; +extern NSString *const ARAnalyticsPriceRangeProperty; +extern NSString *const ARAnalyticsCollectorLevelProperty; + +// Notifications + +extern NSString *const ARAnalyticsEnabledNotificationsProperty; +extern NSString *const ARAnalyticsNotificationReceived; +extern NSString *const ARAnalyticsNotificationTapped; + +// Keep track of some raw numbers re:total installs + +extern NSString *const ARAnalyticsFreshInstall; + +// Initial Splash Screen only, not trial splash + +extern NSString *const ARAnalyticsTappedSignUp; +extern NSString *const ARAnalyticsStartedTrial; +extern NSString *const ARAnalyticsTappedLogIn; + +// Sign up + +extern NSString *const ARAnalyticsStartedSignup; +extern NSString *const ARAnalyticsAmendingDetails; +extern NSString *const ARAnalyticsCompletedSignUp; +extern NSString *const ARAnalyticsUserAlreadyExistedAtSignUp; + +extern NSString *const ARAnalyticsDismissedActiveUserSignUp; + +// Onboarding + +extern NSString *const ARAnalyticsOnboardingStarted; +extern NSString *const ARAnalyticsOnboardingStartedCollectorLevel; +extern NSString *const ARAnalyticsOnboardingCompletedCollectorLevel; +extern NSString *const ARAnalyticsOnboardingStartedPersonalize; +extern NSString *const ARAnalyticsOnboardingSkippedPersonalize; +extern NSString *const ARAnalyticsOnboardingCompletedPersonalize; +extern NSString *const ARAnalyticsOnboardingStartedPriceRange; +extern NSString *const ARAnalyticsOnboardingCompletedPriceRange; +extern NSString *const ARAnalyticsOnboardingCompleted; + +// Sign in + +extern NSString *const ARAnalyticsStartedSignIn; +extern NSString *const ARAnalyticsUserSignedIn; +extern NSString *const ARAnalyticsSignInError; +extern NSString *const ARAnalyticsSignInTwitter; +extern NSString *const ARAnalyticsSignInFacebook; + +// User creation abstracted from specifics like facebook / twitter / email + +extern NSString *const ARAnalyticsUserCreationStarted; +extern NSString *const ARAnalyticsUserCreationCompleted; +extern NSString *const ARAnalyticsUserCreationUserError; +extern NSString *const ARAnalyticsUserCreationUnknownError; + +extern NSString *const ARAnalyticsUserContextEmail; +extern NSString *const ARAnalyticsUserContextTwitter; +extern NSString *const ARAnalyticsUserContextFacebook; + +// Inquiring + +extern NSString *const ARAnalyticsStartedInquiry; +extern NSString *const ARAnalyticsCancelledInquiry; +extern NSString *const ARAnalyticsSubmittedInquiry; + +extern NSString *const ARAnalyticsInquiryContextSpecialist; +extern NSString *const ARAnalyticsInquiryContextGallery; + +// Trial + +extern NSString *const ARAnalyticsShowTrialSplash; +extern NSString *const ARAnalyticsTappedHeroUnit; + +// Sharing +extern NSString *const ARAnalyticsShareStarted; +extern NSString *const ARAnalyticsShareCompleted; +extern NSString *const ARAnalyticsShareCancelled; + +extern NSString *const ARAnalyticsHearted; +extern NSString *const ARAnalyticsUnhearted; + +// Artwork + +extern NSString *const ARAnalyticsArtworkView; +extern NSString *const ARAnalyticsArtworkViewInRoom; +extern NSString *const ARAnalyticsArtworkFavorite; + +// Artist + +extern NSString *const ARAnalyticsArtistView; +extern NSString *const ARAnalyticsArtistFollow; +extern NSString *const ARAnalyticsArtistTappedForSale; + +// Gene + +extern NSString *const ARAnalyticsGeneView; +extern NSString *const ARAnalyticsGeneFollow; + +// Profile + +extern NSString *const ARAnalyticsProfileFollow; +extern NSString *const ARAnalyticsProfileView; + +// Fair + +extern NSString *const ARAnalyticsFairGuideView; +extern NSString *const ARAnalyticsFairGuideArtistSelected; +extern NSString *const ARAnalyticsFairGuideArtworkSelected; +extern NSString *const ARAnalyticsFairGuidePartnerShowSelected; +extern NSString *const ARAnalyticsFairGuideAllExhibitorsSelected; +extern NSString *const ARAnalyticsFairFeaturedLinkSelected; +extern NSString *const ARAnalyticsFairPostSelected; +extern NSString *const ARAnalyticsFairLeaveFromArtist; + +// PartnerShow + +extern NSString *const ARAnalyticsPartnerShowView; + +// Menu + +extern NSString *const ARAnalyticsShowMenu; +extern NSString *const ARAnalyticsMenuTappedHome; +extern NSString *const ARAnalyticsMenuTappedFavorites; +extern NSString *const ARAnalyticsMenuTappedBrowse; +extern NSString *const ARAnalyticsMenuTappedSignUp; +extern NSString *const ARAnalyticsMenuTappedFeature; + +// Search + +extern NSString *const ARAnalyticsSearchOpened; +extern NSString *const ARAnalyticsSearchStartedQuery; +extern NSString *const ARAnalyticsSearchItemSelected; + +// App Session + +extern NSString *const ARAnalyticsTimePerSession; + +// Loading gravity pages in-app +extern NSString *const ARAnalyticsOpenedArtsyGravityURL; + +// Feed +extern NSString *const ARAnalyticsInitialFeedLoadTime; + +// Error + +extern NSString *const ARAnalyticsErrorFailedToGetFacebookCredentials; + + +// Maps +extern NSString *const ARAnalyticsFairMapButtonTapped; +extern NSString *const ARAnalyticsFairMapAnnotationTapped; +extern NSString *const ARAnalyticsFairMapPartnerShowTapped; + +// Auctions +extern NSString *const ARAnalyticsAuctionBidTapped; diff --git a/Artsy/Classes/Constants/ARAnalyticsConstants.m b/Artsy/Classes/Constants/ARAnalyticsConstants.m new file mode 100644 index 00000000000..cdea9a838dd --- /dev/null +++ b/Artsy/Classes/Constants/ARAnalyticsConstants.m @@ -0,0 +1,115 @@ +#import "ARAnalyticsConstants.h" + +NSString *const ARAnalyticsAppUsageCountProperty = @"app_launched_count"; +NSString *const ARAnalyticsCollectorLevelProperty = @"collector_level"; +NSString *const ARAnalyticsPriceRangeProperty = @"collector_price_range"; + +NSString *const ARAnalyticsEnabledNotificationsProperty = @"has_enabled_notifications"; +NSString *const ARAnalyticsNotificationReceived = @"notification_received"; +NSString *const ARAnalyticsNotificationTapped = @"notification_tapped"; + +NSString *const ARAnalyticsFreshInstall = @"first_user_install"; + +NSString *const ARAnalyticsTappedSignUp = @"tapped_sign_up"; +NSString *const ARAnalyticsStartedTrial = @"tapped_start_trial"; +NSString *const ARAnalyticsTappedLogIn = @"tapped_log_in"; + +NSString *const ARAnalyticsStartedSignup = @"started_sign_up"; +NSString *const ARAnalyticsAmendingDetails = @"editing_sign_up"; +NSString *const ARAnalyticsCompletedSignUp = @"signed_up"; +NSString *const ARAnalyticsUserAlreadyExistedAtSignUp = @"user_already_existed_at_signup"; + +NSString *const ARAnalyticsDismissedActiveUserSignUp = @"dismissed_active_user_signup"; + +NSString *const ARAnalyticsStartedSignIn = @"started_sign_in"; +NSString *const ARAnalyticsUserSignedIn = @"user_connected"; +NSString *const ARAnalyticsSignInError = @"sign_in_error"; +NSString *const ARAnalyticsSignInTwitter = @"sign_in_twitter"; +NSString *const ARAnalyticsSignInFacebook = @"sign_in_facebook"; + +NSString *const ARAnalyticsUserCreationStarted = @"artsy_user_creation_started"; +NSString *const ARAnalyticsUserCreationCompleted = @"artsy_user_creation_completed"; +NSString *const ARAnalyticsUserCreationUserError = @"artsy_user_creation_user_error"; +NSString *const ARAnalyticsUserCreationUnknownError = @"artsy_user_error"; + +NSString *const ARAnalyticsUserContextEmail = @"email"; +NSString *const ARAnalyticsUserContextTwitter = @"twitter"; +NSString *const ARAnalyticsUserContextFacebook = @"facebook"; + +NSString *const ARAnalyticsStartedInquiry = @"user_started_inquiry"; +NSString *const ARAnalyticsCancelledInquiry = @"user_cancelled_inquiry"; +NSString *const ARAnalyticsSubmittedInquiry = @"user_submitted_inquiry"; +NSString *const ARAnalyticsInquiryContextSpecialist = @"inquiry_specialist"; +NSString *const ARAnalyticsInquiryContextGallery = @"inquiry_gallery"; + +NSString *const ARAnalyticsOnboardingStarted = @"user_started_onboarding"; +NSString *const ARAnalyticsOnboardingStartedCollectorLevel = @"user_started_collector_level"; +NSString *const ARAnalyticsOnboardingCompletedCollectorLevel = @"user_completed_collector_level"; +NSString *const ARAnalyticsOnboardingStartedPersonalize = @"user_started_personalize"; +NSString *const ARAnalyticsOnboardingSkippedPersonalize = @"user_skipped_personalize"; +NSString *const ARAnalyticsOnboardingCompletedPersonalize = @"user_completed_personalize"; +NSString *const ARAnalyticsOnboardingStartedPriceRange = @"user_started_price_range"; +NSString *const ARAnalyticsOnboardingCompletedPriceRange = @"user_completed_price_range"; +NSString *const ARAnalyticsOnboardingCompleted = @"user_completed_onboarding"; + +NSString *const ARAnalyticsShowTrialSplash = @"user_trial_splash_presented"; + +NSString *const ARAnalyticsTappedHeroUnit = @"tapped_hero_unit"; + +NSString *const ARAnalyticsArtworkView = @"artwork_view"; +NSString *const ARAnalyticsArtworkViewInRoom = @"artwork_view_in_room"; +NSString *const ARAnalyticsArtworkFavorite = @"artwork_favorite"; + +NSString *const ARAnalyticsPartnerShowView = @"partner_show_view"; + +NSString *const ARAnalyticsShareStarted = @"share_started"; +NSString *const ARAnalyticsShareCompleted = @"share_completed"; +NSString *const ARAnalyticsShareCancelled = @"share_cancelled"; + +NSString *const ARAnalyticsHearted = @"user_hearted"; +NSString *const ARAnalyticsUnhearted = @"user_unhearted"; + +NSString *const ARAnalyticsProfileView = @"profile_view"; +NSString *const ARAnalyticsProfileFollow = @"profile_favorite"; + +NSString *const ARAnalyticsFairLeaveFromArtist = @"fair_leave_from_artist"; +NSString *const ARAnalyticsFairGuideView = @"fair_guide_view"; +NSString *const ARAnalyticsFairGuidePartnerShowSelected = @"fair_selected_partner_show"; +NSString *const ARAnalyticsFairGuideArtworkSelected = @"fair_selected_artwork"; +NSString *const ARAnalyticsFairGuideArtistSelected = @"fair_selected_artist"; +NSString *const ARAnalyticsFairGuideAllExhibitorsSelected = @"fair_selected_all_exhibitors"; +NSString *const ARAnalyticsFairFeaturedLinkSelected = @"fair_selected_featured_link"; +NSString *const ARAnalyticsFairPostSelected = @"fair_selected_post"; + +NSString *const ARAnalyticsArtistView = @"artist_view"; +NSString *const ARAnalyticsArtistFollow = @"artist_favorite"; +NSString *const ARAnalyticsArtistTappedForSale = @"artist_tapped_for_sale"; + +NSString *const ARAnalyticsGeneView = @"gene_view"; +NSString *const ARAnalyticsGeneFollow = @"gene_favorite"; + +NSString *const ARAnalyticsShowMenu = @"user_opened_menu"; +NSString *const ARAnalyticsMenuTappedHome = @"user_menu_tapped_home"; +NSString *const ARAnalyticsMenuTappedFavorites = @"user_menu_tapped_favorites"; +NSString *const ARAnalyticsMenuTappedBrowse = @"user_menu_tapped_browse"; +NSString *const ARAnalyticsMenuTappedSignUp = @"user_menu_tapped_signup"; +NSString *const ARAnalyticsMenuTappedFeature = @"user_menu_tapped_feature"; + +NSString *const ARAnalyticsSearchOpened = @"search_opened"; +NSString *const ARAnalyticsSearchStartedQuery = @"seach_started"; +NSString *const ARAnalyticsSearchItemSelected = @"seach_item_selected"; + +NSString *const ARAnalyticsTimePerSession = @"app_session_time"; + +NSString *const ARAnalyticsOpenedArtsyGravityURL = @"has_shown_gravity_url"; + +NSString *const ARAnalyticsErrorFailedToGetFacebookCredentials = @"error_failed_to_get_facebook_credentials"; + +NSString *const ARAnalyticsInitialFeedLoadTime = @"initial_feed_load_time"; + +NSString *const ARAnalyticsFairMapButtonTapped = @"fair_tapped_map_button"; +NSString *const ARAnalyticsFairMapAnnotationTapped = @"fair_tapped_map_annotation"; +NSString *const ARAnalyticsFairMapPartnerShowTapped = @"fair_tapped_map_partner_show"; + +NSString *const ARAnalyticsAuctionBidTapped = @"auction_tapped_bid"; + diff --git a/Artsy/Classes/Constants/ARAppConstants.h b/Artsy/Classes/Constants/ARAppConstants.h new file mode 100644 index 00000000000..ae7ccda6af0 --- /dev/null +++ b/Artsy/Classes/Constants/ARAppConstants.h @@ -0,0 +1,23 @@ +extern const NSString *ARTestAccountLogin; +extern const NSString *ARTestAccountPassword; + +extern const CGFloat ARAnimationQuickDuration; +extern const CGFloat ARAnimationDuration; + +extern const NSString *AROAuthTokenKey; +extern const NSString *AROExpiryDateKey; +extern const NSString *ARXAppToken; + +extern NSString *const ARNetworkAvailableNotification; +extern NSString *const ARNetworkUnavailableNotification; + +typedef NS_OPTIONS(NSUInteger, ARAuctionState) { + ARAuctionStateDefault = 0, + ARAuctionStateStarted = 1 << 0, + ARAuctionStateEnded = 1 << 1, + ARAuctionStateUserIsRegistered = 1 << 2, + ARAuctionStateArtworkHasBids = 1 << 3, + ARAuctionStateUserIsBidder = 1 << 4, + ARAuctionStateUserIsHighBidder = 1 << 5 +}; + diff --git a/Artsy/Classes/Constants/ARAppConstants.m b/Artsy/Classes/Constants/ARAppConstants.m new file mode 100644 index 00000000000..585f1e8fc87 --- /dev/null +++ b/Artsy/Classes/Constants/ARAppConstants.m @@ -0,0 +1,16 @@ +// Don't go wild, this is just a standard account I +// signed up with one day. + +const NSString *ARTestAccountLogin = @"energy_test_bot@gmail.com"; +const NSString *ARTestAccountPassword = @"zaqwsxcde"; + +const CGFloat ARAnimationQuickDuration = 0.15; +const CGFloat ARAnimationDuration = 0.3; + +const NSString *AROExpiryDateKey = @"expires_in"; +const NSString *ARXAppToken = @"xapp_token"; + +const NSString *AROAuthTokenKey = @"access_token"; + +NSString *const ARNetworkAvailableNotification = @"network_available_notification"; +NSString *const ARNetworkUnavailableNotification = @"network_unavailable_notification"; diff --git a/Artsy/Classes/Constants/ARDefaults.h b/Artsy/Classes/Constants/ARDefaults.h new file mode 100644 index 00000000000..b351d25ec89 --- /dev/null +++ b/Artsy/Classes/Constants/ARDefaults.h @@ -0,0 +1,28 @@ +extern NSString *const ARUserIdentifierDefault; +extern NSString *const ARUseStagingDefault; + +extern NSString *const AROAuthTokenDefault; +extern NSString *const AROAuthTokenExpiryDateDefault; + +extern NSString *const ARXAppTokenDefault; +extern NSString *const ARXAppTokenExpiryDateDefault; + +extern NSString *const ARHasSubmittedDeviceTokenDefault; + +#pragma mark - +#pragma mark onboarding + +extern NSString *const AROnboardingSkipPersonalizeDefault; +extern NSString *const AROnboardingSkipCollectorLevelDefault; +extern NSString *const AROnboardingSkipPriceRangeDefault; +extern NSString *const AROnboardingPromptThresholdDefault; + +#pragma mark - +#pragma mark Things we wanna trigger server-side + +extern NSString *const ARShowAuctionResultsButtonDefault; + +@interface ARDefaults : NSObject ++ (void)setOnboardingDefaults:(NSArray *)features; ++ (void)setup; +@end diff --git a/Artsy/Classes/Constants/ARDefaults.m b/Artsy/Classes/Constants/ARDefaults.m new file mode 100644 index 00000000000..c851d8820af --- /dev/null +++ b/Artsy/Classes/Constants/ARDefaults.m @@ -0,0 +1,76 @@ +const static NSInteger AROnboardingPromptDefault = 25; + +NSString *const ARUserIdentifierDefault = @"ARUserIdentifier"; +NSString *const ARUseStagingDefault = @"ARUseStagingDefault"; + +NSString *const AROAuthTokenDefault = @"AROAuthToken"; +NSString *const AROAuthTokenExpiryDateDefault = @"AROAuthTokenExpiryDate"; + +NSString *const ARXAppTokenDefault = @"ARXAppTokenDefault"; +NSString *const ARXAppTokenExpiryDateDefault = @"ARXAppTokenExpiryDateDefault"; + +NSString *const ARHasSubmittedDeviceTokenDefault = @"ARHasSubmittedFullDeviceToken"; + +NSString *const AROnboardingSkipPersonalizeDefault = @"eigen-onboard-skip-personalize"; +NSString *const AROnboardingSkipCollectorLevelDefault = @"eigen-onboard-skip-collector-level"; +NSString *const AROnboardingSkipPriceRangeDefault = @"eigen-onboard-skip-price-range"; +NSString *const AROnboardingPromptThresholdDefault = @"eigen-onboard-prompt-threshold"; +NSString *const ARShowAuctionResultsButtonDefault = @"auction-results"; + +@implementation ARDefaults + ++ (void)setOnboardingDefaults:(NSArray *)features +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSArray *booleans = @[ + AROnboardingSkipPersonalizeDefault, + AROnboardingSkipCollectorLevelDefault, + AROnboardingSkipPriceRangeDefault, + ARShowAuctionResultsButtonDefault + ]; + + + for (SiteFeature *feature in features) { + if ([booleans containsObject:feature.siteFeatureID]) { + [defaults setObject:feature.enabled forKey:feature.siteFeatureID]; + } else if ([feature.siteFeatureID isEqualToString:AROnboardingPromptThresholdDefault]) { + NSNumber *count = [defaults valueForKey:AROnboardingPromptThresholdDefault]; + if (count) { + // we don't wanna reset this + continue; + } + NSArray *counts = feature.parameters[@"counts"]; + if (!counts) { + count = feature.parameters[@"count"]; + } else { + // Very poor man's A/B testing: assign one of the possibilities at random + count = counts[arc4random_uniform((unsigned int)counts.count)]; + } + if (count && ([count integerValue] > 0)) { + [defaults setObject:count forKey:AROnboardingPromptThresholdDefault]; + } + } + } + [defaults synchronize]; +} + ++ (void)setup +{ + BOOL useStagingDefault; +#if DEBUG + useStagingDefault = YES; +#else + useStagingDefault = NO; +#endif + + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + ARUseStagingDefault: @(useStagingDefault), + AROnboardingPromptThresholdDefault: @(AROnboardingPromptDefault), + AROnboardingSkipPersonalizeDefault: @(NO), + AROnboardingSkipCollectorLevelDefault: @(NO), + AROnboardingSkipPriceRangeDefault: @(NO), + AROnboardingPromptThresholdDefault: @(NO) + } + ]; +} +@end diff --git a/Artsy/Classes/Constants/ARFeedConstants.h b/Artsy/Classes/Constants/ARFeedConstants.h new file mode 100644 index 00000000000..5db81897340 --- /dev/null +++ b/Artsy/Classes/Constants/ARFeedConstants.h @@ -0,0 +1,11 @@ +#import + +extern const CGFloat ARFeedCarouselThresholdRatio; + +extern const NSInteger ARFeedHeaderTitleTextSize; +extern const NSInteger ARFeedHeaderSubTitleTextSize; + +extern const NSInteger ARFeedHeaderTitleTextSize; + +extern const NSInteger ARFeedPostTitleTextSize; +extern const NSInteger ARFeedPostBodyTextSize; diff --git a/Artsy/Classes/Constants/ARFeedConstants.m b/Artsy/Classes/Constants/ARFeedConstants.m new file mode 100644 index 00000000000..10ec8f36c71 --- /dev/null +++ b/Artsy/Classes/Constants/ARFeedConstants.m @@ -0,0 +1,12 @@ +#import "ARFeedConstants.h" + +#pragma mark Dimensions + +const CGFloat ARFeedCarouselThresholdRatio = 3.0/2.0; + +#pragma mark Font sizes +const NSInteger ARFeedHeaderTitleTextSize = 20; +const NSInteger ARFeedHeaderSubTitleTextSize = 16; + +const NSInteger ARFeedPostTitleTextSize = 26; +const NSInteger ARFeedPostBodyTextSize = 16; diff --git a/Artsy/Classes/Constants/ARViewTagConstants.h b/Artsy/Classes/Constants/ARViewTagConstants.h new file mode 100644 index 00000000000..034063f302d --- /dev/null +++ b/Artsy/Classes/Constants/ARViewTagConstants.h @@ -0,0 +1,3 @@ +#import + +extern const NSInteger ARSeparatorTag; diff --git a/Artsy/Classes/Constants/ARViewTagConstants.m b/Artsy/Classes/Constants/ARViewTagConstants.m new file mode 100644 index 00000000000..4d11c01a8c9 --- /dev/null +++ b/Artsy/Classes/Constants/ARViewTagConstants.m @@ -0,0 +1 @@ +const NSInteger ARSeparatorTag = 11020170; diff --git a/Artsy/Classes/Constants/Constants.h b/Artsy/Classes/Constants/Constants.h new file mode 100644 index 00000000000..e9973e7808b --- /dev/null +++ b/Artsy/Classes/Constants/Constants.h @@ -0,0 +1,3 @@ +#import "ARDefaults.h" +#import "ARAppConstants.h" +#import "ARViewTagConstants.h" diff --git a/Artsy/Classes/Models/ARFeed.h b/Artsy/Classes/Models/ARFeed.h new file mode 100644 index 00000000000..09ee800d332 --- /dev/null +++ b/Artsy/Classes/Models/ARFeed.h @@ -0,0 +1,9 @@ +@interface ARFeed : NSObject + ++ (NSSet *)feedItemTypes; + +@property (nonatomic, readonly, copy) NSString *cursor; + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void(^)(NSOrderedSet *parsed))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Models/ARFeed.m b/Artsy/Classes/Models/ARFeed.m new file mode 100644 index 00000000000..d334d8e14a2 --- /dev/null +++ b/Artsy/Classes/Models/ARFeed.m @@ -0,0 +1,96 @@ +#import "ARFeedItems.h" + +static NSDictionary *StaticFeedItemMap; +static NSSet *StaticFeedItemTypes; + +static NSString *ARFeedCursorKey = @"ARFeedCursorKey"; + +@interface ARFeed () +@property (nonatomic, copy) NSString *cursor; +@end + +@implementation ARFeed + ++ (void)initialize +{ + if (self == [ARFeed class]) { + StaticFeedItemMap = @{ + @"PartnerShow" : [ARPartnerShowFeedItem class], + @"Post" : [ARPostFeedItem class] + }; + + StaticFeedItemTypes = [NSSet setWithArray:[StaticFeedItemMap allValues]]; + }; +} + ++ (NSSet *)feedItemTypes +{ + return StaticFeedItemTypes; +} + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void(^)(NSOrderedSet *))success failure:(void (^)(NSError *error))failure { + [NSException raise:NSInvalidArgumentException format:@"NSObject %@[%@]: selector not recognized - use a subclass: ", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; +} + +- (NSOrderedSet *)parseItemsFromJSON:(NSDictionary *)result +{ + NSDictionary *feedItemMap = StaticFeedItemMap; + NSMutableOrderedSet *objects = [[NSMutableOrderedSet alloc] init]; + id next = result[@"next"]; + + if (next == [NSNull null]) { + self.cursor = nil; + } else { + if ([next isKindOfClass:[NSString class]]) { + self.cursor = next; + + // Sometimes the cursor is an NSNumber, so stringify it + } else if ([next respondsToSelector:@selector(stringValue)]) { + self.cursor = [next stringValue]; + ARErrorLog(@"Got a weird next cursor for the feed but string'd %@ - ", self.cursor); + + } else { + ARErrorLog(@"Got a weird next cursor for the feed %@ - ", self.cursor); + } + } + + for (NSDictionary *item in result[@"results"]) { + NSString *type = item[@"_type"]; + Class itemClass = feedItemMap[type]; + if (itemClass) { + NSError *error = nil; + id object = [itemClass modelWithJSON:item error:&error]; + if (!error) { + [objects addObject:object]; + } else { + ARErrorLog(@"Error creating %@ - %@", type, error.localizedDescription); + } + } else { + ARActionLog(@"Unknown feed item type %@! Ignoring!", type); + } + } + + // Don't return the mutable set + return [NSOrderedSet orderedSetWithOrderedSet:objects]; +} + +#pragma mark - state restoration + +- (void)encodeRestorableStateWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.cursor forKey:ARFeedCursorKey]; +} + +- (void)decodeRestorableStateWithCoder:(NSCoder *)coder +{ + if ([coder containsValueForKey:ARFeedCursorKey]) { + + id cursor = [coder decodeObjectForKey:ARFeedCursorKey]; + + if ([cursor isKindOfClass:[NSString class]]){ + self.cursor = cursor; + } + } +} + +@end diff --git a/Artsy/Classes/Models/ARFeedSubclasses.h b/Artsy/Classes/Models/ARFeedSubclasses.h new file mode 100644 index 00000000000..2f1694bfee0 --- /dev/null +++ b/Artsy/Classes/Models/ARFeedSubclasses.h @@ -0,0 +1,22 @@ +#import "ARFeed.h" + +/// For taking a feed from a file + +@interface ARFileFeed : ARFeed +- (instancetype)initWithNamedFile:(NSString *)fileName; +@end + +@interface ARShowFeed : ARFileFeed @end + +@interface ARProfileFeed : ARFeed +- (instancetype)initWithProfile:(Profile *)profile; +@end + +@interface ARFairOrganizerFeed : ARFeed +- (instancetype)initWithFairOrganizer:(FairOrganizer *)fairOrganizer; +@end + +@interface ARFairShowFeed : ARFeed +- (instancetype)initWithFair:(Fair *)fair; +- (instancetype)initWithFair:(Fair *)fair partner:(Partner *)partner; +@end diff --git a/Artsy/Classes/Models/ARFeedSubclasses.m b/Artsy/Classes/Models/ARFeedSubclasses.m new file mode 100644 index 00000000000..30be8cbd292 --- /dev/null +++ b/Artsy/Classes/Models/ARFeedSubclasses.m @@ -0,0 +1,209 @@ +#import "ARAppBackgroundFetchDelegate.h" + +@interface ARFeed () +- (NSOrderedSet *)parseItemsFromJSON:(NSDictionary *)result; +@end + +@interface ARFileFeed() +@property (nonatomic, copy) id JSON; +@end + +@implementation ARFileFeed + +- (instancetype)initWithNamedFile:(NSString *)fileName +{ + self = [super init]; + if (!self) { return nil; } + + NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:fileName ofType:@"json"]]; + NSError *error = nil; + _JSON = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error]; + + return self; +} + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void (^)(NSOrderedSet *))success failure:(void (^)(NSError *))failure { + if (success) { + dispatch_async(dispatch_get_main_queue(), ^{ + success([self parseItemsFromJSON:self.JSON]); + }); + } +} + +@end + + +//////////////////////// + + +@interface ARShowFeed () +@property (nonatomic, assign) BOOL parsing; +@end + +@implementation ARShowFeed + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + //NSString *fetchBackgroundFilePath = [ARAppBackgroundFetchDelegate pathForDownloadedShowFeed]; + //self.JSON = [NSKeyedUnarchiver unarchiveObjectWithFile:fetchBackgroundFilePath]; + + return self; +} + + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void (^)(NSOrderedSet *))success failure:(void (^)(NSError *))failure { + @weakify(self); + if (self.JSON) { + // We may get asked multiple times before we finished extracting the data + + if (self.parsing) { + return; + } + + self.parsing = YES; + + // If we've background fetch'd a copy of the feed, we should use that on the first grab of show data + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @strongify(self); + NSOrderedSet *items = [self parseItemsFromJSON:self.JSON]; + + dispatch_async(dispatch_get_main_queue(), ^{ + success(items); + }); + + [[NSFileManager defaultManager] removeItemAtPath:[ARAppBackgroundFetchDelegate pathForDownloadedShowFeed] error:nil]; + self.JSON = nil; + }); + + return; + } + + NSInteger pageSize = (cursor) ? 4 : 1; + [ArtsyAPI getFeedResultsForShowsWithCursor:cursor pageSize:pageSize success:^(id JSON) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @strongify(self); + NSOrderedSet *items = [self parseItemsFromJSON:JSON]; + dispatch_async(dispatch_get_main_queue(), ^{ + success(items); + }); + }); + + } failure:failure]; +} + +@end + + +//////////////////////// + + +@interface ARProfileFeed () +@property (nonatomic, strong) Profile *profile; +@end + +@implementation ARProfileFeed + +- (instancetype)initWithProfile:(Profile *)profile { + self = [super init]; + if (!self) { return nil; } + + _profile = profile; + return self; +} + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void (^)(NSOrderedSet *))success failure:(void (^)(NSError *))failure { + + @weakify(self); + [ArtsyAPI getFeedResultsForProfile:self.profile withCursor:cursor success:^(id JSON) { + @strongify(self); + success([self parseItemsFromJSON:JSON]); + } failure:failure]; +} + +@end + + +//////////////////////// + + +@interface ARFairOrganizerFeed () +@property (nonatomic, strong) FairOrganizer *fairOrganizer; +@end + + +@implementation ARFairOrganizerFeed + +- (instancetype)initWithFairOrganizer:(FairOrganizer *)fairOrganizer +{ + self = [super init]; + if (!self) { return nil; } + + _fairOrganizer = fairOrganizer; + return self; +} + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void (^)(NSOrderedSet *))success failure:(void (^)(NSError *))failure { + @weakify(self); + [ArtsyAPI getFeedResultsForFairOrganizer:self.fairOrganizer withCursor:cursor success:^(id JSON) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @strongify(self); + NSOrderedSet *items = [self parseItemsFromJSON:JSON]; + + dispatch_async(dispatch_get_main_queue(), ^{ + success(items); + }); + }); + } failure:failure]; +} + +@end + + +//////////////////////// + + +@interface ARFairShowFeed () +@property (nonatomic, strong) Fair *fair; +@property (nonatomic, strong) Partner *partner; +@end + + +@implementation ARFairShowFeed + +- (instancetype)initWithFair:(Fair *)fair +{ + return [self initWithFair:fair partner:nil]; +} + +- (instancetype)initWithFair:(Fair *)fair partner:(Partner *)partner +{ + self = [super init]; + if (!self) { return nil; } + + _fair = fair; + _partner = partner; + return self; +} + +- (void)getFeedItemsWithCursor:(NSString *)cursor success:(void (^)(NSOrderedSet *))success failure:(void (^)(NSError *))failure +{ + @weakify(self); + + [ArtsyAPI getFeedResultsForFairShows:self.fair partnerID:self.partner.partnerID withCursor:cursor success:^(id JSON) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @strongify(self); + NSOrderedSet *items = [self parseItemsFromJSON:JSON]; + + dispatch_async(dispatch_get_main_queue(), ^{ + success(items); + }); + }); + } failure:failure]; +} + +@end + diff --git a/Artsy/Classes/Models/ARFeedTimeline.h b/Artsy/Classes/Models/ARFeedTimeline.h new file mode 100644 index 00000000000..61894ac940d --- /dev/null +++ b/Artsy/Classes/Models/ARFeedTimeline.h @@ -0,0 +1,20 @@ +#import "ARFeed.h" + +@class ARFeedItem; + +/// The ARFeedTimeline will take an ARFeed and use it to generate +/// a timeline that makes it easy to deal with showing the data using +/// a tableview. + +@interface ARFeedTimeline : NSObject + +- (id)initWithFeed:(ARFeed *)feed; +- (ARFeedItem *)itemAtIndex:(NSInteger)index; +- (void)getNewItems:(void(^)())success failure:(void (^)(NSError *error))failure; +- (void)getNextPage:(void(^)())success failure:(void (^)(NSError *error))failure completion:(void(^)())completion; +- (void)removeAllItems; + +@property (nonatomic, assign) BOOL hasNext; +@property (nonatomic, assign) NSInteger numberOfItems; +@property (nonatomic, assign, getter=isLoading) BOOL loading; +@end diff --git a/Artsy/Classes/Models/ARFeedTimeline.m b/Artsy/Classes/Models/ARFeedTimeline.m new file mode 100644 index 00000000000..87008f68e3f --- /dev/null +++ b/Artsy/Classes/Models/ARFeedTimeline.m @@ -0,0 +1,111 @@ +#import "ARFeedItem.h" + +@interface ARFeedTimeline() +@property (nonatomic, strong) ARFeed *currentFeed; +@property (nonatomic, copy) NSString *currentlyLoadingCursor; +@property (nonatomic, strong) id representedObject; +@property (nonatomic, strong) NSMutableOrderedSet *items; // set enforces uniqueness of feed items +@end + +@implementation ARFeedTimeline + +- (instancetype)initWithFeed:(ARFeed *)feed +{ + self = [super init]; + if (!self) { return nil; } + + _currentFeed = feed; + _items = [[NSMutableOrderedSet alloc] init]; + + return self; +} + +- (void)removeAllItems +{ + [self.items removeAllObjects]; + self.currentlyLoadingCursor = nil; +} + +// Should this have a "no more items" block? - ./ + +- (void)getNewItems:(void(^)())success failure:(void (^)(NSError *error))failure +{ + @weakify(self); + [_currentFeed getFeedItemsWithCursor:nil success:^(NSOrderedSet *parsedItems) { + @strongify(self); + + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [parsedItems count])]; + NSArray *newItems = [parsedItems array]; + [self.items insertObjects:newItems atIndexes:indexSet]; + if (success) { + success(); + } + } failure:failure]; +} + +- (void)getNextPage:(void(^)())success failure:(void (^)(NSError *error))failure completion:(void (^)())completion +{ + if (![self hasNext]) { + if (completion) { + completion(); + } + return; + } + + self.currentlyLoadingCursor = self.currentFeed.cursor; + + @weakify(self); + void (^successBlock)(id) = ^(NSOrderedSet *parsedItems) { + @strongify(self); + if (parsedItems.count) { + [self.items addObjectsFromArray:[parsedItems array]]; + if (success) { + success(); + } + } else { + if (completion) { + completion(); + } + } + }; + + void (^failureBlock)(NSError *) = ^(NSError *error) { + @strongify(self); + self.currentlyLoadingCursor = nil; + if (failure) { + failure(error); + } + }; + + [self.currentFeed getFeedItemsWithCursor:self.currentFeed.cursor success:successBlock failure:failureBlock]; +} + +#pragma mark - feed items datasource + +- (NSInteger)numberOfItems +{ + return self.items.count; +} + +- (ARFeedItem *)itemAtIndex:(NSInteger)index +{ + return self.items[index]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@" ARFeed (%@ items)", @(self.items.count)]; +} + +- (BOOL)hasNext +{ + return (self.currentFeed.cursor != nil); +} + +- (BOOL)isLoading +{ + return (self.currentlyLoadingCursor && + [self.currentlyLoadingCursor isEqualToString:self.currentFeed.cursor]); +} + +@end diff --git a/Artsy/Classes/Models/ARHasImageBaseURL.h b/Artsy/Classes/Models/ARHasImageBaseURL.h new file mode 100644 index 00000000000..6c0c3b2694e --- /dev/null +++ b/Artsy/Classes/Models/ARHasImageBaseURL.h @@ -0,0 +1,5 @@ +#import + +@protocol ARHasImageBaseURL +- (NSString *)baseImageURL; +@end diff --git a/Artsy/Classes/Models/ARHeartStatus.h b/Artsy/Classes/Models/ARHeartStatus.h new file mode 100644 index 00000000000..e676d16a1c0 --- /dev/null +++ b/Artsy/Classes/Models/ARHeartStatus.h @@ -0,0 +1,5 @@ +typedef NS_ENUM(NSInteger, ARHeartStatus) { + ARHeartStatusNotFetched, + ARHeartStatusNo, + ARHeartStatusYes +}; diff --git a/Artsy/Classes/Models/ARHeroUnitsNetworkModel.h b/Artsy/Classes/Models/ARHeroUnitsNetworkModel.h new file mode 100644 index 00000000000..e9a220977e6 --- /dev/null +++ b/Artsy/Classes/Models/ARHeroUnitsNetworkModel.h @@ -0,0 +1,11 @@ +#import + +@interface ARHeroUnitsNetworkModel : NSObject + +@property (nonatomic, readonly, copy) NSArray *heroUnits; +@property (nonatomic, readonly) BOOL isLoading; + +- (void)getHeroUnitsWithSuccess:(void (^)())success + failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Models/ARHeroUnitsNetworkModel.m b/Artsy/Classes/Models/ARHeroUnitsNetworkModel.m new file mode 100644 index 00000000000..f179f036b86 --- /dev/null +++ b/Artsy/Classes/Models/ARHeroUnitsNetworkModel.m @@ -0,0 +1,62 @@ +#import "ARHeroUnitsNetworkModel.h" +#import "ArtsyAPI+Private.h" + +static NSString *ARHeroUnitsDataSourceItemsKey = @"ARHeroUnitsDataSourceItemsKey"; + +@interface ARHeroUnitsNetworkModel () +@property (nonatomic, copy, readwrite) NSArray *heroUnits; +@property (nonatomic, assign) BOOL isLoading; +@end + +@implementation ARHeroUnitsNetworkModel + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + self.isLoading = NO; + + return self; +} + +- (void)getHeroUnitsWithSuccess:(void (^)())success failure:(void (^)(NSError *error))failure +{ + if (self.isLoading) { + return; + } + + self.isLoading = YES; + @weakify(self); + + // This is generally one of the first networking calls, lets make sure it comes through. + + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + [ArtsyAPI getSiteHeroUnits:^(NSArray *heroUnits) { + + @strongify(self); + self.isLoading = NO; + + if (success) { + NSArray *filteredHeroUnits = [heroUnits select:^BOOL(SiteHeroUnit *unit) { + return unit.isCurrentlyActive; + }]; + self.heroUnits = filteredHeroUnits; + + ar_dispatch_main_queue(^{ + success(); + }); + } + + } failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"There was an error getting Hero Units: %@", error.localizedDescription); + self.isLoading = NO; + if (failure) { + failure(error); + } + }]; + }]; +} + +@end diff --git a/Artsy/Classes/Models/ARPostAttachment.h b/Artsy/Classes/Models/ARPostAttachment.h new file mode 100644 index 00000000000..6d93f0349e1 --- /dev/null +++ b/Artsy/Classes/Models/ARPostAttachment.h @@ -0,0 +1,9 @@ +#import + +@protocol ARPostAttachment + +- (NSURL *)urlForThumbnail; +- (CGFloat)aspectRatio; +- (CGSize)maxSize; + +@end diff --git a/Artsy/Classes/Models/ARUserSettingsViewController.h b/Artsy/Classes/Models/ARUserSettingsViewController.h new file mode 100644 index 00000000000..ce0d394085d --- /dev/null +++ b/Artsy/Classes/Models/ARUserSettingsViewController.h @@ -0,0 +1,7 @@ +#import "FODFormViewController.h" + +@interface ARUserSettingsViewController : FODFormViewController + +- (instancetype)initWithUser:(User *)user; + +@end diff --git a/Artsy/Classes/Models/ARUserSettingsViewController.m b/Artsy/Classes/Models/ARUserSettingsViewController.m new file mode 100644 index 00000000000..459ef9a4b96 --- /dev/null +++ b/Artsy/Classes/Models/ARUserSettingsViewController.m @@ -0,0 +1,208 @@ +#import "ARUserSettingsViewController.h" + +#import + +#import "ARUserManager.h" + +@interface ARUserSettingsViewController() +@property (nonatomic, strong) User *user; +@end + +@implementation ARUserSettingsViewController + +#pragma mark - ARMenuAwareViewController + +- (BOOL)hidesBackButton { + return NO; +} + +- (BOOL)hidesToolbarMenu { + return YES; +} + +- (instancetype)initWithUser:(User *)user +{ + FODForm *form = [ARUserSettingsViewController setUpFormWithUser:user]; + self = [super initWithForm:form userInfo:nil]; + if (!self) { return nil; } + + NSDictionary *nibOverrides = @{ + @"TextInputCellWithTitle" : @"ARTextInputCellWithTitle", + @"SwitchCell" : @"ARSwitchCell" + }; + + self.cellFactory = [[FODCellFactory alloc] initWithOverridesDict:nibOverrides]; + _user = user; + + return self; +} + +- (void)createSaveAndCancelButtons +{ + // don't create these buttons +} + +- (void)createTableView +{ + CGRect frame = self.view.frame; + frame.origin.y += 20; + + self.tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped]; + self.tableView.dataSource = self; + self.tableView.delegate = self; + self.tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + + UIEdgeInsets insetCopy = self.tableView.separatorInset; + insetCopy.right = insetCopy.left; + [self.tableView setSeparatorInset:insetCopy]; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.tableView.backgroundColor = [UIColor whiteColor]; +} + ++ (FODForm *)setUpFormWithUser:(User *)user +{ + FODFormBuilder *builder = [[FODFormBuilder alloc] init]; + + [builder startFormWithTitle:@""]; + + [builder section:@"Settings"]; + + [builder rowWithKey:@"name" + ofClass:[FODTextInputRow class] + andTitle:@"Full Name" + andValue:user.name]; + + [builder rowWithKey:@"defaultProfileID" + ofClass:[FODTextInputRow class] + andTitle:@"Username" + andValue:user.defaultProfileID]; + + [builder rowWithKey:@"email" + ofClass:[FODTextInputRow class] + andTitle:@"Email" + andValue:user.email]; + + [builder rowWithKey:@"phone" + ofClass:[FODTextInputRow class] + andTitle:@"Phone" + andValue:user.phone]; + + [builder section:@"Email Me"]; + + [builder rowWithKey:@"receiveWeeklyEmail" + ofClass:[FODBooleanRow class] + andTitle:@"Weekly featured content" + andValue:@(user.receiveWeeklyEmail)]; + + [builder rowWithKey:@"receiveFollowArtistsEmail" + ofClass:[FODBooleanRow class] + andTitle:@"About artists I follow" + andValue:@(user.receiveFollowArtistsEmail)]; + + [builder rowWithKey:@"receiveFollowUsersEmail" + ofClass:[FODBooleanRow class] + andTitle:@"When someone follows me" + andValue:@(user.receiveFollowUsersEmail)]; + + FODForm *form = [builder finishForm]; + return form; +} + +-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath +{ + [ARUserSettingsViewController tableView:tableView addSeparatorToViewElement:cell]; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + + CGFloat headerWidth = tableView.frame.size.width; + CGFloat headerHeight = [self tableView:tableView heightForHeaderInSection: section]; + UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, headerWidth, headerHeight)]; + + UILabel *label = [ARThemedFactory labelForViewSubHeaders]; + label.frame = header.frame; + NSString *labelText = [self.form.sections[section] title].uppercaseString; + [label setText:labelText withLetterSpacing:0.5]; + label.font = [label.font fontWithSize:14]; + label.backgroundColor = [UIColor clearColor]; + + [ARUserSettingsViewController tableView:tableView addSeparatorToViewElement:header ]; + [header addSubview:label]; + return header; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + return 44; +} + ++ (void)tableView:(UITableView *)tableView addSeparatorToViewElement:(UIView *)view{ + CGFloat height = 1; + // Check to see if we have already added a separator subview + if ([view viewWithTag:ARSeparatorTag]) { + return; + } + + CGFloat width = tableView.frame.size.width - tableView.separatorInset.left - tableView.separatorInset.right; + CGFloat xOrigin = tableView.separatorInset.left; + CGFloat yOrigin = view.frame.size.height - height; + + UIView *separator = [[UIView alloc] initWithFrame: CGRectMake(xOrigin, yOrigin, width, height)]; + separator.tag = ARSeparatorTag; + separator.backgroundColor = [UIColor artsyLightGrey]; + [view addSubview:separator]; +} + +- (void)switchValueChangedTo:(BOOL)newValue userInfo:(id)userInfo +{ + [super switchValueChangedTo:newValue userInfo:userInfo]; + FODFormRow *row = (FODFormRow*)userInfo; + + @weakify(row); + [ArtsyAPI updateCurrentUserProperty:[User JSONKeyPathsByPropertyKey][row.key] + toValue:row.workingValue + success:^(User *user) { + @strongify(row); + [[User currentUser] setValue:[user valueForKey:row.key] forKey:row.key]; + [[ARUserManager sharedManager] storeUserData]; + } + failure:^(NSError *error) { + @strongify(row); + row.workingValue = row.initialValue; + [self.tableView reloadRowsAtIndexPaths:@[row.indexPath] withRowAnimation:UITableViewRowAnimationNone]; + } + ]; +} + +- (void) valueChangedTo:(NSString *)newValue userInfo:(id)userInfo { + [super valueChangedTo:newValue userInfo:userInfo]; + + FODFormRow *row = (FODFormRow*)userInfo; + if ([(NSString *)row.workingValue isEqualToString:(NSString *)row.initialValue]){ + return; + } + + @weakify(row); + [ArtsyAPI updateCurrentUserProperty:[User JSONKeyPathsByPropertyKey][row.key] toValue:row.workingValue + success:^(User *user) { + @strongify(row); + [[User currentUser] setValue:[user valueForKey:row.key] forKey:row.key]; + [[ARUserManager sharedManager] storeUserData]; + } + failure:^(NSError *error) { + @strongify(row); + row.workingValue = row.initialValue; + [self.tableView reloadRowsAtIndexPaths:@[row.indexPath] withRowAnimation:UITableViewRowAnimationNone]; + } + ]; +} + +- (void)formCancelled:(FODForm *)form userInfo:(id)userInfo{ + +} + +- (void)formSaved:(FODForm *)form userInfo:(id)userInfo{ + +} + +@end diff --git a/Artsy/Classes/Models/Artist.h b/Artsy/Classes/Models/Artist.h new file mode 100644 index 00000000000..55217cdf5f9 --- /dev/null +++ b/Artsy/Classes/Models/Artist.h @@ -0,0 +1,27 @@ +#import "MTLModel.h" +#import "ARFollowable.h" +#import "ARShareableObject.h" +#import "ARHasImageBaseURL.h" + +@interface Artist : MTLModel + +@property (readonly, nonatomic, copy) NSString *artistID; +@property (readonly, nonatomic, copy) NSString *name; +@property (readonly, nonatomic, copy) NSString *years; +@property (readonly, nonatomic, copy) NSString *nationality; +@property (readonly, nonatomic, copy) NSString *blurb; +@property (readonly, nonatomic, copy) NSNumber *publishedArtworksCount; +@property (readonly, nonatomic, copy) NSNumber *forSaleArtworksCount; + +- (instancetype)initWithArtistID:(NSString *)artistID; + +- (void)getFollowState:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure; +- (void)getArtworksAtPage:(NSInteger)page andParams:(NSDictionary *)params success:(void (^)(NSArray *artworks))success; + +- (AFJSONRequestOperation *)getRelatedPosts:(void (^)(NSArray *posts))success; +- (AFJSONRequestOperation *)getRelatedArtists:(void (^)(NSArray *artists))success; + +- (NSURL *)smallImageURL; + +- (NSString *)publicURL; +@end diff --git a/Artsy/Classes/Models/Artist.m b/Artsy/Classes/Models/Artist.m new file mode 100644 index 00000000000..6fc5f4d55c8 --- /dev/null +++ b/Artsy/Classes/Models/Artist.m @@ -0,0 +1,158 @@ +#import "ARNetworkConstants.h" + +@interface Artist() { + BOOL _isFollowed; +} +@property (nonatomic, copy, readonly) NSString *urlFormatString; +@end + +@implementation Artist + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"artistID" : @"id", + @"name" : @"name", + @"years" : @"years", + @"nationality" : @"nationality", + @"blurb": @"blurb", + @"publishedArtworksCount": @"published_artworks_count", + @"forSaleArtworksCount": @"forsale_artworks_count", + @"urlFormatString": @"image_url" + }; +} + +- (NSURL *)largeImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"square"]]; +} + +// the smallest is thumb on an artist + +- (NSURL *)smallImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"square"]]; +} + +- (NSString *)baseImageURL +{ + return self.urlFormatString; +} + +- (instancetype)initWithArtistID:(NSString *)artistID +{ + self = [super init]; + if (!self) { return nil; } + + _artistID = artistID; + + return self; +} + +- (void)setFollowed:(BOOL)isFollowed +{ + _isFollowed = isFollowed; +} + +- (BOOL)isFollowed +{ + return _isFollowed; +} + +- (void)followWithSuccess:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + [self setFollowState:YES success:success failure:failure]; +} + +- (void)unfollowWithSuccess:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + [self setFollowState:NO success:success failure:failure]; +} + +- (void)setFollowState:(BOOL)state success:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + @weakify(self); + [ArtsyAPI setFavoriteStatus:state forArtist:self success:^(id response) { + @strongify(self); + self.followed = state; + if (success) { + success(response); + } + } failure:^(NSError *error) { + @strongify(self); + self.followed = !state; + if (failure) { + failure(error); + } + }]; +} + +- (void)getFollowState:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure +{ + if ([User isTrialUser]) { + success(ARHeartStatusNo); + return; + } + + @weakify(self); + [ArtsyAPI checkFavoriteStatusForArtist:self success:^(BOOL result) { + @strongify(self); + self.followed = result; + success(result ? ARHeartStatusYes : ARHeartStatusNo); + } failure:failure]; +} + +- (void)getArtworksAtPage:(NSInteger)page andParams:(NSDictionary *)params success:(void (^)(NSArray *artworks))success { + [ArtsyAPI getArtistArtworks:self andPage:page withParams:params success:^(NSArray *artworks) { + success(artworks); + } failure:^(NSError *error) { + success(@[]); + }]; +} + +- (NSString *)publicURL { + NSString *path = [NSString stringWithFormat:ARArtistInformationURLFormat, self.artistID]; + NSURL *url = [ARSwitchBoard.sharedInstance resolveRelativeUrl:path]; + return [url absoluteString]; +} + +- (BOOL)isEqual:(id)object +{ + if (![object isKindOfClass:[Artist class]]) { + return NO; + } + Artist *other = (Artist *)object; + return [self.artistID isEqualToString:other.artistID]; + +} + +- (NSUInteger)hash +{ + return self.artistID.hash; +} + +- (AFJSONRequestOperation *)getRelatedPosts:(void (^)(NSArray *posts))success +{ + return [ArtsyAPI getRelatedPostsForArtist:self + success:success + failure: ^(NSError *error) { + success(@[]); + }]; +} + +- (AFJSONRequestOperation *)getRelatedArtists:(void (^)(NSArray *artists))success +{ + return [ArtsyAPI getRelatedArtistsForArtist:self + success:success + failure: ^(NSError *error) { + success(@[]); + }]; +} + +#pragma mark ShareableObject +- (NSString *)publicArtsyPath +{ + return [NSString stringWithFormat:@"/artist/%@", self.artistID]; +} + +@end diff --git a/Artsy/Classes/Models/Artwork.h b/Artsy/Classes/Models/Artwork.h new file mode 100644 index 00000000000..8544fe27042 --- /dev/null +++ b/Artsy/Classes/Models/Artwork.h @@ -0,0 +1,126 @@ +#import "ARPostAttachment.h" +#import "Image.h" +#import "ARHasImageBaseURL.h" +#import "SaleArtwork.h" +#import "ARShareableObject.h" +#import +#import "ARHeartStatus.h" + +// TODO: Add support ARFollowable for following status + +@class Artist, Partner, Profile, Sale, Fair; + +typedef NS_ENUM(NSInteger, ARArtworkAvailability) { + ARArtworkAvailabilityNotForSale, + ARArtworkAvailabilityForSale, + ARArtworkAvailabilityOnHold, + ARArtworkAvailabilitySold +}; + +typedef NS_ENUM(NSInteger, ARDimensionMetric) { + ARDimensionMetricInches, + ARDimensionMetricCentimeters, + ARDimensionMetricNoMetric +}; + +@interface Artwork : MTLModel + +@property (nonatomic, copy) NSString *artworkID; +@property (nonatomic, strong) NSNumber *depth; +@property (nonatomic, strong) NSNumber *diameter; +@property (nonatomic, strong) NSNumber *height; +@property (nonatomic, strong) NSNumber *width; +@property (nonatomic) ARDimensionMetric metric; + +@property (nonatomic, copy) NSString *dimensionsCM; +@property (nonatomic, copy) NSString *dimensionsInches; + +@property (nonatomic, strong) Artist *artist; +@property (nonatomic, copy) NSString *imageFormatAddress; + +@property (nonatomic, strong) Partner *partner; +@property (nonatomic, copy) NSString *collectingInstitution; + +// not a property, carried around for fair context +- (Fair *) fair; + +// we're just gonna leave these as dictionaries for now +// I think? +@property (nonatomic, copy) NSArray *editionSets; + +@property (nonatomic, assign) enum ARArtworkAvailability availability; + +@property (nonatomic, copy) NSString *date; +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) NSString *displayTitle; +@property (nonatomic, copy) NSString *exhibitionHistory; +@property (nonatomic, copy) NSString *additionalInfo; +@property (nonatomic, strong) NSNumber *isPriceHidden; +@property (nonatomic, strong, getter = isPublished) NSNumber *published; +@property (nonatomic, copy) NSString *imageRights; +@property (nonatomic, copy) NSString *medium; +@property (nonatomic, copy) NSString *literature; +@property (nonatomic, copy) NSString *provenance; +@property (nonatomic, copy) NSString *series; +@property (nonatomic, copy) NSString *signature; +@property (nonatomic, copy) NSString *category; + +@property (nonatomic, copy) NSString *saleMessage; + + +@property (nonatomic, strong) NSNumber *acquireable; +@property (nonatomic, strong) NSNumber *inquireable; +@property (nonatomic, strong) NSNumber *sold; +@property (nonatomic, strong) NSNumber *forSale; +@property (nonatomic, strong) NSNumber *canShareImage; +@property (nonatomic, strong) NSNumber *auctionResultCount; +@property (nonatomic, strong) Sale *auction; +@property (readonly, nonatomic, assign) BOOL isFollowed; + +@property (nonatomic, copy) NSString *price; + +@property (nonatomic, copy) NSString *blurb; + +@property (nonatomic, strong) NSDate *updatedAt; + +@property (nonatomic, strong) Image *defaultImage; + +- (ARHeartStatus)heartStatus; + +- (AFJSONRequestOperation *)getRelatedArtworks:(void (^)(NSArray *artworks))success; +- (AFJSONRequestOperation *)getRelatedAuctionResults:(void (^)(NSArray *auctionResults))success; +- (AFJSONRequestOperation *)getRelatedFairArtworks:(Fair *)fair success:(void (^)(NSArray *artworks))success; +- (AFJSONRequestOperation *)getRelatedPosts:(void (^)(NSArray *posts))success; + +/// Gets an update from the server and updates itself, triggers defers from onArtworkUpdate +- (void)updateArtwork; +- (void)updateSaleArtwork; +- (void)updateFair; + +/// Adds a callback when the artwork has been update, does not trigger said update. +- (KSPromise *)onArtworkUpdate:(void(^)(void))success failure:(void(^)(NSError *error))failure; +- (KSPromise *)onSaleArtworkUpdate:(void(^)(SaleArtwork *saleArtwork))success failure:(void(^)(NSError *error))failure; +- (KSPromise *)onFairUpdate:(void(^)(Fair *fair))success failure:(void(^)(NSError *error))failure; + +- (void)setFollowState:(BOOL)state success:(void (^)(id))success failure:(void (^)(NSError *))failure; +- (void)getFavoriteStatus:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure; + +- (BOOL)canViewInRoom; +- (BOOL)hasWidth; +- (BOOL)hasHeight; +- (BOOL)hasDepth; +- (BOOL)hasDiameter; +- (BOOL)hasDimensions; +- (BOOL)hasWidthAndHeight; +- (BOOL)hasMoreInfo; +- (BOOL)shouldShowAuctionResults; +- (BOOL)hasMultipleEditions; +- (NSString *)auctionResultsPath; + +- (CGFloat)widthInches; +- (CGFloat)heightInches; +- (CGFloat)diameterInches; + +- (instancetype)initWithArtworkID:(NSString *)artworkID; + +@end diff --git a/Artsy/Classes/Models/Artwork.m b/Artsy/Classes/Models/Artwork.m new file mode 100644 index 00000000000..3bb41988066 --- /dev/null +++ b/Artsy/Classes/Models/Artwork.m @@ -0,0 +1,564 @@ +#import "ARValueTransformer.h" + +@implementation Artwork { + // If we give these as properties they can cause + // chaos with Mantle & State Resotoration. + + KSDeferred *_artworkUpdateDeferred; + KSDeferred *_saleArtworkUpdateDeferred; + KSDeferred *_favDeferred; + KSDeferred *_fairDeferred; + enum ARHeartStatus _heartStatus; + Fair *_fair; +} + +- (instancetype)initWithArtworkID:(NSString *)artworkID +{ + self = [super init]; + if (!self) { return nil; } + + _artworkID = artworkID; + _heartStatus = ARHeartStatusNotFetched; + + return self; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(Artwork.new, artworkID) : @"id", + @keypath(Artwork.new, auctionResultCount) : @"comparables_count", + @keypath(Artwork.new, canShareImage) : @"can_share_image", + @keypath(Artwork.new, collectingInstitution) : @"collecting_institution", + @keypath(Artwork.new, defaultImage) : @"images", + @keypath(Artwork.new, additionalInfo) : @"additional_information", + @keypath(Artwork.new, dimensionsCM) : @"dimensions.cm", + @keypath(Artwork.new, dimensionsInches) : @"dimensions.in", + @keypath(Artwork.new, displayTitle) : @"display", + @keypath(Artwork.new, editionSets) : @"edition_sets", + @keypath(Artwork.new, exhibitionHistory) : @"exhibition_history", + @keypath(Artwork.new, forSale) : @"forsale", + @keypath(Artwork.new, imageRights) : @"image_rights", + @keypath(Artwork.new, published) : @"published", + @keypath(Artwork.new, saleMessage) : @"sale_message", + @keypath(Artwork.new, sold) : @"sold" + }; +} + +#pragma mark Model Upgrades + ++ (NSUInteger)modelVersion +{ + return 1; +} + +- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)fromVersion +{ + if (fromVersion == 0) { + if ([key isEqual:@"additionalInfo"]) { + return [coder decodeObjectForKey:@"info"] ?: [coder decodeObjectForKey:@"additionalInfo"]; + } + } + + return [super decodeValueForKey:key withCoder:coder modelVersion:fromVersion]; +} + ++ (NSValueTransformer *)artistJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Artist class]]; +} + ++ (NSValueTransformer *)partnerJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Partner class]]; +} + ++ (NSValueTransformer *)defaultImageJSONTransformer +{ + return [MTLValueTransformer reversibleTransformerWithForwardBlock: ^ Image *(NSArray *items) { + NSDictionary *defaultImageDict = [[items select: ^(NSDictionary *item) { + return[item[@"is_default"] boolValue]; + }] first]; + return defaultImageDict ? [Image modelWithJSON:defaultImageDict] : nil; + } + reverseBlock:^ NSArray *(Image *image) { + return @[image]; + }]; +} + ++ (NSValueTransformer *)acquireableJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)inquireableJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)forSaleJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)soldJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)provenanceJSONTransformer +{ + return [ARValueTransformer whitespaceTrimmingTransformer]; +} + ++ (NSValueTransformer *)exhibitionHistoryJSONTransformer +{ + return [ARValueTransformer whitespaceTrimmingTransformer]; +} + ++ (NSValueTransformer *)additionalInfoJSONTransformer +{ + return [ARValueTransformer whitespaceTrimmingTransformer]; +} + ++ (NSValueTransformer *)literatureJSONTransformer +{ + return [ARValueTransformer whitespaceTrimmingTransformer]; +} + ++ (NSValueTransformer *)signatureJSONTransformer +{ + return [ARValueTransformer whitespaceTrimmingTransformer]; +} + ++ (NSValueTransformer *)availabilityJSONTransformer +{ + NSDictionary *types = @{ + @"not for sale" : @(ARArtworkAvailabilityNotForSale), + @"for sale" : @(ARArtworkAvailabilityForSale), + @"sold" : @(ARArtworkAvailabilitySold), + @"on hold" :@(ARArtworkAvailabilityOnHold) + }; + return [ARValueTransformer enumValueTransformerWithMap:types]; +} + ++ (NSValueTransformer *)metricJSONTransformer +{ + NSDictionary *metrics = @{@"in": @(ARDimensionMetricInches), @"cm" : @(ARDimensionMetricCentimeters), @"" : @(ARDimensionMetricNoMetric)}; + return [ARValueTransformer enumValueTransformerWithMap:metrics]; +} + +- (CGFloat)aspectRatio +{ + return _defaultImage.aspectRatio ? _defaultImage.aspectRatio : 1; +} + +- (CGSize)maxSize +{ + return CGSizeMake(_defaultImage.originalWidth, _defaultImage.originalHeight); +} + +- (NSURL *)urlForThumbnail +{ + return [_defaultImage urlForThumbnailImage]; +} + +// TODO: Make a URL or call address +- (NSString *)baseImageURL +{ + return _defaultImage.url; +} + +- (AFJSONRequestOperation *)getRelatedArtworks:(void (^)(NSArray *artworks))success +{ + return [ArtsyAPI getRelatedArtworksForArtwork:self success:success + failure: ^(NSError *error) { + success(@[]); + }]; +} + +- (AFJSONRequestOperation *)getRelatedFairArtworks:(Fair *)fair success:(void (^)(NSArray *artworks))success +{ + return [ArtsyAPI getRelatedArtworksForArtwork:self inFair:(fair ?: self.fair) success:success + failure:^(NSError *error) { + success(@[]); + }]; +} + +- (AFJSONRequestOperation *)getRelatedAuctionResults:(void (^)(NSArray *auctionResults))success +{ + return [ArtsyAPI getAuctionComparablesForArtwork:self success:success + failure: ^(NSError *error) { + success(@[]); + }]; +} + +- (AFJSONRequestOperation *)getRelatedPosts:(void (^)(NSArray *posts))success +{ + return [ArtsyAPI getRelatedPostsForArtwork:self success:success + failure: ^(NSError *error) { + success(@[]); + }]; +} + +- (BOOL)hasWidth +{ + return [self.width intValue] > 0; +} + +- (BOOL)hasHeight +{ + return [self.height intValue] > 0; +} + +- (BOOL)hasDiameter +{ + return [self.diameter intValue] > 0; +} + +- (BOOL)hasDepth +{ + return [self.depth intValue] > 0; +} + +- (BOOL)hasWidthAndHeight +{ + return self.hasWidth && self.hasHeight; +} + +- (BOOL)hasDimensions +{ + return self.hasWidthAndHeight || self.hasDiameter; +} + +- (BOOL)canViewInRoom +{ + return (self.hasDimensions + && !self.hasDepth + && [self.category rangeOfString:@"Sculpture"].location == NSNotFound + && [self.category rangeOfString:@"Design"].location == NSNotFound + && [self.category rangeOfString:@"Installation"].location == NSNotFound + && [self.category rangeOfString:@"Architecture"].location == NSNotFound); +} + +- (BOOL)hasMultipleEditions +{ + return (self.editionSets.count > 1); +} + +- (void)updateArtwork +{ + @weakify(self); + __weak KSDeferred *deferred = _artworkUpdateDeferred; + + [ArtsyAPI getArtworkInfo:self.artworkID success:^(id artwork) { + @strongify(self); + [self mergeValuesForKeysFromModel:artwork]; + [deferred resolveWithValue:self]; + + } failure:^(NSError *error) { + [deferred rejectWithError:error]; + }]; +} + +- (BOOL)hasMoreInfo +{ return [self.provenance length] + || [self.exhibitionHistory length] + || [self.signature length] + || [self.additionalInfo length] + || [self.literature length]; +} + +- (KSPromise *)onArtworkUpdate:(void(^)(void))success failure:(void(^)(NSError *error))failure +{ + @weakify(self); + + if (!_artworkUpdateDeferred) { + _artworkUpdateDeferred = [KSDeferred defer]; + } + + return [_artworkUpdateDeferred.promise then: ^(id value) { + if (success) { success(); } + return self; + + } error:^id(NSError *error) { + if (failure) { failure(error); } + + @strongify(self); + ARErrorLog(@"Failed fetching full JSON for artwork %@. Error: %@", self.artworkID, error.localizedDescription); + return error; + }]; +} + +- (KSDeferred *)deferredSaleArtworkUpdate +{ + if (!_saleArtworkUpdateDeferred) { + _saleArtworkUpdateDeferred = [KSDeferred defer]; + } + return _saleArtworkUpdateDeferred; +} + +- (void)updateSaleArtwork +{ + @weakify(self); + + KSDeferred *deferred = [self deferredSaleArtworkUpdate]; + + [ArtsyAPI getSalesWithArtwork:self.artworkID success:^(NSArray *sales) { + + // assume artwork can only be in one auction at most + Sale *auction = nil; + for (Sale *sale in sales) { + if (sale.isAuction) { + auction = sale; + } + break; + } + + if (auction) { + @strongify(self); + [ArtsyAPI getAuctionArtworkWithSale:auction.saleID artwork:self.artworkID success:^(SaleArtwork *saleArtwork) { + saleArtwork.auction = auction; + [deferred resolveWithValue:saleArtwork]; + + } failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"Error fetching auction details for artwork %@: %@", self.artworkID, error.localizedDescription); + [deferred rejectWithError:error]; + }]; + } else { + [deferred resolveWithValue:nil]; + } + + } failure:^(NSError *error) { + @strongify(self); + [deferred rejectWithError:error]; + ARErrorLog(@"Error fetching sales for artwork %@: %@", self.artworkID, error.localizedDescription); + }]; +} + +- (KSPromise *)onSaleArtworkUpdate:(void(^)(SaleArtwork *saleArtwork))success failure:(void(^)(NSError *error))failure +{ + KSDeferred *deferred = [self deferredSaleArtworkUpdate]; + return [deferred.promise then: ^(id value) { + if (success) { + success(value); + } + return self; + } error:^id(NSError *error) { + if (failure) { + failure(error); + } + return error; + }]; +} + +- (KSDeferred *)deferredFairUpdate +{ + if (!_fairDeferred) { + _fairDeferred = [KSDeferred defer]; + } + return _fairDeferred; +} + +- (void)updateFair +{ + @weakify(self); + + KSDeferred *deferred = [self deferredFairUpdate]; + [ArtsyAPI getFairsForArtwork:self success:^(NSArray *fairs) { + @strongify(self); + // we're not checking for count > 0 cause we wanna fulfill with nil if no fairs + Fair *fair = [fairs firstObject]; + self.fair = fair; + [deferred resolveWithValue:fair]; + } failure:^(NSError *error) { + @strongify(self); + [deferred rejectWithError:error]; + ARErrorLog(@"Couldn't get fairs for artwork %@. Error: %@", self.artworkID, error.localizedDescription); + }]; +} + +- (KSPromise *)onFairUpdate:(void (^)(Fair *))success failure:(void (^)(NSError *))failure +{ + KSDeferred *deferred = [self deferredFairUpdate]; + return [deferred.promise then: ^(id value) { + self.fair = value; + if (success) { + success(value); + } + return self; + } error:^id(NSError *error) { + if (failure) { + failure(error); + } + return error; + }]; +} + +- (void)setFollowState:(BOOL)state success:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + @weakify(self); + [ArtsyAPI setFavoriteStatus:state forArtwork:self success:^(id response) { + @strongify(self); + if (!self) { return; } + + self->_heartStatus = state? ARHeartStatusYes : ARHeartStatusNo; + + if (success) { + success(response); + } + } failure:^(NSError *error) { + @strongify(self); + if (!self) { return; } + + self->_heartStatus = ARHeartStatusNo; + + if (failure) { + failure(error); + } + }]; +} + + +- (void)getFavoriteStatus:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure +{ + if ([User isTrialUser]) { + _heartStatus = ARHeartStatusNo; + success(ARHeartStatusNo); + return; + } + + @weakify(self); + + if (!_favDeferred) { + KSDeferred *deferred = [KSDeferred defer]; + [ArtsyAPI checkFavoriteStatusForArtwork:self success:^(BOOL status) { + @strongify(self); + if (!self) { return; } + + self->_heartStatus = status ? ARHeartStatusYes : ARHeartStatusNo; + + [deferred resolveWithValue:@(status)]; + } failure:^(NSError *error) { + [deferred rejectWithError:error]; + }]; + + _favDeferred = deferred; + } + + [_favDeferred.promise then: ^(id value) { + @strongify(self); + + success(self.heartStatus); + return self; + } error: ^(NSError *error) { + // Its a 404 if you have no artworks + NSHTTPURLResponse *response = [error userInfo][AFNetworkingOperationFailingURLResponseErrorKey]; + if (response.statusCode == 404) { + success(ARHeartStatusNo); + } else { + ARErrorLog(@"Failed fetching favorite status for artwork %@. Error: %@", self.artworkID, error.localizedDescription); + failure(error); + } + return error; + }]; +} + +- (NSNumber *)forSale +{ + return @(self.availability == ARArtworkAvailabilityForSale); +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:self.class]) { + return [self.artworkID isEqualToString:[object artworkID]]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.artworkID.hash; +} + +- (ARHeartStatus)heartStatus +{ + return _heartStatus; +} + +- (BOOL)shouldShowAuctionResults +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:ARShowAuctionResultsButtonDefault] + && self.partner + && self.partner.type == ARPartnerTypeGallery + && [self.category rangeOfString:@"Architecture"].location == NSNotFound + && self.auctionResultCount.intValue > 0; +} + +- (CGFloat)dimensionInInches:(CGFloat)dimension { + switch (self.metric) { + case ARDimensionMetricCentimeters: + return dimension * 0.393701; + default: + return dimension; + } +} + +- (CGFloat)widthInches +{ + return [self dimensionInInches:[self.width floatValue]]; +} + +- (CGFloat)heightInches +{ + return [self dimensionInInches:[self.height floatValue]]; +} + +- (CGFloat)diameterInches +{ + return [self dimensionInInches:[self.diameter floatValue]]; +} + +- (NSString *)auctionResultsPath +{ + return [NSString stringWithFormat:@"/artwork/%@/auction_results", self.artworkID]; +} + +- (void)setNilValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"metric"]) { + [self setValue:@(ARDimensionMetricNoMetric) forKey:key]; + } else if ([key isEqualToString:@"availability"]) { + [self setValue:@(ARArtworkAvailabilityNotForSale) forKey:key]; + } else { + [super setNilValueForKey:key]; + } +} + +// NOTE: cannot be a property, otherwise overwritten via updateFair +- (Fair *) fair +{ + return _fair; +} + +- (void) setFair:(Fair *)fair +{ + _fair = fair; +} + +#pragma mark ShareableObject + +- (NSString *)publicArtsyPath +{ + return [NSString stringWithFormat:@"/artwork/%@", self.artworkID]; +} + +- (NSString *)name +{ + return self.title; +} + +@end diff --git a/Artsy/Classes/Models/AuctionLot.h b/Artsy/Classes/Models/AuctionLot.h new file mode 100644 index 00000000000..fe34f8e5236 --- /dev/null +++ b/Artsy/Classes/Models/AuctionLot.h @@ -0,0 +1,18 @@ +#import "MTLModel.h" + +@interface AuctionLot : MTLModel + +@property (nonatomic, copy) NSString *auctionLotID; +@property (nonatomic, copy) NSString *dimensionsCM; +@property (nonatomic, copy) NSString *dimensionsInches; +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) NSString *estimate; +@property (nonatomic, copy) NSString *price; +@property (nonatomic, copy) NSString *dates; +@property (nonatomic, copy) NSString *auctionDateText; +@property (nonatomic, copy) NSString *organization; +@property (nonatomic, copy) NSURL *imageURL; +@property (nonatomic, copy) NSURL *externalURL; +@property (nonatomic, strong) NSDate *auctionDate; + +@end diff --git a/Artsy/Classes/Models/AuctionLot.m b/Artsy/Classes/Models/AuctionLot.m new file mode 100644 index 00000000000..8006ab59916 --- /dev/null +++ b/Artsy/Classes/Models/AuctionLot.m @@ -0,0 +1,49 @@ +#import "ARStandardDateFormatter.h" + +@implementation AuctionLot + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"auctionLotID" : @"id", + @"dimensionsCM" : @"dimensions.cm", + @"dimensionsInches" : @"dimensions.in", + @"estimate" : @"estimate_text", + @"price" : @"price_realized_text", + @"dates" : @"dates_text", + @"auctionDate" : @"auction_date", + @"auctionDateText" : @"auction_dates_text", + @"imageURL" : @"image_url", + @"externalURL" : @"external_url", + }; +} + ++ (NSValueTransformer *)imageURLJSONTransformer { + return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; +} + ++ (NSValueTransformer *)externalURLJSONTransformer { + return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; +} + ++ (NSValueTransformer *)auctionDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + AuctionLot *auctionLot = object; + return [auctionLot.auctionLotID isEqualToString:self.auctionLotID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.auctionLotID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Bid.h b/Artsy/Classes/Models/Bid.h new file mode 100644 index 00000000000..71e5edeb68c --- /dev/null +++ b/Artsy/Classes/Models/Bid.h @@ -0,0 +1,10 @@ +#import "MTLModel.h" +#import "MTLJSONAdapter.h" + +@interface Bid : MTLModel + +@property (nonatomic, strong) NSDecimalNumber *cents; +@property (nonatomic, copy) NSString *bidID; + +- (BOOL)isEqualToBid:(Bid *)otherBid; +@end diff --git a/Artsy/Classes/Models/Bid.m b/Artsy/Classes/Models/Bid.m new file mode 100644 index 00000000000..d9059190e91 --- /dev/null +++ b/Artsy/Classes/Models/Bid.m @@ -0,0 +1,31 @@ +@implementation Bid + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"cents" : @"amount_cents", + @"bidID" : @"id" + }; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) { + return YES; + } else if ([object class] == [self class]) { + return [self isEqualToBid:object]; + } + return NO; +} + +- (BOOL)isEqualToBid:(Bid *)otherBid +{ + return [self.bidID isEqual:otherBid.bidID]; +} + +- (NSUInteger)hash +{ + return self.bidID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Bidder.h b/Artsy/Classes/Models/Bidder.h new file mode 100644 index 00000000000..38d107fdea1 --- /dev/null +++ b/Artsy/Classes/Models/Bidder.h @@ -0,0 +1,8 @@ +#import "MTLModel.h" + +@interface Bidder : MTLModel + +@property (nonatomic, strong) NSString *bidderID; +@property (nonatomic, strong) NSString *saleID; + +@end diff --git a/Artsy/Classes/Models/Bidder.m b/Artsy/Classes/Models/Bidder.m new file mode 100644 index 00000000000..4151858f8c2 --- /dev/null +++ b/Artsy/Classes/Models/Bidder.m @@ -0,0 +1,26 @@ +@implementation Bidder + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"bidderID" : @"id", + @"saleID" : @"sale.id" + }; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Bidder *bidder = object; + return [bidder.bidderID isEqualToString:self.bidderID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.bidderID.hash; +} + +@end diff --git a/Artsy/Classes/Models/BidderPosition.h b/Artsy/Classes/Models/BidderPosition.h new file mode 100644 index 00000000000..4369b35b84c --- /dev/null +++ b/Artsy/Classes/Models/BidderPosition.h @@ -0,0 +1,9 @@ +#import "MTLModel.h" + +@class Bid; + +@interface BidderPosition : MTLModel +@property (nonatomic, copy, readonly) NSString *bidderPositionID; +@property (nonatomic, strong) Bid *highestBid; +@property (nonatomic, strong) NSNumber *maxBidAmountCents; +@end diff --git a/Artsy/Classes/Models/BidderPosition.m b/Artsy/Classes/Models/BidderPosition.m new file mode 100644 index 00000000000..b0bd2236435 --- /dev/null +++ b/Artsy/Classes/Models/BidderPosition.m @@ -0,0 +1,37 @@ +@implementation BidderPosition + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"bidderPositionID" : @"id", + @"highestBid" : @"highest_bid", + @"maxBidAmountCents" : @"max_bid_amount_cents" + }; +} + ++ (NSValueTransformer *)highestBidJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Bid class]]; +} + +- (NSComparisonResult)compare:(BidderPosition *)position +{ + return [self.maxBidAmountCents compare:position.maxBidAmountCents]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + BidderPosition *bidderPosition = object; + return [bidderPosition.bidderPositionID isEqualToString:self.bidderPositionID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.bidderPositionID.hash; +} + +@end diff --git a/Artsy/Classes/Models/ContentLink.h b/Artsy/Classes/Models/ContentLink.h new file mode 100644 index 00000000000..981fcffadbe --- /dev/null +++ b/Artsy/Classes/Models/ContentLink.h @@ -0,0 +1,21 @@ +#import "MTLModel.h" +#import "ARPostAttachment.h" + +@interface ContentLink : MTLModel + +@property (readonly, nonatomic, copy) NSString *linkID; +@property (readonly, nonatomic, copy) NSString *thumbnailUrl; +@property (readonly, nonatomic, copy) NSString *url; + +@property (readonly, nonatomic) NSInteger thumbnailHeight; +@property (readonly, nonatomic) NSInteger thumbnailWidth; +@property (readonly, nonatomic) NSInteger width; +@property (readonly, nonatomic) NSInteger height; + +// Apparently Mantle no longer considers that you might want +// to see something in the JSON but not persist it +// so now we end up with this redundant BS +@property (readonly, nonatomic, copy) NSString *type; + + +@end diff --git a/Artsy/Classes/Models/ContentLink.m b/Artsy/Classes/Models/ContentLink.m new file mode 100644 index 00000000000..7cddc12af20 --- /dev/null +++ b/Artsy/Classes/Models/ContentLink.m @@ -0,0 +1,71 @@ +#import "ContentLink.h" +#import "VideoContentLink.h" +#import "PhotoContentLink.h" + +@implementation ContentLink + ++ (NSDictionary *)JSONKeyPathsByPropertyKey { + return @{ + @"linkID" : @"id", + @"thumbnailUrl" : @"oembed_json.thumbnail_url", + @"url" : @"oembed_json.url", + @"thumbnailHeight" : @"oembed_json.thumbnail_height", + @"thumbnailWidth" : @"oembed_json.thumbnail_width", + @"width" : @"oembed_json.width", + @"height" : @"oembed_json.height", + @"type" : @"oembed_json.type" + }; +} + ++ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError *__autoreleasing *)error { + NSString *type = [dictionaryValue valueForKeyPath:@"type"]; + if ([type isEqualToString:@"video"]) { + return [[VideoContentLink alloc] initWithDictionary:dictionaryValue error:error]; + + } else if ([type isEqualToString:@"photo"]) { + return [[PhotoContentLink alloc] initWithDictionary:dictionaryValue error:error]; + + } else if ([type isEqualToString:@"link"]) { + + //TODO: this - maybe?! + return [[PhotoContentLink alloc] initWithDictionary:dictionaryValue error:error]; + } else { + NSLog(@"Error! Unknown content link type '%@'", type); + return nil; + } +} + +- (CGFloat)aspectRatio { + if (!self.height || !self.width) { + return 1; + } + return (CGFloat)self.width/self.height; +} + +- (CGSize)maxSize { + if (!self.height || !self.width) { + return CGSizeZero; + } + return CGSizeMake(self.width, self.height); +} + +- (NSURL *)urlForThumbnail { + return [NSURL URLWithString: self.url]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + ContentLink *contentLink = object; + return [contentLink.linkID isEqualToString:self.linkID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.linkID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Fair.h b/Artsy/Classes/Models/Fair.h new file mode 100644 index 00000000000..3fbf641be67 --- /dev/null +++ b/Artsy/Classes/Models/Fair.h @@ -0,0 +1,35 @@ +#import "MTLModel.h" +#import "ARFeedSubclasses.h" +#import "ARFeedTimeline.h" +#import "Map.h" + +@interface Fair : MTLModel + +- (instancetype)initWithFairID:(NSString *)fairID; + +- (void)downloadShows; +- (void)updateFair:(void(^)(void))success; + +- (KSPromise *)onShowsUpdate:(void (^)(NSArray *shows))success failure:(void(^)(NSError *error))failure; + +- (void)getPosts:(void (^)(ARFeedTimeline *feedTimeline))success; +- (void)getOrderedSets:(void (^)(NSMutableDictionary *orderedSets))success; +- (void)getFairMaps:(void (^)(NSArray *))success; + +- (PartnerShow *)findShowForPartner:(Partner *)partner; + +- (NSString *)ausstellungsdauer; +- (NSString *)location; +- (NSString *)bannerAddress; + +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSString *fairID; +@property (nonatomic, copy, readonly) NSArray *maps; +@property (nonatomic, strong, readonly) NSSet *shows; +@property (nonatomic, copy, readonly) NSString *city; +@property (nonatomic, copy, readonly) NSString *state; +@property (nonatomic, strong, readonly) NSDate *startDate; +@property (nonatomic, strong, readonly) NSDate *endDate; +@property (nonatomic, assign, readonly) NSInteger partnersCount; +@property (nonatomic, copy, readonly) FairOrganizer* organizer; +@end diff --git a/Artsy/Classes/Models/Fair.m b/Artsy/Classes/Models/Fair.m new file mode 100644 index 00000000000..8047a7bdd3c --- /dev/null +++ b/Artsy/Classes/Models/Fair.m @@ -0,0 +1,338 @@ +#import "ARStandardDateFormatter.h" +#import "NSDate+DateRange.h" +#import "ARPartnerShowFeedItem.h" +#import "ARFileUtils.h" + +@interface Fair (){ + ARFairShowFeed *_showsFeed; + ARFairOrganizerFeed *_postsFeed; + ARFeedTimeline *_postsFeedTimeline; + NSMutableSet *_showsLoadedFromArchive; +} + +@property (readwrite, nonatomic, strong) KSDeferred *showsDeferred; +@property (nonatomic, copy) NSArray *maps; +// Note: *must* be strong and not copy, because copy will make a non-mutable copy. +@property (nonatomic, strong) NSMutableSet *shows; +@property (nonatomic, copy) NSString *rawImageURLString; +@property (nonatomic, copy) NSArray *imageVersions; + +@end + +@implementation Fair + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(Fair.new, name) : @"name", + @keypath(Fair.new, fairID) : @"id", + @keypath(Fair.new, organizer) : @"organizer", + @keypath(Fair.new, startDate): @"start_at", + @keypath(Fair.new, endDate) : @"end_at", + @keypath(Fair.new, city) : @"location.city", + @keypath(Fair.new, state) : @"location.state", + @keypath(Fair.new, rawImageURLString): @"image_url", + @keypath(Fair.new, imageVersions): @"image_versions", + + @keypath(Fair.new, partnersCount) : @"partners_count", + + // Hide these from Mantle + // + // This can be removed in Mantle 2.0 which won't have implicit mapping + @keypath(Fair.new, showsDeferred) : NSNull.null, + @keypath(Fair.new, maps) : NSNull.null, + }; +} + ++ (NSValueTransformer *)startDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)endDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)organizerJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[FairOrganizer class]]; +} + +- (NSString *)ausstellungsdauer +{ + return [self.startDate ausstellungsdauerToDate:self.endDate]; +} + +- (void)getPosts:(void (^)(ARFeedTimeline *feedTimeline))success +{ + _postsFeed = [[ARFairOrganizerFeed alloc] initWithFairOrganizer:[self organizer]]; + _postsFeedTimeline = [[ARFeedTimeline alloc] initWithFeed:_postsFeed]; + + __weak ARFeedTimeline *weakTimeline = _postsFeedTimeline; + [_postsFeedTimeline getNewItems:^{ + success(weakTimeline); + } failure:^(NSError *error) { + // TODO: don't swallow error + success(weakTimeline); + }]; +} + +- (instancetype)initWithFairID:(NSString *)fairID +{ + self = [super init]; + if (!self) { return nil; } + + _fairID = fairID; + + return self; +} + +- (void)updateFair:(void(^)(void))success +{ + @weakify(self); + + [ArtsyAPI getFairInfo:self.fairID success:^(id fair) { + @strongify(self); + + NSArray *tempMaps = self.maps; + [self mergeValuesForKeysFromModel:fair]; + self.maps = tempMaps; + + success(); + + } failure:^(NSError *error) { + success(); + }]; +} + +- (void)downloadShows +{ + _showsFeed = [[ARFairShowFeed alloc] initWithFair:self]; + + @weakify(self); + + NSString *path = self.pathForLocalShowStorage; + + ar_dispatch_async(^{ + NSMutableSet *shows = [[NSKeyedUnarchiver unarchiveObjectWithFile:path] mutableCopy]; + + ar_dispatch_main_queue(^{ + @strongify(self); + if (!self) { return; } + + [self willChangeValueForKey:@keypath(Fair.new, shows)]; + self->_showsLoadedFromArchive = shows ? [NSMutableSet setWithSet:shows] : nil; + self.shows = shows ?: [NSMutableSet set]; + [self didChangeValueForKey:@keypath(Fair.new, shows)]; + + // download once an hour at the most + NSError *error = nil; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error]; + NSTimeInterval distanceBetweenDatesSeconds = error ? -1 : [[NSDate date] timeIntervalSinceDate:[attributes fileModificationDate]]; + if (distanceBetweenDatesSeconds < 0 || distanceBetweenDatesSeconds / 3600.f > 1) { + [self downloadPastShowSet]; + } + }); + }); +} + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key +{ + if ([key isEqualToString:@keypath(Fair.new, shows)]) { + return NO; + } + + return [super automaticallyNotifiesObserversForKey:key]; +} + +// TODO: Could the show downloading etc move into it's own class on fair? + +- (void)downloadPastShowSet +{ + @weakify(self); + + [_showsFeed getFeedItemsWithCursor:_showsFeed.cursor success:^(NSOrderedSet *parsed) { + + @strongify(self); + if(parsed.count > 0) { + [self addFeedItemsToShows:parsed]; + [self downloadPastShowSet]; + } else { + [self finishedDownloadingShows]; + } + + } failure:^(NSError *error) { + + @strongify(self); + NSLog(@"failed to get shows %@", error.localizedDescription); + [self performSelector:@selector(downloadPastShowSet) withObject:nil afterDelay:0.5]; + }]; +} + +- (NSString *)pathForLocalShowStorage +{ + return [ARFileUtils cachesPathWithFolder:@"Fairs" filename:NSStringWithFormat(@"%@.showdata", self.fairID)]; +} + +- (void)finishedDownloadingShows +{ + if (_showsLoadedFromArchive && _showsLoadedFromArchive.count > 0) { + [self willChangeValueForKey:@keypath(Fair.new, shows)]; + // remove any shows that were loaded from archive, but not downloaded + [(NSMutableSet *)self.shows minusSet:_showsLoadedFromArchive]; + [self didChangeValueForKey:@keypath(Fair.new, shows)]; + } + + if(!ARIsRunningInDemoMode) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + if(![NSKeyedArchiver archiveRootObject:self.shows toFile:self.pathForLocalShowStorage]){ + ARErrorLog(@"Issue saving show data for fair %@", self.fairID); + } + }); + } +} + +- (void)addFeedItemsToShows:(NSOrderedSet *)feedItems +{ + [self willChangeValueForKey:@keypath(Fair.new, shows)]; + + @weakify(self); + [feedItems enumerateObjectsUsingBlock:^(ARPartnerShowFeedItem *feedItem, NSUInteger idx, BOOL *stop) { + @strongify(self); + if (!self) { return; } + + // So, you're asking, why is there C++ in my Obj-C? + // Well we want to be able to _update_ objects in a mutable set, which is a lower level API + // than just adding. Uses toll-free bridging to switch to CF and updates the set. ./ + + if (feedItem.show) { + if (self->_showsLoadedFromArchive) { + [self->_showsLoadedFromArchive removeObject:feedItem.show]; + } + CFSetSetValue((__bridge CFMutableSetRef)self.shows, (__bridge const void *)feedItem.show); + } + }]; + + [self didChangeValueForKey:@keypath(Fair.new, shows)]; +} + +- (KSPromise *)onShowsUpdate:(void (^)(NSArray *shows))success failure:(void(^)(NSError *error))failure +{ + @weakify(self); + + if (!self.showsDeferred) { + self.showsDeferred = [KSDeferred defer]; + + [ArtsyAPI + getPartnerShowsForFair:self + success:^(NSArray *shows) { + @strongify(self); + + [self.showsDeferred resolveWithValue:shows]; + } + failure:^(NSError *error) { + [self.showsDeferred rejectWithError:error]; + }]; + } + + return [self.showsDeferred.promise + then:^(NSArray *shows) { + @strongify(self); + + if (success) { + success(shows); + } + + return self; + } + error:^(NSError *error) { + if (failure) { + failure(error); + } + + return error; + }]; +} + +- (void)getOrderedSets:(void (^)(NSMutableDictionary *))success +{ + [ArtsyAPI getOrderedSetsWithOwnerType:@"Fair" andID:self.fairID success:success failure:^(NSError *error) { + success([[NSMutableDictionary alloc] init]); + }]; +} + +- (void)getFairMaps:(void (^)(NSArray *))success +{ + @weakify(self); + + [ArtsyAPI getMapInfoForFair:self success:^(NSArray *maps) { + @strongify(self); + if (!self) { return; } + self.maps = maps; + if (success) { + success(maps); + } + } failure:^(NSError *error) { + if (success) { + success(nil); + } + }]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:self.class]){ + return [[object fairID] isEqualToString:self.fairID]; + } + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.fairID.hash; +} + +- (PartnerShow *)findShowForPartner:(Partner *)partner +{ + return [self.shows.allObjects find:^BOOL(PartnerShow *show) { + return [show.partner isEqual:partner]; + }]; +} + +- (NSString *)location +{ + BOOL hasCity = self.city.length > 0; + BOOL hasState = self.state.length > 0; + + if (hasCity && hasState) { + return [NSString stringWithFormat:@"%@, %@", self.city, self.state]; + } else if (hasCity) { + return self.city; + } else if (hasState) { + return self.state; + } else { + return nil; + } +} + +- (NSString *)bannerAddress +{ + if (self.rawImageURLString.length == 0) { + return nil; + } + + // In order of preference + NSArray *desiredVersions = @[@"wide", @"large_rectange", @"square"]; + + NSArray *possibleVersions = [desiredVersions intersectionWithArray:self.imageVersions]; + NSString *version = possibleVersions.firstObject; + + if (version.length == 0) { + return nil; + } + + return [self.rawImageURLString stringByReplacingOccurrencesOfString:@":version" withString:version]; +} + +@end diff --git a/Artsy/Classes/Models/FairOrganizer.h b/Artsy/Classes/Models/FairOrganizer.h new file mode 100644 index 00000000000..d845dce0b39 --- /dev/null +++ b/Artsy/Classes/Models/FairOrganizer.h @@ -0,0 +1,10 @@ +#import "MTLModel.h" + +@interface FairOrganizer : MTLModel + +@property (nonatomic, copy, readonly) NSString *fairOrganizerID; +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSString *profileID; +@property (nonatomic, copy, readonly) NSString *defaultFairID; + +@end diff --git a/Artsy/Classes/Models/FairOrganizer.m b/Artsy/Classes/Models/FairOrganizer.m new file mode 100644 index 00000000000..48cce6d7b39 --- /dev/null +++ b/Artsy/Classes/Models/FairOrganizer.m @@ -0,0 +1,27 @@ +@implementation FairOrganizer + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"fairOrganizerID" : @"id", + @"profileID" : @"profile_id", + @"defaultFairID" : @"default_fair_id", + }; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + FairOrganizer *fairOrganizer = object; + return [fairOrganizer.fairOrganizerID isEqualToString:self.fairOrganizerID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.fairOrganizerID.hash; +} + +@end diff --git a/Artsy/Classes/Models/FeaturedLink.h b/Artsy/Classes/Models/FeaturedLink.h new file mode 100644 index 00000000000..76566c9e3eb --- /dev/null +++ b/Artsy/Classes/Models/FeaturedLink.h @@ -0,0 +1,22 @@ +#import "MTLModel.h" + +@interface FeaturedLink : MTLModel + +@property (readonly, nonatomic, copy) NSString *featuredLinkID; + +@property (readonly, nonatomic, copy) NSString *title; +@property (readonly, nonatomic, copy) NSString *subtitle; + +@property (readonly, nonatomic, copy) NSString *href; + +@property (readonly, nonatomic, assign) BOOL displayOnMobile; + + +- (NSURL *)largeImageURL; +- (NSURL *)smallImageURL; +- (NSURL *)smallSquareImageURL; +- (NSURL *)largeSquareImageURL; + +- (NSString *)linkedObjectID; + +@end diff --git a/Artsy/Classes/Models/FeaturedLink.m b/Artsy/Classes/Models/FeaturedLink.m new file mode 100644 index 00000000000..b4fc625ef7e --- /dev/null +++ b/Artsy/Classes/Models/FeaturedLink.m @@ -0,0 +1,78 @@ +@interface FeaturedLink() +@property (nonatomic, copy, readonly) NSString *urlFormatString; +@end + +@implementation FeaturedLink + +#pragma mark - Lifecycle + +- (instancetype)init { + self = [super init]; + if (!self) { return nil; } + + _displayOnMobile = YES; + + return self; +} + +#pragma mark - MTLJSONSerializing + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(FeaturedLink.new, featuredLinkID): @"id", + @keypath(FeaturedLink.new, urlFormatString): @"image_url", + @keypath(FeaturedLink.new, title): @"title", + @keypath(FeaturedLink.new, subtitle): @"subtitle", + @keypath(FeaturedLink.new, href): @"href", + @keypath(FeaturedLink.new, displayOnMobile): @"display_on_mobile" + }; +} + +#pragma mark - Properties + +- (NSURL *)largeImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"large_rectangle"]]; +} + +- (NSURL *)smallImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"medium_rectangle"]]; +} + +- (NSURL *)smallSquareImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"small_square"]]; +} + +- (NSURL *)largeSquareImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"large_square"]]; +} + +- (NSString *)linkedObjectID +{ + if (self.href.length) { + return [self.href componentsSeparatedByString:@"/"][2]; + } + + return nil; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + FeaturedLink *featuredLink = object; + return [featuredLink.featuredLinkID isEqualToString:self.featuredLinkID] || self == object; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.featuredLinkID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARFeedItem.h b/Artsy/Classes/Models/Feed Items/ARFeedItem.h new file mode 100644 index 00000000000..07a7a06f644 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFeedItem.h @@ -0,0 +1,17 @@ +#import +#import "MTLModel.h" + +@interface ARFeedItem : MTLModel + +@property (nonatomic, copy, readonly) NSString *feedItemID; +@property (nonatomic, copy, readonly) NSDate *feedTimestamp; + ++ (NSString *)cellIdentifier; ++ (NSValueTransformer *)standardDateTransformer; + +- (NSString *)cellIdentifier; +- (NSArray *)dataForActivities; + +- (NSString *)localizedStringForActivity; + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARFeedItem.m b/Artsy/Classes/Models/Feed Items/ARFeedItem.m new file mode 100644 index 00000000000..d376c870499 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFeedItem.m @@ -0,0 +1,59 @@ +#import "ARFeedItem.h" +#import "ARStandardDateFormatter.h" + +static ARStandardDateFormatter *staticDateFormatter; + +@implementation ARFeedItem + ++ (NSValueTransformer *)standardDateTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)feedTimestampJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"feedItemID" : @"id" + }; +} + ++ (NSString *)cellIdentifier +{ + return @"GenericCellIdentifier"; +} + +- (NSString *)cellIdentifier +{ + return [[self class] cellIdentifier]; +} + +- (NSArray *)dataForActivities +{ + return nil; +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[self class]]) { + ARFeedItem *item = object; + return [_feedItemID isEqualToString:item.feedItemID]; + } + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return _feedItemID.hash; +} + +- (NSString *)localizedStringForActivity +{ + return @""; +} + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARFeedItems.h b/Artsy/Classes/Models/Feed Items/ARFeedItems.h new file mode 100644 index 00000000000..cd4dfe08613 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFeedItems.h @@ -0,0 +1,7 @@ +#import "ARPostFeedItem.h" +#import "ARFollowFairFeedItem.h" +#import "ARFollowArtistFeedItem.h" +#import "ARRepostFeedItem.h" +#import "ARSavedArtworkSetFeedItem.h" +#import "ARPublishedArtworkSetFeedItem.h" +#import "ARPartnerShowFeedItem.h" diff --git a/Artsy/Classes/Models/Feed Items/ARFollowArtistFeedItem.h b/Artsy/Classes/Models/Feed Items/ARFollowArtistFeedItem.h new file mode 100644 index 00000000000..d31b8429777 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFollowArtistFeedItem.h @@ -0,0 +1,9 @@ +#import "ARFeedItem.h" + +@interface ARFollowArtistFeedItem : ARFeedItem + +@property (nonatomic, copy, readonly) NSArray *artworks; +@property (nonatomic, readonly) Artist *artist; +@property (nonatomic, readonly) Profile *profile; + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARFollowArtistFeedItem.m b/Artsy/Classes/Models/Feed Items/ARFollowArtistFeedItem.m new file mode 100644 index 00000000000..eb4b864421d --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFollowArtistFeedItem.m @@ -0,0 +1,68 @@ +#import "ARFollowArtistFeedItem.h" + +@implementation ARFollowArtistFeedItem + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{ + @"artist" : @"artist" , + @"feedTimestamp" : @"created_at", + @"artworks" : @"artworks", + @"profile" : @"profile" + }]; +} + + ++ (NSValueTransformer *)artworksJSONTransformer +{ + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[Artwork class]]; +} + ++ (NSValueTransformer *)profileJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Profile class]]; +} + ++ (NSValueTransformer *)artistJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Artist class]]; +} + ++ (NSString *)cellIdentifier +{ + return @"FollowArtistCellIdentifier"; +} + +- (NSArray *)dataForActivities +{ + return @[self]; +} + +- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(NSString *)activityType +{ +// NSString *twitterKey = @"com.apple.UIKit.activity.PostToTwitter"; +// if ([activityType isEqualToString:twitterKey]) { +// return [NSString stringWithFormat:@"%@ on @artsy %@", self.artist.name, self.artist.publicURL]; +// +// } else { +// return [NSString stringWithFormat:@"Check out this amazing artist I found on Artsy, %@. Isn't he just great? You can see his works at %@", self.artist.name, self.artist.publicURL]; +// } + return nil; +} + +- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController +{ + return @""; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"FeedItem - Follow Artist ( %@ followed %@ ) ", self.profile.profileID, self.artist.name]; +} + +- (NSString *)localizedStringForActivity +{ + return NSLocalizedString(@"Followed", @"Followed Artist text for Feed Item"); +} + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARFollowFairFeedItem.h b/Artsy/Classes/Models/Feed Items/ARFollowFairFeedItem.h new file mode 100644 index 00000000000..1a49997065f --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFollowFairFeedItem.h @@ -0,0 +1,5 @@ +#import "ARFeedItem.h" + +@interface ARFollowFairFeedItem : ARFeedItem + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARFollowFairFeedItem.m b/Artsy/Classes/Models/Feed Items/ARFollowFairFeedItem.m new file mode 100644 index 00000000000..409942199dc --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARFollowFairFeedItem.m @@ -0,0 +1,5 @@ +#import "ARFollowFairFeedItem.h" + +@implementation ARFollowFairFeedItem + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARPartnerShowFeedItem.h b/Artsy/Classes/Models/Feed Items/ARPartnerShowFeedItem.h new file mode 100644 index 00000000000..02995c9c500 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARPartnerShowFeedItem.h @@ -0,0 +1,9 @@ +#import "ARFeedItem.h" +#import "Partner.h" + +@interface ARPartnerShowFeedItem : ARFeedItem + +@property (nonatomic, strong, readonly) PartnerShow *show; +@property (nonatomic, copy, readonly) NSString *showID; + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARPartnerShowFeedItem.m b/Artsy/Classes/Models/Feed Items/ARPartnerShowFeedItem.m new file mode 100644 index 00000000000..296c060bd77 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARPartnerShowFeedItem.m @@ -0,0 +1,37 @@ +#import "ARPartnerShowFeedItem.h" + +@interface ARPartnerShowFeedItem() +@property (nonatomic, strong) PartnerShow *show; +@end + +@implementation ARPartnerShowFeedItem + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{ + @"showID" : @"id", + @"feedTimestamp" : @"created_at", + }]; +} + ++ (NSString *)cellIdentifier +{ + return @"PartnerShowCellIdentifier"; +} + +- (NSString *)localizedStringForActivity +{ + return NSLocalizedString(@"New Show", @"New Show Items text for Feed Item"); +} + +- (SEL)setHostPropertySelector +{ + return @selector(setShow:); +} + +- (Class)hostedObjectClass; +{ + return [PartnerShow class]; +} + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARPostFeedItem.h b/Artsy/Classes/Models/Feed Items/ARPostFeedItem.h new file mode 100644 index 00000000000..63c04e03bc2 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARPostFeedItem.h @@ -0,0 +1,32 @@ +#import "ARFeedItem.h" +#import "User.h" + +typedef NS_ENUM(NSUInteger, ARPostType) { + ARPostTypeSingleColumn, + ARPostTypeTwoColumn, + ARPostTypeTextOnly +}; + +@interface ARPostFeedItem : ARFeedItem + +@property (nonatomic, copy, readonly) NSString *postID; +@property (nonatomic, copy, readonly) NSString *bodyHTML; +@property (nonatomic, copy, readonly) NSString *title; +@property (nonatomic, copy, readonly) NSString *shareableImageURL; +@property (nonatomic, copy, readonly) NSString *imageURL; + +@property (nonatomic, readonly) ARPostType type; +@property (nonatomic, readonly) Profile *profile; +@property (nonatomic, readonly) NSArray *artworks; +@property (nonatomic, readonly) NSArray *contentLinks; +@property (nonatomic, readonly) NSArray *postImages; + +- (NSInteger)attachmentCount; + +- (NSArray *)allAttachments; + +- (BOOL)hasTitle; + +- (void)updatePost:(void (^)(BOOL updateSuccessful))success; + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARPostFeedItem.m b/Artsy/Classes/Models/Feed Items/ARPostFeedItem.m new file mode 100644 index 00000000000..a5dbe0acf17 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARPostFeedItem.m @@ -0,0 +1,141 @@ +#import "ARPostFeedItem.h" +#import "ContentLink.h" +#import "PostImage.h" + +@implementation ARPostFeedItem + ++ (NSDictionary *)JSONKeyPathsByPropertyKey { + return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{ + @"postID" : @"id", + @"bodyHTML" : @"body", + @"title" : @"title", + @"type" : @"layout", + @"profile" : @"profile", + @"artworks" : @"artworks", + @"contentLinks" : @"content_links", + @"postImages" : @"post_images", + @"feedTimestamp" : @"created_at", + @"shareableImageURL" : @"shareable_image_url" + }]; +} + ++ (NSValueTransformer *)profileJSONTransformer { + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Profile class]]; +} + ++ (NSValueTransformer *)artworksJSONTransformer { + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[Artwork class]]; +} + ++ (NSValueTransformer *)postImagesJSONTransformer { + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[PostImage class]]; +} + ++ (NSValueTransformer *)contentLinksJSONTransformer { + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[ContentLink class]]; +} + ++ (NSValueTransformer *)bodyJSONTransformer { + return [MTLValueTransformer transformerWithBlock:^id(id str) { + return [[str stringByReplacingOccurrencesOfString:@"\n" withString:@" "] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + }]; +} + ++ (NSValueTransformer *)typeJSONTransformer { + NSDictionary *types = @{ + @"singlecolumn" : @(ARPostTypeSingleColumn), + @"twocolumn" : @(ARPostTypeTwoColumn), + @"textonly" : @(ARPostTypeTextOnly) + }; + + return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) { + return types[str]; + } reverseBlock:^(NSNumber *type) { + return [types allKeysForObject:type].lastObject; + }]; +} + ++ (NSString *)cellIdentifier { + return @"PostCellIdentifier"; +} + +- (NSInteger)attachmentCount { + //TODO: write fold for primitives + NSInteger count = 0; + for (NSArray *arr in @[self.artworks, self.postImages, self.contentLinks]) { + if (arr) { + count += arr.count; + } + } + return count; +} + +- (NSArray *)allAttachments { + NSMutableArray *ret = [[NSMutableArray alloc] init]; + for (NSArray *arr in @[self.artworks, self.postImages, self.contentLinks]) { + if (arr) { + [ret addObjectsFromArray:arr]; + } + } + return ret; +} + +- (NSString *)publicURL { + return [NSString stringWithFormat:@"http://art.sy/%@/post/%@", self.profile.profileID, self.postID]; +} + +- (NSArray *)dataForActivities { + return @[self]; +} + +- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(NSString *)activityType { + if (self.profile && self.title && ![self.title isEqualToString:@""]) { + return [NSString stringWithFormat:@"%@: %@ via @artsy %@", self.profile.profileName, self.title, self.publicURL]; + + } else if (self.profile) { + return [NSString stringWithFormat:@"%@ on @artsy %@", self.profile.profileName, self.publicURL]; + + } else { + //TODO: this case exists in Gravity's code, but is there any way to + // generate a link to an authorless post? Gravity would choke here + return @""; + } +} + +- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { + return @""; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"FeedItem - Post ( %@ by %@ ) ", _title, self.profile.profileID]; +} + +- (NSString *)localizedStringForActivity { + return NSLocalizedString(@"Posted", @"Posted a post header text for Feed Item"); +} + +- (BOOL)hasTitle { + return (_title && ![_title isEqualToString:@""]); +} + +- (void)updatePost:(void (^)(BOOL updateSuccessful))success { + [ArtsyAPI getPostForPostID:_postID success:^(id post) { + [self mergeValuesForKeysFromModel:post]; + success(YES); + + } failure:^(NSError *error) { + success(NO); + }]; +} + +-(NSString *)imageURL +{ + NSString *imageURL = self.shareableImageURL; + if (!imageURL && self.profile) { + imageURL = self.profile.iconURL; + } + return imageURL; +} + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARPublishedArtworkSetFeedItem.h b/Artsy/Classes/Models/Feed Items/ARPublishedArtworkSetFeedItem.h new file mode 100644 index 00000000000..0c56f38f8db --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARPublishedArtworkSetFeedItem.h @@ -0,0 +1,11 @@ +#import "ARFeedItem.h" +#import "Gene.h" + +@interface ARPublishedArtworkSetFeedItem : ARFeedItem + +@property (readonly, nonatomic) Artist *artist; +@property (readonly, nonatomic) Gene *gene; +@property (readonly, nonatomic) NSArray *artworks; + +- (NSString *)entityName; +@end diff --git a/Artsy/Classes/Models/Feed Items/ARPublishedArtworkSetFeedItem.m b/Artsy/Classes/Models/Feed Items/ARPublishedArtworkSetFeedItem.m new file mode 100644 index 00000000000..ec9a1174fd3 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARPublishedArtworkSetFeedItem.m @@ -0,0 +1,41 @@ +#import "ARPublishedArtworkSetFeedItem.h" + +@implementation ARPublishedArtworkSetFeedItem + ++ (NSDictionary *)JSONKeyPathsByPropertyKey { + return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{ + @"artist" : @"_source_artist", + @"artworks" : @"artworks", + @"gene" : @"_source_gene" + }]; +} + ++ (NSValueTransformer *)artworksJSONTransformer { + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[Artwork class]]; +} + ++ (NSValueTransformer *)artistJSONTransformer { + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Artist class]]; +} + ++ (NSValueTransformer *)geneJSONTransformer { + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Gene class]]; +} + + ++ (NSString *)cellIdentifier { + return @"PublishedArtworkSetCellIdentifier"; +} + +- (NSString *)entityName { + return self.artist ? self.artist.name : self.gene.name; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"FeedItem - Published Artworks ( %@ published %@ artworks ) ", self.artist.name, @(self.artworks.count) ]; +} + +- (NSString *)localizedStringForActivity { + return NSLocalizedString(@"Followed Artist", @"Followed Artist text for Feed Item"); +} +@end diff --git a/Artsy/Classes/Models/Feed Items/ARRepostFeedItem.h b/Artsy/Classes/Models/Feed Items/ARRepostFeedItem.h new file mode 100644 index 00000000000..2df350662a5 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARRepostFeedItem.h @@ -0,0 +1,5 @@ +#import "ARFeedItem.h" + +@interface ARRepostFeedItem : ARFeedItem + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARRepostFeedItem.m b/Artsy/Classes/Models/Feed Items/ARRepostFeedItem.m new file mode 100644 index 00000000000..607d12cf525 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARRepostFeedItem.m @@ -0,0 +1,5 @@ +#import "ARRepostFeedItem.h" + +@implementation ARRepostFeedItem + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARSavedArtworkSetFeedItem.h b/Artsy/Classes/Models/Feed Items/ARSavedArtworkSetFeedItem.h new file mode 100644 index 00000000000..7c9d4104d05 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARSavedArtworkSetFeedItem.h @@ -0,0 +1,9 @@ +#import "ARFeedItem.h" +#import "User.h" + +@interface ARSavedArtworkSetFeedItem : ARFeedItem + +@property (readonly, nonatomic) Profile *profile; +@property (readonly, nonatomic) NSArray *artworks; + +@end diff --git a/Artsy/Classes/Models/Feed Items/ARSavedArtworkSetFeedItem.m b/Artsy/Classes/Models/Feed Items/ARSavedArtworkSetFeedItem.m new file mode 100644 index 00000000000..a2f352148a7 --- /dev/null +++ b/Artsy/Classes/Models/Feed Items/ARSavedArtworkSetFeedItem.m @@ -0,0 +1,33 @@ +#import "ARSavedArtworkSetFeedItem.h" + +@implementation ARSavedArtworkSetFeedItem ++ (NSDictionary *)JSONKeyPathsByPropertyKey { + return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{ + @"profile" : @"profile", + @"artworks" : @"artworks", + @"feedTimestamp" : @"created_at" + }]; +} + + ++ (NSValueTransformer *)profileJSONTransformer { + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Profile class]]; +} + ++ (NSValueTransformer *)artworksJSONTransformer { + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[Artwork class]]; +} + ++ (NSString *)cellIdentifier { + return @"SavedArtworkSetCellIdentifier"; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"FeedItem - Saved Artworks ( %@ saved %@ artworks ) ", _profile.profileID, @(_artworks.count)]; +} + +- (NSString *)localizedStringForActivity { + return NSLocalizedString(@"Saved", @"Saved Artworks header text for Feed Item"); +} + +@end diff --git a/Artsy/Classes/Models/Follow.h b/Artsy/Classes/Models/Follow.h new file mode 100644 index 00000000000..4e981da1803 --- /dev/null +++ b/Artsy/Classes/Models/Follow.h @@ -0,0 +1,7 @@ +@interface Follow : MTLModel + +@property (nonatomic, copy, readonly) NSString *followID; +@property (nonatomic, copy, readonly) Artist* artist; +@property (nonatomic, copy, readonly) Profile* profile; + +@end diff --git a/Artsy/Classes/Models/Follow.m b/Artsy/Classes/Models/Follow.m new file mode 100644 index 00000000000..3f9ebec66e4 --- /dev/null +++ b/Artsy/Classes/Models/Follow.m @@ -0,0 +1,36 @@ +@implementation Follow + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(Follow.new, followID) : @"id" + }; +} + ++ (NSValueTransformer *)profileJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Profile class]]; +} + ++ (NSValueTransformer *)artistJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Artist class]]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Follow *follow = object; + return [follow.followID isEqualToString:self.followID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.followID.hash; +} + +@end + diff --git a/Artsy/Classes/Models/Gene.h b/Artsy/Classes/Models/Gene.h new file mode 100644 index 00000000000..2e0509004c0 --- /dev/null +++ b/Artsy/Classes/Models/Gene.h @@ -0,0 +1,30 @@ +#import "MTLModel.h" +#import "ARFollowable.h" +#import "ARShareableObject.h" +#import "ARHasImageBaseURL.h" + +@interface Gene : MTLModel + +@property (readonly, nonatomic, copy) NSString *name; +@property (readonly, nonatomic, copy) NSString *geneID; + +@property (readonly, nonatomic, copy) NSString *geneDescription; + +@property (readonly, nonatomic, copy) NSNumber *artistCount; +@property (readonly, nonatomic, copy) NSNumber *artworkCount; +@property (readonly, nonatomic, copy) NSNumber *followCount; +@property (readonly, nonatomic, copy) NSNumber *published; +@property (readonly, nonatomic, copy) NSArray *urlFormats; + +@property (readonly, nonatomic, copy) NSArray *artworks; + +- (instancetype)initWithGeneID:(NSString *)geneID; + +- (void)updateGene:(void(^)(void))success; +- (void)getFollowState:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure; + +- (AFJSONRequestOperation *)getArtworksAtPage:(NSInteger)page success:(void (^)(NSArray *artworks))success; + +- (NSURL *)smallImageURL; +- (NSURL *)largeImageURL; +@end diff --git a/Artsy/Classes/Models/Gene.m b/Artsy/Classes/Models/Gene.m new file mode 100644 index 00000000000..6de9ef461e2 --- /dev/null +++ b/Artsy/Classes/Models/Gene.m @@ -0,0 +1,146 @@ +@interface Gene() { + BOOL _isFollowed; +} + +@property (nonatomic, copy, readonly) NSString *urlFormatString; +@end + +@implementation Gene + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"geneID": @"id", + @"name" : @"name", + @"geneDescription" : @"description", + @"artistCount": @"counts.artists", + @"artworkCount": @"counts.artworks", + @"followCount": @"follow_count", + @"urlFormatString": @"image_url", + @"urlFormats": @"image_versions" + }; +} + +- (NSString *)baseImageURL +{ + return self.urlFormatString; +} + +- (NSURL *)largeImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"square"]]; +} + +- (NSURL *)smallImageURL +{ + return [NSURL URLWithString:[self.urlFormatString stringByReplacingOccurrencesOfString:@":version" withString:@"thumb"]]; +} + +- (instancetype)initWithGeneID:(NSString *)geneID +{ + self = [super init]; + if (!self) { return nil; } + + _geneID = geneID; + + return self; +} + +- (void)updateGene:(void(^)(void))success +{ + @weakify(self); + [ArtsyAPI getGeneForGeneID:self.geneID success:^(id gene) { + @strongify(self); + [self mergeValuesForKeysFromModel:gene]; + success(); + } failure:^(NSError *error) { + success(); + }]; +} + +- (void)setFollowed:(BOOL)isFollowed +{ + _isFollowed = isFollowed; +} + +- (BOOL)isFollowed +{ + return _isFollowed; +} + +- (void)followWithSuccess:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + [self setFollowState:YES success:success failure:failure]; +} + +- (void)unfollowWithSuccess:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + [self setFollowState:NO success:success failure:failure]; +} + +- (void)setFollowState:(BOOL)state success:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + @weakify(self); + [ArtsyAPI setFavoriteStatus:state forGene:self success:^(id response) { + @strongify(self); + self.followed = state; + if (success) { + success(response); + } + } failure:^(NSError *error) { + @strongify(self); + self.followed = !state; + if (failure) { + failure(error); + } + }]; +} + +- (void)getFollowState:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure +{ + if ([User isTrialUser]) { + success(ARHeartStatusNo); + return; + } + + @weakify(self); + [ArtsyAPI checkFavoriteStatusForGene:self success:^(BOOL result) { + @strongify(self); + self.followed = result; + success(result ? ARHeartStatusYes : ARHeartStatusNo); + } failure:failure]; +} + +- (AFJSONRequestOperation *)getArtworksAtPage:(NSInteger)page success:(void (^)(NSArray *artworks))success +{ + return [ArtsyAPI getArtworksForGene:self atPage:page success:^(NSArray *artworks) { + success(artworks); + + } failure:^(NSError *error) { + success(nil); + }]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Gene *gene = object; + return [gene.geneID isEqualToString:self.geneID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.geneID.hash; +} + +#pragma mark ShareableObject + +- (NSString *)publicArtsyPath +{ + return [NSString stringWithFormat:@"/gene/%@", self.geneID]; +} + +@end diff --git a/Artsy/Classes/Models/Image.h b/Artsy/Classes/Models/Image.h new file mode 100644 index 00000000000..a70a77b4a90 --- /dev/null +++ b/Artsy/Classes/Models/Image.h @@ -0,0 +1,32 @@ +#import "MTLModel.h" +#import "ARHasImageBaseURL.h" + +@interface Image : MTLModel + +@property (readonly, nonatomic, copy) NSString *imageID; +@property (readonly, nonatomic, copy) NSString *url; + +@property (readonly, nonatomic) CGFloat originalHeight; +@property (readonly, nonatomic) CGFloat originalWidth; +@property (readonly, nonatomic) CGFloat aspectRatio; + +@property (readonly, nonatomic) NSInteger maxTiledHeight; +@property (readonly, nonatomic) NSInteger maxTiledWidth; +@property (readonly, nonatomic) NSInteger tileSize; +@property (readonly, nonatomic, copy) NSString *tileBaseUrl; +@property (readonly, nonatomic, copy) NSString *tileFormat; +@property (readonly, nonatomic) NSInteger maxTileLevel; +@property (readonly, nonatomic) BOOL downloadable; +@property (readonly, nonatomic) NSArray *imageVersions; + ++ (NSInteger)minimumZoomLevel; +- (BOOL)needsTiles; + +- (NSURL *)urlForThumbnailImage; +- (NSURL *)urlForDetailImage; +- (NSURL *)urlForSquareImage; + +- (NSURL *)urlTileForLevel:(NSInteger)level atX:(NSInteger)x andY:(NSInteger)y; +- (NSURL *)urlTileWithFormatName:(NSString *)formatName; + +@end diff --git a/Artsy/Classes/Models/Image.m b/Artsy/Classes/Models/Image.m new file mode 100644 index 00000000000..78df4f75c1f --- /dev/null +++ b/Artsy/Classes/Models/Image.m @@ -0,0 +1,125 @@ +static NSInteger ARTiledZoomMinLevel = 11; + +@interface Image () +@property (nonatomic, assign) NSInteger maxTileLevel; +@end + +@implementation Image + ++ (NSInteger)minimumZoomLevel +{ + return ARTiledZoomMinLevel; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"imageID" : @"id", + @"url" : @"image_url", + @"originalHeight" : @"original_height", + @"originalWidth" : @"original_width", + @"aspectRatio" : @"aspect_ratio", + @"maxTiledHeight" : @"max_tiled_height", + @"maxTiledWidth" : @"max_tiled_width", + @"tileSize" : @"tile_size", + @"tileBaseUrl" : @"tile_base_url", + @"tileFormat" : @"tile_format", + @"imageVersions" : @"image_versions" + }; +} + ++ (NSValueTransformer *)downloadableTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + +- (void)setNilValueForKey:(NSString *)key +{ + if ([@[@"aspectRatio", @"originalHeight", @"originalWidth"] containsObject:key]) { + [self setValue:@1 forKey:key]; + } else if ([@[@"maxTiledHeight", @"maxTiledWidth", @"tileSize"] containsObject:key]) { + [self setValue:@0 forKey:key]; + } else { + [super setNilValueForKey:key]; + } +} + +#pragma - fetching URLs + +- (NSURL *)urlForSquareImage +{ + return [self imageURLWithFormatName:@"square"]; +} + +- (NSURL *)urlForThumbnailImage +{ + return [self imageURLWithFormatName:@"large"]; +} + +- (NSURL *)urlForDetailImage +{ + return [self imageURLWithFormatName:@"larger"]; +} + +- (NSURL *)imageURLWithFormatName:(NSString *)formatName +{ + if (self.imageVersions && [self.imageVersions containsObject:formatName]) { + NSString *url = [self.url stringByReplacingOccurrencesOfString:@":version" withString:formatName]; + if ([self.url hasPrefix:@"http"]) { + return [NSURL URLWithString:url]; + } else { + return [NSURL fileURLWithPath:url]; + } + } else { + return nil; + } +} + +- (NSURL *)urlTileForLevel:(NSInteger)level atX:(NSInteger)x andY:(NSInteger)y +{ + NSString *url = [NSString stringWithFormat:@"%@/%@/%@_%@.%@", self.tileBaseUrl, @(level), @(x), @(y), self.tileFormat]; + return [NSURL URLWithString:url]; +} + +- (NSURL *)urlTileWithFormatName:(NSString *)formatName +{ + NSString *url = [NSString stringWithFormat:@"%@/%@.%@", self.tileBaseUrl, formatName, self.tileFormat]; + return [NSURL URLWithString:url]; +} + +- (BOOL)needsTiles +{ + return (self.maxTileLevel >= ARTiledZoomMinLevel) && self.maxTiledWidth && self.maxTiledHeight; +} + +- (NSInteger)maxTileLevel +{ + if (_maxTileLevel <= 0) { + NSInteger width = _maxTiledWidth; + NSInteger height = _maxTiledHeight; + _maxTileLevel = ceil(log(MAX(width, height))/log(2)); + } + return _maxTileLevel; +} + +- (NSString *)baseImageURL +{ + return self.url; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Image *image = object; + return [image.imageID isEqualToString:self.imageID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.imageID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Map.h b/Artsy/Classes/Models/Map.h new file mode 100644 index 00000000000..752328b9e12 --- /dev/null +++ b/Artsy/Classes/Models/Map.h @@ -0,0 +1,9 @@ +#import "ARFeedHostItem.h" + +@interface Map : MTLModel + +@property (readonly, nonatomic, copy) NSString *mapID; +@property (readonly, nonatomic, copy) NSArray *features; +@property (readonly, nonatomic, strong) Image *image; + +@end diff --git a/Artsy/Classes/Models/Map.m b/Artsy/Classes/Models/Map.m new file mode 100644 index 00000000000..0a0970e969b --- /dev/null +++ b/Artsy/Classes/Models/Map.m @@ -0,0 +1,45 @@ +@interface Map () +@property (nonatomic, strong) Image *image; +@end + +@implementation Map + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(Map.new, features) :@"map_features", + @keypath(Map.new, mapID) : @"id", + }; +} + ++ (NSValueTransformer *)featuresJSONTransformer +{ + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:MapFeature.class]; +} + +- (SEL)setHostPropertySelector; +{ + return @selector(setImage:); +} + +- (Class)hostedObjectClass +{ + return Image.class; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Map *map = object; + return [map.mapID isEqualToString:self.mapID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.mapID.hash; +} + +@end diff --git a/Artsy/Classes/Models/MapFeature.h b/Artsy/Classes/Models/MapFeature.h new file mode 100644 index 00000000000..d785c592a03 --- /dev/null +++ b/Artsy/Classes/Models/MapFeature.h @@ -0,0 +1,38 @@ +NS_ENUM(NSInteger , ARMapFeatureType){ + ARMapFeatureTypeDefault, + ARMapFeatureTypeArtsy, + ARMapFeatureTypeDrink, + ARMapFeatureTypeCoatCheck, + ARMapFeatureTypeFood, + ARMapFeatureTypeInfo, + ARMapFeatureTypeLounge, + ARMapFeatureTypeRestroom, + ARMapFeatureTypeSaved, + ARMapFeatureTypeSearch, + ARMapFeatureTypeVIP, + ARMapFeatureTypeHighlighted, + ARMapFeatureTypeEntrance, + ARMapFeatureTypeTicket, + ARMapFeatureTypeExit, + ARMapFeatureTypeBooks, + ARMapFeatureTypeInstallation, + ARMapFeatureTypeTransport, + ARMapFeatureTypeGenericEvent, + ARMapFeatureTypeMax +}; + +FOUNDATION_EXPORT NSString *NSStringFromARMapFeatureType(enum ARMapFeatureType featureType); + +@interface MapFeature : MTLModel + +@property (readonly, nonatomic, assign) enum ARMapFeatureType featureType; +@property (readonly, nonatomic, copy) NSString *name; +@property (readonly, nonatomic, copy) NSString *imagePath; +@property (readonly, nonatomic, copy) NSString *href; + +@property (readonly, nonatomic, assign) CGFloat x; +@property (readonly, nonatomic, assign) CGFloat y; + +- (CGPoint)coordinateOnImage:(Image *)image; + +@end diff --git a/Artsy/Classes/Models/MapFeature.m b/Artsy/Classes/Models/MapFeature.m new file mode 100644 index 00000000000..4c6cefcb386 --- /dev/null +++ b/Artsy/Classes/Models/MapFeature.m @@ -0,0 +1,79 @@ +#import "ARValueTransformer.h" + +@implementation MapFeature + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(MapFeature.new, featureType) : @"feature_type", + }; +} + +- (CGPoint)coordinateOnImage:(Image *)image +{ + return CGPointMake(image.maxTiledWidth * self.x, image.maxTiledHeight - (image.maxTiledHeight * self.y)); +} + ++ (NSValueTransformer *)featureTypeJSONTransformer +{ + NSDictionary *types = @{ + @"artsy": @(ARMapFeatureTypeArtsy), + @"drink" : @(ARMapFeatureTypeDrink), + @"coat-check" : @(ARMapFeatureTypeCoatCheck), + @"food" : @(ARMapFeatureTypeFood), + @"info" : @(ARMapFeatureTypeInfo), + @"lounge" : @(ARMapFeatureTypeLounge), + @"restroom" : @(ARMapFeatureTypeRestroom), + @"search" : @(ARMapFeatureTypeSearch), + @"vip" : @(ARMapFeatureTypeVIP), + @"entrance" : @(ARMapFeatureTypeEntrance), + @"ticket" : @(ARMapFeatureTypeTicket), + @"exit" : @(ARMapFeatureTypeExit), + @"books" : @(ARMapFeatureTypeBooks), + @"installation" : @(ARMapFeatureTypeInstallation), + @"transport" : @(ARMapFeatureTypeTransport), + @"event" : @(ARMapFeatureTypeGenericEvent) + }; + return [ARValueTransformer enumValueTransformerWithMap:types]; +} + +- (void)setNilValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"featureType"]) { + [self setValue:@(ARMapFeatureTypeGenericEvent) forKey:key]; + } else { + [super setNilValueForKey:key]; + } +} + +@end + +NSString * NSStringFromARMapFeatureType(enum ARMapFeatureType featureType) { + static NSDictionary *mapFeatureToStringDictionary = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mapFeatureToStringDictionary = @{ + @(ARMapFeatureTypeDefault): @"Default", + @(ARMapFeatureTypeArtsy): @"Artsy", + @(ARMapFeatureTypeDrink): @"Drink", + @(ARMapFeatureTypeCoatCheck): @"CoatCheck", + @(ARMapFeatureTypeFood): @"Food", + @(ARMapFeatureTypeLounge): @"Lounge", + @(ARMapFeatureTypeRestroom): @"Restroom", + @(ARMapFeatureTypeSaved): @"Saved", + @(ARMapFeatureTypeSearch): @"Search", + @(ARMapFeatureTypeVIP): @"VIP", + @(ARMapFeatureTypeHighlighted): @"Highlighted", + @(ARMapFeatureTypeGenericEvent): @"Event", + @(ARMapFeatureTypeEntrance): @"Entrance", + @(ARMapFeatureTypeTicket): @"Ticket", + @(ARMapFeatureTypeExit): @"Exit", + @(ARMapFeatureTypeBooks): @"Books", + @(ARMapFeatureTypeInstallation): @"Installation", + @(ARMapFeatureTypeTransport): @"Transport", + @(ARMapFeatureTypeInfo): @"Info" + }; + }); + + return [mapFeatureToStringDictionary objectForKey:@(featureType)]; +} diff --git a/Artsy/Classes/Models/MapPoint.h b/Artsy/Classes/Models/MapPoint.h new file mode 100644 index 00000000000..d5cc7d88cd1 --- /dev/null +++ b/Artsy/Classes/Models/MapPoint.h @@ -0,0 +1,9 @@ +@interface MapPoint : MTLModel + +@property (nonatomic, assign, readonly) CGFloat z; +@property (nonatomic, assign, readonly) CGFloat y; +@property (nonatomic, assign, readonly) CGFloat x; + +- (CGPoint)coordinateOnImage:(Image *)image; + +@end diff --git a/Artsy/Classes/Models/MapPoint.m b/Artsy/Classes/Models/MapPoint.m new file mode 100644 index 00000000000..1c9a6fed4a3 --- /dev/null +++ b/Artsy/Classes/Models/MapPoint.m @@ -0,0 +1,33 @@ +@implementation MapPoint + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"x" : @"x", + @"y" : @"y", + @"z" : @"z", + }; +} + +- (CGPoint)coordinateOnImage:(Image *)image +{ + return CGPointMake(image.maxTiledWidth * self.x, image.maxTiledHeight - (image.maxTiledHeight * self.y)); +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + MapPoint *mapPoint = object; + return mapPoint.x == self.x && mapPoint.y == self.y && mapPoint.z == self.z; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.x + self.y + self.z; +} + + +@end diff --git a/Artsy/Classes/Models/Models.h b/Artsy/Classes/Models/Models.h new file mode 100644 index 00000000000..bd50dcf913b --- /dev/null +++ b/Artsy/Classes/Models/Models.h @@ -0,0 +1,24 @@ +#import "Artwork.h" +#import "Artist.h" +#import "User.h" +#import "ProfileOwner.h" +#import "Profile.h" +#import "Gene.h" +#import "Partner.h" +#import "PartnerShow.h" +#import "Sale.h" +#import "SaleArtwork.h" +#import "FairOrganizer.h" +#import "Fair.h" +#import "Post.h" +#import "SiteHeroUnit.h" +#import "SiteFeature.h" +#import "FeaturedLink.h" +#import "OrderedSet.h" +#import "Tag.h" +#import "AuctionLot.h" +#import "Video.h" +#import "Follow.h" +#import "PartnerShowFairLocation.h" +#import "MapFeature.h" +#import "SystemTime.h" diff --git a/Artsy/Classes/Models/OrderedSet.h b/Artsy/Classes/Models/OrderedSet.h new file mode 100644 index 00000000000..79658dfc9fe --- /dev/null +++ b/Artsy/Classes/Models/OrderedSet.h @@ -0,0 +1,14 @@ +@interface OrderedSet : MTLModel + +@property (readonly, nonatomic, copy) NSString *orderedSetID; + +@property (readonly, nonatomic, copy) NSString *key; +@property (readonly, nonatomic, copy) NSString *name; +@property (readonly, nonatomic, copy) NSString *orderedSetDescription; +@property (readonly, nonatomic, copy) NSString *itemType; + +- (void)getItems:(void (^)(NSArray *items))success; + +// TODO: owner + +@end diff --git a/Artsy/Classes/Models/OrderedSet.m b/Artsy/Classes/Models/OrderedSet.m new file mode 100644 index 00000000000..4a3f51b9bc1 --- /dev/null +++ b/Artsy/Classes/Models/OrderedSet.m @@ -0,0 +1,45 @@ +@implementation OrderedSet + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"orderedSetID": @"id", + @"key": @"key", + @"name": @"name", + @"orderedSetDescription": @"description", + @"itemType": @"item_type" + }; +} + ++ (NSArray *)supportedItemTypes +{ + return @[@"Post", @"Profile", @"Gene", @"Artwork", @"Artist", @"FeaturedLink", @"OrderedSet", @"Sale", @"User", @"PartnerShow", @"Video"]; +} + +- (void)getItems:(void (^)(NSArray *items))success +{ + // supported types are in https://github.com/artsy/gravity/blob/master/app/models/domain/ordered_set.rb#L47 + + NSAssert([[self class].supportedItemTypes includes:self.itemType], @"Unsupported item type: %@", self.itemType); + + [ArtsyAPI getOrderedSetItems:self.orderedSetID withType:NSClassFromString(self.itemType) success:success failure:^(NSError *error) { + success(@[]); + }]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + OrderedSet *orderedSet = object; + return [orderedSet.orderedSetID isEqualToString:self.orderedSetID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.orderedSetID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Partner.h b/Artsy/Classes/Models/Partner.h new file mode 100644 index 00000000000..b269acedee4 --- /dev/null +++ b/Artsy/Classes/Models/Partner.h @@ -0,0 +1,26 @@ +#import "MTLModel.h" + +typedef NS_ENUM(NSInteger, ARPartnerType) { + ARPartnerTypeGallery, + ARPartnerTypeMuseum, + ARPartnerTypeArtistEstate, + ARPartnerTypePrivateCollection, + ARPartnerTypeFoundation, + ARPartnerTypePublicDomain, + ARPartnerTypeImageArchive, + ARPartnerTypeNonProfit +}; + +@interface Partner : MTLModel + +@property (readonly, nonatomic, assign) BOOL defaultProfilePublic; +@property (readonly, nonatomic, copy) NSString *name; +@property (readonly, nonatomic, copy) NSString *shortName; +@property (readonly, nonatomic, copy) NSString *partnerID; +@property (readonly, nonatomic, copy) NSString *profileID; +@property (readonly, nonatomic, copy) NSString *website; +@property (readonly, nonatomic, assign) ARPartnerType type; + +- (NSURL *)imageURLWithFormatName:(NSString *)formatName; + +@end diff --git a/Artsy/Classes/Models/Partner.m b/Artsy/Classes/Models/Partner.m new file mode 100644 index 00000000000..5d920f8ddd8 --- /dev/null +++ b/Artsy/Classes/Models/Partner.m @@ -0,0 +1,76 @@ +#import "ARValueTransformer.h" + +@interface Partner() +@property (nonatomic, copy, readonly) NSString *imageAddress; +@end + + +@implementation Partner + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(Partner.new, name) : @"name", + @keypath(Partner.new, shortName) : @"short_name", + @keypath(Partner.new, partnerID) : @"id", + @keypath(Partner.new, defaultProfilePublic) : @"default_profile_public", + @keypath(Partner.new, profileID) : @"default_profile_id", + @keypath(Partner.new, imageAddress) : @"image_url", + }; +} + ++ (NSValueTransformer *)typeJSONTransformer +{ + NSDictionary *types = @{ + @"Gallery": @(ARPartnerTypeGallery), + @"Museum": @(ARPartnerTypeMuseum), + @"Artist Estate": @(ARPartnerTypeArtistEstate), + @"Private Collection": @(ARPartnerTypePrivateCollection), + @"Foundation": @(ARPartnerTypeFoundation), + @"Public Domain": @(ARPartnerTypePublicDomain), + @"Image Archive": @(ARPartnerTypeImageArchive), + @"Non Profit": @(ARPartnerTypeNonProfit), + }; + + return [ARValueTransformer enumValueTransformerWithMap:types]; +} + +- (NSURL *)imageURLWithFormatName:(NSString *)formatName +{ + NSString *url = [self.imageAddress stringByReplacingOccurrencesOfString:@":version" withString:formatName]; + return [NSURL URLWithString:url]; +} + ++ (NSValueTransformer *)defaultProfilePublicJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + +- (void)setNilValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"type"]) { + [self setValue:@(ARPartnerTypeGallery) forKey:key]; + } else if ([key isEqualToString:@"defaultProfilePublic"]) { + [self setValue:@NO forKey:key]; + } else { + [super setNilValueForKey:key]; + } +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Partner *partner = object; + return [partner.partnerID isEqualToString:self.partnerID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.partnerID.hash; +} + + +@end diff --git a/Artsy/Classes/Models/PartnerShow.h b/Artsy/Classes/Models/PartnerShow.h new file mode 100644 index 00000000000..0afa4ff6d6c --- /dev/null +++ b/Artsy/Classes/Models/PartnerShow.h @@ -0,0 +1,47 @@ +#import "ARShareableObject.h" + +@class Partner, Fair, MapPoint, PartnerShowFairLocation; + +@interface PartnerShow : MTLModel + +@property (nonatomic, strong, readonly) Partner *partner; +@property (nonatomic, strong, readonly) Fair *fair; + +@property (nonatomic, copy, readonly) NSString *showID; + +@property (nonatomic, copy, readonly) NSString *name; + +@property (nonatomic, copy, readonly) NSArray *artists; +@property (nonatomic, copy, readonly) NSArray *artworks; +@property (nonatomic, copy, readonly) NSArray *posts; + +@property (nonatomic, copy, readonly) NSArray *installationShots; + +@property (nonatomic, readonly) NSDate *startDate; +@property (nonatomic, readonly) NSDate *endDate; + +@property (nonatomic, copy, readonly) NSString *location; +@property (nonatomic, copy, readonly) NSString *locationInFair; + +@property (nonatomic, strong, readonly) PartnerShowFairLocation *fairLocation; + +- (id)initWithShowID:(NSString *)showID; + +/// Titles for display +- (NSString *)title; + +/// Subtitles for display +- (NSString *)subtitle; + +/// A string with the date range for the show, or corrosponding fair +- (NSString *)ausstellungsdauer; + +/// A useful method for date range and location +- (NSString *)ausstellungsdauerAndLocation; + +- (BOOL)hasMapLocation; + +/// Show image url, could be an artwork / installation shot +- (NSURL *)imageURLWithFormatName:(NSString *)formatName; + +@end diff --git a/Artsy/Classes/Models/PartnerShow.m b/Artsy/Classes/Models/PartnerShow.m new file mode 100644 index 00000000000..32431334350 --- /dev/null +++ b/Artsy/Classes/Models/PartnerShow.m @@ -0,0 +1,174 @@ +#import "ARStandardDateFormatter.h" +#import "NSDate+DateRange.h" + +static ARStandardDateFormatter *staticDateFormatter; + +@interface PartnerShow() +@property (nonatomic, copy, readonly) NSString *imageAddress; +@property (nonatomic, copy, readonly) NSArray *imageVersions; +@end + +@implementation PartnerShow + +- (id)initWithShowID:(NSString *)showID +{ + self = [super init]; + if (!self) { return nil; } + + _showID = showID; + + return self; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(PartnerShow.new, showID) : @"id", + @keypath(PartnerShow.new, partner): @"partner", + @keypath(PartnerShow.new, artworks) : @"artworks", + @keypath(PartnerShow.new, artists) : @"artists", + @keypath(PartnerShow.new, fair) : @"fair", + @keypath(PartnerShow.new, installationShots): @"installation_shots", + @keypath(PartnerShow.new, posts): @"posts", + @keypath(PartnerShow.new, name) : @"name", + @keypath(PartnerShow.new, startDate): @"start_at", + @keypath(PartnerShow.new, endDate) : @"end_at", + @keypath(PartnerShow.new, imageAddress) : @"image_url", + @keypath(PartnerShow.new, imageVersions) : @"image_versions", + @keypath(PartnerShow.new, location) : @"location", + @keypath(PartnerShow.new, locationInFair) : @"fair_location.display", + @keypath(PartnerShow.new, fairLocation) : @"fair_location", + }; +} + ++ (NSValueTransformer *)fairJSONTransformer +{ + return [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Fair class]]; +} + ++ (NSValueTransformer *)artworksJSONTransformer +{ + return [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:[Artwork class]]; +} + ++ (NSValueTransformer *)partnerJSONTransformer +{ + return [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Partner class]]; +} + ++ (NSValueTransformer *)fairLocationJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[PartnerShowFairLocation class]]; +} + ++ (NSValueTransformer *)startDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)endDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)locationJSONTransformer +{ + return [MTLValueTransformer transformerWithBlock:^(NSDictionary *item) { + if (!item) { + return @""; + } + return (NSString *)item[@"city"]; + }]; +} + + +- (NSString *)title +{ + if (self.partner && self.partner.name) { + return self.partner.name; + + } else if(self.name) { + return self.name; + + } else if (self.artists.count) { + return [self.artists[0] name]; + } + + return nil; +} + +- (NSString *)subtitle +{ + if (self.fair) { + return self.fair.name; + } + + return self.name; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"Show ( %@ at %@ ) ", self.name, self.ausstellungsdauer]; +} + + +- (NSString *)ausstellungsdauer +{ + if (self.fair) { + return self.fair.ausstellungsdauer; + } + return [self.startDate ausstellungsdauerToDate:self.endDate]; +} + +// Used in a maps predicate +- (BOOL)hasMapLocation +{ + return [self.fairLocation.mapPoints count] > 0; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:self.class]){ + return [self.showID isEqualToString:[object showID]]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.showID.hash; +} + +- (NSURL *)imageURLWithFormatName:(NSString *)formatName +{ + if (self.imageAddress && self.imageVersions.count > 0 && [self.imageVersions includes:formatName]) { + NSString *url = [self.imageAddress stringByReplacingOccurrencesOfString:@":version" withString:formatName]; + return [NSURL URLWithString:url]; + } else { + return nil; + } +} + +- (NSString *)ausstellungsdauerAndLocation +{ + NSString *ausstellungsdauer = self.ausstellungsdauer; + if (self.fair && self.locationInFair && ![self.locationInFair isEqualToString:@""]) { + return [NSString stringWithFormat: @"%@, %@", ausstellungsdauer, self.locationInFair]; + + } else if (self.location && ![self.location isEqualToString:@""]) { + return [NSString stringWithFormat: @"%@, %@", ausstellungsdauer, self.location]; + + } else { + return self.ausstellungsdauer; + } +} + +#pragma mark ShareableObject + +- (NSString *)publicArtsyPath +{ + return [NSString stringWithFormat:@"/show/%@", self.showID]; +} + +@end diff --git a/Artsy/Classes/Models/PartnerShowFairLocation.h b/Artsy/Classes/Models/PartnerShowFairLocation.h new file mode 100644 index 00000000000..ccb99352962 --- /dev/null +++ b/Artsy/Classes/Models/PartnerShowFairLocation.h @@ -0,0 +1,17 @@ +#import "MapPoint.h" + +@interface PartnerShowFairLocation : MTLModel + +@property (nonatomic, copy, readonly) NSString *display; + +@property (nonatomic, copy, readonly) NSString *booth; +@property (nonatomic, copy, readonly) NSString *room; +@property (nonatomic, copy, readonly) NSString *floor; +@property (nonatomic, copy, readonly) NSString *pier; +@property (nonatomic, copy, readonly) NSString *section; +@property (nonatomic, copy, readonly) NSString *hall; +@property (nonatomic, copy, readonly) NSString *special; + +@property (nonatomic, copy, readonly) NSArray *mapPoints; + +@end diff --git a/Artsy/Classes/Models/PartnerShowFairLocation.m b/Artsy/Classes/Models/PartnerShowFairLocation.m new file mode 100644 index 00000000000..5c885e8d011 --- /dev/null +++ b/Artsy/Classes/Models/PartnerShowFairLocation.m @@ -0,0 +1,18 @@ +@interface PartnerShowFairLocation () +@end + +@implementation PartnerShowFairLocation + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(PartnerShowFairLocation.new, mapPoints) : @"map_points", + }; +} + ++ (NSValueTransformer *)mapPointsJSONTransformer +{ + return [MTLValueTransformer mtl_JSONArrayTransformerWithModelClass:[MapPoint class]]; +} + +@end diff --git a/Artsy/Classes/Models/PhotoContentLink.h b/Artsy/Classes/Models/PhotoContentLink.h new file mode 100644 index 00000000000..69a6c02db26 --- /dev/null +++ b/Artsy/Classes/Models/PhotoContentLink.h @@ -0,0 +1,5 @@ +#import "ContentLink.h" + +@interface PhotoContentLink : ContentLink + +@end diff --git a/Artsy/Classes/Models/PhotoContentLink.m b/Artsy/Classes/Models/PhotoContentLink.m new file mode 100644 index 00000000000..7a3c6d9de46 --- /dev/null +++ b/Artsy/Classes/Models/PhotoContentLink.m @@ -0,0 +1,5 @@ +#import "PhotoContentLink.h" + +@implementation PhotoContentLink + +@end diff --git a/Artsy/Classes/Models/Post.h b/Artsy/Classes/Models/Post.h new file mode 100644 index 00000000000..3a2759d876d --- /dev/null +++ b/Artsy/Classes/Models/Post.h @@ -0,0 +1,8 @@ +#import "MTLModel.h" + +@interface Post : MTLModel + +@property (nonatomic, copy, readonly) NSString *postID; +@property (nonatomic, copy, readonly) NSString *title; + +@end diff --git a/Artsy/Classes/Models/Post.m b/Artsy/Classes/Models/Post.m new file mode 100644 index 00000000000..d4f256d7826 --- /dev/null +++ b/Artsy/Classes/Models/Post.m @@ -0,0 +1,26 @@ +@implementation Post + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"postID" : @"id", + @"title" : @"title" + }; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Post *post = object; + return [post.postID isEqualToString:self.postID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.postID.hash; +} + +@end diff --git a/Artsy/Classes/Models/PostImage.h b/Artsy/Classes/Models/PostImage.h new file mode 100644 index 00000000000..b8e1702e8b3 --- /dev/null +++ b/Artsy/Classes/Models/PostImage.h @@ -0,0 +1,14 @@ +#import "MTLModel.h" +#import "ARPostAttachment.h" +#import "ARHasImageBaseURL.h" + +@interface PostImage : MTLModel + +@property (readonly, nonatomic, copy) NSString *imageID; +@property (readonly, nonatomic, copy) NSString *url; + +@property (readonly, nonatomic) CGFloat originalHeight; +@property (readonly, nonatomic) CGFloat originalWidth; +@property (readwrite, nonatomic) CGFloat aspectRatio; + +@end diff --git a/Artsy/Classes/Models/PostImage.m b/Artsy/Classes/Models/PostImage.m new file mode 100644 index 00000000000..47c7ef614aa --- /dev/null +++ b/Artsy/Classes/Models/PostImage.m @@ -0,0 +1,59 @@ +#import "PostImage.h" + +#define THUMBNAIL_SIZE @"large" + +@implementation PostImage + ++ (NSDictionary *)JSONKeyPathsByPropertyKey { + return @{ + @"imageID" : @"id", + @"url" : @"image_url", + @"originalHeight" : @"original_height", + @"originalWidth" : @"original_width", + @"aspectRatio" : @"aspect_ratio" + }; +} + +- (CGSize)maxSize { + return CGSizeMake(self.originalWidth, self.originalHeight); +} + +- (NSURL *)urlForThumbnail { + NSString *url = [self.url stringByReplacingOccurrencesOfString:@":version" withString:THUMBNAIL_SIZE]; + return [NSURL URLWithString: url]; +} + +- (void)setNilValueForKey:(NSString *)key { + if ([@[@"aspectRatio", @"originalHeight", @"originalWidth"] containsObject:key]) { + [self setValue:@1 forKey:key]; + } else { + [super setNilValueForKey:key]; + } +} + +- (CGFloat)aspectRatio { + return _aspectRatio ? _aspectRatio : 1; +} + +- (NSString*)baseImageURL +{ + return self.url; +} + + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + PostImage *postImage = object; + return [postImage.imageID isEqualToString:self.imageID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.imageID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Profile.h b/Artsy/Classes/Models/Profile.h new file mode 100644 index 00000000000..9734c43a27c --- /dev/null +++ b/Artsy/Classes/Models/Profile.h @@ -0,0 +1,19 @@ +#import "MTLModel.h" +#import "ProfileOwner.h" + +/// A profile is a model that represents something +/// a user can log in to, like a User account or a Partner account. + +@interface Profile : MTLModel + +@property (nonatomic, copy) NSString *profileID; +@property (nonatomic, copy) NSString *bio; +@property (nonatomic, strong) NSNumber *followCount; +@property (nonatomic, strong, readonly) NSObject *profileOwner; + +- (NSString *)iconURL; +- (instancetype)initWithProfileID:(NSString *)profileID; +- (void)updateProfile:(void(^)(void))success; +- (NSString *)profileName; + +@end diff --git a/Artsy/Classes/Models/Profile.m b/Artsy/Classes/Models/Profile.m new file mode 100644 index 00000000000..7875e376433 --- /dev/null +++ b/Artsy/Classes/Models/Profile.m @@ -0,0 +1,168 @@ +@interface Profile (){ + BOOL _followed; +} + +@property (nonatomic, copy) NSString *iconAddress; +@property (nonatomic, copy) NSString *iconVersion; +@property (nonatomic, copy) NSArray *iconImageVersions; +@property (nonatomic, strong) NSString *ownerClassString; +@property (nonatomic, copy) NSDictionary *ownerJSON; +@end + +@implementation Profile +@synthesize profileOwner = _profileOwner; + + +// Doing some silly hackery to create the profile's owner from both the `owner_type` keypath and the `owner` keypath +// until https://github.com/Mantle/Mantle/pull/270 is merged into master. ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"profileID" : @"id", + @"ownerJSON" : @"owner", + @"ownerClassString" : @"owner_type", + @"followCount" : @"follow_count", + @"iconAddress" : @"icon.image_url", + @"iconVersion" : @"default_icon_version", + @"iconImageVersions" : @"icon.image_versions" + }; +} + +// Had problems converting the `owner_type` string into a Class with Mantle, so we'll do this instead. +- (Class)ownerClass +{ + NSString *className = self.ownerClassString; + if ([className isEqualToString:@"User"]) { + return [User class]; + } else if ([className isEqualToString:@"FairOrganizer"]) { + return [FairOrganizer class]; + } else if ([className isEqualToString:@"Fair"]) { + return [Fair class]; + } else if ([className isEqualToString:@"PartnerGallery"]) { + return [Partner class]; + } else { + return nil; + } +} + +- (NSObject *)profileOwner +{ + // Create the owner by combining the information we got from `owner` and `owner_type` JSON. + if (!_profileOwner) { + _profileOwner = [[self ownerClass] modelWithJSON:self.ownerJSON]; + } + return _profileOwner; +} + +- (void)updateProfile:(void(^)(void))success +{ + @weakify(self); + + if (self.profileID) { + [ArtsyAPI getProfileForProfileID:self.profileID success:^(Profile *profile) { + @strongify(self); + + [self mergeValuesForKeysFromModel:profile]; + success(); + + } failure:^(NSError *error) { + success(); + }]; + } else { + success(); + } +} + +- (NSString *)iconURL +{ + NSString *iconFormat = nil; + if (self.iconImageVersions.count > 0){ + if (self.iconVersion && [self.iconImageVersions includes:self.iconVersion]) { + iconFormat = self.iconVersion; + } else { + iconFormat = self.iconImageVersions[0]; + } + } else { + return nil; + } + NSString *format = [NSString stringWithFormat:@"%@.png", iconFormat]; + return [self.iconAddress stringByReplacingOccurrencesOfString:@":version.jpg" withString:format]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"Profile %@ - (%@)", _profileID, self.profileName]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Profile *profile = object; + return [profile.profileID isEqualToString:_profileID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.profileID.hash; +} + +- (instancetype)initWithProfileID:(NSString *)profileID +{ + self = [super init]; + if (!self) { return nil; } + + _profileID = profileID; + + return self; +} + +- (NSString *)profileName +{ + return [_profileOwner name]; +} + +- (void)setFollowed:(BOOL)followed +{ + _followed = followed; +} + +- (BOOL)isFollowed +{ + return _followed; +} + +- (void)followWithSuccess:(void (^)(id response))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI followProfile:self success:success failure:failure]; +} + +- (void)unfollowWithSuccess:(void (^)(id response))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI unfollowProfile:self success:success failure:failure]; +} + +- (void)setFollowState:(BOOL)state success:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + if(state){ + [self followWithSuccess:success failure:failure]; + } else { + [self unfollowWithSuccess:success failure:failure]; + } +} + +- (void)getFollowState:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure +{ + if ([User isTrialUser]) { + success(ARHeartStatusNo); + return; + } + + [ArtsyAPI checkFollowProfile:self success:^(BOOL status) { + success(ARHeartStatusYes); + } failure:failure]; +} + +@end diff --git a/Artsy/Classes/Models/ProfileOwner.h b/Artsy/Classes/Models/ProfileOwner.h new file mode 100644 index 00000000000..5229c480a70 --- /dev/null +++ b/Artsy/Classes/Models/ProfileOwner.h @@ -0,0 +1,5 @@ +@protocol ProfileOwner + +@property (nonatomic, copy, readonly) NSString *name; + +@end diff --git a/Artsy/Classes/Models/Sale.h b/Artsy/Classes/Models/Sale.h new file mode 100644 index 00000000000..684492acf32 --- /dev/null +++ b/Artsy/Classes/Models/Sale.h @@ -0,0 +1,16 @@ +#import "MTLModel.h" +#import "MTLJSONAdapter.h" + +@interface Sale : MTLModel + +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSString *saleID; + +@property (nonatomic, strong, readonly) NSDate *startDate; +@property (nonatomic, strong, readonly) NSDate *endDate; + +@property (nonatomic, readonly) BOOL isAuction; + +- (BOOL)isCurrentlyActive; + +@end diff --git a/Artsy/Classes/Models/Sale.m b/Artsy/Classes/Models/Sale.m new file mode 100644 index 00000000000..39ec9f5abee --- /dev/null +++ b/Artsy/Classes/Models/Sale.m @@ -0,0 +1,47 @@ +#import "ARStandardDateFormatter.h" + +@implementation Sale + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"saleID" : @"id", + @"isAuction" : @"is_auction", + @"startDate": @"start_at", + @"endDate" : @"end_at" + }; +} + ++ (NSValueTransformer *)startDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)endDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + +- (BOOL)isCurrentlyActive +{ + NSDate *now = [ARSystemTime date]; + return (([now compare:self.startDate] != NSOrderedAscending) && + ([now compare:self.endDate] != NSOrderedDescending)); +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Sale *sale = object; + return [sale.saleID isEqualToString:self.saleID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.saleID.hash; +} + +@end diff --git a/Artsy/Classes/Models/SaleArtwork.h b/Artsy/Classes/Models/SaleArtwork.h new file mode 100644 index 00000000000..8008d7ccfdc --- /dev/null +++ b/Artsy/Classes/Models/SaleArtwork.h @@ -0,0 +1,34 @@ +#import "MTLModel.h" +#import "Sale.h" +#import "Bidder.h" +#import "BidderPosition.h" +#import "Bid.h" + + +typedef NS_ENUM(NSInteger, ARReserveStatus) { + ARReserveStatusNoReserve, + ARReserveStatusReserveNotMet, + ARReserveStatusReserveMet +}; + +@interface SaleArtwork : MTLModel + +- (BidderPosition *)userMaxBidderPosition; +- (BOOL)hasEstimate; +- (NSString *)estimateString; + +@property (nonatomic, copy, readonly) NSString *saleArtworkID; +@property (nonatomic, strong) Sale *auction; +@property (nonatomic, strong) Bidder *bidder; +@property (nonatomic, strong) Bid *saleHighestBid; +@property (nonatomic, strong) NSNumber *artworkNumPositions; +@property (nonatomic, strong) BidderPosition *userBidderPosition; +@property (nonatomic, strong) NSArray *positions; +@property (nonatomic, strong) NSNumber *openingBidCents; +@property (nonatomic, strong) NSNumber *minimumNextBidCents; +@property (nonatomic, strong) NSNumber *lowEstimateCents; +@property (nonatomic, strong) NSNumber *highEstimateCents; +@property (nonatomic, assign, readonly) ARAuctionState auctionState; +@property (nonatomic, assign) ARReserveStatus reserveStatus; + +@end diff --git a/Artsy/Classes/Models/SaleArtwork.m b/Artsy/Classes/Models/SaleArtwork.m new file mode 100644 index 00000000000..43b697cf3be --- /dev/null +++ b/Artsy/Classes/Models/SaleArtwork.m @@ -0,0 +1,154 @@ +static NSNumberFormatter *dollarFormatter; + +@implementation SaleArtwork + ++ (void)initialize +{ + if (self == [SaleArtwork class]) { + dollarFormatter = [[NSNumberFormatter alloc] init]; + dollarFormatter.numberStyle = NSNumberFormatterCurrencyStyle; + + // This is always dollars, so let's make sure that's how it shows up + // regardless of locale. + + dollarFormatter.currencyGroupingSeparator = @","; + dollarFormatter.currencySymbol = @"$"; + dollarFormatter.maximumFractionDigits = 0; + } +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"saleArtworkID" : @"id", + @"openingBidCents" : @"opening_bid_cents", + @"minimumNextBidCents" : @"minimum_next_bid_cents", + @"saleHighestBid" : @"highest_bid", + @"artworkNumPositions" : @"bidder_positions_count", + @"lowEstimateCents" : @"low_estimate_cents", + @"highEstimateCents" : @"high_estimate_cents", + @"reserveStatus" : @"reserve_status" + }; +} + +- (ARAuctionState)auctionState +{ + ARAuctionState state = ARAuctionStateDefault; + NSDate *now = [ARSystemTime date]; + + if ([self.auction.startDate compare:now] == NSOrderedAscending) { + state |= ARAuctionStateStarted; + } + + if ([self.auction.endDate compare:now] == NSOrderedAscending) { + state |= ARAuctionStateEnded; + } + + if (self.bidder) { + state |= ARAuctionStateUserIsRegistered; + } + + if (self.saleHighestBid) { + state |= ARAuctionStateArtworkHasBids; + } + + if (self.positions.count) { + state |= ARAuctionStateUserIsBidder; + } + + for (BidderPosition *position in self.positions) { + if ([self.saleHighestBid isEqual:position.highestBid]) { + state |= ARAuctionStateUserIsHighBidder; + break; + } + } + + return state; +} + ++ (NSValueTransformer *)saleHighestBidJSONTransformer +{ + return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:[Bid class]]; +} + ++ (NSValueTransformer *)reserveStatusJSONTransformer +{ + NSDictionary *types = @{ + @"no_reserve" : @(ARReserveStatusNoReserve), + @"reserve_not_met" : @(ARReserveStatusReserveNotMet), + @"reserve_met" : @(ARReserveStatusReserveMet) + }; + + return [MTLValueTransformer reversibleTransformerWithForwardBlock:^id(NSString *str) { + if(!str) str = @"not for sale"; + return types[str]; + } reverseBlock:^id(NSNumber *type) { + if (!type) { type = @0; } + return [types allKeysForObject:type].lastObject; + }]; +} + +- (void)setPositions:(NSArray *)positions +{ + _positions = positions; +} + +- (BidderPosition *)userMaxBidderPosition +{ + return [self.positions valueForKeyPath:@"@max.self"]; +} + +- (NSString *)dollarsFromCents:(NSNumber *)cents +{ + NSNumber *dollars = @(roundf(cents.floatValue / 100)) ; + + if ([dollars integerValue] == 0) { + return @"$0"; + } + + NSString *centString = [dollarFormatter stringFromNumber:dollars]; + if (centString.length < 3) { + // just covering this very degenerate case + // that hopefully this never happens + return @"$1"; + } + return centString; +} + +- (BOOL)hasEstimate +{ + return self.lowEstimateCents || self.highEstimateCents; +} + +- (NSString *)estimateString +{ + NSString *ret; + if (self.lowEstimateCents && self.highEstimateCents) { + ret = [NSString stringWithFormat:@"%@ – %@", [self dollarsFromCents:self.lowEstimateCents], [self dollarsFromCents:self.highEstimateCents]]; + } else if (self.lowEstimateCents) { + ret = [self dollarsFromCents:self.lowEstimateCents]; + } else if (self.highEstimateCents) { + ret = [self dollarsFromCents:self.highEstimateCents]; + } else { + ARErrorLog(@"Asked for estimate from an artwork with no estimate data %@", self); + ret = @"$0"; + } + return [NSString stringWithFormat:@"Estimate: %@", ret]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + SaleArtwork *saleArtwork = object; + return [saleArtwork.saleArtworkID isEqualToString:self.saleArtworkID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.saleArtworkID.hash; +} + +@end diff --git a/Artsy/Classes/Models/SearchResult.h b/Artsy/Classes/Models/SearchResult.h new file mode 100644 index 00000000000..7a4793cc419 --- /dev/null +++ b/Artsy/Classes/Models/SearchResult.h @@ -0,0 +1,15 @@ +#import "Mantle.h" + +@interface SearchResult : MTLModel + +@property (readonly, nonatomic, copy) Class model; + +@property (readonly, nonatomic, copy) NSString *modelID; +@property (readonly, nonatomic, copy) NSString *displayText; +@property (readonly, nonatomic, copy) NSString *label; +@property (readonly, nonatomic, copy) NSString *searchDetail; +@property (readonly, nonatomic, strong) NSNumber *isPublished; + ++ (BOOL)searchResultIsSupported:(NSDictionary *)dict; +- (NSURLRequest *)imageRequest; +@end diff --git a/Artsy/Classes/Models/SearchResult.m b/Artsy/Classes/Models/SearchResult.m new file mode 100644 index 00000000000..38d90838420 --- /dev/null +++ b/Artsy/Classes/Models/SearchResult.m @@ -0,0 +1,67 @@ +#import "SearchResult.h" +#import "ARRouter.h" + +static NSDictionary *classMap; + +@implementation SearchResult + ++ (BOOL)searchResultIsSupported:(NSDictionary *)dict +{ + return [[classMap allKeys] containsObject:dict[@"model"]]; +} + ++ (void)initialize +{ + classMap = @{ + @"artwork": [Artwork class], + @"gene": [Gene class], + @"artist": [Artist class], + @"profile" : [Profile class], + @"feature" : [SiteFeature class], + + // This is _NOT_ from the API, but comes from ARFairSearchVC + @"partnershow" : [PartnerShow class] + + }; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"modelID" : @"id", + @"displayText" : @"display", + @"model" : @"model", + @"label" : @"label", + @"searchDetail" : @"search_detail", + @"isPublished" : @"published", + }; +} + ++ (NSValueTransformer *)modelJSONTransformer +{ + return [MTLValueTransformer transformerWithBlock:^(NSString *str) { + return classMap[str]; + }]; +} + +- (NSURLRequest *)imageRequest +{ + return [ARRouter directImageRequestForModel:self.model andSlug:self.modelID]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:self.class]){ + return [self.model isEqual:[object model]] && [self.modelID isEqualToString:[object modelID]]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return NSStringWithFormat(@"%@:%@", self.model, self.modelID).hash; +} + + +@end diff --git a/Artsy/Classes/Models/SiteFeature.h b/Artsy/Classes/Models/SiteFeature.h new file mode 100644 index 00000000000..07d83a3ae9c --- /dev/null +++ b/Artsy/Classes/Models/SiteFeature.h @@ -0,0 +1,10 @@ +#import "Mantle.h" + +@interface SiteFeature : MTLModel + +@property (nonatomic, readonly) NSString *siteFeatureID; +@property (nonatomic, readonly) NSString *name; +@property (nonatomic, readonly) NSNumber *enabled; +@property (nonatomic, readonly) NSDictionary *parameters; + +@end diff --git a/Artsy/Classes/Models/SiteFeature.m b/Artsy/Classes/Models/SiteFeature.m new file mode 100644 index 00000000000..b2844836c78 --- /dev/null +++ b/Artsy/Classes/Models/SiteFeature.m @@ -0,0 +1,28 @@ +@implementation SiteFeature + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ @"siteFeatureID" : @"id" }; +} + ++ (NSValueTransformer *)enabledTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + SiteFeature *siteFeature = object; + return [siteFeature.siteFeatureID isEqualToString:self.siteFeatureID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.siteFeatureID.hash; +} + +@end diff --git a/Artsy/Classes/Models/SiteHeroUnit.h b/Artsy/Classes/Models/SiteHeroUnit.h new file mode 100644 index 00000000000..ed21f28328a --- /dev/null +++ b/Artsy/Classes/Models/SiteHeroUnit.h @@ -0,0 +1,43 @@ +#import "Mantle.h" + +typedef NS_ENUM(NSInteger, ARHeroUnitImageColor){ + ARHeroUnitImageColorBlack, + ARHeroUnitImageColorWhite +}; + +typedef NS_ENUM(NSInteger, ARHeroUnitAlignment){ + ARHeroUnitAlignmentLeft, + ARHeroUnitAlignmentRight +}; + +@interface SiteHeroUnit : MTLModel + +@property (nonatomic, copy, readonly) NSString *siteHeroUnitID; +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSString *link; +@property (nonatomic, copy, readonly) NSString *linkText; +@property (nonatomic, copy, readonly) NSString *creditLine; + +@property (nonatomic, copy, readonly) NSString *heading; +@property (nonatomic, copy, readonly) NSString *title; +@property (nonatomic, copy, readonly) NSString *titleImageAddress; +@property (nonatomic, copy, readonly) NSString *body; + +@property (nonatomic, copy, readonly) NSString *backgroundColor; +@property (nonatomic, copy, readonly) NSString *mobileBackgroundColor; + +@property (nonatomic, copy, readonly) NSString *mobileBackgroundImageAddress; +@property (nonatomic, copy, readonly) NSString *backgroundImageAddress; +@property (nonatomic, assign, readonly, getter = isCurrentlyActive) BOOL currentlyActive; + +@property (nonatomic, assign, readonly) ARHeroUnitImageColor backgroundStyle; +@property (nonatomic, assign, readonly) ARHeroUnitAlignment alignment; +@property (nonatomic, assign, readonly) NSURL *preferredImageURL; +@property (nonatomic, assign, readonly) NSURL *titleImageURL; + +@property (nonatomic, strong, readonly) NSDate *startDate; +@property (nonatomic, strong, readonly) NSDate *endDate; +@property (nonatomic, assign, readonly) NSInteger position; +- (UIColor *)buttonColor; +- (UIColor *)inverseButtonColor; +@end diff --git a/Artsy/Classes/Models/SiteHeroUnit.m b/Artsy/Classes/Models/SiteHeroUnit.m new file mode 100644 index 00000000000..209cd64c236 --- /dev/null +++ b/Artsy/Classes/Models/SiteHeroUnit.m @@ -0,0 +1,125 @@ +#import "ARStandardDateFormatter.h" + +@interface SiteHeroUnit () +@property (nonatomic, copy, readonly) NSString *buttonColorHex; +@property (nonatomic, copy, readonly) NSString *inverseButtonColorHex; +@end + +@implementation SiteHeroUnit + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(SiteHeroUnit.new, siteHeroUnitID): @"id", + @keypath(SiteHeroUnit.new, heading): @"heading", + @keypath(SiteHeroUnit.new, title): @"mobile_title", + @keypath(SiteHeroUnit.new, titleImageAddress): @"title_image_retina_url", + @keypath(SiteHeroUnit.new, body): @"mobile_description", + @keypath(SiteHeroUnit.new, backgroundColor): @"menu_color_class", + @keypath(SiteHeroUnit.new, mobileBackgroundColor): @"mobile_menu_color_class", + @keypath(SiteHeroUnit.new, name): @"name", + @keypath(SiteHeroUnit.new, link): @"link", + @keypath(SiteHeroUnit.new, linkText): @"link_text", + @keypath(SiteHeroUnit.new, buttonColorHex): @"link_color_off_hex", + @keypath(SiteHeroUnit.new, inverseButtonColorHex): @"link_color_hover_hex", + @keypath(SiteHeroUnit.new, creditLine): @"credit_line", + @keypath(SiteHeroUnit.new, mobileBackgroundImageAddress) : @"background_image_mobile_url", + @keypath(SiteHeroUnit.new, backgroundImageAddress): @"background_image_url", + @keypath(SiteHeroUnit.new, position): @"position", + @keypath(SiteHeroUnit.new, startDate): @"start_at", + @keypath(SiteHeroUnit.new, endDate): @"end_at", + @keypath(SiteHeroUnit.new, alignment): @"type" + }; +} + ++ (NSValueTransformer *)enabledJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)displayOnMobileJSONTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)startDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)endDateJSONTransformer +{ + return [ARStandardDateFormatter sharedFormatter].stringTransformer; +} + ++ (NSValueTransformer *)alignmentJSONTransformer +{ + return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{ + @"left":@(ARHeroUnitAlignmentLeft), + @"right":@(ARHeroUnitAlignmentRight) + } defaultValue:@(ARHeroUnitAlignmentLeft) reverseDefaultValue:@"left"]; +} + +- (enum ARHeroUnitImageColor)backgroundStyle +{ + NSString *color = self.backgroundColor; + + // Prioritise mobile prefixed colours + if (self.mobileBackgroundColor && self.mobileBackgroundColor.length > 0) { + color = self.mobileBackgroundColor; + } + + if ([color isEqualToString:@"black"]){ + return ARHeroUnitImageColorBlack; + } + return ARHeroUnitImageColorWhite; +} + +- (NSURL *)titleImageURL +{ + NSString *address = self.titleImageAddress; + return [NSURL URLWithString:[address stringByAddingPercentEscapesUsingEncoding: NSASCIIStringEncoding]]; +} + +- (NSURL *)preferredImageURL +{ + NSString *address = self.backgroundImageAddress; + if (self.mobileBackgroundImageAddress && ![UIDevice isPad]) { + address = self.mobileBackgroundImageAddress; + } + return [NSURL URLWithString:[address stringByAddingPercentEscapesUsingEncoding: NSASCIIStringEncoding]]; +} + +- (BOOL)isCurrentlyActive +{ + NSDate *now = [ARSystemTime date]; + return (([now compare:self.startDate] != NSOrderedAscending) && + ([now compare:self.endDate] != NSOrderedDescending)); +} + +- (UIColor *)buttonColor +{ + return [UIColor colorWithHexString:self.buttonColorHex]; +} + +- (UIColor *)inverseButtonColor +{ + return [UIColor colorWithHexString:self.inverseButtonColorHex]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + SiteHeroUnit *siteHeroUnit = object; + return [siteHeroUnit.siteHeroUnitID isEqualToString:self.siteHeroUnitID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.siteHeroUnitID.hash; +} + +@end diff --git a/Artsy/Classes/Models/SystemTime.h b/Artsy/Classes/Models/SystemTime.h new file mode 100644 index 00000000000..20df471251f --- /dev/null +++ b/Artsy/Classes/Models/SystemTime.h @@ -0,0 +1,20 @@ +#import "MTLModel.h" + +@interface SystemTime : MTLModel +/** + * System (server-side) date. + * + * @return Returns the server-side date. + */ +- (NSDate *)date; + +/** + * Time difference since date. + * + * @param date Date to compare. + * + * @return A time interval. + */ +- (NSTimeInterval)timeIntervalSinceDate:(NSDate *)date; + +@end diff --git a/Artsy/Classes/Models/SystemTime.m b/Artsy/Classes/Models/SystemTime.m new file mode 100644 index 00000000000..c1bffac56e8 --- /dev/null +++ b/Artsy/Classes/Models/SystemTime.m @@ -0,0 +1,41 @@ +#import + +@interface SystemTime() +@property (nonatomic, readonly, strong) NSString *time; +@property (nonatomic, readonly, assign) NSInteger day; +@property (nonatomic, readonly, assign) NSInteger wday; +@property (nonatomic, readonly, assign) NSInteger month; +@property (nonatomic, readonly, assign) NSInteger hour; +@property (nonatomic, readonly, assign) NSInteger year; +@property (nonatomic, readonly, assign) NSInteger min; +@property (nonatomic, readonly, assign) NSInteger sec; +@property (nonatomic, readonly, assign) BOOL dst; +@property (nonatomic, readonly, assign) NSInteger unix; +@property (nonatomic, readonly, assign) float utcOffset; +@property (nonatomic, readonly, strong) NSString *zome; +@property (nonatomic, readonly, strong) NSString *iso8601; +@end + +@implementation SystemTime + +#pragma mark - MTLJSONSerializing + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(SystemTime.new, utcOffset): @"utc_offset", + }; +} + +- (NSDate *)date +{ + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + return [dateFormatter dateFromString:self.iso8601]; +} + +- (NSTimeInterval)timeIntervalSinceDate:(NSDate *)date +{ + return [self.date timeIntervalSinceDate:date]; +} + +@end diff --git a/Artsy/Classes/Models/Tag.h b/Artsy/Classes/Models/Tag.h new file mode 100644 index 00000000000..7b21477631b --- /dev/null +++ b/Artsy/Classes/Models/Tag.h @@ -0,0 +1,8 @@ +#import "MTLModel.h" + +@interface Tag : MTLModel + +@property (readonly, nonatomic, copy) NSString *name; +@property (readonly, nonatomic, copy) NSString *tagID; + +@end diff --git a/Artsy/Classes/Models/Tag.m b/Artsy/Classes/Models/Tag.m new file mode 100644 index 00000000000..2b1b5893fc4 --- /dev/null +++ b/Artsy/Classes/Models/Tag.m @@ -0,0 +1,26 @@ +@implementation Tag + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"tagID" : @"id", + @"name" : @"name" + }; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Tag *tag = object; + return [tag.tagID isEqualToString:self.tagID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.tagID.hash; +} + +@end diff --git a/Artsy/Classes/Models/User.h b/Artsy/Classes/Models/User.h new file mode 100644 index 00000000000..296ed36aa0e --- /dev/null +++ b/Artsy/Classes/Models/User.h @@ -0,0 +1,40 @@ +#import "MTLModel.h" +#import "MTLJSONAdapter.h" + +@class Profile; + +@interface User : MTLModel + +typedef NS_ENUM(NSInteger, ARCollectorLevel) { + ARCollectorLevelNo = 1, + ARCollectorLevelInterested, + ARCollectorLevelCollector +}; + +@property (nonatomic, strong) Profile *profile; + +@property (nonatomic, copy, readonly) NSString *userID; +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSString *email; +@property (nonatomic, copy, readonly) NSString *phone; + +@property (nonatomic, copy, readonly) NSString *defaultProfileID; +@property (nonatomic, assign) ARCollectorLevel collectorLevel; +@property (nonatomic, assign) NSInteger priceRange; //upper limit in $ + +@property (nonatomic, readonly) BOOL receiveWeeklyEmail; +@property (nonatomic, readonly) BOOL receiveFollowArtistsEmail; +@property (nonatomic, readonly) BOOL receiveFollowArtistsEmailAll; +@property (nonatomic, readonly) BOOL receiveFollowUsersEmail; + ++ (User *)currentUser; ++ (BOOL)isTrialUser; + +- (void)userFollowsProfile:(Profile *)profile success:(void(^)(BOOL doesFollow))success failure:(void (^)(NSError *error))failure; + +- (void)setRemoteUpdateCollectorLevel:(enum ARCollectorLevel)collectorLevel success:(void(^)(User *user))success failure:(void (^)(NSError *error))failure; +- (void)setRemoteUpdatePriceRange:(NSInteger)maximumRange success:(void(^)(User *user))success failure:(void (^)(NSError *error))failure; + +- (void)updateProfile:(void (^)(void))success; + +@end diff --git a/Artsy/Classes/Models/User.m b/Artsy/Classes/Models/User.m new file mode 100644 index 00000000000..18bada41ae6 --- /dev/null +++ b/Artsy/Classes/Models/User.m @@ -0,0 +1,122 @@ +#import "ARUserManager.h" + +@implementation User + ++ (User *)currentUser +{ + return [[ARUserManager sharedManager] currentUser]; +} + ++ (BOOL)isTrialUser +{ + ARUserManager *userManager = [ARUserManager sharedManager]; + return (userManager.currentUser == nil) && userManager.hasValidXAppToken; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @keypath(User.new, userID) : @"id", + @keypath(User.new, email) : @"email", + @keypath(User.new, name) : @"name", + @keypath(User.new, phone) : @"phone", + @keypath(User.new, defaultProfileID) : @"default_profile_id", + @keypath(User.new, receiveWeeklyEmail) : @"receive_weekly_email", + @keypath(User.new, receiveFollowArtistsEmail) : @"receive_follow_artists_email", + @keypath(User.new, receiveFollowArtistsEmailAll) : @"receive_follow_artists_email_all", + @keypath(User.new, receiveFollowUsersEmail) : @"receive_follow_users_email", + @keypath(User.new, priceRange) : @"price_range" + }; +} + +#pragma mark Model Upgrades + ++ (NSUInteger)modelVersion +{ + return 1; +} + +- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)fromVersion +{ + if (fromVersion == 0) { + if ([key isEqual:@"userID"]) { + return [coder decodeObjectForKey:@"userId"] ?: [coder decodeObjectForKey:@"userID"]; + } else if ([key isEqual:@"defaultProfileID"]) { + return [coder decodeObjectForKey:@"defaultProfileId"] ?: [coder decodeObjectForKey:@"defaultProfileID"]; + } + } + + return [super decodeValueForKey:key withCoder:coder modelVersion:fromVersion]; +} + ++ (NSValueTransformer *)receiveFollowArtistsEmailTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + ++ (NSValueTransformer *)receiveWeeklyEmailTransformer +{ + return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName]; +} + +- (void)userFollowsProfile:(Profile *)profile success:(void(^)(BOOL doesFollow))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI checkFollowProfile:profile success:success failure:failure]; +} + +- (void)updateProfile:(void (^)(void))success +{ + if(!_profile) _profile = [[Profile alloc] initWithProfileID:_defaultProfileID]; + + [self.profile updateProfile:^{ + success(); + }]; +} + +// Not the greatest APIs but eh + +- (void)setRemoteUpdateCollectorLevel:(enum ARCollectorLevel)collectorLevel success:(void(^)(User *user))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI updateCurrentUserProperty:@"collector_level" toValue:@(collectorLevel) success:success failure:failure]; +} + +- (void)setRemoteUpdatePriceRange:(NSInteger)maximumRange success:(void(^)(User *user))success failure:(void (^)(NSError *error))failure +{ + NSString *stringRange = [NSString stringWithFormat:@"-1:%@", @(maximumRange)]; + if (maximumRange == 1000000) { + stringRange = @"1000000:1000000000000"; + } + [ArtsyAPI updateCurrentUserProperty:@"price_range" toValue:stringRange success:success failure:failure]; + +} + +- (void)setNilValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"priceRange"]) { + [self setValue:@(-1) forKey:key]; + } else { + [super setNilValueForKey:key]; + } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"User %@ - (%@)", self.name, self.userID]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + User *user = object; + return [user.userID isEqualToString:self.userID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.userID.hash; +} + +@end diff --git a/Artsy/Classes/Models/Video.h b/Artsy/Classes/Models/Video.h new file mode 100644 index 00000000000..78c8ccce73d --- /dev/null +++ b/Artsy/Classes/Models/Video.h @@ -0,0 +1,9 @@ +#import "MTLModel.h" + +@interface Video : MTLModel + +@property (nonatomic, copy, readonly) NSString *videoID; +@property (nonatomic, copy, readonly) NSString *title; +@property (nonatomic, copy, readonly) NSString *subtitle; + +@end diff --git a/Artsy/Classes/Models/Video.m b/Artsy/Classes/Models/Video.m new file mode 100644 index 00000000000..7943e3923e1 --- /dev/null +++ b/Artsy/Classes/Models/Video.m @@ -0,0 +1,28 @@ +@implementation Video + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"videoID" : @"id", + @"title" : @"title", + @"subtitle" : @"subtitle" + }; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:[self class]]) { + Video *video = object; + return [video.videoID isEqualToString:self.videoID]; + } + + return [super isEqual:object]; +} + +- (NSUInteger)hash +{ + return self.videoID.hash; +} + +@end + diff --git a/Artsy/Classes/Models/VideoContentLink.h b/Artsy/Classes/Models/VideoContentLink.h new file mode 100644 index 00000000000..3de3556fccd --- /dev/null +++ b/Artsy/Classes/Models/VideoContentLink.h @@ -0,0 +1,5 @@ +#import "ContentLink.h" + +@interface VideoContentLink : ContentLink + +@end diff --git a/Artsy/Classes/Models/VideoContentLink.m b/Artsy/Classes/Models/VideoContentLink.m new file mode 100644 index 00000000000..97cf21acec0 --- /dev/null +++ b/Artsy/Classes/Models/VideoContentLink.m @@ -0,0 +1,23 @@ +#import "VideoContentLink.h" + +@implementation VideoContentLink + +- (CGFloat)aspectRatio { + if (!self.thumbnailHeight || !self.thumbnailWidth) { + return 1; + } + return (CGFloat)self.thumbnailWidth/self.thumbnailHeight; +} + +- (CGSize)maxSize { + if (!self.thumbnailHeight || !self.thumbnailWidth) { + return CGSizeZero; + } + return CGSizeMake(self.thumbnailWidth, self.thumbnailHeight); +} + +- (NSURL *)urlForThumbnail { + return [NSURL URLWithString: self.thumbnailUrl]; +} + +@end diff --git a/Artsy/Classes/Networking/ARAuthProviders.h b/Artsy/Classes/Networking/ARAuthProviders.h new file mode 100644 index 00000000000..214093085ed --- /dev/null +++ b/Artsy/Classes/Networking/ARAuthProviders.h @@ -0,0 +1,11 @@ +#import +typedef NS_ENUM(NSInteger, ARAuthProviderType) { + ARAuthProviderTwitter, + ARAuthProviderFacebook +}; + + +@interface ARAuthProviders : NSObject ++ (void)getReverseAuthTokenForTwitter:(void(^)(NSString *token, NSString *secret))success failure:(void(^)(NSError *error))failure; ++ (void)getTokenForFacebook:(void (^)(NSString *token, NSString *email, NSString *name))success failure:(void(^)(NSError *error))failure; +@end diff --git a/Artsy/Classes/Networking/ARAuthProviders.m b/Artsy/Classes/Networking/ARAuthProviders.m new file mode 100644 index 00000000000..ec7edb75849 --- /dev/null +++ b/Artsy/Classes/Networking/ARAuthProviders.m @@ -0,0 +1,76 @@ +#import "ARAuthProviders.h" +#import "ARNetworkConstants.h" +#import "AFOAuth1Client.h" +#import "FBSession.h" +#import "FBRequest.h" +#import "FBAccessTokenData.h" +#import +#import "ARAnalyticsConstants.h" +#import + +@implementation ARAuthProviders + ++ (void)getReverseAuthTokenForTwitter:(void(^)(NSString *token, NSString *secret))success failure:(void (^)(NSError *))failure +{ + NSParameterAssert(success); + AFOAuth1Client *client = nil; + client = [[AFOAuth1Client alloc] initWithBaseURL:[NSURL URLWithString:@"https://api.twitter.com/"] + key:[ArtsyKeys new].artsyTwitterKey + secret:[ArtsyKeys new].artsyTwitterSecret]; + + [client authorizeUsingOAuthWithRequestTokenPath:@"/oauth/request_token" + userAuthorizationPath:@"/oauth/authorize" + callbackURL:[NSURL URLWithString:ARTwitterCallbackPath] + accessTokenPath:@"/oauth/access_token" + accessMethod:@"POST" scope:nil + success:^(AFOAuth1Token *accessToken, id responseObject) { + success(accessToken.key, accessToken.secret); + } failure:^(NSError *error) { + if (failure) { + failure(error); + } + }]; + +} + ++ (void)getTokenForFacebook:(void (^)(NSString *token, NSString *email, NSString *name))success failure:(void (^)(NSError *))failure +{ + NSParameterAssert(success); + [FBSession openActiveSessionWithReadPermissions:@[@"public_profile", @"email"] + allowLoginUI:YES + completionHandler:^(FBSession *session, + FBSessionState status, + NSError *error) { + + // If we open a new session while the old one is active + // the old one calls this handler to let us know it's closed + // but guess what, Facebook? We don't care + + if (status == FBSessionStateClosed) { + return; + } + + NSString *token = [[session accessTokenData] accessToken]; + + if (!error && token) { + [[FBRequest requestForMe] + startWithCompletionHandler:^(FBRequestConnection *connection, NSDictionary *user, NSError *error) { + if (!error) { + NSString *email = user[@"email"]; + NSString *name = user[@"name"]; + success(token, email, name); + } else { + ARErrorLog(@"Couldn't get user info from Facebook"); + failure(error); + } + }]; + } else { + NSString *description = error ? [error description] : @"token was nil"; + [ARAnalytics event:ARAnalyticsErrorFailedToGetFacebookCredentials withProperties:@{ @"error" : description }]; + ARErrorLog(@"Couldn't get Facebook credentials"); + failure(error); + } + }]; +} + +@end diff --git a/Artsy/Classes/Networking/ARGeneArtworksNetworkModel.h b/Artsy/Classes/Networking/ARGeneArtworksNetworkModel.h new file mode 100644 index 00000000000..c7fb4d28643 --- /dev/null +++ b/Artsy/Classes/Networking/ARGeneArtworksNetworkModel.h @@ -0,0 +1,11 @@ +@class AREmbeddedModelsViewController; + +@interface ARGeneArtworksNetworkModel : NSObject + +- (id)initWithGene:(Gene *)gene; +- (void)getNextArtworkPage:(void (^)(NSArray *artworks))success; + +@property (nonatomic, strong, readonly) Gene *gene; +@property (nonatomic, assign, readonly) NSInteger currentPage; + +@end diff --git a/Artsy/Classes/Networking/ARGeneArtworksNetworkModel.m b/Artsy/Classes/Networking/ARGeneArtworksNetworkModel.m new file mode 100644 index 00000000000..bda0f4a0fb5 --- /dev/null +++ b/Artsy/Classes/Networking/ARGeneArtworksNetworkModel.m @@ -0,0 +1,45 @@ +#import "ARGeneArtworksNetworkModel.h" + +@interface ARGeneArtworksNetworkModel() +@property (nonatomic, assign) NSInteger currentPage; +@property (readwrite, nonatomic, assign) BOOL allDownloaded; +@property (readwrite, nonatomic, assign) BOOL downloadLock; +@end + +@implementation ARGeneArtworksNetworkModel + +- (id)initWithGene:(Gene *)gene +{ + self = [super init]; + if (!self) { return nil; } + + _gene = gene; + _currentPage = 1; + + return self; +} + +- (void)getNextArtworkPage:(void (^)(NSArray *artworks))success +{ + if (self.allDownloaded || self.downloadLock) { return; } + + self.downloadLock = YES; + + @weakify(self); + + [self.gene getArtworksAtPage:self.currentPage success:^(NSArray *artworks) { + @strongify(self); + if (!self) { return; } + + self.currentPage++; + self.downloadLock = NO; + + if (artworks.count == 0) { + self.allDownloaded = YES; + } + + if(success) { success(artworks); } + }]; +} + +@end diff --git a/Artsy/Classes/Networking/ARNetworkConstants.h b/Artsy/Classes/Networking/ARNetworkConstants.h new file mode 100644 index 00000000000..c1024d79731 --- /dev/null +++ b/Artsy/Classes/Networking/ARNetworkConstants.h @@ -0,0 +1,98 @@ +extern NSString *const ARBaseWebURL; +extern NSString *const ARBaseMobileWebURL; +extern NSString *const ARBaseApiURL; +extern NSString *const ARStagingBaseWebURL; +extern NSString *const ARStagingBaseMobileWebURL; +extern NSString *const ARStagingBaseApiURL; + +extern NSString *const ARPersonalizePath; + +extern NSString *const ARTwitterCallbackPath; + +extern NSString *const ARAuthHeader; +extern NSString *const ARXappHeader; +extern NSString *const ARTotalHeader; + +extern NSString *const AROAuthURL; +extern NSString *const ARXappURL; +extern NSString *const ARCreateUserURL; +extern NSString *const ARForgotPasswordURL; + +extern NSString *const ARAlbumsURL; +extern NSString *const ARArtworkInformationURLFormat; +extern NSString *const ARAdditionalImagesURLFormat; +extern NSString *const ARMyInfoURL; +extern NSString *const ARMyFeedURL; + +extern NSString *const ARNewArtworksURL; +extern NSString *const ARNewArtworkInfoURLFormat; +extern NSString *const ARNewRelatedArtworksURLFormat; +extern NSString *const ARArtworkComparablesURLFormat; +extern NSString *const ARAddArtworkToFavoritesURLFormat; +extern NSString *const ARFavoritesURL; +extern NSString *const ARSalesForArtworkURL; +extern NSString *const ARMyBiddersURL; +extern NSString *const ARBidderPositionsForSaleAndArtworkURL; +extern NSString *const ARSaleArtworkForSaleAndArtworkURLFormat; +extern NSString *const ARSaleArtworksURLFormat; +extern NSString *const ARArtworkFairsURLFormat; + +extern NSString *const ARRelatedShowsURL; +extern NSString *const ARShowsFeaturingArtistsURLFormat; + +extern NSString *const ARArtistArtworksURLFormat; +extern NSString *const ARArtistInformationURLFormat; +extern NSString *const ARFollowArtistURL; +extern NSString *const ARFollowArtistsURL; +extern NSString *const ARUnfollowArtistURLFormat; +extern NSString *const ARSampleArtistsURL; + +extern NSString *const ARFollowProfileURL; +extern NSString *const ARUnfollowProfileURLFormat; +extern NSString *const ARFollowProfilesURL; +extern NSString *const ARFollowingProfileURLFormat; + +extern NSString *const ARRelatedArtistsURL; + +extern NSString *const ARGeneArtworksURLFormat; +extern NSString *const ARGeneInformationURLFormat; +extern NSString *const ARFollowGeneURL; +extern NSString *const ARFollowGenesURL; +extern NSString *const ARUnfollowGeneURLFormat; + +extern NSString *const ARProfileFeedURLFormat; +extern NSString *const ARPostInformationURLFormat; +extern NSString *const ARProfileInformationURLFormat; + +extern NSString *const ARNewSearchURL; +extern NSString *const ARNewArtistSearchURL; + +extern NSString *const ARShowFeedURL; +extern NSString *const ARShowArtworksURLFormat; +extern NSString *const ARShowInformationURLFormat; +extern NSString *const ARShowImagesURLFormat; + +extern NSString *const ARSiteHeroUnitsURL; + +extern NSString *const AROnDutyRepresentativesURL; +extern NSString *const ARArtworkInquiryRequestURL; + +extern NSString *const AROrderedSetsURL; +extern NSString *const AROrderedSetItemsURLFormat; + +extern NSString *const ARSiteFeaturesURL; + +extern NSString *const ARMyDevicesURL; +extern NSString *const ARSiteUpURL; + +extern NSString *const ARProfilePostsURLFormat; + +extern NSString *const ARNewFairInfoURLFormat; +extern NSString *const ARNewFairShowsURLFormat; +extern NSString *const ARNewFairMapURLFormat; + +extern NSString *const ARNewRelatedPostsURL; + +extern NSString *const ARSystemTimeURL; + +extern NSString *const ARCreatePendingOrderURL; diff --git a/Artsy/Classes/Networking/ARNetworkConstants.m b/Artsy/Classes/Networking/ARNetworkConstants.m new file mode 100644 index 00000000000..c1042e8eef7 --- /dev/null +++ b/Artsy/Classes/Networking/ARNetworkConstants.m @@ -0,0 +1,105 @@ +NSString *const ARBaseWebURL = @"https://artsy.net/"; +NSString *const ARBaseMobileWebURL = @"https://m.artsy.net/"; +NSString *const ARBaseApiURL = @"https://api.artsy.net/"; + +NSString *const ARStagingBaseWebURL = @"http://staging.artsy.net/"; +NSString *const ARStagingBaseMobileWebURL = @"http://m-staging.artsy.net/"; +NSString *const ARStagingBaseApiURL = @"https://stagingapi.artsy.net/"; + +NSString *const ARPersonalizePath = @"personalize"; + +NSString *const ARTwitterCallbackPath = @"artsy://twitter-callback"; + +NSString *const ARAuthHeader = @"X-Access-Token"; +NSString *const ARXappHeader = @"X-Xapp-Token"; +NSString *const ARTotalHeader = @"X-Total-Count"; + +NSString *const AROAuthURL = @"/oauth2/access_token"; +NSString *const ARXappURL = @"/api/v1/xapp_token"; + +NSString *const ARCreateUserURL = @"/api/v1/user"; + +NSString *const ARForgotPasswordURL = @"/api/v1/users/send_reset_password_instructions"; + +NSString *const ARAlbumsURL = @"/api/v1/me/albums"; +NSString *const ARAdditionalImagesURLFormat = @"/api/v1/artwork/%@/images"; + +NSString *const ARMyInfoURL = @"/api/v1/me"; +NSString *const ARMyFeedURL = @"/api/v1/me/feed"; + +NSString *const ARNewArtworksURL = @"/api/v1/artworks/new"; +NSString *const ARNewArtworkInfoURLFormat = @"/api/v1/artwork/%@"; +NSString *const ARNewRelatedArtworksURLFormat = @"/api/v1/related/layer/%@/%@/artworks"; +NSString *const ARArtworkComparablesURLFormat = @"/api/v1/artwork/%@/comparable_sales"; +NSString *const ARAddArtworkToFavoritesURLFormat = @"/api/v1/collection/saved-artwork/artwork/%@"; +NSString *const ARFavoritesURL = @"/api/v1/collection/saved-artwork/artworks"; +NSString *const ARSalesForArtworkURL = @"/api/v1/related/sales"; +NSString *const ARMyBiddersURL = @"/api/v1/me/bidders"; +NSString *const ARBidderPositionsForSaleAndArtworkURL = @"/api/v1/me/bidder_positions"; +NSString *const ARSaleArtworkForSaleAndArtworkURLFormat = @"/api/v1/sale/%@/sale_artwork/%@"; +NSString *const ARSaleArtworksURLFormat = @"/api/v1/sale/%@/sale_artworks"; +NSString *const ARArtworkFairsURLFormat = @"/api/v1/related/fairs"; + +NSString *const ARRelatedShowsURL = @"/api/v1/related/shows"; + +NSString *const ARArtistArtworksURLFormat = @"/api/v1/artist/%@/artworks"; +NSString *const ARArtistInformationURLFormat = @"/api/v1/artist/%@"; + +NSString *const ARRelatedArtistsURL = @"/api/v1/related/artists"; +NSString *const ARSampleArtistsURL = @"/api/v1/artists/sample"; + +NSString *const ARFollowArtistURL = @"/api/v1/me/follow/artist"; +NSString *const ARUnfollowArtistURLFormat = @"/api/v1/me/follow/artist/%@"; +NSString *const ARFollowArtistsURL = @"/api/v1/me/follow/artists"; + +NSString *const ARFollowGeneURL = @"/api/v1/me/follow/gene"; +NSString *const ARUnfollowGeneURLFormat = @"/api/v1/me/follow/gene/%@"; +NSString *const ARFollowGenesURL = @"/api/v1/me/follow/genes"; + +NSString *const ARFollowProfileURL = @"/api/v1/me/follow/profile"; +NSString *const ARFollowingProfileURLFormat = @"/api/v1/me/follow/profile/%@"; +NSString *const ARUnfollowProfileURLFormat = @"/api/v1/me/follow/profile/%@"; +NSString *const ARFollowProfilesURL = @"/api/v1/me/follow/profiles"; + +NSString *const ARProfileFeedURLFormat = @"/api/v1/profile/%@/posts"; + +NSString *const ARGeneInformationURLFormat = @"/api/v1/gene/%@"; +NSString *const ARArtworkInformationURLFormat = @"/api/v1/artwork/%@"; +NSString *const ARPostInformationURLFormat = @"/api/v1/posts/%@"; +NSString *const ARProfileInformationURLFormat = @"/api/v1/profile/%@"; + +NSString *const ARShowFeedURL = @"/api/v1/shows/feed"; +NSString *const ARShowInformationURLFormat = @"/api/v1/show/%@"; + +NSString *const ARNewSearchURL = @"/api/v1/match"; +NSString *const ARNewArtistSearchURL = @"/api/v1/match/artists"; + +NSString *const ARSiteHeroUnitsURL = @"/api/v1/site_hero_units"; + +NSString *const AROnDutyRepresentativesURL = @"/api/v1/admins/available_representatives"; +NSString *const ARArtworkInquiryRequestURL = @"/api/v1/me/artwork_inquiry_request"; + +NSString *const ARGeneArtworksURLFormat = @"/api/v1/search/filtered/gene/%@"; +NSString *const ARShowArtworksURLFormat = @"/api/v1/partner/%@/show/%@/artworks"; +NSString *const ARShowImagesURLFormat = @"/api/v1/partner_show/%@/images"; + +NSString *const AROrderedSetsURL = @"/api/v1/sets"; +NSString *const AROrderedSetItemsURLFormat = @"/api/v1/set/%@/items"; + +NSString *const ARSiteFeaturesURL = @"/api/v1/site_features/"; + +NSString *const ARMyDevicesURL = @"/api/v1/me/device"; +NSString *const ARSiteUpURL = @"/api/v1/system/up"; + +NSString *const ARProfilePostsURLFormat = @"/api/v1/profile/%@/posts"; + +NSString *const ARNewFairInfoURLFormat = @"/api/v1/fair/%@"; +NSString *const ARNewFairShowsURLFormat = @"/api/v1/fair/%@/shows"; +NSString *const ARNewFairMapURLFormat = @"/api/v1/maps?fair_id=%@"; + +NSString *const ARNewRelatedPostsURL = @"/api/v1/related/posts"; +NSString *const ARShowsFeaturingArtistsURLFormat = @"/api/v1/fair/%@/shows"; + +NSString *const ARSystemTimeURL = @"/api/v1/system/time"; + +NSString *const ARCreatePendingOrderURL = @"/api/v1/me/order/pending/items"; diff --git a/Artsy/Classes/Networking/ARRouter.h b/Artsy/Classes/Networking/ARRouter.h new file mode 100644 index 00000000000..0214099fb74 --- /dev/null +++ b/Artsy/Classes/Networking/ARRouter.h @@ -0,0 +1,138 @@ +// TODO: This file needs some literal cleanup, spacing documentation etc. + +@interface ARRouter : NSObject + ++ (void)setup; ++ (NSSet *)artsyHosts; + ++ (NSURL *)baseWebURL; + ++ (AFHTTPClient *)httpClient; + ++ (BOOL)isInternalURL:(NSURL *)url; ++ (BOOL)isWebURL:(NSURL *)url; + ++ (void)setAuthToken:(NSString *)token; ++ (void)setXappToken:(NSString *)token; + ++ (NSURLRequest *)requestForURL:(NSURL *)url; + ++ (NSURLRequest *)newOAuthRequestWithUsername:(NSString *)username password:(NSString *)password; ++ (NSURLRequest *)newTwitterOAuthRequestWithToken:(NSString *)token andSecret:(NSString *)secret; ++ (NSURLRequest *)newFacebookOAuthRequestWithToken:(NSString *)token; + ++ (NSURLRequest *)newUserInfoRequest; ++ (NSURLRequest *)newUserEditRequestWithParams:(NSDictionary *)params; ++ (NSURLRequest *)newXAppTokenRequest; + ++ (NSURLRequest *)newCreateUserRequestWithName:(NSString *)name email:(NSString *)email password:(NSString *)password; ++ (NSURLRequest *)newCreateUserViaFacebookRequestWithToken:(NSString *)token email:(NSString *)email name:(NSString *)name; ++ (NSURLRequest *)newCreateUserViaTwitterRequestWithToken:(NSString *)token secret:(NSString *)secret email:(NSString *)email name:(NSString *)name; + ++ (NSURLRequest *)newCheckFollowingProfileHeadRequest:(NSString *)profileID; + ++ (NSURLRequest *)newMyFollowProfileRequest:(NSString *)profileID; ++ (NSURLRequest *)newMyUnfollowProfileRequest:(NSString *)profileID; ++ (NSURLRequest *)newFollowingProfilesRequestWithFair:(Fair *)fair; + ++ (NSURLRequest *)newFeedRequestWithCursor:(NSString *)cursor pageSize:(NSInteger)size; ++ (NSURLRequest *)newShowFeedRequestWithCursor:(NSString *)cursor pageSize:(NSInteger)size; + ++ (NSURLRequest *)newPostsRequestForProfileID:(NSString *)profileID WithCursor:(NSString *)cursor pageSize:(NSInteger)size; ++ (NSURLRequest *)newPostsRequestForProfile:(Profile *)profile WithCursor:(NSString *)cursor pageSize:(NSInteger)size; ++ (NSURLRequest *)newPostsRequestForFairOrganizer:(FairOrganizer *)fairOrganizer WithCursor:(NSString *)cursor pageSize:(NSInteger)size; + ++ (NSURLRequest *)newArtworkInfoRequestForArtworkID:(NSString *)artworkID; ++ (NSURLRequest *)newNewArtworksRequestWithParams:(NSDictionary *)params; ++ (NSURLRequest *)newArtworksRelatedToArtworkRequest:(Artwork *)artwork; ++ (NSURLRequest *)newArtworksRelatedToArtwork:(Artwork *)artwork inFairRequest:(Fair *)fair; ++ (NSURLRequest *)newArtworkComparablesRequest:(Artwork *)artwork; + ++ (NSURLRequest *)newArtworkFavoritesRequestWithFair:(Fair *)fair; ++ (NSURLRequest *)newSetArtworkFavoriteRequestForArtwork:(Artwork *)artwork status:(BOOL)status; + ++ (NSURLRequest *)newArtworksFromUsersFavoritesRequestWithID:(NSString *)userID page:(NSInteger)page; ++ (NSURLRequest *)newCheckFavoriteStatusRequestForArtwork:(Artwork *)artwork; ++ (NSURLRequest *)newCheckFavoriteStatusRequestForArtworks:(NSArray *)artworks; ++ (NSURLRequest *)newFairsRequestForArtwork:(Artwork *)artwork; ++ (NSURLRequest *)newShowsRequestForArtworkID:(NSString *)artworkID andFairID:(NSString *)fairID; + ++ (NSURLRequest *)newArtistsFromPersonalCollectionAtPage:(NSInteger)page; ++ (NSURLRequest *)newArtistCountFromPersonalCollectionRequest; ++ (NSURLRequest *)newArtistArtworksRequestWithParams:(NSDictionary *)params andArtistID:(NSString *)artistID; ++ (NSURLRequest *)newArtistInfoRequestWithID:(NSString *)artistID; ++ (NSURLRequest *)newFollowingArtistsRequestWithFair:(Fair *)fair; ++ (NSURLRequest *)newFollowingRequestForArtist:(Artist *)artists; ++ (NSURLRequest *)newFollowingRequestForArtists:(NSArray *)artists; ++ (NSURLRequest *)newFollowArtistRequest:(Artist *)artist; ++ (NSURLRequest *)newUnfollowArtistRequest:(Artist *)artist; ++ (NSURLRequest *)newArtistsRelatedToArtistRequest:(Artist *)artist; ++ (NSURLRequest *)newShowsRequestForArtist:(NSString *)artistID; ++ (NSURLRequest *)newArtistsFromSampleAtPage:(NSInteger)page; + ++ (NSURLRequest *)newSearchRequestWithQuery:(NSString *)query; ++ (NSURLRequest *)newSearchRequestWithFairID:(NSString *)fairID andQuery:(NSString *)query; ++ (NSURLRequest *)newArtistSearchRequestWithQuery:(NSString *)query; + ++ (NSURLRequest *)directImageRequestForModel:(Class)model andSlug:(NSString *)slug; + ++ (NSURLRequest *)newPostInfoRequestWithID:(NSString *)postID; ++ (NSURLRequest *)newProfileInfoRequestWithID:(NSString *)profileID; + ++ (NSURLRequest *)newSiteHeroUnitsRequest; ++ (NSURLRequest *)newOnDutyRepresentativeRequest; + ++ (NSURLRequest *)newArtworkInquiryRequestForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + analyticsDictionary:(NSDictionary *)analyticsDictionary + shouldContactGallery:(BOOL)contactGallery; + ++ (NSURLRequest *)newGenesFromPersonalCollectionAtPage:(NSInteger)page; ++ (NSURLRequest *)newGeneCountFromPersonalCollectionRequest; ++ (NSURLRequest *)newGeneInfoRequestWithID:(NSString *)geneID; + ++ (NSURLRequest *)newFollowingRequestForGene:(Gene *)gene; ++ (NSURLRequest *)newFollowingRequestForGenes:(NSArray *)genes; + ++ (NSURLRequest *)newFollowGeneRequest:(Gene *)gene; ++ (NSURLRequest *)newUnfollowGeneRequest:(Gene *)gene; ++ (NSURLRequest *)newArtworksFromGeneRequest:(NSString *)gene atPage:(NSInteger)page; + ++ (NSURLRequest *)newArtworksFromShowRequest:(PartnerShow *)show atPage:(NSInteger)page; ++ (NSURLRequest *)newImagesFromShowRequest:(PartnerShow *)show atPage:(NSInteger)page; ++ (NSURLRequest *)newShowInfoRequestWithID:(NSString *)showID; ++ (NSURLRequest *)newShowsRequestForArtistID:(NSString *)artistID inFairID:(NSString *)fairID; + ++ (NSURLRequest *)newForgotPasswordRequestWithEmail:(NSString *)email; ++ (NSURLRequest *)newSiteFeaturesRequest; + ++ (NSURLRequest *)newSetDeviceAPNTokenRequest:(NSString *)token forDevice:(NSString *)device; +/// We don't actually use this anywhere but it's good for diagnosing network problems ++ (NSURLRequest *)newUptimeURLRequest; + ++ (NSURLRequest *)salesWithArtworkRequest:(NSString *)artworkID; ++ (NSURLRequest *)artworksForSaleRequest:(NSString *)saleID; + ++ (NSURLRequest *)biddersRequest; ++ (NSURLRequest *)bidderPositionsRequestForSaleID:(NSString *)saleID artworkID:(NSString *)artworkID; ++ (NSURLRequest *)saleArtworkRequestForSaleID:(NSString *)saleID artworkID:(NSString *)artworkID; + ++ (NSURLRequest *)newFairInfoRequestWithID:(NSString *)fairID; ++ (NSURLRequest *)newFairShowsRequestWithFair:(Fair *)fair; ++ (NSURLRequest *)newFairMapRequestWithFair:(Fair *)fair; ++ (NSURLRequest *)newFairShowFeedRequestWithFair:(Fair *)fair partnerID:(NSString *)partnerID cursor:(NSString *)cursor pageSize:(NSInteger)size; + ++ (NSURLRequest *)orderedSetsWithOwnerType:(NSString *)ownerType andID:(NSString *)ownerID; ++ (NSURLRequest *)orderedSetsWithKey:(NSString *)key; ++ (NSURLRequest *)orderedSetItems:(NSString *)orderedSetID; ++ (NSURLRequest *)orderedSetItems:(NSString *)orderedSetID atPage:(NSInteger)page; + ++ (NSURLRequest *)newPostsRelatedToArtwork:(Artwork *)artwork; ++ (NSURLRequest *)newPostsRelatedToArtist:(Artist *)artist; + ++ (NSURLRequest *)newSystemTimeRequest; + ++ (NSURLRequest *)newPendingOrderWithArtworkID:(NSString *)artworkID; +@end diff --git a/Artsy/Classes/Networking/ARRouter.m b/Artsy/Classes/Networking/ARRouter.m new file mode 100644 index 00000000000..d632cb49a15 --- /dev/null +++ b/Artsy/Classes/Networking/ARRouter.m @@ -0,0 +1,848 @@ +#import "ARNetworkConstants.h" +#import "ARRouter.h" +#import "ARUserManager.h" +#import +#import + +static AFHTTPClient *staticHTTPClient = nil; +static NSSet *artsyHosts = nil; + +@implementation ARRouter + ++ (void)setup +{ + artsyHosts = [NSSet setWithObjects:@"art.sy", @"artsyapi.com", @"artsy.net", @"m.artsy.net", @"staging.artsy.net", @"m-staging.artsy.net", nil]; + + [ARRouter setupWithBaseApiURL:[ARRouter baseApiURL]]; + + [self setupUserAgent]; +} + ++ (NSSet *)artsyHosts +{ + return artsyHosts; +} + ++ (NSURL *)baseApiURL +{ + if ([AROptions boolForOption:ARUseStagingDefault]) { + return [NSURL URLWithString:ARStagingBaseApiURL]; + } else { + return [NSURL URLWithString:ARBaseApiURL]; + } +} + ++ (NSURL *)baseWebURL +{ + if ([AROptions boolForOption:ARUseStagingDefault]) { + return [NSURL URLWithString:[UIDevice isPad] ? ARStagingBaseWebURL : ARStagingBaseMobileWebURL]; + } else { + return [NSURL URLWithString:[UIDevice isPad] ? ARBaseWebURL : ARBaseMobileWebURL]; + } +} + ++ (void)setupWithBaseApiURL:(NSURL *)baseApiURL +{ + staticHTTPClient = [AFHTTPClient clientWithBaseURL:baseApiURL]; + + [staticHTTPClient setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { + switch (status) { + case AFNetworkReachabilityStatusUnknown: + break; // do nothing + case AFNetworkReachabilityStatusNotReachable: + [[NSNotificationCenter defaultCenter] postNotificationName:ARNetworkUnavailableNotification object:nil]; + break; + default: + [[NSNotificationCenter defaultCenter] postNotificationName:ARNetworkAvailableNotification object:nil]; + break; + } + }]; + + // Ensure the keychain is empty incase you've uninstalled and cleared user data + if (![[ARUserManager sharedManager] hasExistingAccount]) { + [[ARUserManager sharedManager] logout]; + } + + NSString *token = [UICKeyChainStore stringForKey:AROAuthTokenDefault]; + if(token) { + ARActionLog(@"Found OAuth token in keychain"); + [ARRouter setAuthToken:token]; + + } else { + ARActionLog(@"Found trial XApp token in keychain"); + NSString *xapp = [UICKeyChainStore stringForKey:ARXAppTokenDefault]; + [ARRouter setXappToken:xapp]; + } +} + ++ (void)setupUserAgent +{ + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + NSString *build = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; + NSString *header = [staticHTTPClient defaultValueForHeader:@"User-Agent"]; + NSString *agentString = [NSString stringWithFormat:@"Artsy-Mobile/%@ Eigen/%@", version, build]; + NSString *userAgent = [header stringByReplacingOccurrencesOfString:@"Artsy" withString:agentString]; + + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"UserAgent" : userAgent } ]; + [staticHTTPClient setDefaultHeader:@"User-Agent" value:userAgent]; +} + ++ (BOOL)isWebURL:(NSURL *)url +{ + return (!url.scheme || ([url.scheme isEqual:@"http"] || [url.scheme isEqual:@"https"])); +} + ++ (BOOL)isInternalURL:(NSURL *)url +{ + // Is it a touch link? + if ([url.scheme isEqual:@"applewebdata"] || [url.scheme isEqual:@"artsy"]) { + return YES; + } + + if (![self isWebURL:url]) { + return NO; + } + + NSString *host = url.host; + if ([host hasPrefix:@"www"]) { + host = [host substringFromIndex:4]; + } + //if there's no host, we'll assume it's relative + return (!host || (host && [artsyHosts containsObject:host])); +} + ++ (NSURLRequest *)requestForURL:(NSURL *)url +{ + NSMutableURLRequest *request = [staticHTTPClient requestWithMethod:@"GET" path:[url absoluteString] parameters:nil]; + if (![ARRouter isInternalURL:url]) { + [request setValue:nil forHTTPHeaderField:ARAuthHeader]; + [request setValue:nil forHTTPHeaderField:ARXappHeader]; + } + + return request; +} + ++ (AFHTTPClient *)httpClient +{ + return staticHTTPClient; +} + +#pragma mark - +#pragma mark OAuth + ++ (void)setAuthToken:(NSString *)token +{ + [staticHTTPClient setDefaultHeader:ARAuthHeader value:token]; +} + ++ (NSURLRequest *)newOAuthRequestWithUsername:(NSString *)username password:(NSString *)password +{ + NSDictionary *params = @{ + @"email" : username, + @"password" : password, + @"client_id" : [ArtsyKeys new].artsyAPIClientKey, + @"client_secret" : [ArtsyKeys new].artsyAPIClientSecret, + @"grant_type" : @"credentials", + @"scope" : @"offline_access" + }; + return [staticHTTPClient requestWithMethod:@"GET" path:AROAuthURL parameters:params]; +} + ++ (NSURLRequest *)newFacebookOAuthRequestWithToken:(NSString *)token +{ + NSDictionary *params = @{ + @"oauth_provider" : @"facebook", + @"oauth_token" : token, + @"client_id" : [ArtsyKeys new].artsyAPIClientKey, + @"client_secret" : [ArtsyKeys new].artsyAPIClientSecret, + @"grant_type" : @"oauth_token", + @"scope" : @"offline_access" + }; + return [staticHTTPClient requestWithMethod:@"GET" path:AROAuthURL parameters:params]; +} + ++ (NSURLRequest *)newTwitterOAuthRequestWithToken:(NSString *)token andSecret:(NSString *)secret +{ + NSDictionary *params = @{ + @"oauth_provider" : @"twitter", + @"oauth_token" : token, + @"oauth_token_secret" : secret, + @"client_id" : [ArtsyKeys new].artsyAPIClientKey, + @"client_secret" : [ArtsyKeys new].artsyAPIClientSecret, + @"grant_type" : @"oauth_token", + @"scope" : @"offline_access" + }; + return [staticHTTPClient requestWithMethod:@"GET" path:AROAuthURL parameters:params]; +} + + + +#pragma mark - +#pragma mark XApp + ++ (void)setXappToken:(NSString *)token +{ + [staticHTTPClient setDefaultHeader:ARXappHeader value:token]; +} + ++ (NSURLRequest *)newXAppTokenRequest +{ + NSDictionary *params = @{ + @"client_id" : [ArtsyKeys new].artsyAPIClientKey, + @"client_secret" : [ArtsyKeys new].artsyAPIClientSecret, + }; + return [staticHTTPClient requestWithMethod:@"GET" path:ARXappURL parameters:params]; + +} + +#pragma mark - +#pragma mark User creation + ++ (NSURLRequest *)newCreateUserRequestWithName:(NSString *)name + email:(NSString *)email + password:(NSString *)password +{ + NSDictionary *params = @{ + @"email" : email, + @"password" : password, + @"name" : name + }; + return [staticHTTPClient requestWithMethod:@"POST" path:ARCreateUserURL parameters:params]; +} + ++ (NSURLRequest *)newCreateUserViaFacebookRequestWithToken:(NSString *)token email:(NSString *)email name:(NSString *)name +{ + NSDictionary *params = @{ + @"provider": @"facebook", + @"oauth_token": token, + @"email" : email, + @"name" : name + }; + + return [staticHTTPClient requestWithMethod:@"POST" path:ARCreateUserURL parameters:params]; +} + ++ (NSURLRequest *)newCreateUserViaTwitterRequestWithToken:(NSString *)token secret:(NSString *)secret email:(NSString *)email name:(NSString *)name +{ + NSDictionary *params = @{ + @"provider": @"twitter", + @"oauth_token": token, + @"oauth_token_secret": secret, + @"email" : email, + @"name" : name + }; + + return [staticHTTPClient requestWithMethod:@"POST" path:ARCreateUserURL parameters:params]; + +} + +#pragma mark - +#pragma mark User + ++ (NSURLRequest *)newUserInfoRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARMyInfoURL parameters:nil]; +} + ++ (NSURLRequest *)newUserEditRequestWithParams:(NSDictionary *)params +{ + return [staticHTTPClient requestWithMethod:@"PUT" path:ARMyInfoURL parameters:params]; +} + ++ (NSURLRequest *)newCheckFollowingProfileHeadRequest:(NSString *)profileID +{ + NSString *path = NSStringWithFormat(ARFollowingProfileURLFormat, profileID); + return [staticHTTPClient requestWithMethod:@"GET" path:path parameters:nil]; +} + ++ (NSURLRequest *)newMyFollowProfileRequest:(NSString *)profileID +{ + return [staticHTTPClient requestWithMethod:@"POST" path:ARFollowProfileURL parameters:@{ @"profile_id" : profileID }]; +} + ++ (NSURLRequest *)newMyUnfollowProfileRequest:(NSString *)profileID +{ + NSString *path = NSStringWithFormat(ARUnfollowProfileURLFormat, profileID); + return [staticHTTPClient requestWithMethod:@"DELETE" path:path parameters:@{ @"profile_id" : profileID }]; +} + ++ (NSURLRequest *)newFollowingProfilesRequestWithFair:(Fair *)fair +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowProfilesURL parameters:@{ @"fair_id": fair.fairID }]; +} + +#pragma mark - +#pragma mark Feed + ++ (NSURLRequest *)newFeedRequestWithCursor:(NSString *)cursor pageSize:(NSInteger)size +{ + if (!cursor) { cursor = @""; } + return [staticHTTPClient requestWithMethod:@"GET" path:ARMyFeedURL parameters:@{@"cursor" : cursor, @"size" : @(size)}]; +} + ++ (NSURLRequest *)newShowFeedRequestWithCursor:(NSString *)cursor pageSize:(NSInteger)size +{ + NSMutableDictionary *params = [@{ + @"size" : @(size), + @"feed" : @"shows" + } mutableCopy]; + + if (cursor) [params setObject:cursor forKey:@"cursor"]; + + return [staticHTTPClient requestWithMethod:@"GET" path:ARShowFeedURL parameters:params]; +} + ++ (NSURLRequest *)newFairShowFeedRequestWithFair:(Fair *)fair partnerID:(NSString *)partnerID cursor:(NSString *)cursor pageSize:(NSInteger)size +{ + NSMutableDictionary *params = [@{ @"size" : @(size) } mutableCopy]; + if (cursor) [params setObject:cursor forKey:@"cursor"]; + if (partnerID) [params setObject:partnerID forKey:@"partner"]; + + NSString *path = NSStringWithFormat(ARNewFairShowsURLFormat, fair.fairID); + return [staticHTTPClient requestWithMethod:@"GET" path:path parameters:params]; +} + ++ (NSURLRequest *)newPostsRequestForProfileID:(NSString *)profileID WithCursor:(NSString *)cursor pageSize:(NSInteger)size +{ + NSString *url = [NSString stringWithFormat:ARProfileFeedURLFormat, profileID]; + NSMutableDictionary *params = [@{ @"size" : @(size) } mutableCopy]; + if (cursor) [params setObject:cursor forKey:@"cursor"]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:params]; +} + ++ (NSURLRequest *)newPostsRequestForProfile:(Profile *)profile WithCursor:(NSString *)cursor pageSize:(NSInteger)size +{ + return [ARRouter newPostsRequestForProfileID:profile.profileID WithCursor:cursor pageSize:size]; +} + ++ (NSURLRequest *)newPostsRequestForFairOrganizer:(FairOrganizer *)fairOrganizer WithCursor:(NSString *)cursor pageSize:(NSInteger)size +{ + return [ARRouter newPostsRequestForProfileID:fairOrganizer.profileID WithCursor:cursor pageSize:size]; +} + +#pragma mark - +#pragma mark Artworks + ++ (NSURLRequest *)newArtworkInfoRequestForArtworkID:(NSString *)artworkID +{ + NSString *address = [NSString stringWithFormat:ARNewArtworkInfoURLFormat, artworkID]; + return [staticHTTPClient requestWithMethod:@"GET" path:address parameters:nil]; +} + ++ (NSURLRequest *)newArtworksRelatedToArtworkRequest:(Artwork *)artwork +{ + NSDictionary *params = @{ @"artwork" : @[artwork.artworkID] }; + NSString *address = [NSString stringWithFormat:ARNewRelatedArtworksURLFormat, @"synthetic", @"main"]; + return [staticHTTPClient requestWithMethod:@"GET" path:address parameters:params]; +} + ++ (NSURLRequest *)newArtworksRelatedToArtwork:(Artwork *)artwork inFairRequest:(Fair *)fair +{ + NSDictionary *params = @{@"artwork" : @[artwork.artworkID]}; + NSString *address = [NSString stringWithFormat:ARNewRelatedArtworksURLFormat, @"fair", fair.fairID]; + return [staticHTTPClient requestWithMethod:@"GET" path:address parameters:params]; +} + ++ (NSURLRequest *)newPostsRelatedToArtwork:(Artwork *)artwork +{ + NSDictionary *params = @{@"artwork" : @[artwork.artworkID]}; + return [staticHTTPClient requestWithMethod:@"GET" path:ARNewRelatedPostsURL parameters:params]; +} + ++ (NSURLRequest *)newPostsRelatedToArtist:(Artist *)artist +{ + NSDictionary *params = @{@"artist" : @[artist.artistID]}; + return [staticHTTPClient requestWithMethod:@"GET" path:ARNewRelatedPostsURL parameters:params]; +} + ++ (NSURLRequest *)newArtworkComparablesRequest:(Artwork *)artwork +{ + NSString *address = [NSString stringWithFormat:ARArtworkComparablesURLFormat, artwork.artworkID]; + return [staticHTTPClient requestWithMethod:@"GET" path:address parameters:nil]; +} + ++ (NSURLRequest *)newAdditionalImagesRequestForArtworkWithID:(NSString *)artworkID +{ + NSString *url = [NSString stringWithFormat:ARAdditionalImagesURLFormat, artworkID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newNewArtworksRequestWithParams:(NSDictionary *)params +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARNewArtworksURL parameters:params]; +} + ++ (NSURLRequest *)newArtistArtworksRequestWithParams:(NSDictionary *)params andArtistID:(NSString *)artistID +{ + NSString *url = [NSString stringWithFormat:ARArtistArtworksURLFormat, artistID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:params]; +} + +#pragma mark - +#pragma mark Artwork Favorites (items in the saved-artwork collection) + ++ (NSURLRequest *)newArtworkFavoritesRequestWithFair:(Fair *)fair +{ + NSDictionary *params = @{ + @"fair_id": fair.fairID, + @"user_id": [User currentUser].userID ?: @"", + @"private": @YES + }; + + NSMutableURLRequest *request = [staticHTTPClient requestWithMethod:@"GET" path:ARFavoritesURL parameters:params]; + + request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + + return request; +} + ++ (NSURLRequest *)newSetArtworkFavoriteRequestForArtwork:(Artwork *)artwork status:(BOOL)status +{ + NSString *method = status ? @"POST" : @"DELETE"; + NSString *url = [NSString stringWithFormat:ARAddArtworkToFavoritesURLFormat, artwork.artworkID]; + return [staticHTTPClient requestWithMethod:method + path:url + parameters:@{ @"user_id" : [User currentUser].userID ?: @"" }]; +} + + ++ (NSURLRequest *)newArtworksFromUsersFavoritesRequestWithID:(NSString *)userID page:(NSInteger)page +{ + NSDictionary *params = @{ + @"size" : @15, + @"page": @(page), + @"sort": @"-position", + @"total_count": @1, + @"user_id": userID ?: @"", + @"private" : ARIsRunningInDemoMode ? @"false" :@"true" + }; + + return [staticHTTPClient requestWithMethod:@"GET" path:ARFavoritesURL parameters:params]; +} + ++ (NSURLRequest *)newCheckFavoriteStatusRequestForArtwork:(Artwork *)artwork +{ + return [self newCheckFavoriteStatusRequestForArtworks:@[artwork]]; +} + ++ (NSURLRequest *)newCheckFavoriteStatusRequestForArtworks:(NSArray *)artworks +{ + NSArray *slugs = [artworks map: ^(Artwork *artwork) { + return artwork.artworkID; + }]; + + NSDictionary *params = @{ + @"artworks":slugs, + @"user_id" : [User currentUser].userID ?: @"", + @"private" : @"true" + }; + return [staticHTTPClient requestWithMethod:@"GET" path:ARFavoritesURL parameters:params]; +} + ++ (NSURLRequest *)newFairsRequestForArtwork:(Artwork *)artwork +{ + NSDictionary *params = @{@"artwork": @[artwork.artworkID]}; + return [staticHTTPClient requestWithMethod:@"GET" path:ARArtworkFairsURLFormat parameters:params]; +} + ++ (NSURLRequest *)newShowsRequestForArtworkID:(NSString *)artworkID andFairID:(NSString *)fairID +{ + NSDictionary *params = @{@"artwork": @[ artworkID ], @"fair_id": fairID}; + return [staticHTTPClient requestWithMethod:@"GET" path:ARRelatedShowsURL parameters:params]; +} + +#pragma mark - +#pragma mark Artist + ++ (NSURLRequest *)newArtistsFromSampleAtPage:(NSInteger)page +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARSampleArtistsURL parameters: @{ @"page": @(page) } ]; +} + + ++ (NSURLRequest *)newArtistsFromPersonalCollectionAtPage:(NSInteger)page +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowArtistsURL parameters: @{ @"page": @(page) } ]; +} + ++ (NSURLRequest *)newArtistCountFromPersonalCollectionRequest; +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowArtistsURL parameters: @{ + @"total_count": @1 + }]; +} + ++ (NSURLRequest *)newArtistInfoRequestWithID:(NSString *)artistID +{ + NSString *url = [NSString stringWithFormat:ARArtistInformationURLFormat, artistID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFollowArtistRequest:(Artist *)artist +{ + return [staticHTTPClient requestWithMethod:@"POST" + path:ARFollowArtistURL + parameters:@{ @"artist_id" : artist.artistID }]; +} + ++ (NSURLRequest *)newUnfollowArtistRequest:(Artist *)artist +{ + NSString *url = [NSString stringWithFormat:ARUnfollowArtistURLFormat, artist.artistID]; + return [staticHTTPClient requestWithMethod:@"DELETE" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFollowingRequestForArtist:(Artist *)artists +{ + return [self newFollowingRequestForArtists:@[artists]]; +} + ++ (NSURLRequest *)newFollowingRequestForArtists:(NSArray *)artists +{ + NSArray *slugs = [artists map: ^(Artist *artist) { + return artist.artistID; + }]; + + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowArtistsURL parameters:@{ @"artists" : slugs }]; +} + ++ (NSURLRequest *)newFollowingArtistsRequestWithFair:(Fair *)fair +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowArtistsURL parameters:@{ @"fair_id": fair.fairID }]; +} + ++ (NSURLRequest *)newArtistsRelatedToArtistRequest:(Artist *)artist +{ + NSDictionary *params = @{ @"artist" : @[artist.artistID] }; + return [staticHTTPClient requestWithMethod:@"GET" path:ARRelatedArtistsURL parameters:params]; +} + ++ (NSURLRequest *)newShowsRequestForArtist:(NSString *)artistID +{ + NSDictionary *params = @{@"artist": @[ artistID ]}; + return [staticHTTPClient requestWithMethod:@"GET" path:ARRelatedShowsURL parameters:params]; +} + ++ (NSURLRequest *)newShowsRequestForArtistID:(NSString *)artistID inFairID:(NSString *)fairID +{ + NSDictionary *params = @{ @"artist": artistID }; + return [staticHTTPClient requestWithMethod:@"GET" path:NSStringWithFormat(ARShowsFeaturingArtistsURLFormat, fairID) parameters:params]; +} + + +#pragma mark - Genes + ++ (NSURLRequest *)newGeneCountFromPersonalCollectionRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowGenesURL parameters: @{ @"total_count": @1 }]; +} + ++ (NSURLRequest *)newGenesFromPersonalCollectionAtPage:(NSInteger)page +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowGenesURL parameters: @{ @"page": @(page) } ]; +} + ++ (NSURLRequest *)newGeneInfoRequestWithID:(NSString *)geneID +{ + NSString *url = [NSString stringWithFormat:ARGeneInformationURLFormat, geneID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFollowingRequestForGene:(Gene *)gene +{ + return [self newFollowingRequestForGenes:@[gene]]; +} + ++ (NSURLRequest *)newFollowGeneRequest:(Gene *)gene +{ + return [staticHTTPClient requestWithMethod:@"POST" path:ARFollowGeneURL parameters:@{ @"gene_id" : gene.geneID }]; +} + ++ (NSURLRequest *)newUnfollowGeneRequest:(Gene *)gene +{ + NSString *url = [NSString stringWithFormat:ARUnfollowGeneURLFormat, gene.geneID]; + return [staticHTTPClient requestWithMethod:@"DELETE" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFollowingRequestForGenes:(NSArray *)genes +{ + NSArray *slugs = [genes map:^(Gene *gene) { return gene.geneID; }]; + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowGenesURL parameters:@{ @"genes" : slugs }]; +} + +#pragma mark - Shows + ++ (NSURLRequest *)newShowInfoRequestWithID:(NSString *)showID +{ + NSString *url = [NSString stringWithFormat:ARShowInformationURLFormat, showID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + +#pragma mark - Models + ++ (NSURLRequest *)newPostInfoRequestWithID:(NSString *)postID +{ + NSString *url = [NSString stringWithFormat:ARPostInformationURLFormat, postID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newProfileInfoRequestWithID:(NSString *)profileID +{ + NSString *url = [NSString stringWithFormat:ARProfileInformationURLFormat, profileID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newArtworkInfoRequestWithID:(NSString *)artworkID +{ + NSString *url = [NSString stringWithFormat:ARArtworkInformationURLFormat, artworkID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + +#pragma mark - +#pragma mark Search + ++ (NSURLRequest *)newSearchRequestWithQuery:(NSString *)query +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARNewSearchURL parameters:@{ @"term" : query }]; +} + ++ (NSURLRequest *)newSearchRequestWithFairID:(NSString *)fairID andQuery:(NSString *)query +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARNewSearchURL parameters:@{ @"term" : query, @"fair_id" : fairID }]; +} + ++ (NSURLRequest *)newArtistSearchRequestWithQuery:(NSString *)query +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARNewArtistSearchURL parameters:@{@"term": query}]; +} + ++ (NSURLRequest *)directImageRequestForModel:(Class)model andSlug:(NSString *)slug +{ + // Note: should these be moved to network constants? + + NSDictionary *paths = @{ + @"Artwork" : @"/api/v1/artwork/%@/default_image.jpg", + @"Artist" : @"/api/v1/artist/%@/image", + @"Gene" : @"/api/v1/gene/%@/image", + @"Tag" : @"/api/v1/tag/%@/image", + @"Profile" : @"/api/v1/profile/%@/image", + @"SiteFeature" : @"/api/v1/feature/%@/image", + @"PartnerShow" : @"/api/v1/partner_show/%@/default_image.jpg", + }; + + NSString *key = NSStringFromClass(model); + NSString *path = [NSString stringWithFormat:paths[key], slug]; + return [staticHTTPClient requestWithMethod:@"GET" path: path parameters:nil]; +} + +#pragma mark - +#pragma mark Fairs + ++ (NSURLRequest *)newFairInfoRequestWithID:(NSString *)fairID +{ + NSString *url = [NSString stringWithFormat:ARNewFairInfoURLFormat, fairID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFairShowsRequestWithFair:(Fair *)fair +{ + NSString *url = [NSString stringWithFormat:ARNewFairShowsURLFormat, fair.fairID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFairMapRequestWithFair:(Fair *)fair +{ + NSString *url = [NSString stringWithFormat:ARNewFairMapURLFormat, fair.fairID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)newFollowArtistRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowArtistsURL parameters:nil]; +} + ++ (NSURLRequest *)newFollowArtistRequestWithFair:(Fair *)fair +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARFollowArtistsURL parameters:@{ @"fair_id": fair.fairID }]; +} + +#pragma mark - +#pragma mark Misc Site + ++ (NSURLRequest *)newSiteHeroUnitsRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARSiteHeroUnitsURL parameters:@{ @"mobile": @"true", @"enabled":@"true" }]; +} + ++ (NSURLRequest *)newOnDutyRepresentativeRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:AROnDutyRepresentativesURL parameters:nil]; +} + ++ (NSURLRequest *)newArtworkInquiryRequestForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + analyticsDictionary:(NSDictionary *)analyticsDictionary + shouldContactGallery:(BOOL)contactGallery +{ + NSParameterAssert(artwork); + NSParameterAssert(message); + + NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{ + @"artwork" : artwork.artworkID, + @"message" : message, + @"contact_gallery" : @(contactGallery) + }]; + + [analyticsDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([key isEqualToString:ArtsyAPIInquiryAnalyticsInquiryURL]) { + params[@"inquiry_url"] = obj; + } else if ([key isEqualToString:ArtsyAPIInquiryAnalyticsReferralURL]) { + params[@"referring_url"] = obj; + } else if ([key isEqualToString:ArtsyAPIInquiryAnalyticsLandingURL]) { + params[@"landing_url"] = obj; + } + }]; + + if ([User isTrialUser]) { + NSParameterAssert(name); + NSParameterAssert(email); + [params setValue:name forKey:@"name"]; + [params setValue:email forKey:@"email"]; + [params setValue:[ARUserManager sharedManager].trialUserUUID forKey:@"session_id"]; + } else { + NSParameterAssert(! name); + NSParameterAssert(! email); + } + + return [staticHTTPClient requestWithMethod:@"POST" path:ARArtworkInquiryRequestURL parameters:params]; +} + ++ (NSURLRequest *)newArtworksFromShowRequest:(PartnerShow *)show atPage:(NSInteger)page +{ + NSString *url = [NSString stringWithFormat:ARShowArtworksURLFormat, show.partner.partnerID, show.showID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:@{ + @"page": @(page), @"published" : @YES, @"size" : @10 + }]; +} + ++ (NSURLRequest *)newImagesFromShowRequest:(PartnerShow *)show atPage:(NSInteger)page +{ + NSString *url = [NSString stringWithFormat:ARShowImagesURLFormat, show.showID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:@{ + @"default" : @(NO), @"page": @(page), @"size" : @10 + }]; +} + ++ (NSURLRequest *)newArtworksFromGeneRequest:(NSString *)gene atPage:(NSInteger)page +{ + NSDictionary *params = @{ + @"page": @(page), + @"sort": @"-date_added" + }; + NSString *url = [NSString stringWithFormat:ARGeneArtworksURLFormat, gene]; + + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:params]; +} + ++ (NSURLRequest *)newForgotPasswordRequestWithEmail:(NSString *)email +{ + NSDictionary *params = @{@"email": email}; + return [staticHTTPClient requestWithMethod:@"POST" path:ARForgotPasswordURL parameters:params]; +} + ++ (NSURLRequest *)newSiteFeaturesRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARSiteFeaturesURL parameters:nil]; +} + ++ (NSURLRequest *)newSetDeviceAPNTokenRequest:(NSString *)token forDevice:(NSString *)device +{ + NSString *bundleID = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]; + + NSDictionary *params = @{ + @"name": device, + @"token": token, + @"app_id": bundleID + }; + return [staticHTTPClient requestWithMethod:@"POST" path:ARMyDevicesURL parameters:params]; +} + ++ (NSURLRequest *)newUptimeURLRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARSiteUpURL parameters:nil]; +} + ++ (NSURLRequest *)salesWithArtworkRequest:(NSString *)artworkID +{ + NSDictionary *params = @{ @"artwork[]" : artworkID }; + return [staticHTTPClient requestWithMethod:@"GET" path:ARSalesForArtworkURL parameters:params]; +} + ++ (NSURLRequest *)artworksForSaleRequest:(NSString *)saleID +{ + NSString *url = [NSString stringWithFormat:ARSaleArtworksURLFormat, saleID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)biddersRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARMyBiddersURL parameters:nil]; +} + ++ (NSURLRequest *)bidderPositionsRequestForSaleID:(NSString *)saleID artworkID:(NSString *)artworkID +{ + NSDictionary *params = @{ @"sale_id" : saleID, @"artwork_id" : artworkID }; + return [staticHTTPClient requestWithMethod:@"GET" path:ARBidderPositionsForSaleAndArtworkURL parameters:params]; +} + ++ (NSURLRequest *)saleArtworkRequestForSaleID:(NSString *)saleID artworkID:(NSString *)artworkID +{ + NSString *path = [NSString stringWithFormat:ARSaleArtworkForSaleAndArtworkURLFormat, saleID, artworkID]; + NSMutableURLRequest *req = [staticHTTPClient requestWithMethod:@"GET" path:path parameters:nil]; + req.cachePolicy = NSURLRequestReloadIgnoringCacheData; + return req; +} + ++ (NSURLRequest *)orderedSetsWithOwnerType:(NSString *)ownerType andID:(NSString *)ownerID +{ + ownerType = ownerType ?: @""; + NSDictionary *params = @{ @"owner_type" : ownerType, @"owner_id" : ownerID, @"sort" : @"key", @"mobile" : @"true", @"published" : @"true" }; + return [staticHTTPClient requestWithMethod:@"GET" path:AROrderedSetsURL parameters:params]; +} + ++ (NSURLRequest *)orderedSetsWithKey:(NSString *)key +{ + NSDictionary *params = @{ @"key" : key, @"sort" : @"key", @"mobile" : @"true", @"published" : @"true" }; + return [staticHTTPClient requestWithMethod:@"GET" path:AROrderedSetsURL parameters:params]; +} + ++ (NSURLRequest *)orderedSetItems:(NSString *)orderedSetID +{ + NSString *url = [NSString stringWithFormat:AROrderedSetItemsURLFormat, orderedSetID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:nil]; +} + ++ (NSURLRequest *)orderedSetItems:(NSString *)orderedSetID atPage:(NSInteger)page +{ + NSString *url = [NSString stringWithFormat:AROrderedSetItemsURLFormat, orderedSetID]; + return [staticHTTPClient requestWithMethod:@"GET" path:url parameters:@{ @"page": @(page), @"size" : @10 }]; +} + ++ (NSURLRequest *)newSystemTimeRequest +{ + return [staticHTTPClient requestWithMethod:@"GET" path:ARSystemTimeURL parameters:nil]; +} + ++ (NSURLRequest *)newPendingOrderWithArtworkID:(NSString *)artworkID +{ + NSDictionary *params = @{ + @"artwork_id": artworkID, + @"replace_order": @YES, + @"session_id": [[NSUUID UUID] UUIDString] // TODO: preserve across session? + }; + + return [staticHTTPClient requestWithMethod:@"POST" path:ARCreatePendingOrderURL parameters:params]; +} + +@end diff --git a/Artsy/Classes/Networking/ARSystemTime.h b/Artsy/Classes/Networking/ARSystemTime.h new file mode 100644 index 00000000000..12aaba327be --- /dev/null +++ b/Artsy/Classes/Networking/ARSystemTime.h @@ -0,0 +1,27 @@ +@interface ARSystemTime : NSObject + +/** + * Retrieve server-side date and time and store for future time adjustments. + */ ++ (void)sync; + +/** + * Reset server-side date and time, require a new sync. + */ ++ (void)reset; + +/** + * Returns YES if the server-side date and time have been retrieved. + * + * @return Whether the server-side date and time have been retrieved. + */ ++ (BOOL)inSync; + +/** + * Current server-side date and time based on a previously retrieved interval. + * + * @return Server-side date and time. + */ ++ (NSDate *)date; + +@end diff --git a/Artsy/Classes/Networking/ARSystemTime.m b/Artsy/Classes/Networking/ARSystemTime.m new file mode 100644 index 00000000000..0bbf774d98e --- /dev/null +++ b/Artsy/Classes/Networking/ARSystemTime.m @@ -0,0 +1,39 @@ +@implementation ARSystemTime + +static NSTimeInterval ARSystemTimeInterval = NSTimeIntervalSince1970; + ++ (void)sync +{ + [ArtsyAPI getSystemTime:^(SystemTime *systemTime) { + @synchronized(self) { + ARSystemTimeInterval = [[NSDate date] timeIntervalSinceDate:systemTime.date]; + ARActionLog(@"Synchronized clock with server, local time is %.2f second(s) %@", ABS(ARSystemTimeInterval), ARSystemTimeInterval > 0 ? @"ahead" : @"behind"); + } + } failure:^(NSError *error) { + ARErrorLog(@"Error retrieving system time, %@", error.localizedDescription); + [self performSelector:_cmd withObject:nil afterDelay:3]; + }]; +} + ++ (BOOL)inSync +{ + return ARSystemTimeInterval != NSTimeIntervalSince1970; +} + ++ (NSDate *)date +{ + @synchronized(self) { + NSDate *now = [NSDate date]; + return ARSystemTime.inSync ? [now dateByAddingTimeInterval:-ARSystemTimeInterval] : now; + } +} + ++ (void)reset +{ + @synchronized(self) { + ARSystemTimeInterval = NSTimeIntervalSince1970; + } +} + +@end + diff --git a/Artsy/Classes/Networking/ARUserManager.h b/Artsy/Classes/Networking/ARUserManager.h new file mode 100644 index 00000000000..445b51cbb64 --- /dev/null +++ b/Artsy/Classes/Networking/ARUserManager.h @@ -0,0 +1,52 @@ +#import + +@interface ARUserManager : NSObject + ++ (ARUserManager *)sharedManager; + ++ (void)identifyAnalyticsUser; + +- (User *)currentUser; +- (void)storeUserData; + +@property (nonatomic, strong) NSString *trialUserName; +@property (nonatomic, strong) NSString *trialUserEmail; +@property (nonatomic, strong, readonly) NSString *trialUserUUID; + +- (void)resetTrialUserUUID; + +- (BOOL)hasExistingAccount; +- (BOOL)hasValidAuthenticationToken; +- (BOOL)hasValidXAppToken; + +- (void)logout; + +- (void)startTrial:(void(^)())callback failure:(void (^)(NSError *error))failure; + +- (void)loginWithUsername:(NSString *)username + password:(NSString *)password + successWithCredentials:(void(^)(NSString *accessToken, NSDate *expirationDate))credentials + gotUser:(void(^)(User *currentUser))gotUser + authenticationFailure:(void (^)(NSError *error))authenticationFailure + networkFailure:(void (^)(NSError *error))networkFailure; + +- (void)loginWithFacebookToken:(NSString *)token + successWithCredentials:(void(^)(NSString *accessToken, NSDate *expirationDate))credentials + gotUser:(void(^)(User *currentUser))gotUser + authenticationFailure:(void (^)(NSError *error))authenticationFailure + networkFailure:(void (^)(NSError *error))networkFailure; + +- (void)loginWithTwitterToken:(NSString *)token + secret:(NSString *)secret + successWithCredentials:(void(^)(NSString *accessToken, NSDate *expirationDate))credentials + gotUser:(void(^)(User *currentUser))gotUser + authenticationFailure:(void (^)(NSError *error))authenticationFailure + networkFailure:(void (^)(NSError *error))networkFailure; + +- (void)createUserWithName:(NSString *)name email:(NSString *)email password:(NSString *)password success:(void(^)(User *user))success failure:(void (^)(NSError *error, id JSON))failure; +- (void)createUserViaFacebookWithToken:(NSString *)token email:(NSString *)email name:(NSString *)name success:(void(^)(User *user))success failure:(void (^)(NSError *error, id JSON))failure; +- (void)createUserViaTwitterWithToken:(NSString *)token secret:(NSString *)secret email:(NSString *)email name:(NSString *)name success:(void(^)(User *user))success failure:(void (^)(NSError *error, id JSON))failure; + +- (void)sendPasswordResetForEmail:(NSString *)email success:(void(^)(void))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ARUserManager.m b/Artsy/Classes/Networking/ARUserManager.m new file mode 100644 index 00000000000..487c56dcd1c --- /dev/null +++ b/Artsy/Classes/Networking/ARUserManager.m @@ -0,0 +1,550 @@ +#import "ARUserManager.h" +#import "NSDate+Util.h" +#import "ARRouter.h" +#import +#import +#import +#import "ARFileUtils.h" +#import "ArtsyAPI+Private.h" +#import "NSKeyedUnarchiver+ErrorLogging.h" +#import +#import "ARAnalyticsConstants.h" + +NSString *ARTrialUserNameKey = @"ARTrialUserName"; +NSString *ARTrialUserEmailKey = @"ARTrialUserEmail"; +NSString *ARTrialUserUUID = @"ARTrialUserUUID"; + +@interface ARUserManager() +@property (nonatomic, strong) User *currentUser; +@end + +@implementation ARUserManager + ++ (ARUserManager *)sharedManager +{ + static ARUserManager *_sharedManager = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedManager = [[self alloc] init]; + }); + return _sharedManager; +} + ++ (void)identifyAnalyticsUser +{ + NSString *analyticsUserID = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + [ARAnalytics identifyUserWithID:analyticsUserID andEmailAddress:nil]; + + User *user = [User currentUser]; + if (user) { + [ARAnalytics setUserProperty:@"$email" toValue:user.email]; + [ARAnalytics setUserProperty:@"user_id" toValue:user.userID]; + [ARAnalytics setUserProperty:@"user_uuid" toValue:[ARUserManager sharedManager].trialUserUUID]; + [[Mixpanel sharedInstance] registerSuperProperties: @{ + @"user_id" : user.userID ?: @"", + @"user_uuid" : [ARUserManager sharedManager].trialUserUUID + }]; + } else { + [ARAnalytics setUserProperty:@"user_uuid" toValue:[ARUserManager sharedManager].trialUserUUID]; + [[Mixpanel sharedInstance] registerSuperProperties: @{ + @"user_uuid" : [ARUserManager sharedManager].trialUserUUID + }]; + } +} + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + NSString *userDataFolderPath = [self userDataPath]; + NSString *userDataPath = [userDataFolderPath stringByAppendingPathComponent:@"User.data"]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:userDataPath]) { + _currentUser = [NSKeyedUnarchiver unarchiveObjectWithFile:userDataPath + exceptionBlock:^id(NSException *exception) { + ARErrorLog(@"%@", exception.reason); + [[NSFileManager defaultManager] removeItemAtPath:userDataPath error:nil]; + return nil; + } + ]; + + // safeguard + if (!self.currentUser.userID) { + ARErrorLog(@"Deserialized user %@ does not have an ID.", self.currentUser); + _currentUser = nil; + } + } + + return self; +} + +- (BOOL)hasExistingAccount +{ + return ( _currentUser && [self hasValidAuthenticationToken] ) || [self hasValidXAppToken]; +} + +- (BOOL)hasValidAuthenticationToken +{ + NSString *authToken = [UICKeyChainStore stringForKey:AROAuthTokenDefault]; + NSDate *expiryDate = [[NSUserDefaults standardUserDefaults] objectForKey:AROAuthTokenExpiryDateDefault]; + + BOOL tokenValid = expiryDate && [[[ARSystemTime date] GMTDate] earlierDate:expiryDate] != expiryDate; + return authToken && tokenValid; +} + +- (BOOL)hasValidXAppToken +{ + NSString *xapp = [UICKeyChainStore stringForKey:ARXAppTokenDefault]; + NSDate *expiryDate = [[NSUserDefaults standardUserDefaults] objectForKey:ARXAppTokenExpiryDateDefault]; + + BOOL tokenValid = expiryDate && [[[ARSystemTime date] GMTDate] earlierDate:expiryDate] != expiryDate; + return xapp && tokenValid; +} + +- (void)loginWithUsername:(NSString *)username password:(NSString *)password + successWithCredentials:(void(^)(NSString *accessToken, NSDate *expirationDate))credentials + gotUser:(void(^)(User *currentUser))gotUser + authenticationFailure:(void (^)(NSError *error))authenticationFailure + networkFailure:(void (^)(NSError *error))networkFailure { + + NSURLRequest *request = [ARRouter newOAuthRequestWithUsername:username password:password]; + + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *oauthRequest, NSHTTPURLResponse *response, id JSON) { + + NSString *token = JSON[AROAuthTokenKey]; + NSString *expiryDateString = JSON[AROExpiryDateKey]; + + [ARRouter setAuthToken:token]; + + // Create an Expiration Date + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSDate *expiryDate = [dateFormatter dateFromString:expiryDateString]; + + // Let clients perform any actions once we've got the tokens sorted + if (credentials) { + credentials(token, expiryDate); + } + + NSURLRequest *userRequest = [ARRouter newUserInfoRequest]; + AFJSONRequestOperation *userOp = [AFJSONRequestOperation JSONRequestOperationWithRequest:userRequest success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + + User *user = [User modelWithJSON:JSON]; + + self.currentUser = user; + [self storeUserData]; + [user updateProfile:^{ + [self storeUserData]; + }]; + + // Store the credentials for next app launch + [UICKeyChainStore setString:token forKey:AROAuthTokenDefault]; + [UICKeyChainStore removeItemForKey:ARXAppTokenDefault]; + + [[NSUserDefaults standardUserDefaults] removeObjectForKey:ARXAppTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] setObject:expiryDate forKey:AROAuthTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] synchronize]; + gotUser(user); + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (authenticationFailure) { + authenticationFailure(error); + } + }]; + [userOp start]; + } + failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (JSON) { + if (authenticationFailure) { + authenticationFailure(error); + } + } else { + if (networkFailure) { + networkFailure(error); + } + } + } + ]; + [op start]; +} + +- (void)loginWithFacebookToken:(NSString *)token + successWithCredentials:(void (^)(NSString *, NSDate *))credentials + gotUser:(void (^)(User *))gotUser + authenticationFailure:(void (^)(NSError *error))authenticationFailure + networkFailure:(void (^)(NSError *))networkFailure +{ + NSURLRequest *request = [ARRouter newFacebookOAuthRequestWithToken:token]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *oauthRequest, NSHTTPURLResponse *response, id JSON) { + + NSString *token = JSON[AROAuthTokenKey]; + NSString *expiryDateString = JSON[AROExpiryDateKey]; + + [ARRouter setAuthToken:token]; + + // Create an Expiration Date + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSDate *expiryDate = [dateFormatter dateFromString:expiryDateString]; + + // Let clients perform any actions once we've got the tokens sorted + if (credentials) { + credentials(token, expiryDate); + } + + NSURLRequest *userRequest = [ARRouter newUserInfoRequest]; + AFJSONRequestOperation *userOp = [AFJSONRequestOperation JSONRequestOperationWithRequest:userRequest success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + + User *user = [User modelWithJSON:JSON]; + + self.currentUser = user; + [self storeUserData]; + [user updateProfile:^{ + [self storeUserData]; + }]; + + // Store the credentials for next app launch + [UICKeyChainStore setString:token forKey:AROAuthTokenDefault]; + [UICKeyChainStore removeItemForKey:ARXAppTokenDefault]; + + [[NSUserDefaults standardUserDefaults] removeObjectForKey:ARXAppTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] setObject:expiryDate forKey:AROAuthTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] synchronize]; + gotUser(user); + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (authenticationFailure) { + authenticationFailure(error); + } + }]; + [userOp start]; + } + failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (JSON) { + if (authenticationFailure) { + authenticationFailure(error); + } + } else { + if (networkFailure) { + networkFailure(error); + } + } + + }]; + [op start]; + +} + +- (void)loginWithTwitterToken:(NSString *)token secret:(NSString *)secret + successWithCredentials:(void (^)(NSString *, NSDate *))credentials + gotUser:(void (^)(User *))gotUser + authenticationFailure:(void (^)(NSError *error))authenticationFailure + networkFailure:(void (^)(NSError *))networkFailure +{ + NSURLRequest *request = [ARRouter newTwitterOAuthRequestWithToken:token andSecret:secret]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *oauthRequest, NSHTTPURLResponse *response, id JSON) { + + NSString *token = JSON[AROAuthTokenKey]; + NSString *expiryDateString = JSON[AROExpiryDateKey]; + + [ARRouter setAuthToken:token]; + + // Create an Expiration Date + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSDate *expiryDate = [dateFormatter dateFromString:expiryDateString]; + + // Let clients perform any actions once we've got the tokens sorted + if (credentials) { + credentials(token, expiryDate); + } + + NSURLRequest *userRequest = [ARRouter newUserInfoRequest]; + AFJSONRequestOperation *userOp = [AFJSONRequestOperation JSONRequestOperationWithRequest:userRequest success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + + User *user = [User modelWithJSON:JSON]; + + self.currentUser = user; + [self storeUserData]; + [user updateProfile:^{ + [self storeUserData]; + }]; + + // Store the credentials for next app launch + [UICKeyChainStore setString:token forKey:AROAuthTokenDefault]; + [UICKeyChainStore removeItemForKey:ARXAppTokenDefault]; + + [[NSUserDefaults standardUserDefaults] removeObjectForKey:ARXAppTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] setObject:expiryDate forKey:AROAuthTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] synchronize]; + gotUser(user); + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (authenticationFailure) { + authenticationFailure(error); + } + }]; + [userOp start]; + } + failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (JSON) { + if (authenticationFailure) { + authenticationFailure(error); + } + } else { + networkFailure(error); + } + }]; + + [op start]; + +} + +- (void)startTrial:(void(^)())callback failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + [UICKeyChainStore setString:xappToken forKey:ARXAppTokenDefault]; + [[NSUserDefaults standardUserDefaults] setObject:expirationDate forKey:ARXAppTokenExpiryDateDefault]; + callback(); + } failure:failure]; +} + +- (void)createUserWithName:(NSString *)name email:(NSString *)email password:(NSString *)password success:(void (^)(User *))success failure:(void (^)(NSError *error, id JSON))failure +{ + [ARAnalytics event:ARAnalyticsUserCreationStarted withProperties:@{ + @"context" : ARAnalyticsUserContextEmail + }]; + + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + + ARActionLog(@"Got Xapp. Creating a new user account."); + + NSURLRequest *request = [ARRouter newCreateUserRequestWithName:name email:email password:password]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSError *error; + User *user = [User modelWithJSON:JSON error:&error]; + if (error) { + ARErrorLog(@"Couldn't create user model from fresh user. Error: %@,\nJSON: %@", error.localizedDescription, JSON); + [ARAnalytics event:ARAnalyticsUserCreationUnknownError]; + failure(error, JSON); + return; + } + + self.currentUser = user; + [self storeUserData]; + + if(success) success(user); + [ARAnalytics event:ARAnalyticsUserCreationCompleted]; + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + ARErrorLog(@"Creating a new user account failed. Error: %@,\nJSON: %@", error.localizedDescription, JSON); + failure(error, JSON); + [ARAnalytics event:ARAnalyticsUserCreationUnknownError]; + }]; + + [op start]; + + }]; +} + +- (void)createUserViaFacebookWithToken:(NSString *)token email:(NSString *)email name:(NSString *)name success:(void (^)(User *))success failure:(void (^)(NSError *, id))failure +{ + [ARAnalytics event:ARAnalyticsUserCreationStarted withProperties:@{ + @"context" : ARAnalyticsUserContextFacebook + }]; + + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + NSURLRequest *request = [ARRouter newCreateUserViaFacebookRequestWithToken:token email:email name:name]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSError *error; + User *user = [User modelWithJSON:JSON error:&error]; + if (error) { + ARErrorLog(@"Couldn't create user model from fresh Facebook user. Error: %@,\nJSON: %@", error.localizedDescription, JSON); + [ARAnalytics event:ARAnalyticsUserCreationUnknownError]; + failure(error, JSON); + return; + } + self.currentUser = user; + [self storeUserData]; + + if (success) { success(user); } + + [ARAnalytics event:ARAnalyticsUserCreationCompleted]; + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + failure(error, JSON); + [ARAnalytics event:ARAnalyticsUserCreationUnknownError]; + + }]; + [op start]; + }]; +} + +- (void)createUserViaTwitterWithToken:(NSString *)token secret:(NSString *)secret email:(NSString *)email name:(NSString *)name success:(void (^)(User *))success failure:(void (^)(NSError *, id))failure +{ + [ARAnalytics event:ARAnalyticsUserCreationStarted withProperties:@{ + @"context" : ARAnalyticsUserContextTwitter + }]; + + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + NSURLRequest *request = [ARRouter newCreateUserViaTwitterRequestWithToken:token secret:secret email:email name:name]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSError *error; + User *user = [User modelWithJSON:JSON error:&error]; + if (error) { + ARErrorLog(@"Couldn't create user model from fresh Twitter user. Error: %@,\nJSON: %@", error.localizedDescription, JSON); + [ARAnalytics event:ARAnalyticsUserCreationUnknownError]; + failure(error, JSON); + return; + } + self.currentUser = user; + [self storeUserData]; + + if(success) success(user); + + [ARAnalytics event:ARAnalyticsUserCreationCompleted]; + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + failure(error, JSON); + [ARAnalytics event:ARAnalyticsUserCreationUnknownError]; + }]; + [op start]; + }]; +} + +- (void)sendPasswordResetForEmail:(NSString *)email success:(void (^)(void))success failure:(void (^)(NSError *))failure +{ + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + NSURLRequest *request = [ARRouter newForgotPasswordRequestWithEmail:email]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + if (success) { + success(); + } + } + failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (failure) { + failure(error); + } + }]; + [op start]; + }]; +} + +- (void)storeUserData +{ + NSString *userDataPath = [ARFileUtils userDocumentsPathWithFile:@"User.data"]; + if (userDataPath) { + [NSKeyedArchiver archiveRootObject:self.currentUser toFile:userDataPath]; + + [ARUserManager identifyAnalyticsUser]; + + [[NSUserDefaults standardUserDefaults] setObject:self.currentUser.userID forKey:ARUserIdentifierDefault]; + [[NSUserDefaults standardUserDefaults] synchronize]; + } +} + +- (void)logout +{ + [self deleteUserData]; + + [UICKeyChainStore removeItemForKey:AROAuthTokenDefault]; + [UICKeyChainStore removeItemForKey:ARXAppTokenDefault]; + + [[NSUserDefaults standardUserDefaults] removeObjectForKey:AROAuthTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:ARXAppTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:ARUserIdentifierDefault]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + [ARRouter setAuthToken:nil]; + [self deleteHTTPCookies]; + + self.currentUser = nil; +} + +- (void)deleteHTTPCookies +{ + NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (NSHTTPCookie *cookie in cookieStorage.cookies) { + if ([ARRouter.artsyHosts containsObject:cookie.domain]) { + [cookieStorage deleteCookie:cookie]; + } + } +} + +- (void)deleteUserData +{ + // Delete the user data + NSString * userDataPath = [self userDataPath]; + if (userDataPath) { + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:userDataPath error:&error]; + if (error) { + ARErrorLog(@"Error Deleting User Data %@", error.localizedDescription); + } + } +} + +#pragma mark - +#pragma mark Utilities + +- (NSString *)userDataPath { + NSString *userID = [[NSUserDefaults standardUserDefaults] objectForKey:ARUserIdentifierDefault]; + if (!userID) { return nil; } + + NSArray *directories =[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; + NSString *documentsPath = [[directories lastObject] relativePath]; + return [documentsPath stringByAppendingPathComponent:userID]; +} + +#pragma mark - +#pragma mark Trial User + +- (void)setTrialUserName:(NSString *)trialUserName +{ + if (trialUserName) { + [UICKeyChainStore setString:trialUserName forKey:ARTrialUserNameKey]; + } else { + [UICKeyChainStore removeItemForKey:ARTrialUserNameKey]; + } +} + +- (void)setTrialUserEmail:(NSString *)trialUserEmail +{ + if (trialUserEmail) { + [UICKeyChainStore setString:trialUserEmail forKey:ARTrialUserEmailKey]; + } else { + [UICKeyChainStore removeItemForKey:ARTrialUserEmailKey]; + } +} + +- (NSString *)trialUserName +{ + return [UICKeyChainStore stringForKey:ARTrialUserNameKey]; +} + +- (NSString *)trialUserEmail +{ + return [UICKeyChainStore stringForKey:ARTrialUserEmailKey]; +} + +- (NSString *)trialUserUUID +{ + NSString *uuid = [UICKeyChainStore stringForKey:ARTrialUserUUID]; + if (!uuid) { + uuid = [[NSUUID UUID] UUIDString]; + [UICKeyChainStore setString:uuid forKey:ARTrialUserUUID]; + } + return uuid; +} + +- (void)resetTrialUserUUID +{ + [UICKeyChainStore removeItemForKey:ARTrialUserUUID]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Artists.h b/Artsy/Classes/Networking/ArtsyAPI+Artists.h new file mode 100644 index 00000000000..b7a349e6813 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Artists.h @@ -0,0 +1,9 @@ +@interface ArtsyAPI (Artists) + ++ (void)getArtistForArtistID:(NSString *)artistID success:(void (^)(Artist *artist))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getShowsForArtistID:(NSString *)artistID success:(void (^)(NSArray *shows))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getShowsForArtistID:(NSString *)artistID inFairID:(NSString *)fairID success:(void (^)(NSArray *shows))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Artists.m b/Artsy/Classes/Networking/ArtsyAPI+Artists.m new file mode 100644 index 00000000000..57702c8487c --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Artists.m @@ -0,0 +1,23 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Artists) + ++ (void)getArtistForArtistID:(NSString *)artistID success:(void (^)(Artist *artist))success failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + [self getRequest:[ARRouter newArtistInfoRequestWithID:artistID] parseIntoAClass:[Artist class] success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getShowsForArtistID:(NSString *)artistID success:(void (^)(NSArray *shows))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newShowsRequestForArtist:artistID]; + return [self getRequest:request parseIntoAnArrayOfClass:[PartnerShow class] success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getShowsForArtistID:(NSString *)artistID inFairID:(NSString *)fairID success:(void (^)(NSArray *shows))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newShowsRequestForArtistID:artistID inFairID:fairID]; + return [self getRequest:request parseIntoAnArrayOfClass:[PartnerShow class] fromDictionaryWithKey:@"results" success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Artworks.h b/Artsy/Classes/Networking/ArtsyAPI+Artworks.h new file mode 100644 index 00000000000..70987a75f6f --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Artworks.h @@ -0,0 +1,18 @@ +@class Artwork, Artist, Gene, Fair, PartnerShow; + +@interface ArtsyAPI (Artworks) + ++ (void)getArtworkInfo:(NSString *)artworkID success:(void (^)(Artwork *artwork))success failure:(void (^)(NSError *error))failure; ++ (void)getArtistArtworks:(Artist *)artist andPage:(NSInteger)page withParams:(NSDictionary *)params success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + ++ (void)getArtworkFromUserFavorites:(NSString *)userID page:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getFairsForArtwork:(Artwork *)artwork success:(void (^)(NSArray *fairs))success failure:(void (^)(NSError *error))failure; ++ (AFJSONRequestOperation *)getShowsForArtworkID:(NSString *)artworkID inFairID:(NSString *)fairID success:(void (^)(NSArray *shows))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getArtworksForGene:(Gene *)gene atPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getAuctionComparablesForArtwork:(Artwork *)artwork success:(void (^)(NSArray *comparables))success failure:(void (^)(NSError *error))failure; ++ (void)getAuctionArtworkWithSale:(NSString *)saleID artwork:(NSString *)artworkID success:(void (^)(id auctionArtwork))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Artworks.m b/Artsy/Classes/Networking/ArtsyAPI+Artworks.m new file mode 100644 index 00000000000..67eab476ab3 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Artworks.m @@ -0,0 +1,123 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Artworks) + ++ (void)getArtworkInfo:(NSString *)artworkID success:(void (^)(Artwork *artwork))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworkInfoRequestForArtworkID:artworkID]; + [self getRequest:request parseIntoAClass:Artwork.class success:success failure:failure]; +} + ++ (void)getArtistArtworks:(Artist *)artist andPage:(NSInteger)page withParams:(NSDictionary *)params success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + NSMutableDictionary *newParams = [[NSMutableDictionary alloc] initWithDictionary:@{@"size" : @10, @"page" : @(page)}]; + [newParams addEntriesFromDictionary:params]; + NSURLRequest *request = [ARRouter newArtistArtworksRequestWithParams:newParams andArtistID:artist.artistID]; + [self getRequest:request parseIntoAnArrayOfClass:Artwork.class success:success failure:failure]; +} + ++ (void)getArtworkFromUserFavorites:(NSString *)userID page:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworksFromUsersFavoritesRequestWithID:userID page:page]; + [self getRequest:request parseIntoAnArrayOfClass:Artwork.class success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getArtworksForGene:(Gene *)gene atPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworksFromGeneRequest:gene.geneID atPage:page]; + return [self getRequest:request parseIntoAnArrayOfClass:Artwork.class success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getAuctionComparablesForArtwork:(Artwork *)artwork success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + NSURLRequest *request = [ARRouter newArtworkComparablesRequest:artwork]; + return [self getRequest:request parseIntoAnArrayOfClass:AuctionLot.class success:success failure:failure]; +} + +// TODO: This method should be moved into ARRouter, exposing our http client shouldn't really happen ++ (void)getAuctionArtworkWithSale:(NSString *)saleID artwork:(NSString *)artworkID success:(void (^)(id auctionArtwork))success failure:(void (^)(NSError *error))failure +{ + // get sale artwork + NSURLRequest *request = [ARRouter saleArtworkRequestForSaleID:saleID artworkID:artworkID]; + AFJSONRequestOperation *saleArtworkOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:nil failure:nil]; + + // get bidder registration + request = [ARRouter biddersRequest]; + AFJSONRequestOperation *biddersOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:nil failure:nil]; + + // get bidder position + request = [ARRouter bidderPositionsRequestForSaleID:saleID artworkID:artworkID]; + AFJSONRequestOperation *positionsOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:nil failure:nil]; + + NSArray *operations = @[ biddersOperation, saleArtworkOperation, positionsOperation ]; + AFHTTPClient *client = [ARRouter httpClient]; + [client enqueueBatchOfHTTPRequestOperations:operations progressBlock:nil completionBlock:^(NSArray *operations) { + + // Doing all parsing here since completion blocks fire async per: https://github.com/AFNetworking/AFNetworking/issues/362 + + // Parse sale artwork + SaleArtwork *saleArtwork = nil; + if (saleArtworkOperation.hasAcceptableStatusCode) { + saleArtwork = [SaleArtwork modelWithJSON:saleArtworkOperation.responseJSON]; + } + + // Parse bidders + if (biddersOperation.hasAcceptableStatusCode) { + for (NSDictionary *dictionary in biddersOperation.responseJSON) { + Bidder *bidder = [Bidder modelWithJSON:dictionary]; + if ([bidder.saleID isEqualToString:saleID]) { + saleArtwork.bidder = bidder; + break; + } + } + } + + // Parse bidder positions + if (positionsOperation.hasAcceptableStatusCode) { + saleArtwork.positions = [positionsOperation.responseJSON map:^id(NSDictionary *dictionary) { + NSError *error = nil; + BidderPosition *position =[BidderPosition modelWithJSON:dictionary error:&error]; + if (error) { + ARErrorLog(@"Couldn't parse bidder position. Error: %@", error.localizedDescription); + } + return position; + + }]; + } + + if (success) { + success(saleArtwork); + } + }]; +} + ++ (AFJSONRequestOperation *)getFairsForArtwork:(Artwork *)artwork success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + // The API returns related fairs regardless of whether or not they have the associated + // data necessary to render the fair view. Fairs without an organizer/profile should + // not be rendered as a fair. It is up to the client to make this distinction. + + NSURLRequest *request = [ARRouter newFairsRequestForArtwork:artwork]; + return [self getRequest:request + parseIntoAnArrayOfClass:[Fair class] + success:^(NSArray *fairs) { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"organizer.profileID!=nil"]; + fairs = [fairs filteredArrayUsingPredicate:predicate]; + success(fairs); + } + failure:failure]; +} + ++ (AFJSONRequestOperation *)getShowsForArtworkID:(NSString *)artworkID + inFairID:(NSString *)fairID + success:(void (^)(NSArray *shows))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newShowsRequestForArtworkID:artworkID andFairID:fairID]; + return [self getRequest:request + parseIntoAnArrayOfClass:[PartnerShow class] + success:success + failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Browse.h b/Artsy/Classes/Networking/ArtsyAPI+Browse.h new file mode 100644 index 00000000000..9eb9bd7768a --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Browse.h @@ -0,0 +1,10 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (Browse) + ++ (void)getFeaturedLinksForGenesWithSuccess:(void (^)(NSArray *genes))success failure:(void (^)(NSError *error))failure; ++ (void)getFeaturedLinkCategoriesForGenesWithSuccess:(void (^)(NSArray *sets))success failure:(void (^)(NSError *error))failure; ++ (void)getPersonalizeGenesWithSuccess:(void (^)(NSArray *genes))success failure:(void (^)(NSError *error))failure; ++ (void)getFeedLinksWithSuccess:(void (^)(NSArray *links))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Browse.m b/Artsy/Classes/Networking/ArtsyAPI+Browse.m new file mode 100644 index 00000000000..9e5e4857ab5 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Browse.m @@ -0,0 +1,23 @@ +@implementation ArtsyAPI (Browse) + ++ (void)getFeaturedLinksForGenesWithSuccess:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + [self getOrderedSetItemsWithKey:@"browse:featured-genes" andName:@"Featured Categories" success:success failure:failure]; +} + ++ (void)getFeaturedLinkCategoriesForGenesWithSuccess:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + [self getOrderedSetItemsWithKey:@"browse:gene-categories" andName:@"Gene Categories" success:success failure:failure]; +} + ++ (void)getPersonalizeGenesWithSuccess:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + [self getOrderedSetItemsWithKey:@"eigen-personalize:suggested-genes" success:success failure:failure]; +} + ++ (void)getFeedLinksWithSuccess:(void (^)(NSArray *links))success failure:(void (^)(NSError *error))failure +{ + [self getOrderedSetItemsWithKey:@"eigen:feed-links" success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+CurrentUserFunctions.h b/Artsy/Classes/Networking/ArtsyAPI+CurrentUserFunctions.h new file mode 100644 index 00000000000..9c36bcdafb1 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+CurrentUserFunctions.h @@ -0,0 +1,7 @@ +@class Profile, User; + +@interface ArtsyAPI (CurrentUserFunctions) + ++ (void)updateCurrentUserProperty:(NSString *)property toValue:(id)value success:(void (^)(User *user))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+CurrentUserFunctions.m b/Artsy/Classes/Networking/ArtsyAPI+CurrentUserFunctions.m new file mode 100644 index 00000000000..9525587e87e --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+CurrentUserFunctions.m @@ -0,0 +1,15 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (CurrentUserFunctions) + ++ (void)updateCurrentUserProperty:(NSString *)property toValue:(id)value success:(void (^)(User *user))success failure:(void (^)(NSError *error))failure { + NSParameterAssert(value); + + NSDictionary *params = @{ property: value }; + NSURLRequest *request = [ARRouter newUserEditRequestWithParams:params]; + + [self getRequest:request parseIntoAClass:[User class] success:success failure:failure]; +} + + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+DeviceTokens.h b/Artsy/Classes/Networking/ArtsyAPI+DeviceTokens.h new file mode 100644 index 00000000000..d7a14ff3196 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+DeviceTokens.h @@ -0,0 +1,7 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (DeviceTokens) + ++ (AFJSONRequestOperation *)setAPNTokenForCurrentDevice:(NSData *)token success:(void (^)(id response))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+DeviceTokens.m b/Artsy/Classes/Networking/ArtsyAPI+DeviceTokens.m new file mode 100644 index 00000000000..c1703c3e215 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+DeviceTokens.m @@ -0,0 +1,28 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (DeviceTokens) + ++ (AFJSONRequestOperation *)setAPNTokenForCurrentDevice:(NSData *)token success:(void (^)(id response))success failure:(void (^)(NSError *error))failure +{ + // http://stackoverflow.com/questions/9372815/how-can-i-convert-my-device-token-nsdata-into-an-nsstring + const unsigned *tokenBytes = [token bytes]; + NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x", + ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), + ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), + ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; + + NSString *name = [[UIDevice currentDevice] name]; + + if (hexToken && name) { + NSURLRequest *request = [ARRouter newSetDeviceAPNTokenRequest:hexToken forDevice:name]; + return [ArtsyAPI performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; + } + + return nil; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+ErrorHandlers.h b/Artsy/Classes/Networking/ArtsyAPI+ErrorHandlers.h new file mode 100644 index 00000000000..b3c4e66bf2e --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+ErrorHandlers.h @@ -0,0 +1,35 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (ErrorHandlers) + +/** + * Handle a matching HTTP error from an Artsy API. + * + * @param error HTTP Error + * @param statusCode expected status code + * @param errorMessage expected error message + * @param success matching callback + * @param failure non-matching callback + */ ++ (void)handleHTTPError:(NSError *)error + statusCode:(NSInteger)statusCode + errorMessage:(NSString *)errorMessage + success:(void (^)(NSError *error))success + failure:(void (^)(NSError *error))failure; + +/** + * Handle a matching HTTP error from an Artsy API. + * + * @param error HTTP Error + * @param statusCode expected status code + * @param errorMessages expected error message(s) + * @param success matching callback + * @param failure non-matching callback + */ ++ (void)handleHTTPError:(NSError *)error + statusCode:(NSInteger)statusCode + errorMessages:(NSArray *)errorMessages + success:(void (^)(NSError *error))success + failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+ErrorHandlers.m b/Artsy/Classes/Networking/ArtsyAPI+ErrorHandlers.m new file mode 100644 index 00000000000..a70940debd5 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+ErrorHandlers.m @@ -0,0 +1,49 @@ +@implementation ArtsyAPI (ErrorHandlers) + ++ (void)handleHTTPError:(NSError *)error + statusCode:(NSInteger)statusCode + errorMessage:(NSString *)errorMessage + success:(void (^)(NSError *error))success + failure:(void (^)(NSError *error))failure +{ + [self handleHTTPError:error + statusCode:statusCode + errorMessages:errorMessage ? [NSArray arrayWithObject:errorMessage] : @[] + success:success + failure:failure]; +} + ++ (void)handleHTTPError:(NSError *)error + statusCode:(NSInteger)statusCode + errorMessages:(NSArray *)errorMessages + success:(void (^)(NSError *error))success + failure:(void (^)(NSError *error))failure +{ + NSHTTPURLResponse *response = (NSHTTPURLResponse *) error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey]; + if (response.statusCode == statusCode) { + if (errorMessages.count == 0) { + if (success) { + success(error); + } + return; + } + id errorData = error.userInfo[NSLocalizedRecoverySuggestionErrorKey]; + errorData = [errorData dataUsingEncoding:NSUTF8StringEncoding]; + if (errorData) { + NSDictionary *recoverySuggestion = [NSJSONSerialization JSONObjectWithData:errorData options:0 error:nil]; + for(NSString *errorMessage in errorMessages) { + if ([recoverySuggestion[@"error"] isEqualToString:errorMessage]) { + if (success) { + success(error); + } + return; + } + } + } + } + if (failure) { + failure(error); + } +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Fairs.h b/Artsy/Classes/Networking/ArtsyAPI+Fairs.h new file mode 100644 index 00000000000..423fc2db183 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Fairs.h @@ -0,0 +1,15 @@ +@class Fair, Map; + +@interface ArtsyAPI (Fairs) + ++ (void)getFairInfo:(NSString *)fairID success:(void (^)(Fair *fair))success failure:(void (^)(NSError *error))failure; + ++ (void)getPartnerShowsForFair:(Fair *)fair success:(void (^)(NSArray *partnerShows))success failure:(void (^)(NSError *error))failure; + ++ (void)getMapInfoForFair:(Fair *)fair success:(void (^)(NSArray *maps))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getArtworkFavoritesForFair:(Fair *)fair success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; ++ (void)getArtistFollowsForFair:(Fair *)fair success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure; ++ (void)getProfileFollowsForFair:(Fair *)fair success:(void (^)(NSArray *profiles))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Fairs.m b/Artsy/Classes/Networking/ArtsyAPI+Fairs.m new file mode 100644 index 00000000000..f8e087ec579 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Fairs.m @@ -0,0 +1,41 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Fairs) + ++ (void)getFairInfo:(NSString *)fairID success:(void (^)(Fair *fair))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFairInfoRequestWithID:fairID]; + [self getRequest:request parseIntoAClass:Fair.class success:success failure:failure]; +} + ++ (void)getPartnerShowsForFair:(Fair *)fair success:(void (^)(NSArray *partnerShows))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFairShowsRequestWithFair:fair]; + [self getRequest:request parseIntoAnArrayOfClass:PartnerShow.class fromDictionaryWithKey:@"results" success:success failure:failure]; +} + ++ (void)getMapInfoForFair:(Fair *)fair success:(void (^)(NSArray *maps))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFairMapRequestWithFair:fair]; + [self getRequest:request parseIntoAnArrayOfClass:Map.class success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getArtworkFavoritesForFair:(Fair *)fair success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworkFavoritesRequestWithFair:fair]; + return [self getRequest:request parseIntoAnArrayOfClass:Artwork.class success:success failure:failure]; +} + ++ (void)getArtistFollowsForFair:(Fair *)fair success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFollowingArtistsRequestWithFair:fair]; + [self getRequest:request parseIntoAnArrayOfClass:Follow.class success:success failure:failure]; +} + ++ (void)getProfileFollowsForFair:(Fair *)fair success:(void (^)(NSArray *profiles))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFollowingProfilesRequestWithFair:fair]; + [self getRequest:request parseIntoAnArrayOfClass:Follow.class success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Feed.h b/Artsy/Classes/Networking/ArtsyAPI+Feed.h new file mode 100644 index 00000000000..ecd064911d1 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Feed.h @@ -0,0 +1,13 @@ +#import "ArtsyAPI.h" +#import "Models.h" + +@interface ArtsyAPI (Feed) + ++ (void)getFeedResultsForMainFeedWithCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure; ++ (void)getFeedResultsForProfile:(Profile *)profile withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure; ++ (void)getFeedResultsForShowsWithCursor:(NSString *)cursor pageSize:(NSInteger)pageSize success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure; ++ (void)getFeaturedWorks:(void (^)(NSArray *works))success failure:(void (^)(NSError *error))failure; ++ (void)getFeedResultsForFairOrganizer:(FairOrganizer *)fairOrganizer withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure; ++ (void)getFeedResultsForFairShows:(Fair *)fair withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure; ++ (void)getFeedResultsForFairShows:(Fair *)fair partnerID:(NSString *)partnerID withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure; +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Feed.m b/Artsy/Classes/Networking/ArtsyAPI+Feed.m new file mode 100644 index 00000000000..6ebfd8997b8 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Feed.m @@ -0,0 +1,77 @@ +#import "ARFileUtils.h" +#import "ARRouter.h" +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Feed) + ++ (void)getFeedResultsForMainFeedWithCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure +{ + NSInteger pageSize = (cursor) ? 5 : 10; + NSURLRequest *request = [ARRouter newFeedRequestWithCursor:cursor pageSize:pageSize]; + [self _getFeedWithURLRequest:request cursor:cursor success:success failure:failure]; +} + ++ (void)getFeedResultsForProfile:(Profile *)profile withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure +{ + NSInteger pageSize = (cursor) ? 5 : 10; + NSURLRequest *request = [ARRouter newPostsRequestForProfile:profile WithCursor:cursor pageSize:pageSize]; + [self _getFeedWithURLRequest:request cursor:cursor success:success failure:failure]; +} + ++ (void)getFeedResultsForFairOrganizer:(FairOrganizer *)fairOrganizer withCursor:(NSString *)cursor success:(void (^)(id))success failure:(void (^)(NSError *))failure +{ + NSInteger pageSize = (cursor) ? 5 : 10; + NSURLRequest *request = [ARRouter newPostsRequestForFairOrganizer:fairOrganizer WithCursor:cursor pageSize:pageSize]; + [self _getFeedWithURLRequest:request cursor:cursor success:success failure:failure]; +} + ++ (void)getFeedResultsForShowsWithCursor:(NSString *)cursor pageSize:(NSInteger)pageSize success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newShowFeedRequestWithCursor:cursor pageSize:pageSize]; + [self _getFeedWithURLRequest:request cursor:cursor success:success failure:failure]; +} + ++ (void)getFeedResultsForFairShows:(Fair *)fair withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure +{ + return [self getFeedResultsForFairShows:fair partnerID:nil withCursor:cursor success:success failure:failure]; +} + ++ (void)getFeedResultsForFairShows:(Fair *)fair partnerID:(NSString *)partnerID withCursor:(NSString *)cursor success:(void (^)(id JSON))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFairShowFeedRequestWithFair:fair partnerID:partnerID cursor:cursor pageSize:15]; + [self _getFeedWithURLRequest:request cursor:cursor success:success failure:failure]; +} + ++ (void)_getFeedWithURLRequest:(NSURLRequest *)request cursor:(NSString *)cursor + success:(void (^)(id JSON))success + failure:(void (^)(NSError *error))failure +{ + __weak AFJSONRequestOperation *feedOperation = nil; + feedOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^ (NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + + BOOL isFirstPageOfMainFeed = (!cursor); + if (success) { + success(JSON); + } + if(isFirstPageOfMainFeed) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *path = [ARFileUtils userDocumentsPathWithFile:request.URL.absoluteString]; + [feedOperation.responseData writeToFile:path options:NSDataWritingAtomic error:nil]; + }); + } + } failure:^ (NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + [ArtsyAPI handleXappTokenError:error]; + if (failure) { + failure(error); + } + }]; + [feedOperation start]; +} + ++ (void)getFeaturedWorks:(void (^)(NSArray *works))success failure:(void (^)(NSError *error))failure +{ + [self getOrderedSetItemsWithKey:@"homepage:featured-artworks" success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Following.h b/Artsy/Classes/Networking/ArtsyAPI+Following.h new file mode 100644 index 00000000000..3ef898ba74a --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Following.h @@ -0,0 +1,37 @@ +#import "ArtsyAPI.h" + +@class Gene, Artist, Artwork; + +@interface ArtsyAPI (Following) + +#pragma mark - Artwork + ++ (void)setFavoriteStatus:(BOOL)status forArtwork:(Artwork *)artwork success:(void (^)(id response))success failure:(void (^)(NSError *error))failure; + ++ (void)checkFavoriteStatusForArtwork:(Artwork *)artwork + success:(void (^)(BOOL result))success + failure:(void (^)(NSError *error))failure; + +#pragma mark - Artist + ++ (void)checkFavoriteStatusForArtist:(Artist *)artist + success:(void (^)(BOOL result))success + failure:(void (^)(NSError *error))failure; + ++ (void)setFavoriteStatus:(BOOL)status + forArtist:(Artist *)artist + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure; + +#pragma mark - Gene + ++ (void)checkFavoriteStatusForGene:(Gene *)gene + success:(void (^)(BOOL result))success + failure:(void (^)(NSError *error))failure; + ++ (void)setFavoriteStatus:(BOOL)status + forGene:(Gene *)gene + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Following.m b/Artsy/Classes/Networking/ArtsyAPI+Following.m new file mode 100644 index 00000000000..dee2377e34f --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Following.m @@ -0,0 +1,151 @@ +#import "ARRouter.h" +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Following) + ++ (void)setFavoriteStatus:(BOOL)status + forArtwork:(Artwork *)artwork + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newSetArtworkFavoriteRequestForArtwork:artwork status:status]; + [self performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)checkFavoriteStatusForArtwork:(Artwork *)artwork + success:(void (^)(BOOL result))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newCheckFavoriteStatusRequestForArtwork:artwork]; + [self performRequest:request success:^(NSArray *response) { + if (success) { + success([response count] > 0); + } + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (response.statusCode == 404) { + if (success) { + success(NO); + } + } else if (failure) { + failure(error); + } + }]; +} + +#pragma mark - Artist + ++ (void)setFavoriteStatus:(BOOL)status + forArtist:(Artist *)artist + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure +{ + if (status) { + [self followArtist:artist success:success failure:failure]; + } else { + [self unFollowArtist:artist success:success failure:failure]; + } +} + ++ (void)followArtist:(Artist *)artist + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFollowArtistRequest:artist]; + [self performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)unFollowArtist:(Artist *)artist + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newUnfollowArtistRequest:artist]; + [self performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)checkFavoriteStatusForArtist:(Artist *)artist + success:(void (^)(BOOL result))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFollowingRequestForArtist:artist]; + [self performRequest:request success:^(NSArray *response) { + if (success) { + success([response count] > 0); + } + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (response.statusCode == 404) { + if (success) { + success(NO); + } + } else if (failure) { + failure(error); + } + }]; +} + +#pragma mark - Genes + ++ (void)setFavoriteStatus:(BOOL)status + forGene:(Gene *)gene + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure +{ + if (status) { + [self followGene:gene success:success failure:failure]; + } else { + [self unFollowGene:gene success:success failure:failure]; + } +} + ++ (void)followGene:(Gene *)gene + success:(void (^)(id response))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFollowGeneRequest:gene]; + [self performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)unFollowGene:(Gene *)gene success:(void (^)(id response))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newUnfollowGeneRequest:gene]; + [self performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)checkFavoriteStatusForGene:(Gene *)gene success:(void (^)(BOOL result))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newFollowingRequestForGene:gene]; + [self performRequest:request success:^(NSArray *response) { + if (success) { + success([response count] > 0); + } + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (response.statusCode == 404) { + if (success) { + success(NO); + } + } else if (failure) { + failure(error); + } + }]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Genes.h b/Artsy/Classes/Networking/ArtsyAPI+Genes.h new file mode 100644 index 00000000000..9f29f2f0207 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Genes.h @@ -0,0 +1,5 @@ +@interface ArtsyAPI (Genes) + ++ (void)getGeneForGeneID:(NSString *)geneID success:(void (^)(id gene))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Genes.m b/Artsy/Classes/Networking/ArtsyAPI+Genes.m new file mode 100644 index 00000000000..b4e4bbc9816 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Genes.m @@ -0,0 +1,11 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Genes) + ++ (void)getGeneForGeneID:(NSString *)geneID success:(void (^)(id gene))success failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + [self getRequest:[ARRouter newGeneInfoRequestWithID:geneID] parseIntoAClass:[Gene class] success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+ListCollection.h b/Artsy/Classes/Networking/ArtsyAPI+ListCollection.h new file mode 100644 index 00000000000..42f62e1d80a --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+ListCollection.h @@ -0,0 +1,15 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (ListCollection) + ++ (void)getGenesFromPersonalCollectionAtPage:(NSInteger)page success:(void (^)(NSArray *genes))success failure:(void (^)(NSError *error))failure; + ++ (void)getGenesFromPersonalCollectionCount:(void (^)(NSNumber *count))success failure:(void (^)(NSError *error))failure; + ++ (void)getArtistsFromPersonalCollectionAtPage:(NSInteger)page success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure; + ++ (void)getArtistsFromPersonalCollectionCount:(void (^)(NSNumber *count))success failure:(void (^)(NSError *error))failure; + ++ (void)getArtistsFromSampleAtPage:(NSInteger)page success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+ListCollection.m b/Artsy/Classes/Networking/ArtsyAPI+ListCollection.m new file mode 100644 index 00000000000..53245022489 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+ListCollection.m @@ -0,0 +1,63 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (ListCollection) + ++ (void)getGenesFromPersonalCollectionCount:(void (^)(NSNumber *count))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newGeneCountFromPersonalCollectionRequest]; + [self getCountForCollectionFromRequest:request success:success failure:failure]; +} + ++ (void)getGenesFromPersonalCollectionAtPage:(NSInteger)page success:(void (^)(NSArray *genes))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newGenesFromPersonalCollectionAtPage:page]; + [ArtsyAPI getRequest:request parseIntoAnArrayOfClass:[Gene class] withKey:@"gene" success:success failure:failure]; +} + ++ (void)getArtistsFromPersonalCollectionCount:(void (^)(NSNumber *count))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtistCountFromPersonalCollectionRequest]; + [self getCountForCollectionFromRequest:request success:success failure:failure]; +} + ++ (void)getArtistsFromPersonalCollectionAtPage:(NSInteger)page success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtistsFromPersonalCollectionAtPage:page]; + [ArtsyAPI getRequest:request parseIntoAnArrayOfClass:[Artist class] withKey:@"artist" success:success failure:failure]; +} + ++ (void)getArtistsFromSampleAtPage:(NSInteger)page success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtistsFromSampleAtPage:page]; + [ArtsyAPI getRequest:request parseIntoAnArrayOfClass:[Artist class] success:success failure:failure]; +} + ++ (void)getCountForCollectionFromRequest:(NSURLRequest *)request success:(void (^)(NSNumber *count))success failure:(void (^)(NSError *error))failure +{ + __weak AFJSONRequestOperation *setsOperation = nil; + setsOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSDictionary *JSON) { + NSString *stringCount = response.allHeaderFields[@"X-Total-Count"]; + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; + NSNumber *count = [formatter numberFromString:stringCount]; + + if (count) { + if (success) { + success(count); + } + } else { + if (failure) { + failure(nil); + } + } + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (failure) { + failure(error); + } + }]; + + [setsOperation start]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+OrderedSets.h b/Artsy/Classes/Networking/ArtsyAPI+OrderedSets.h new file mode 100644 index 00000000000..967555c38d4 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+OrderedSets.h @@ -0,0 +1,32 @@ +@interface ArtsyAPI (OrderedSets) + ++ (AFJSONRequestOperation *)getOrderedSetsWithOwnerType:(NSString *)ownerType + andID:(NSString *)ownerID + success:(void (^)(NSMutableDictionary *orderedSets))success + failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *) getOrderedSetWithKey:(NSString *)key + success:(void (^)(OrderedSet *set))success + failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *) getOrderedSetItemsWithKey:(NSString *)key + success:(void (^)(NSArray *items))success + failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *) getOrderedSetItemsWithKey:(NSString *)key + andName:(NSString *)name + success:(void (^)(NSArray *items))success + failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getOrderedSetItems:(NSString *)orderedSetID + withType:(Class)class + success:(void (^)(NSArray *orderedSets))success + failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getOrderedSetItems:(NSString *)orderedSetID + atPage:(NSInteger)page + withType:(Class)class + success:(void (^)(NSArray *orderedSets))success + failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+OrderedSets.m b/Artsy/Classes/Networking/ArtsyAPI+OrderedSets.m new file mode 100644 index 00000000000..de8cfb00a94 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+OrderedSets.m @@ -0,0 +1,80 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (OrderedSets) + ++ (AFJSONRequestOperation *)getOrderedSetsWithOwnerType:(NSString *)ownerType + andID:(NSString *)ownerID + success:(void (^)(NSMutableDictionary *orderedSets))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter orderedSetsWithOwnerType:ownerType andID:ownerID]; + return [self getRequest:request parseIntoAnArrayOfClass:[OrderedSet class] success:^(NSArray * orderedSets) { + NSMutableDictionary *orderedSetsByKey = [[NSMutableDictionary alloc] init]; + for (OrderedSet * orderedSet in orderedSets) { + NSArray *sets = orderedSetsByKey[orderedSet.key] ?: @[]; + orderedSetsByKey[orderedSet.key] = [sets arrayByAddingObject:orderedSet]; + } + if (success) { + success(orderedSetsByKey); + } + } failure:failure]; +} + ++ (AFJSONRequestOperation *) getOrderedSetWithKey:(NSString *)key + success:(void (^)(OrderedSet *set))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter orderedSetsWithKey:key]; + return [self getRequest:request parseIntoAnArrayOfClass:[OrderedSet class] success:^(NSArray *orderedSets) { + return success(orderedSets.firstObject); + } failure:failure]; +} + ++ (AFJSONRequestOperation *) getOrderedSetItemsWithKey:(NSString *)key + success:(void (^)(NSArray *sets))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter orderedSetsWithKey:key]; + return [self getRequest:request parseIntoAnArrayOfClass:[OrderedSet class] success:^(NSArray *orderedSets) { + for (OrderedSet *orderedSet in orderedSets) { + [orderedSet getItems:success]; + } + } failure:failure]; +} + ++ (AFJSONRequestOperation *) getOrderedSetItemsWithKey:(NSString *)key + andName:(NSString *)name + success:(void (^)(NSArray *items))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter orderedSetsWithKey:key]; + return [self getRequest:request parseIntoAnArrayOfClass:[OrderedSet class] success:^(NSArray *orderedSets) { + for (OrderedSet *orderedSet in orderedSets) { + if ([orderedSet.name isEqualToString:name]) { + [orderedSet getItems:success]; + } + } + } failure:failure]; +} + ++ (AFJSONRequestOperation *)getOrderedSetItems:(NSString *)orderedSetID + withType:(Class)class + success:(void (^)(NSArray *orderedSets))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter orderedSetItems:orderedSetID]; + return [self getRequest:request parseIntoAnArrayOfClass:class success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getOrderedSetItems:(NSString *)orderedSetID + atPage:(NSInteger)page + withType:(Class)class + success:(void (^)(NSArray *orderedSets))success + failure:(void (^)(NSError *error))failure; +{ + NSURLRequest *request = [ARRouter orderedSetItems:orderedSetID atPage:page]; + return [self getRequest:request parseIntoAnArrayOfClass:class success:success failure:failure]; + +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Posts.h b/Artsy/Classes/Networking/ArtsyAPI+Posts.h new file mode 100644 index 00000000000..43bdd552f68 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Posts.h @@ -0,0 +1,5 @@ +@interface ArtsyAPI (Posts) + ++ (void)getPostForPostID:(NSString *)postID success:(void (^)(Post *post))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Posts.m b/Artsy/Classes/Networking/ArtsyAPI+Posts.m new file mode 100644 index 00000000000..70872c1521e --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Posts.m @@ -0,0 +1,11 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Posts) + ++ (void)getPostForPostID:(NSString *)postID success:(void (^)(Post *post))success failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + [self getRequest:[ARRouter newPostInfoRequestWithID:postID] parseIntoAClass:[Post class] success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Private.h b/Artsy/Classes/Networking/ArtsyAPI+Private.h new file mode 100644 index 00000000000..dda5f355d64 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Private.h @@ -0,0 +1,23 @@ +#import "ARRouter.h" + +@interface ArtsyAPI (Private) + +/// A simple method for performing a ARJSONRequest and passing back the returned JSON as a native object ++ (AFJSONRequestOperation *)performRequest:(NSURLRequest *)request success:(void (^)(id))success failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure; + ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAClass:(Class)klass success:(void (^)(id))success failure:(void (^)(NSError *error))failure; ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAnArrayOfClass:(Class)klass success:(void (^)(NSArray *))success failure:(void (^)(NSError *error))failure; + +/// If the object you're after is hidden behind a key, this function is for you ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAClass:(Class)klass withKey:(NSString *)key success:(void (^)(id))success failure:(void (^)(NSError *error))failure; + +/// If the array you're after is hidden behind a key, this function is for you ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAnArrayOfClass:(Class)klass withKey:(NSString *)key success:(void (^)(NSArray *))success failure:(void (^)(NSError *error))failure; + +/// If you're dealing with dictionary as the root object and want an array of objects ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAnArrayOfClass:(Class)klass fromDictionaryWithKey:(NSString *)key success:(void (^)(NSArray *))success failure:(void (^)(NSError *error))failure; + ++ (void)getXappTokenWithCompletion:(void(^)(NSString *xappToken, NSDate *expirationDate))callback; ++ (void)getXappTokenWithCompletion:(void(^)(NSString *xappToken, NSDate *expirationDate))callback failure:(void (^)(NSError *error))failure; ++ (void)handleXappTokenError:(NSError *)error; +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Profiles.h b/Artsy/Classes/Networking/ArtsyAPI+Profiles.h new file mode 100644 index 00000000000..9eb02a2cb22 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Profiles.h @@ -0,0 +1,11 @@ +@interface ArtsyAPI (Profiles) + ++ (void)getProfileForProfileID:(NSString *)profileID success:(void (^)(Profile *profile))success failure:(void (^)(NSError *error))failure; + ++ (void)checkFollowProfile:(Profile *)profile success:(void (^)(BOOL doesFollow))success failure:(void (^)(NSError *error))failure; + ++ (void)followProfile:(Profile *)profile success:(void (^)(Profile *returnedProfile))success failure:(void (^)(NSError *error))failure; + ++ (void)unfollowProfile:(Profile *)profile success:(void (^)(Profile *returnedProfile))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Profiles.m b/Artsy/Classes/Networking/ArtsyAPI+Profiles.m new file mode 100644 index 00000000000..8e5cabda4ac --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Profiles.m @@ -0,0 +1,66 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Profiles) + ++ (void)getProfileForProfileID:(NSString *)profileID success:(void (^)(Profile *profile))success failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + [self getRequest:[ARRouter newProfileInfoRequestWithID:profileID] parseIntoAClass:[Profile class] success:success failure:failure]; +} + ++ (void)checkFollowProfile:(Profile *)profile success:(void (^)(BOOL doesFollow))success failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + NSURLRequest *headRequest = [ARRouter newCheckFollowingProfileHeadRequest:profile.profileID]; + + [self performRequest:headRequest success:^(NSArray *response) { + if (success) { + success([response count] > 0); + } + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (response.statusCode == 404) { + if (success) { + success(NO); + } + } else failure(error); + }]; +} + ++ (void)followProfile:(Profile *)profile success:(void (^)(Profile *returnedProfile))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newMyFollowProfileRequest:profile.profileID]; + + [self getRequest:request parseIntoAClass:[Profile class] success:^(Profile *returnedProfile) { + if (success) { + success(returnedProfile); + } + + } failure:^(NSError *error) { + + ARErrorLog(@"Could not Follow Profile: %@", error.localizedDescription); + if (failure) { failure(error); } + }]; +} + ++ (void)unfollowProfile:(Profile *)profile success:(void (^)(Profile *returnedProfile))success failure:(void (^)(NSError *error))failure { + NSURLRequest *request = [ARRouter newMyUnfollowProfileRequest:[profile profileID]]; + + [self getRequest:request parseIntoAClass:[Profile class] success:^(Profile *returnedProfile) { + if (success) { + success(returnedProfile); + } + } failure:^(NSError *error) { + [ArtsyAPI handleHTTPError:error statusCode:404 errorMessage:@"Profile Not Followed" success:^(NSError *error) { + if (success) { + success(profile); + } + } failure:^(NSError *error) { + ARErrorLog(@"Could not Unfollow Profile: %@", error.localizedDescription); + if (failure) { + failure(error); + } + }]; + }]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+RelatedModels.h b/Artsy/Classes/Networking/ArtsyAPI+RelatedModels.h new file mode 100644 index 00000000000..cfa3585ae85 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+RelatedModels.h @@ -0,0 +1,13 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (RelatedModels) + ++ (AFJSONRequestOperation *)getRelatedArtistsForArtist:(Artist *)artist success:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getRelatedArtworksForArtwork:(Artwork *)artwork success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; ++ (AFJSONRequestOperation *)getRelatedArtworksForArtwork:(Artwork *)artwork inFair:(Fair *)fair success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getRelatedPostsForArtwork:(Artwork *)artwork success:(void (^)(NSArray *posts))success failure:(void (^)(NSError *error))failure; ++ (AFJSONRequestOperation *)getRelatedPostsForArtist:(Artist *)artist success:(void (^)(NSArray *posts))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+RelatedModels.m b/Artsy/Classes/Networking/ArtsyAPI+RelatedModels.m new file mode 100644 index 00000000000..2833224d052 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+RelatedModels.m @@ -0,0 +1,47 @@ +#import "ArtsyAPI+Private.h" +#import "ARPostFeedItem.h" + +@implementation ArtsyAPI (RelatedModels) + ++ (AFJSONRequestOperation *)getRelatedArtistsForArtist:(Artist *)artist + success:(void (^)(NSArray *artists))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtistsRelatedToArtistRequest:artist]; + return [self getRequest:request parseIntoAnArrayOfClass:[Artist class] fromDictionaryWithKey:@"best_matches" success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getRelatedArtworksForArtwork:(Artwork *)artwork + success:(void (^)(NSArray *artworks))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworksRelatedToArtworkRequest:artwork]; + return [self getRequest:request parseIntoAnArrayOfClass:[Artwork class] success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getRelatedArtworksForArtwork:(Artwork *)artwork + inFair:(Fair *)fair + success:(void (^)(NSArray *))success + failure:(void (^)(NSError *))failure +{ + NSURLRequest *request = [ARRouter newArtworksRelatedToArtwork:artwork inFairRequest:fair]; + return [self getRequest:request parseIntoAnArrayOfClass:[Artwork class] success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getRelatedPostsForArtwork:(Artwork *)artwork + success:(void (^)(NSArray *posts))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newPostsRelatedToArtwork:artwork]; + return [self getRequest:request parseIntoAnArrayOfClass:[ARPostFeedItem class] success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getRelatedPostsForArtist:(Artist *)artist + success:(void (^)(NSArray *posts))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newPostsRelatedToArtist:artist]; + return [self getRequest:request parseIntoAnArrayOfClass:[ARPostFeedItem class] success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Sales.h b/Artsy/Classes/Networking/ArtsyAPI+Sales.h new file mode 100644 index 00000000000..d397dafbfb3 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Sales.h @@ -0,0 +1,13 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (Sales) + ++ (void)getSalesWithArtwork:(NSString *)artworkID + success:(void (^)(NSArray *sales))success + failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getArtworksForSale:(NSString *)saleID + success:(void (^)(NSArray *artworks))success + failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Sales.m b/Artsy/Classes/Networking/ArtsyAPI+Sales.m new file mode 100644 index 00000000000..1a8278d682b --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Sales.m @@ -0,0 +1,21 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Sales) + ++ (void)getSalesWithArtwork:(NSString *)artworkID + success:(void (^)(NSArray *sales))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter salesWithArtworkRequest:artworkID]; + [self getRequest:request parseIntoAnArrayOfClass:[Sale class] success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getArtworksForSale:(NSString *)saleID + success:(void (^)(NSArray *))success + failure:(void (^)(NSError *))failure +{ + NSURLRequest *request = [ARRouter artworksForSaleRequest:saleID]; + return [self getRequest:request parseIntoAnArrayOfClass:[Artwork class] withKey:@"artwork" success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Search.h b/Artsy/Classes/Networking/ArtsyAPI+Search.h new file mode 100644 index 00000000000..26e8f14cce5 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Search.h @@ -0,0 +1,9 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (Search) + ++ (AFJSONRequestOperation *)searchWithQuery:(NSString *)query success:(void(^)(NSArray *results))success failure:(void (^)(NSError *error))failure; ++ (AFJSONRequestOperation *)searchWithFairID:(NSString *)fairID andQuery:(NSString *)query success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure; ++ (AFJSONRequestOperation *)artistSearchWithQuery:(NSString *)query success:(void(^)(NSArray *results))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Search.m b/Artsy/Classes/Networking/ArtsyAPI+Search.m new file mode 100644 index 00000000000..ee0d736e4d5 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Search.m @@ -0,0 +1,77 @@ +#import "ARRouter.h" +#import "SearchResult.h" + +@implementation ArtsyAPI (Search) + ++ (AFJSONRequestOperation *)searchWithQuery:(NSString *)query success:(void(^)(NSArray *results))success failure:(void (^)(NSError *error))failure +{ + return [self searchWithFairID:nil andQuery:query success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)searchWithFairID:(NSString *)fairID andQuery:(NSString *)query success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + NSParameterAssert(success); + + NSURLRequest *request = fairID ? [ARRouter newSearchRequestWithFairID:fairID andQuery:query] : [ARRouter newSearchRequestWithQuery:query]; + AFJSONRequestOperation *searchOperation = nil; + searchOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSArray *jsonDictionaries = JSON; + NSMutableArray *returnArray = [NSMutableArray array]; + + for (NSDictionary *dictionary in jsonDictionaries) { + if ([SearchResult searchResultIsSupported:dictionary]) { + NSError *error = nil; + SearchResult *result = [[SearchResult class] modelWithJSON:dictionary error:&error]; + if (error) { + ARErrorLog(@"Error creating search result. Error: %@", error.localizedDescription); + } else { + [returnArray addObject:result]; + } + } + } + + success(returnArray); + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (failure) { + failure(error); + } + }]; + + [searchOperation start]; + return searchOperation; +} + ++ (AFJSONRequestOperation *)artistSearchWithQuery:(NSString *)query success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + NSParameterAssert(success); + + NSURLRequest *request = [ARRouter newArtistSearchRequestWithQuery:query]; + AFJSONRequestOperation *searchOperation = nil; + searchOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSArray *jsonDictionaries = JSON; + NSMutableArray *returnArray = [NSMutableArray array]; + + for (NSDictionary *dictionary in jsonDictionaries) { + NSError *error = nil; + Artist *result = [Artist modelWithJSON:dictionary error:&error]; + if (error) { + ARErrorLog(@"Error creating search result. Error: %@", error.localizedDescription); + } else { + [returnArray addObject:result]; + } + } + + success(returnArray); + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (failure) { + failure(error); + } + }]; + + [searchOperation start]; + return searchOperation; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Shows.h b/Artsy/Classes/Networking/ArtsyAPI+Shows.h new file mode 100644 index 00000000000..7f7f39f3174 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Shows.h @@ -0,0 +1,9 @@ +@interface ArtsyAPI (Shows) + ++ (AFJSONRequestOperation *)getShowInfo:(PartnerShow *)show success:(void (^)(PartnerShow *show))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getArtworksForShow:(PartnerShow *)show atPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + ++ (AFJSONRequestOperation *)getImagesForShow:(PartnerShow *)show atPage:(NSInteger)page success:(void (^)(NSArray *images))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+Shows.m b/Artsy/Classes/Networking/ArtsyAPI+Shows.m new file mode 100644 index 00000000000..c7039bacabd --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+Shows.m @@ -0,0 +1,23 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (Shows) + ++ (AFJSONRequestOperation *)getShowInfo:(PartnerShow *)show success:(void (^)(PartnerShow *show))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newShowInfoRequestWithID:show.showID]; + return [self getRequest:request parseIntoAClass:PartnerShow.class success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getArtworksForShow:(PartnerShow *)show atPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworksFromShowRequest:show atPage:page]; + return [self getRequest:request parseIntoAnArrayOfClass:Artwork.class success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getImagesForShow:(PartnerShow *)show atPage:(NSInteger)page success:(void (^)(NSArray *images))success failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newImagesFromShowRequest:show atPage:page]; + return [self getRequest:request parseIntoAnArrayOfClass:Image.class success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+SiteFunctions.h b/Artsy/Classes/Networking/ArtsyAPI+SiteFunctions.h new file mode 100644 index 00000000000..041e59f3957 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+SiteFunctions.h @@ -0,0 +1,42 @@ +#import "ArtsyAPI.h" + +extern NSString * const ArtsyAPIInquiryAnalyticsInquiryURL; // Who made the inquiry +extern NSString * const ArtsyAPIInquiryAnalyticsReferralURL; // Where they came from before Artsy.app +extern NSString * const ArtsyAPIInquiryAnalyticsLandingURL; // Where the first went + +@class User, Profile; +@interface ArtsyAPI (SiteFunctions) + +/// Gets the Hero Units from the site. + ++ (void)getSiteHeroUnits:(void (^)(NSArray *heroUnits))success failure:(void (^)(NSError *error))failure; + +/// Get an Inquiry Contact, initially returns with a stubbed User and then will return with a full user profile +/// allowing you to get the name quickly and then later having full access to the whole stack of data + ++ (void)getInquiryContact:(void (^)(User *contactStub))success + withProfile:(void (^)(Profile *contactProfile))profile + failure:(void (^)(NSError *error))failure; + +/// Send a request to an Artsy Partner inquiring about an artwork ++ (void)createPartnerArtworkInquiryForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + analyticsDictionary:(NSDictionary *)analyticsDictionary + success:(void (^)(id message))success + failure:(void (^)(NSError *error))failure; + +/// Send a request to an Artsy Representative inquiring about an artwork ++ (void)createRepresentativeArtworkInquiryForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + analyticsDictionary:(NSDictionary *)analyticsDictionary + success:(void (^)(id message))success + failure:(void (^)(NSError *error))failure; + +/// Get "site features" (essentially site-wide labs) ++ (void)getSiteFeatures:(void (^)(NSArray *features))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+SiteFunctions.m b/Artsy/Classes/Networking/ArtsyAPI+SiteFunctions.m new file mode 100644 index 00000000000..99d7e1569df --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+SiteFunctions.m @@ -0,0 +1,92 @@ +#import "ArtsyAPI+Private.h" + +NSString * const ArtsyAPIInquiryAnalyticsInquiryURL = @"ArtsyAPIInquiryAnalyticsInquiryURL"; +NSString * const ArtsyAPIInquiryAnalyticsReferralURL = @"ArtsyAPIInquiryAnalyticsReferralURL"; +NSString * const ArtsyAPIInquiryAnalyticsLandingURL = @"ArtsyAPIInquiryAnalyticsLandingURL"; + +@implementation ArtsyAPI (SiteFunctions) + ++ (void)getSiteHeroUnits:(void (^)(NSArray *heroUnits))success failure:(void (^)(NSError *error))failure { + NSURLRequest *request = [ARRouter newSiteHeroUnitsRequest]; + [self getRequest:request parseIntoAnArrayOfClass:[SiteHeroUnit class] success:success failure:failure]; +} + ++ (void)getInquiryContact:(void (^)(User *contactStub))success + withProfile:(void (^)(Profile *contactProfile))profile + failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + @weakify(self); + + NSURLRequest *request = [ARRouter newOnDutyRepresentativeRequest]; + [self performRequest:request success:^(NSArray *results) { + @strongify(self); + if ([results count] == 0) { + success(nil); + } else { + NSError *error = nil; + User *contact = [User modelWithJSON:results[0] error:&error]; + if (error) { + ARErrorLog(@"Error parsing the admin on duty"); + success(nil); + return; + } + + success(contact); + + NSURLRequest *profileRequest = [ARRouter newProfileInfoRequestWithID:contact.defaultProfileID]; + [self getRequest:profileRequest parseIntoAClass:[Profile class] success:profile failure:failure]; + } + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)createPartnerArtworkInquiryForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + analyticsDictionary:(NSDictionary *)analyticsDictionary + success:(void (^)(id message))success + failure:(void (^)(NSError *error))failure +{ + [self createArtworkInquiryForArtwork:artwork name:name email:email message:message shouldContactGallery:YES analyticsDictionary:analyticsDictionary success:success failure:failure]; +} + ++ (void)createRepresentativeArtworkInquiryForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + analyticsDictionary:(NSDictionary *)analyticsDictionary + success:(void (^)(id message))success + failure:(void (^)(NSError *error))failure +{ + [self createArtworkInquiryForArtwork:artwork name:name email:email message:message shouldContactGallery:NO analyticsDictionary:analyticsDictionary success:success failure:failure]; +} + ++ (void)createArtworkInquiryForArtwork:(Artwork *)artwork + name:(NSString *)name + email:(NSString *)email + message:(NSString *)message + shouldContactGallery:(BOOL)shouldContactGallery + analyticsDictionary:(NSDictionary *)analyticsDictionary + success:(void (^)(id message))success + failure:(void (^)(NSError *error))failure +{ + NSURLRequest *request = [ARRouter newArtworkInquiryRequestForArtwork:artwork name:name email:email message:message analyticsDictionary:analyticsDictionary shouldContactGallery:shouldContactGallery]; + [self performRequest:request success:success failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { + if (failure) { + failure(error); + } + }]; +} + ++ (void)getSiteFeatures:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + NSURLRequest *request = [ARRouter newSiteFeaturesRequest]; + [self getRequest:request parseIntoAnArrayOfClass:[SiteFeature class] success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+SystemTime.h b/Artsy/Classes/Networking/ArtsyAPI+SystemTime.h new file mode 100644 index 00000000000..afe5860bc47 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+SystemTime.h @@ -0,0 +1,7 @@ +#import "ArtsyAPI.h" + +@interface ArtsyAPI (SystemTime) + ++ (void)getSystemTime:(void (^)(SystemTime *systemTime))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI+SystemTime.m b/Artsy/Classes/Networking/ArtsyAPI+SystemTime.m new file mode 100644 index 00000000000..196301d91d7 --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI+SystemTime.m @@ -0,0 +1,11 @@ +#import "ArtsyAPI+Private.h" + +@implementation ArtsyAPI (SystemTime) + ++ (void)getSystemTime:(void (^)(SystemTime *systemTime))success failure:(void (^)(NSError *error))failure +{ + [self getRequest:[ARRouter newSystemTimeRequest] parseIntoAClass:[SystemTime class] success:success failure:failure]; +} + + +@end diff --git a/Artsy/Classes/Networking/ArtsyAPI.h b/Artsy/Classes/Networking/ArtsyAPI.h new file mode 100644 index 00000000000..8870aec125d --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI.h @@ -0,0 +1,23 @@ +@interface ArtsyAPI : NSObject +@end + +#import "ArtsyAPI+Artworks.h" +#import "ArtsyAPI+Browse.h" +#import "ArtsyAPI+CurrentUserFunctions.h" +#import "ArtsyAPI+DeviceTokens.h" +#import "ArtsyAPI+Feed.h" +#import "ArtsyAPI+Following.h" +#import "ArtsyAPI+Profiles.h" +#import "ArtsyAPI+Posts.h" +#import "ArtsyAPI+Artists.h" +#import "ArtsyAPI+Genes.h" +#import "ArtsyAPI+ListCollection.h" +#import "ArtsyAPI+RelatedModels.h" +#import "ArtsyAPI+SiteFunctions.h" +#import "ArtsyAPI+Search.h" +#import "ArtsyAPI+Sales.h" +#import "ArtsyAPI+Fairs.h" +#import "ArtsyAPI+OrderedSets.h" +#import "ArtsyAPI+Shows.h" +#import "ArtsyAPI+SystemTime.h" +#import "ArtsyAPI+ErrorHandlers.h" diff --git a/Artsy/Classes/Networking/ArtsyAPI.m b/Artsy/Classes/Networking/ArtsyAPI.m new file mode 100644 index 00000000000..937c42d395a --- /dev/null +++ b/Artsy/Classes/Networking/ArtsyAPI.m @@ -0,0 +1,221 @@ +#import "ArtsyAPI+Private.h" +#import +#import + +@implementation ArtsyAPI + ++ (AFJSONRequestOperation *)performRequest:(NSURLRequest *)request success:(void (^)(id))success failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure +{ + NSParameterAssert(success); + + __weak AFJSONRequestOperation *performOperation = nil; + performOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^ (NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + success(JSON); + } + failure:^ (NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + if (failure) { + [ArtsyAPI handleXappTokenError:error]; + failure(request, response, error); + } + }]; + + [performOperation start]; + return performOperation; +} + ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAClass:(Class)klass success:(void (^)(id))success failure:(void (^)(NSError *error))failure { + return [self getRequest:request parseIntoAClass:klass withKey:nil success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAnArrayOfClass:(Class)klass success:(void (^)(NSArray *))success failure:(void (^)(NSError *error))failure { + return [self getRequest:request parseIntoAnArrayOfClass:klass withKey:nil success:success failure:failure]; +} + ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAClass:(Class)klass withKey:(NSString *)key success:(void (^)(id))success failure:(void (^)(NSError *error))failure +{ + __weak AFJSONRequestOperation *getOperation = nil; + getOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^ (NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + + NSDictionary *jsonDictionary = JSON; + id object = nil; + if (key) { + if (jsonDictionary[key]) { + object = [klass modelWithJSON:jsonDictionary[key] error:nil]; + } + } else { + object = [klass modelWithJSON:jsonDictionary error:nil]; + } + if (success) { + dispatch_async(dispatch_get_main_queue(), ^{ + success(object); + }); + } + } + failure:^ (NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + [ArtsyAPI handleXappTokenError:error]; + if (failure) { + failure(error); + } + }]; + getOperation.successCallbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [getOperation start]; + return getOperation; +} + ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAnArrayOfClass:(Class)klass withKey:(NSString *)key success:(void (^)(NSArray *))success failure:(void (^)(NSError *error))failure { + NSParameterAssert(success); + + __weak AFJSONRequestOperation *getOperation = nil; + getOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^ (NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSArray *jsonDictionaries = JSON; + NSMutableArray *returnArray = [NSMutableArray array]; + + for (NSDictionary *dictionary in jsonDictionaries) { + id object = nil; + if (![dictionary isKindOfClass:[NSDictionary class]]) { + DDLogDebug(@"skipping %@", dictionary); + continue; + } + + if (key) { + if ([dictionary.allKeys containsObject:key]) { + object = [klass modelWithJSON:dictionary[key] error:nil]; + } + } else { + object = [klass modelWithJSON:dictionary error:nil]; + } + + if (object) { + [returnArray addObject:object]; + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + success(returnArray); + }); + } + failure:^ (NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + [ArtsyAPI handleXappTokenError:error]; + if (failure) { + failure(error); + } + }]; + getOperation.successCallbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [getOperation start]; + return getOperation; +} + ++ (AFJSONRequestOperation *)getRequest:(NSURLRequest *)request parseIntoAnArrayOfClass:(Class)klass fromDictionaryWithKey:(NSString *)key success:(void (^)(NSArray *))success failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(success); + + __weak AFJSONRequestOperation *getOperation = nil; + getOperation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^ (NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSMutableArray *returnArray = [NSMutableArray array]; + + NSArray *jsonDictionaries = JSON[key]; + for (NSDictionary *dictionary in jsonDictionaries) { + id object = [klass modelWithJSON:dictionary error:nil]; + + if (object) { + [returnArray addObject:object]; + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + success(returnArray); + }); + } + failure:^ (NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + [ArtsyAPI handleXappTokenError:error]; + if (failure) { + failure(error); + } + }]; + getOperation.successCallbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [getOperation start]; + return getOperation; +} + +#pragma mark - +#pragma mark Xapp tokens + ++ (void)getXappTokenWithCompletion:(void(^)(NSString *xappToken, NSDate *expirationDate))callback +{ + [self getXappTokenWithCompletion:callback failure:nil]; +} + ++ (void)getXappTokenWithCompletion:(void(^)(NSString *xappToken, NSDate *expirationDate))callback failure:(void (^)(NSError *error))failure +{ + // Check if we already have a token for xapp or oauth and run the block + + NSDate *date = [[NSUserDefaults standardUserDefaults] objectForKey:ARXAppTokenExpiryDateDefault]; + NSString *xappToken = [UICKeyChainStore stringForKey:ARXAppTokenDefault]; + NSString *oauthToken = [UICKeyChainStore stringForKey:AROAuthTokenDefault]; + + if (date && (xappToken || oauthToken)) { + if (callback) { + callback(xappToken ?: oauthToken, date); + } + return; + } + + NSURLRequest *tokenRequest = [ARRouter newXAppTokenRequest]; + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:tokenRequest + success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + + NSString *token = JSON[ARXAppToken]; + NSString *date = JSON[AROExpiryDateKey]; + + ISO8601DateFormatter *dateFormatter = [[ISO8601DateFormatter alloc] init]; + NSDate *expiryDate = [dateFormatter dateFromString:date]; + + NSString *oldxToken = [UICKeyChainStore stringForKey:ARXAppTokenDefault]; + if (oldxToken) { + if (callback) { + callback(token, expiryDate); + } + return ; + } + + [ARRouter setXappToken:token]; + [UICKeyChainStore setString:token forKey:ARXAppTokenDefault]; + [[NSUserDefaults standardUserDefaults] setObject:expiryDate forKey:ARXAppTokenExpiryDateDefault]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + if (callback) { + callback(token, expiryDate); + } + + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + + //TODO: handle this less stupid + ARErrorLog(@"Couldn't get an Xapp token."); + + NSError *cleanError = [NSError errorWithDomain:@"Auth" code:404 userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't reach Artsy" }]; + [ARNetworkErrorManager presentActiveErrorModalWithError:cleanError]; + + if (failure) { failure(error); } + } + ]; + + [op start]; +} + +/** + * Reset XAPP token on an access denied. + * + * @param error AFNetworking error. + */ ++ (void)handleXappTokenError:(NSError *)error +{ + NSHTTPURLResponse *response = (NSHTTPURLResponse *) error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey]; + if (response.statusCode == 401) { + NSDictionary *recoverySuggestion = [NSJSONSerialization JSONObjectWithData:[error.userInfo[NSLocalizedRecoverySuggestionErrorKey] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; + if ([recoverySuggestion[@"error"] isEqualToString:@"Unauthorized"] && [recoverySuggestion[@"text"] isEqualToString:@"The XAPP token is invalid or has expired."]) { + NSLog(@"Resetting XAPP token after error: %@", error.localizedDescription); + [UICKeyChainStore removeItemForKey:ARXAppTokenDefault]; + [ARRouter setXappToken:nil]; + } + } +} + +@end diff --git a/Artsy/Classes/Networking/Mantle Extensions/MTLModel+Dictionary.h b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+Dictionary.h new file mode 100644 index 00000000000..fd13b40cede --- /dev/null +++ b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+Dictionary.h @@ -0,0 +1,7 @@ +#import "MTLModel.h" + +@interface MTLModel (Dictionary) + ++ (instancetype)modelFromDictionary:(NSDictionary *)JSONdictionary; + +@end diff --git a/Artsy/Classes/Networking/Mantle Extensions/MTLModel+Dictionary.m b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+Dictionary.m new file mode 100644 index 00000000000..4d035c5671a --- /dev/null +++ b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+Dictionary.m @@ -0,0 +1,14 @@ +@implementation MTLModel (Dictionary) + ++ (instancetype)modelFromDictionary:(NSDictionary *)dictionaryValue +{ + NSError *error = nil; + id instance = [self modelWithDictionary:dictionaryValue error:&error]; + if (!instance && error) { + [NSException raise:@"Error creating instance from dictionary" format:@"%@", error]; + } + return instance; +} + +@end + diff --git a/Artsy/Classes/Networking/Mantle Extensions/MTLModel+JSON.h b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+JSON.h new file mode 100644 index 00000000000..d848b6f4cc6 --- /dev/null +++ b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+JSON.h @@ -0,0 +1,11 @@ +#import "MTLModel.h" + +@interface MTLModel (JSON) + +/// There isn't a logical one-liner to create a JSON model without jumping to a non-relevant class. + ++ (NSArray *)arrayOfModelsWithJSON:(NSArray *)dictionaries; ++ (instancetype)modelWithJSON:(NSDictionary *)JSONdictionary; ++ (instancetype)modelWithJSON:(NSDictionary *)JSONdictionary error:(NSError **)error; + +@end diff --git a/Artsy/Classes/Networking/Mantle Extensions/MTLModel+JSON.m b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+JSON.m new file mode 100644 index 00000000000..5c7dc39a4b4 --- /dev/null +++ b/Artsy/Classes/Networking/Mantle Extensions/MTLModel+JSON.m @@ -0,0 +1,45 @@ +@implementation MTLModel (JSON) + ++ (NSArray *)arrayOfModelsWithJSON:(NSArray *)dictionaries +{ + return [dictionaries map:^id(NSDictionary *dictionary) { + return [self modelWithJSON:dictionary]; + }]; +} + + ++ (instancetype)modelWithJSON:(NSDictionary *)JSONdictionary +{ + NSError *error = nil; + id instance = [self modelWithJSON:JSONdictionary error:&error]; + if (!instance && error) { + [NSException raise:@"Error creating instance from JSON" format:@"%@", error]; + } + return instance; +} + ++ (instancetype)modelWithJSON:(NSDictionary *)JSONdictionary error:(NSError **)error +{ + id initialObject = [MTLJSONAdapter modelOfClass:self fromJSONDictionary:JSONdictionary error:error]; + + + // For some feed items we're given the feed item JSON and object it represents + // in the same JSON structure, to deal with this we allow declaring it as a host object. + // This means sending the same JSON data to property on the feed item class itself. + + if ([initialObject conformsToProtocol:@protocol(ARFeedHostItem)]) { + + Class secondaryObjectClass = [initialObject hostedObjectClass]; + SEL setSecondaryObject = [initialObject setHostPropertySelector]; + id secondObject = [secondaryObjectClass modelWithJSON:JSONdictionary error:error]; + + # pragma clang diagnostic push + # pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [initialObject performSelector:setSecondaryObject withObject:secondObject]; + # pragma clang diagnostic pop + } + + return initialObject; +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARArtistFavoritesNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARArtistFavoritesNetworkModel.h new file mode 100644 index 00000000000..605baa384ba --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARArtistFavoritesNetworkModel.h @@ -0,0 +1,5 @@ +#import "ARFavoritesNetworkModel.h" + +@interface ARArtistFavoritesNetworkModel : ARFavoritesNetworkModel + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARArtistFavoritesNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARArtistFavoritesNetworkModel.m new file mode 100644 index 00000000000..b97f1cdc3a0 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARArtistFavoritesNetworkModel.m @@ -0,0 +1,14 @@ +#import "ARArtistFavoritesNetworkModel.h" + +@implementation ARArtistFavoritesNetworkModel + +- (void)performNetworkRequestAtPage:(NSInteger)page withSuccess:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + if (self.useSampleFavorites) { + [ArtsyAPI getArtistsFromSampleAtPage:page success:success failure:failure]; + } else { + [ArtsyAPI getArtistsFromPersonalCollectionAtPage:page success:success failure:failure]; + } +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARArtistNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARArtistNetworkModel.h new file mode 100644 index 00000000000..0fe8be768c7 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARArtistNetworkModel.h @@ -0,0 +1,12 @@ +@interface ARArtistNetworkModel : NSObject + +- (instancetype)initWithArtist:(Artist *)artist; +@property (readonly, nonatomic, strong) Artist * artist; + +- (void)getArtistInfoWithSuccess:(void (^)(Artist *artist))success failure:(void (^)(NSError *error))failure; + +- (void)getArtistArtworksAtPage:(NSInteger)page params:(NSDictionary *)params success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + +- (void)setFavoriteStatus:(BOOL)status success:(void (^)(id response))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARArtistNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARArtistNetworkModel.m new file mode 100644 index 00000000000..fe8898427fb --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARArtistNetworkModel.m @@ -0,0 +1,32 @@ +#import "ARArtistNetworkModel.h" + +@implementation ARArtistNetworkModel + +- (instancetype)initWithArtist:(Artist *)artist +{ + self = [super init]; + if (!self) return nil; + + _artist = artist; + + return self; +} + +- (void)getArtistInfoWithSuccess:(void (^)(Artist *artist))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI getArtistForArtistID:self.artist.artistID success:success failure:failure]; +} + +- (void)getArtistArtworksAtPage:(NSInteger)page params:(NSDictionary *)params success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure + +{ + [ArtsyAPI getArtistArtworks:self.artist andPage:page withParams:params success:success failure:failure]; +} + +- (void)setFavoriteStatus:(BOOL)status success:(void (^)(id response))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI setFavoriteStatus:status forArtist:self.artist success:success failure:failure]; +} + + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARArtworkFavoritesNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARArtworkFavoritesNetworkModel.h new file mode 100644 index 00000000000..0ca1b8cb186 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARArtworkFavoritesNetworkModel.h @@ -0,0 +1,6 @@ +#import "ARFavoritesNetworkModel.h" + +@interface ARArtworkFavoritesNetworkModel : ARFavoritesNetworkModel + +@end + diff --git a/Artsy/Classes/Networking/Network Models/ARArtworkFavoritesNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARArtworkFavoritesNetworkModel.m new file mode 100644 index 00000000000..97255c60fa4 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARArtworkFavoritesNetworkModel.m @@ -0,0 +1,14 @@ +#import "ARArtworkFavoritesNetworkModel.h" + +@implementation ARArtworkFavoritesNetworkModel + +- (void)performNetworkRequestAtPage:(NSInteger)page withSuccess:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + // When in Demo mode we show the details for app-store-reviewer + // https://artsy.net/app-store-reviewer/favorites + + NSString *userID = self.useSampleFavorites ? @"502d15746e721400020006fa" : [User currentUser].userID ; + [ArtsyAPI getArtworkFromUserFavorites:userID page:page success:success failure:failure]; +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel+Private.h b/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel+Private.h new file mode 100644 index 00000000000..10acfd6f2c2 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel+Private.h @@ -0,0 +1,9 @@ +#import "ARFairFavoritesNetworkModel.h" + +@interface ARFairFavoritesNetworkModel () + +- (void)handleShowButtonPress:(PartnerShow *)show fair:(Fair *)fair; +- (void)handleArtworkButtonPress:(Artwork *)artwork fair:(Fair *)fair; +- (void)handleArtistButtonPress:(Artist *)artist fair:(Fair *)fair; + +@end \ No newline at end of file diff --git a/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel.h new file mode 100644 index 00000000000..9e0690c5b16 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel.h @@ -0,0 +1,23 @@ +#import + +@class ARFairFavoritesNetworkModel; + +@protocol ARFairFavoritesNetworkModelDelegate + +-(void)fairFavoritesNetworkModel:(ARFairFavoritesNetworkModel *)fairFavoritesNetworkModel shouldPresentViewController:(UIViewController *)viewController; + +@end + +@interface ARFairFavoritesNetworkModel : NSObject + +- (void)getFavoritesForNavigationsButtonsForFair:(Fair *)fair + artwork:(void (^)(NSArray *artworks))work + exhibitors:(void (^)(NSArray *exhibitorsArray))exhibitors + artists:(void (^)(NSArray *artistsArray))artists + failure:(void (^)(NSError *error))failure; + +@property (readonly, nonatomic, assign) BOOL isDownloading; + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel.m new file mode 100644 index 00000000000..9d4d553db20 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFairFavoritesNetworkModel.m @@ -0,0 +1,226 @@ +#import "ARFairFavoritesNetworkModel.h" +#import "ARNavigationButtonsViewController.h" +#import "ARArtworkSetViewController.h" +#import "ARButtonWithImage.h" +#import "ARFairShowViewController.h" +#import "ARFairFavoritesNetworkModel+Private.h" +#import "ARPartnerShowFeedItem.h" + +const NSInteger ARFairFavoritesNetworkModelMaxRandomExhibitors = 10; + +@implementation ARFairFavoritesNetworkModel + +- (void)getFavoritesForNavigationsButtonsForFair:(Fair *)fair + artwork:(void (^)(NSArray *artworks))work + exhibitors:(void (^)(NSArray *exhibitors))exhibitors + artists:(void (^)(NSArray *artists))artists + failure:(void (^)(NSError *error))failure +{ + _isDownloading = YES; + __block BOOL artworkFavorites = NO; + __block BOOL profileFollows = NO; + __block BOOL artistFollows = NO; + + @weakify(self); + + void (^completionCheck)() = ^(){ + @strongify(self); + if (!self) { return; } + + if (artworkFavorites && profileFollows && artistFollows) { + self->_isDownloading = NO; + } + }; + + // "Work" tab + [ArtsyAPI getArtworkFavoritesForFair:fair success:^(NSArray *artworks) { + NSArray *buttons = [artworks map:^id(Artwork *artwork) { + return [self navigationButtonForArtwork:artwork inFair:fair]; + }]; + + if(work){ + work(buttons); + } + + artworkFavorites = YES; + completionCheck(); + } failure:^(NSError *error) { + artworkFavorites = YES; + completionCheck(); + if(failure) { + failure(error); + } + }]; + + // "Exhibitors" tab + [ArtsyAPI getProfileFollowsForFair:fair success:^(NSArray *follows) { + // Create a subject that'll receive partners from either of our two code paths, below + RACSubject *partnerSubject = [[RACSubject subject] setNameWithFormat:@"getProfileFollowsForFair:success:"]; + + [[RACSignal + combineLatest:@[[[RACObserve(fair, shows) ignore:nil] take:1], partnerSubject] + reduce:^id(NSSet *shows, NSArray *partnersArray) { + return partnersArray; + }] subscribeNext:^(NSArray *partnersArray) { + NSArray *buttons = [partnersArray map:^id(Partner *partner) { + return [self navigationButtonForPartner:partner inFair:fair]; + }]; + + if (exhibitors) { + exhibitors(buttons); + } + + profileFollows = YES; + completionCheck(); + }]; + + if (follows.count == 0) { + // No content returned from API – just use random content generated once the shows property has been set + [[[RACObserve(fair, shows) ignore:nil] take:1] subscribeNext:^(NSSet *shows) { + [partnerSubject sendNext:[[shows.allObjects take:ARFairFavoritesNetworkModelMaxRandomExhibitors] map:^id(PartnerShow *show) { + return show.partner; + }]]; + }]; + } else { + // Content returned from API – filter out non-partner owners and return the owners, sending to our subject + [partnerSubject sendNext:[[follows select:^BOOL(Follow *follow) { + Profile *profile = follow.profile; + return [profile.profileOwner isKindOfClass:[Partner class]]; + }] map:^id(Follow *follow) { + return follow.profile.profileOwner; + }]]; + } + + if (!fair.shows) { + [fair downloadShows]; + } + } failure:^(NSError *error) { + profileFollows = YES; + completionCheck(); + if(failure) { + failure(error); + } + }]; + + // "Artists" Tab + [ArtsyAPI getArtistFollowsForFair:fair success:^(NSArray *follows) { + NSArray *buttons = [follows map:^id(Follow *follow) { + return [self navigationButtonForArtist:follow.artist inFair:fair]; + }]; + + if (artists) { + artists(buttons); + } + artistFollows = YES; + completionCheck(); + } failure:^(NSError *error) { + artistFollows = YES; + completionCheck(); + if(failure) { + failure(error); + } + }]; +} + +// Partner = sanserif title, serif subtitle // name | location + +- (NSDictionary *)navigationButtonForPartner:(Partner *)partner inFair:(Fair *)fair +{ + // avoid having to lookup a partner -> show server-side when possible + PartnerShow *show = [fair findShowForPartner:partner]; + + @weakify(self); + return @{ + ARNavigationButtonClassKey: ARButtonWithImage.class, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARButtonWithImage.new, title): partner.name ?: [NSNull null], + @keypath(ARButtonWithImage.new, subtitle): (show ? show.locationInFair : [NSNull null]) ?: [NSNull null], + @keypath(ARButtonWithImage.new, image): show ? [NSNull null] : [UIImage imageNamed:@"SearchThumb_LightGrey"], + @keypath(ARButtonWithImage.new, imageURL): (show ? [show imageURLWithFormatName:@"square"] : [NSNull null]) ?: [NSNull null] + }, + ARNavigationButtonHandlerKey: ^(ARButtonWithImage *sender) { + @strongify(self); + if (show) { + [self handleShowButtonPress:show fair:fair]; + } else { + [self handlePartnerButtonPress:partner fair:fair]; + } + } + }; +} + +// Artwork = italics serif title, sansserif subtitle // name | artist name + +- (id)navigationButtonForArtwork:(Artwork *)artwork inFair:(Fair *)fair +{ + @weakify(self); + return @{ + ARNavigationButtonClassKey: ARButtonWithImage.class, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARButtonWithImage.new, title): artwork.title ?: [NSNull null], + @keypath(ARButtonWithImage.new, subtitle): [artwork.artist.name uppercaseString] ?: [NSNull null], + @keypath(ARButtonWithImage.new, imageURL): [artwork.defaultImage urlForSquareImage] ?: [NSNull null], + @keypath(ARButtonWithImage.new, titleFont): [UIFont serifItalicFontWithSize:12], + @keypath(ARButtonWithImage.new, subtitleFont): [UIFont sansSerifFontWithSize:12] + }, + ARNavigationButtonHandlerKey: ^(ARButtonWithImage *sender) { + @strongify(self); + [self handleArtworkButtonPress:artwork fair:fair]; + } + }; +} + +// Artist = sanserif title, serif subtitle // name | number of works in fair + +- (id)navigationButtonForArtist:(Artist *)artist inFair:(Fair *)fair +{ + @weakify(self); + return @{ + ARNavigationButtonClassKey: ARButtonWithImage.class, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARButtonWithImage.new, title): [artist.name uppercaseString] ?: [NSNull null], + @keypath(ARButtonWithImage.new, subtitle): @"", // TODO: number of works exhibited at this fair + @keypath(ARButtonWithImage.new, imageURL): [artist smallImageURL] ?: [NSNull null], + @keypath(ARButtonWithImage.new, titleFont): [UIFont sansSerifFontWithSize:12], + @keypath(ARButtonWithImage.new, subtitleFont): [UIFont serifFontWithSize:12] + }, + ARNavigationButtonHandlerKey: ^(ARButtonWithImage *sender) { + @strongify(self); + [self handleArtistButtonPress:artist fair:fair]; + } + }; +} + +- (void)handlePartnerButtonPress:(Partner *)partner fair:(Fair *)fair +{ + ARFairShowFeed *feed = [[ARFairShowFeed alloc] initWithFair:fair partner:partner]; + @weakify(self); + [feed getFeedItemsWithCursor:feed.cursor success:^(NSOrderedSet *parsed) { + @strongify(self); + ARPartnerShowFeedItem *showFeedItem = parsed.firstObject; + if (feed && showFeedItem) { // check feed to retain it + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadShow:showFeedItem.show fair:fair]; + [self.delegate fairFavoritesNetworkModel:self shouldPresentViewController:viewController]; + } + } failure:nil]; +} + +- (void)handleShowButtonPress:(PartnerShow *)show fair:(Fair *)fair +{ + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadShow:show fair:fair]; + [self.delegate fairFavoritesNetworkModel:self shouldPresentViewController:viewController]; +} + +- (void)handleArtworkButtonPress:(Artwork *)artwork fair:(Fair *)fair +{ + ARArtworkSetViewController *viewController = [[ARSwitchBoard sharedInstance] loadArtwork:artwork inFair:fair]; + [self.delegate fairFavoritesNetworkModel:self shouldPresentViewController:viewController]; +} + +- (void)handleArtistButtonPress:(Artist *)artist fair:(Fair *)fair +{ + id viewController = [[ARSwitchBoard sharedInstance] loadArtistWithID:artist.artistID inFair:fair]; + [self.delegate fairFavoritesNetworkModel:viewController shouldPresentViewController:viewController]; +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARFavoritesNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARFavoritesNetworkModel.h new file mode 100644 index 00000000000..49fae949a63 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFavoritesNetworkModel.h @@ -0,0 +1,8 @@ +@interface ARFavoritesNetworkModel : NSObject + +@property (readonly, nonatomic, assign) BOOL allDownloaded; +@property (readwrite, nonatomic, assign) BOOL useSampleFavorites; + +- (void)getFavorites:(void (^)(NSArray *genes))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARFavoritesNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARFavoritesNetworkModel.m new file mode 100644 index 00000000000..1d8d8a4e2fd --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFavoritesNetworkModel.m @@ -0,0 +1,55 @@ +#import "ARFavoritesNetworkModel.h" + +@interface ARFavoritesNetworkModel () +@property (readwrite, nonatomic, assign) BOOL downloadLock; +@property (readwrite, nonatomic, assign) NSInteger currentPage; +@end + +@implementation ARFavoritesNetworkModel + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _currentPage = 1; + return self; +} + +- (void)getFavorites:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + if (self.downloadLock) { return; } + + _downloadLock = YES; + @weakify(self); + + [self performNetworkRequestAtPage:self.currentPage withSuccess:^(NSArray *items) { + @strongify(self); + if (!self) { return; } + + self.currentPage++; + self.downloadLock = NO; + + if (items.count == 0) { + self->_allDownloaded = YES; + } + + if(success) success(items); + + } failure:^(NSError *error) { + @strongify(self); + if (!self) { return; } + + self->_allDownloaded = YES; + + self.downloadLock = NO; + + if(success) success(@[]); + }]; +} + +- (void)performNetworkRequestAtPage:(NSInteger)page withSuccess:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + [NSException raise:NSInvalidArgumentException format:@"NSObject %@[%@]: selector not recognized - use a subclass: ", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARFollowableNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARFollowableNetworkModel.h new file mode 100644 index 00000000000..fc9971f24f8 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFollowableNetworkModel.h @@ -0,0 +1,12 @@ +#import "ARFollowable.h" + +@interface ARFollowableNetworkModel : NSObject + +- (id)initWithFollowableObject:(id )representedObject; + +@property (readonly, nonatomic, strong) id representedObject; + +/// You should observe changes on this for network fallbacks +@property (nonatomic, assign, getter=isFollowing) BOOL following; + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARFollowableNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARFollowableNetworkModel.m new file mode 100644 index 00000000000..13f8d967809 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARFollowableNetworkModel.m @@ -0,0 +1,61 @@ +#import "ARFollowableNetworkModel.h" + +@interface ARFollowableNetworkModel () +@end + +@implementation ARFollowableNetworkModel + +- (id)initWithFollowableObject:(id )representedObject +{ + self = [super init]; + if (!self) { return nil; } + + _representedObject = representedObject; + + @weakify(self); + [self.representedObject getFollowState:^(ARHeartStatus status) { + @strongify(self); + if (!self) { return; } + + [self willChangeValueForKey:@"following"]; + self->_following = (status == ARHeartStatusYes); + [self didChangeValueForKey:@"following"]; + } failure:^(NSError *error) { + ARErrorLog(@"Error checking follow status for %@ - %@", self.representedObject, error.localizedDescription); + }]; + + return self; +} + +- (void)setFollowing:(BOOL)following +{ + if(following == _following) return; + + @weakify(self); + if(following){ + [_representedObject followWithSuccess:nil failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"Error following %@ - %@", self.representedObject, error.localizedDescription); + [self _setFollowing:NO]; + }]; + + } else { + [_representedObject unfollowWithSuccess:nil failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"Error following %@ - %@", self.representedObject, error.localizedDescription); + [self _setFollowing:YES]; + }]; + } + + [self _setFollowing:following]; +} + +- (void)_setFollowing:(BOOL)isFollowing +{ + [self willChangeValueForKey:@"following"]; + self->_following = isFollowing; + self.representedObject.followed = isFollowing; + [self didChangeValueForKey:@"following"]; +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARGeneFavoritesNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARGeneFavoritesNetworkModel.h new file mode 100644 index 00000000000..d096900c756 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARGeneFavoritesNetworkModel.h @@ -0,0 +1,5 @@ +#import "ARFavoritesNetworkModel.h" + +@interface ARGeneFavoritesNetworkModel : ARFavoritesNetworkModel + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARGeneFavoritesNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARGeneFavoritesNetworkModel.m new file mode 100644 index 00000000000..23a1f065cf4 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARGeneFavoritesNetworkModel.m @@ -0,0 +1,22 @@ +#import "ARGeneFavoritesNetworkModel.h" + +@implementation ARGeneFavoritesNetworkModel + +- (void)performNetworkRequestAtPage:(NSInteger)page withSuccess:(void (^)(NSArray *artists))success failure:(void (^)(NSError *error))failure +{ + if (self.useSampleFavorites) { + @weakify(self); + + [ArtsyAPI getOrderedSetWithKey:@"favorites:suggested-genes" success:^(OrderedSet *set) { + @strongify(self); + if (!self) { return; } + + [ArtsyAPI getOrderedSetItems:set.orderedSetID.copy atPage:page withType:Gene.class success:success failure:failure]; + + } failure:failure]; + } else { + [ArtsyAPI getGenesFromPersonalCollectionAtPage:page success:success failure:failure]; + } +} + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARShowNetworkModel.h b/Artsy/Classes/Networking/Network Models/ARShowNetworkModel.h new file mode 100644 index 00000000000..8c1aa6ea232 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARShowNetworkModel.h @@ -0,0 +1,21 @@ +#import + +@interface ARShowNetworkModel : NSObject + +- (instancetype)initWithFair:(Fair *)fair show:(PartnerShow *)show; + +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, strong, readonly) PartnerShow *show; + +- (void)getShowInfo:(void (^)(PartnerShow *show))success failure:(void (^)(NSError *error))failure; + +- (void)getFairMaps:(void (^)(NSArray *maps))success; + +- (void)getArtworksAtPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure; + +- (void)getFairBoothArtworksAndInstallShots:(PartnerShow *)show + gotInstallImages:(void (^)(NSArray *images))gotInstallImages + gotArtworks:(void (^)(NSArray *images))gotArtworkImages + noImages:(void (^)(void))noImages; + +@end diff --git a/Artsy/Classes/Networking/Network Models/ARShowNetworkModel.m b/Artsy/Classes/Networking/Network Models/ARShowNetworkModel.m new file mode 100644 index 00000000000..9e41132bdc8 --- /dev/null +++ b/Artsy/Classes/Networking/Network Models/ARShowNetworkModel.m @@ -0,0 +1,108 @@ +#import "ARShowNetworkModel.h" + +@interface ARShowNetworkModel () + +@property (nonatomic, strong, readwrite) Fair *fair; +@property (nonatomic, strong, readwrite) PartnerShow *show; + +@end + +@implementation ARShowNetworkModel + +- (instancetype)initWithFair:(Fair *)fair show:(PartnerShow *)show +{ + self = [super init]; + if (self == nil) { return nil; } + + _fair = fair; + _show = show; + + return self; +} + +- (void)getShowInfo:(void (^)(PartnerShow *))success failure:(void (^)(NSError *))failure +{ + @weakify(self); + [ArtsyAPI getShowInfo:_show success:^(PartnerShow *show) { + @strongify(self); + + if (!self.fair) { + self.fair = show.fair; + } + + if (success) { + success(show); + } + } failure:^(NSError *error) { + if (failure) { + failure(error); + } + }]; +} + +- (void)getFairMaps:(void (^)(NSArray *maps))success +{ + [self.fair getFairMaps:^(NSArray *maps) { + if (success) { + success(maps); + } + }]; +} + +- (void)getArtworksAtPage:(NSInteger)page success:(void (^)(NSArray *artworks))success failure:(void (^)(NSError *error))failure +{ + [ArtsyAPI getArtworksForShow:self.show atPage:page success:^(NSArray *artworks) { + if (success) { + success(artworks); + } + } failure:^(NSError *error) { + if (failure) { + failure(error); + } + }]; +} + +- (void)getFairBoothArtworksAndInstallShots:(PartnerShow *)show + gotInstallImages:(void (^)(NSArray *images))gotInstallImages + gotArtworks:(void (^)(NSArray *images))gotArtworkImages + noImages:(void (^)(void))noImages; +{ + [ArtsyAPI getImagesForShow:show atPage:1 success:^(NSArray *images) { + if (images.count) { + gotInstallImages(images); + + } else { + [self downloadArtworksForShow:show gotArtworks:gotArtworkImages noImages:noImages]; + } + + + } failure:^(NSError *error) { + [self downloadArtworksForShow:show gotArtworks:gotArtworkImages noImages:noImages]; + }]; +} + +- (void)downloadArtworksForShow:(PartnerShow *)show gotArtworks:(void (^)(NSArray *images))gotArtworkImages noImages:(void (^)(void))noImages; +{ + [ArtsyAPI getArtworksForShow:show atPage:1 success:^(NSArray *artworks) { + + NSArray *artworkImages = [[artworks reject:^BOOL(Artwork *artwork) { + return !artwork.isPublished.boolValue; + + }] map:^id(Artwork *artwork) { + return artwork.defaultImage; + }]; + + if (artworkImages.count) { + gotArtworkImages(artworkImages); + + } else { + noImages(); + } + + } failure:^(NSError *error) { + noImages(); + }]; +} + + +@end diff --git a/Artsy/Classes/Protocols/ARFairAwareObject.h b/Artsy/Classes/Protocols/ARFairAwareObject.h new file mode 100644 index 00000000000..b2d9b26bcdb --- /dev/null +++ b/Artsy/Classes/Protocols/ARFairAwareObject.h @@ -0,0 +1,6 @@ +@protocol ARFairAwareObject + +-(Fair *)fair; +-(void)setFair:(Fair *)fair; + +@end \ No newline at end of file diff --git a/Artsy/Classes/Protocols/ARFeedHostItem.h b/Artsy/Classes/Protocols/ARFeedHostItem.h new file mode 100644 index 00000000000..e7a07d6f3e3 --- /dev/null +++ b/Artsy/Classes/Protocols/ARFeedHostItem.h @@ -0,0 +1,10 @@ +@protocol ARFeedHostItem + +/// The selector that is used to pass the hosted object back to the +/// host with. +- (SEL)setHostPropertySelector; + +/// The class that the hosted object will be created with. +- (Class)hostedObjectClass; + +@end diff --git a/Artsy/Classes/Protocols/ARFollowable.h b/Artsy/Classes/Protocols/ARFollowable.h new file mode 100644 index 00000000000..301a6503c92 --- /dev/null +++ b/Artsy/Classes/Protocols/ARFollowable.h @@ -0,0 +1,12 @@ +#import "ARHeartStatus.h" + +@protocol ARFollowable + +@property (nonatomic, assign, getter=isFollowed) BOOL followed; + +- (void)followWithSuccess:(void (^)(id response))success failure:(void (^)(NSError *error))failure; +- (void)unfollowWithSuccess:(void (^)(id response))success failure:(void (^)(NSError *error))failure; +- (void)setFollowState:(BOOL)state success:(void (^)(id))success failure:(void (^)(NSError *))failure; +- (void)getFollowState:(void (^)(ARHeartStatus status))success failure:(void (^)(NSError *error))failure; + +@end diff --git a/Artsy/Classes/Protocols/ARShareableObject.h b/Artsy/Classes/Protocols/ARShareableObject.h new file mode 100644 index 00000000000..86494fd4ab5 --- /dev/null +++ b/Artsy/Classes/Protocols/ARShareableObject.h @@ -0,0 +1,6 @@ +#import + +@protocol ARShareableObject +- (NSString *)publicArtsyPath; +@property (nonatomic, copy, readonly) NSString *name; +@end diff --git a/Artsy/Classes/Theming/ARTheme+HeightAdditions.h b/Artsy/Classes/Theming/ARTheme+HeightAdditions.h new file mode 100644 index 00000000000..fccdbc66fea --- /dev/null +++ b/Artsy/Classes/Theming/ARTheme+HeightAdditions.h @@ -0,0 +1,9 @@ +#import "ARTheme.h" + +@interface ARTheme (HeightAdditions) + +/// Gets the combined numberical value of the elements added together + +- (CGFloat)combinedFloatValueOfLayoutElementsWithKeys:(NSArray *)keys; + +@end diff --git a/Artsy/Classes/Theming/ARTheme+HeightAdditions.m b/Artsy/Classes/Theming/ARTheme+HeightAdditions.m new file mode 100644 index 00000000000..356a2caae2c --- /dev/null +++ b/Artsy/Classes/Theming/ARTheme+HeightAdditions.m @@ -0,0 +1,29 @@ +#import "ARTheme+HeightAdditions.h" + +@interface ARTheme() +- (id)itemWithKey:(id )key; +@end + +@implementation ARTheme (HeightAdditions) + +// In FLKLayout because there are dimensions relative to the place where you +// are connecting, it can be natural to have negative margins which +// wouldn't actually subtract from the total amount. So we ensure a negative +// margin does not subtract from the returned value. + +- (CGFloat)combinedFloatValueOfLayoutElementsWithKeys:(NSArray *)keys +{ + CGFloat sum = 0; + for (id key in keys) { + NSString *layoutValue = [self itemWithKey:key]; + if (!layoutValue) { + ARErrorLog(@"Could not find value for %@", key); + } + + sum += [layoutValue floatValue]; + } + return sum; +} + + +@end diff --git a/Artsy/Classes/Theming/ARTheme.h b/Artsy/Classes/Theming/ARTheme.h new file mode 100644 index 00000000000..b2e73fc82bb --- /dev/null +++ b/Artsy/Classes/Theming/ARTheme.h @@ -0,0 +1,66 @@ +#import "ARThemedFactory.h" + +@class ARThemeFontVendor, ARThemeLayoutVendor, ARThemeColorVendor, ARThemeConstantVendor; + +/// The ARTheme will use a bundled JSON file to create a dictionary of values used +/// dealing with the visual aspects of the app. It uses object subscripting on other classes +/// to give types instances back. + +/// Basically considered to be deprecated, you should not be using this in new code. + +@interface ARTheme : NSObject + +/// Get the shared instance ++ (void)setupWithBundleNamed:(NSString *)name; + +/// Get the default theme, named themes will revert +/// to this if they can't find results in their own dicts ++ (ARTheme *)defaultTheme; + +/// Get a named theme for scoped theming ++ (ARTheme *)themeNamed:(NSString *)theme; + +/// Subscripting enabled accessor for fonts, caches +@property (nonatomic, readonly) ARThemeFontVendor *fonts; + +/// Subscripting enabled accessor for strings +@property (nonatomic, readonly) ARThemeLayoutVendor *layout; + +/// Subscripting enabled accessor for colors, caches +@property (nonatomic, readonly) ARThemeColorVendor *colors; + +/// Returns a floating point value from the theme +- (CGFloat)floatForKey:(NSString *)defaultName; + +@end + +// We want to use subscripting, but we also want to have +// a way of typing what comes back, so [ARTheme theme].fonts[@"Main"] +// can return a UIFont * instead of an id. + +@interface ARThemeVendor : NSObject + +- (instancetype)initWithTheme:(ARTheme *)theme; + +@end + +@interface ARThemeLayoutVendor : ARThemeVendor + +- (NSString *)objectForKeyedSubscript:(id )key; +- (void)setObject:(NSString *)obj forKeyedSubscript:(id )key; + +@end + +@interface ARThemeColorVendor : ARThemeVendor + +- (UIColor *)objectForKeyedSubscript:(id )key; +- (void)setObject:(NSString *)obj forKeyedSubscript:(id )key; + +@end + +@interface ARThemeFontVendor : ARThemeVendor + +- (UIFont *)objectForKeyedSubscript:(id )key; +- (void)setObject:(UIFont *)obj forKeyedSubscript:(id )key; + +@end diff --git a/Artsy/Classes/Theming/ARTheme.m b/Artsy/Classes/Theming/ARTheme.m new file mode 100644 index 00000000000..c537c4a314a --- /dev/null +++ b/Artsy/Classes/Theming/ARTheme.m @@ -0,0 +1,245 @@ +static NSMutableDictionary *staticThemes; +static ARTheme *defaultTheme; + +@interface ARTheme () +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) ARThemeFontVendor *fonts; +@property (nonatomic, strong) ARThemeLayoutVendor *layout; +@property (nonatomic, strong) ARThemeColorVendor *colors; + +@property (nonatomic, strong) NSDictionary *fontShortcuts; +@property (nonatomic, strong) NSMutableDictionary *themeDictionary; + +@property (nonatomic, strong) NSCache *colorCache; +@property (nonatomic, strong) NSCache *fontCache; +@end + +@implementation ARTheme + ++ (void)load +{ + [ARTheme setupWithBundleNamed:@"Theme"]; +} + ++ (void)setupWithBundleNamed:(NSString *)name; +{ + [self themesFromLocalBundleNamed:name]; +} + ++ (NSArray *)themesFromLocalBundleNamed:(NSString *)name +{ + NSError *error = nil; + NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:@"json"]]; + NSArray *themeDictionaries = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error) { + ARErrorLog(@"Error parsing JSON for themes with filename %@ - %@ %@", name, error.localizedDescription, error.localizedFailureReason); + return nil; + } + + if (!staticThemes) { + staticThemes = [NSMutableDictionary dictionary]; + } + + for (NSDictionary *dictionary in themeDictionaries) { + ARTheme *theme = [[ARTheme alloc] initWithName:dictionary[@"name"] content:dictionary[@"content"]]; + [staticThemes setObject:theme forKey:theme.name]; + + if ([theme.name isEqualToString:@"default"]) { + defaultTheme = theme; + } + } + + return staticThemes.allValues; +} + ++ (ARTheme *)defaultTheme +{ + return defaultTheme; +} + ++ (ARTheme *)themeNamed:(NSString *)themeName +{ + return staticThemes[themeName]; +} + +- (instancetype)initWithName:(NSString *)name content:(NSDictionary *)content +{ + self = [super init]; + if (!self) { return nil; } + + self.colorCache = [[NSCache alloc] init]; + self.fontCache = [[NSCache alloc] init]; + + self.name = name; + self.themeDictionary = [content mutableCopy]; + self.fontShortcuts = @{ + @"Avant": @"AvantGardeGothicITCW01Dm", + @"Garamond": @"AGaramondPro-Regular", + @"GaramondBold": @"AGaramondPro-Bold" + }; + + self.fonts = [[ARThemeFontVendor alloc] initWithTheme:self]; + self.layout = [[ARThemeLayoutVendor alloc] initWithTheme:self]; + self.colors = [[ARThemeColorVendor alloc] initWithTheme:self]; + + return self; +} + +- (id)itemWithKey:(id )key +{ + id object = (self.themeDictionary[key])? self.themeDictionary[key] : [self.class defaultTheme].themeDictionary[key]; + if (!object) { + ARErrorLog(@"ARTheme: Could not find item for key %@ in %@", self.name, key); + } + return object; +} + +- (UIFont *)fontWithKey:(id )key +{ + // JSON String format = Fontname@Size + // If _anything_ is wrong we should just bail out and give a system font + + UIFont *cachedFont = [self.fontCache objectForKey:key]; + if (cachedFont) { return cachedFont; } + + NSString *fontString = [self itemWithKey:key]; + NSArray *components = [fontString split:@"@"]; + + if (components.count != 2) { + return [UIFont systemFontOfSize:16]; + } + + fontString = components[0]; + CGFloat fontSize = [components[1] floatValue]; + + if (fontSize < 1) { + fontSize = 15; + } + + fontString = (self.fontShortcuts[fontString])? self.fontShortcuts[fontString] : fontString; + + UIFont *font = [UIFont fontWithName:fontString size:fontSize]; + if (font) { + [self.fontCache setObject:font forKey:key]; + return font; + } + + return [UIFont systemFontOfSize:16]; +} + +// Based on http://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string + +- (UIColor *)colorWithKey:(id )key +{ + // JSON String format = #11FF33 + + UIColor *cachedColor = [self.colorCache objectForKey:key]; + if (cachedColor) { + return cachedColor; + } + + UIColor *hotPink = [UIColor colorWithHex:0xecb5d5]; + + NSString *hexString = [self itemWithKey:key]; + if (!hexString) { + return hotPink; + } + + if (![[hexString substringToIndex:1] isEqualToString:@"#"]) { + return hotPink; + } + + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + UIColor *color = [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 + green:((rgbValue & 0xFF00) >> 8)/255.0 + blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; + + return color ? color : hotPink; +} + +- (CGFloat)floatForKey:(NSString *)defaultName +{ + return [self numberWithKey:defaultName].floatValue; +} + +- (NSNumber *)numberWithKey:(id )key +{ + NSNumber *number = [self itemWithKey:key]; + if ([number isKindOfClass:[NSString class]]) { + number = @([number floatValue]); + } + + if (!number || ![number isKindOfClass:[NSNumber class]]) { + return @(-1); + } + + return number; +} + +@end + +@interface ARThemeVendor() +@property (nonatomic, strong) ARTheme *theme; +@end + +@implementation ARThemeVendor + +- (instancetype)initWithTheme:(ARTheme *)theme +{ + self = [super init]; + if (!self) { return nil; } + + _theme = theme; + + return self; +} + +@end + + +@implementation ARThemeFontVendor + +- (UIFont *)objectForKeyedSubscript:(id )key; +{ + return [self.theme fontWithKey:key]; +} + +- (void)setObject:(UIFont *)obj forKeyedSubscript:(id )key; +{ + self.theme.themeDictionary[key] = obj; +} + +@end + + +@implementation ARThemeLayoutVendor + +- (NSString *)objectForKeyedSubscript:(id )key +{ + return [self.theme itemWithKey:key]; +} + +- (void)setObject:(id)obj forKeyedSubscript:(id )key +{ + self.theme.themeDictionary[key] = obj; +} + +@end + + +@implementation ARThemeColorVendor + +- (UIColor *)objectForKeyedSubscript:(id )key +{ + return [self.theme colorWithKey:key]; +} + +- (void)setObject:(id)obj forKeyedSubscript:(id )key +{ + self.theme.themeDictionary[key] = obj; +} + +@end diff --git a/Artsy/Classes/Theming/ARThemedFactory.h b/Artsy/Classes/Theming/ARThemedFactory.h new file mode 100644 index 00000000000..083c7849ed5 --- /dev/null +++ b/Artsy/Classes/Theming/ARThemedFactory.h @@ -0,0 +1,41 @@ +#import + +/// The factory is so that we can have one place that a lot of the +/// common theme related views are created. This is mainly +/// so that we can make fast changes throughout the app without +/// having to go and find & replace all over the shop + +@interface ARThemedFactory : NSObject + +/// This will create the standard seperator and add it to the subview ++ (UIView *)viewForFeedItemSeperatorAttachedToView:(UIView *)container; + +/// Label for the section headings on tableviews of feed items ++ (UILabel *)labelForFeedSectionHeaders; + +/// Label for the headers inside a feed item ++ (UILabel *)labelForFeedItemHeaders; + +/// Label for subheadings in a feed item, like the date range in the show feed ++ (UILabel *)labelForFeedItemSubheadings; + +/// Commonly used label for showing text ++ (UILabel *)labelForBodyText; + +/// Headings used further down in view controllers ++ (UILabel *)labelForViewSubHeaders; + +/// Headings alternative with serifs ++ (UILabel *)labelForSerifHeaders; + +/// Subheadings alternative with serifs ++ (UILabel *)labelForSerifSubHeaders; + +/// Title used in featured posts for a fair ++ (UILabel *)labelForLinkItemTitles; + +/// Subtitle used in featured posts for a fair ++ (UILabel *)labelForLinkItemSubtitles; + + +@end diff --git a/Artsy/Classes/Theming/ARThemedFactory.m b/Artsy/Classes/Theming/ARThemedFactory.m new file mode 100644 index 00000000000..d79db3ce995 --- /dev/null +++ b/Artsy/Classes/Theming/ARThemedFactory.m @@ -0,0 +1,88 @@ +@implementation ARThemedFactory + ++ (UIView *)viewForFeedItemSeperatorAttachedToView:(UIView *)container +{ + ARTheme *theme = [ARTheme defaultTheme]; + ARThemeLayoutVendor *layout = [theme layout]; + + ARSeparatorView *separatorView = [[ARSeparatorView alloc] init]; + separatorView.backgroundColor = theme.colors[@"FeedItemSeperatorBackgroundColor"]; + + // This has to be added in this function or the constraints won't work + [container addSubview:separatorView]; + + [separatorView constrainHeight:layout[@"FeedItemSeperatorHeight"]]; + [separatorView constrainWidthToView:container predicate:layout[@"FeedItemSeperatorHorizontalMargin"]]; + [separatorView alignCenterXWithView:container predicate:layout[@"FeedItemSeperatorXOffset"]]; + [separatorView alignAttribute:NSLayoutAttributeBottom toAttribute:NSLayoutAttributeBottom ofView:container predicate:layout[@"FeedItemSeperatorBottomMargin"]]; + + return separatorView; +} + ++ (UILabel *)labelForFeedSectionHeaders +{ + return [self _labelForIdentifier:@"FeedSectionTitle"]; +} + ++ (UILabel *)labelForFeedItemHeaders +{ + return [self _labelForIdentifier:@"FeedHeaderTitle"]; +} + ++ (UILabel *)labelForFeedItemSubheadings +{ + return [self _labelForIdentifier:@"FeedHeaderSubtitle"]; +} + ++ (UILabel *)labelForBodyText +{ + return [self _labelForIdentifier:@"BodyText"]; +} + ++ (UILabel *)labelForViewSubHeaders +{ + UILabel *titleLabel = [self _labelForIdentifier:@"ViewSubHeader"]; + titleLabel.textAlignment = NSTextAlignmentCenter; + titleLabel.preferredMaxLayoutWidth = 220; + return titleLabel; +} + ++ (UILabel *)labelForSerifHeaders +{ + UILabel *titleLabel = [self _labelForIdentifier:@"AltViewHeader"]; + titleLabel.textAlignment = NSTextAlignmentCenter; + titleLabel.preferredMaxLayoutWidth = 200; + return titleLabel; +} + ++ (UILabel *)labelForSerifSubHeaders +{ + UILabel *titleLabel = [self _labelForIdentifier:@"AltViewSubHeader"]; + titleLabel.textAlignment = NSTextAlignmentCenter; + titleLabel.preferredMaxLayoutWidth = 220; + return titleLabel; +} + + ++ (UILabel *)_labelForIdentifier:(NSString *)identifier +{ + UILabel *label = [[UILabel alloc] init]; + label.backgroundColor = [UIColor whiteColor]; + label.opaque = YES; + label.font = [ARTheme defaultTheme].fonts[identifier]; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + return label; +} + ++ (UILabel *)labelForLinkItemTitles +{ + return [self _labelForIdentifier:@"LinkItemTitle"]; +} + ++ (UILabel *)labelForLinkItemSubtitles +{ + return [self _labelForIdentifier:@"LinkItemSubtitle"]; +} + +@end diff --git a/Artsy/Classes/Theming/Theme.json b/Artsy/Classes/Theming/Theme.json new file mode 100644 index 00000000000..b3dfb23b6e9 --- /dev/null +++ b/Artsy/Classes/Theming/Theme.json @@ -0,0 +1,103 @@ +[{ + "name": "default", + + "content": { + + "DefaultNavigationAnimationDuration": 0.3, + "DefaultNavigationAnimationYMovement": 16, + "DefaultNavigationAnimationXMovement": 8, + "DefaultNavigationAnimationAlphaTarget": 0.2, + + "LoginForm" : "Garamond@17", + "LoginMessage" : "Avant@16", + "LoginError" : "Garamond@17", + + "FeedSectionTitle" : "Avant@13", + "FeedHeaderTitle": "Garamond@21", + "FeedHeaderSubtitle": "Garamond@15", + + "ViewHeader" : "Avant@18", + "ViewSubHeader" : "Avant@16", + "AltViewHeader" : "Garamond@21", + "AltViewSubHeader" : "Garamond@18", + + "ViewHeaderTopMargin" : "20", + "ViewHeaderSideMargin" : "120", + + "ButtonFont": "Avant@14", + "BodyText": "Garamond@16", + + "Error":"#ca0814", + + "FeedSectionTitleVerticalMargin" : "15", + "FeedSectionTitleVerticalOffset" : "0", + + "FeedItemTopMargin": "20", + + "FeedItemSeperatorHeight": "1", + "FeedItemSeperatorHorizontalMargin": "-40", + "FeedItemSeperatorXOffset": "0", + "FeedItemSeperatorBottomMargin": "0", + "FeedItemSeperatorBackgroundColor": "#E5E5E5", + + "LinkItemTitle": "Garamond@18", + "LinkItemSubtitle": "Avant@10" + } +}, +{ + "name": "InquireForm", + + "content": { + "BackgroundTitle" : "Avant@14", + "BackgroundAlpha" : 0.6 + } +}, +{ + "name": "ShowFeed", + + "content": { + "ShowFeedItemPartnerNameHorizontalMargin" : "40", + "ShowFeedItemPartnerNameXOffset" : "0", + + "ShowFeedItemShowTitleTopMargin": "4", + "ShowFeedItemShowTitleHorizontalMargin": "40", + "ShowFeedItemShowTitleXOffset": "0", + + "ShowFeedItemSubtitleTopMargin": "4", + "ShowFeedItemSubtitleHorizontalMargin": "40", + "ShowFeedItemSubtitleXOffset": "0", + + "ShowFeedItemArtworksTopMargin": "20", + "ShowFeedItemArtworksHorizontalMargin": "0", + "ShowFeedItemArtworksXOffset": "0", + "ShowFeedItemArtworksHeight": "255", + + "FeedItemSeperatorHeight": "6", + "FeedItemSeperatorHorizontalMargin": "0", + "FeedItemSeperatorXOffset": "0", + "FeedItemSeperatorBottomMargin": "0" + } + }, + { + "name": "InquirePopover", + + "content": { + "BorderColor": "#E5E5E5" + } + }, + { + "name": "Artist", + + "content": { + "Title": "Avant@18", + "Bio": "Garamond@18" + } + }, + { + "name": "Artwork", + + "content": { + "ButtonMargin": "-8" + } +} +] \ No newline at end of file diff --git a/Artsy/Classes/Tooling/ARDeveloperOptions.h b/Artsy/Classes/Tooling/ARDeveloperOptions.h new file mode 100644 index 00000000000..29674861f80 --- /dev/null +++ b/Artsy/Classes/Tooling/ARDeveloperOptions.h @@ -0,0 +1,13 @@ +// Per-developer settings in a .eigen file +// accessible like [ARDeveloperOptions options][@"username"]; + +@interface ARDeveloperOptions : NSObject + ++ (instancetype)options; + +- (BOOL)isDeveloper; + +- (id)objectForKeyedSubscript:(id )key; +- (void)updateWithStringContents:(NSString *)contents; + +@end diff --git a/Artsy/Classes/Tooling/ARDeveloperOptions.m b/Artsy/Classes/Tooling/ARDeveloperOptions.m new file mode 100644 index 00000000000..6b1725cd098 --- /dev/null +++ b/Artsy/Classes/Tooling/ARDeveloperOptions.m @@ -0,0 +1,81 @@ +@interface ARDeveloperOptions() +@property (nonatomic, strong, readonly) NSDictionary *data; +@end + +@implementation ARDeveloperOptions + ++ (instancetype)options +{ + static ARDeveloperOptions *_sharedOptions = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedOptions = [[self alloc] init]; + }); + + return _sharedOptions; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + + NSString *initialPath = [[@"~" stringByExpandingTildeInPath] componentsSeparatedByString:@"Library"][0]; + NSString *dotFilePath = [initialPath stringByAppendingString:@".eigen"]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:dotFilePath]) { + NSString *defaultContents = [NSString stringWithContentsOfFile:dotFilePath encoding:NSASCIIStringEncoding error:nil]; + [self updateWithStringContents:defaultContents]; + } + + return self; +} + +- (BOOL)isDeveloper +{ + return _data != nil; +} + +- (void)updateWithStringContents:(NSString *)contents +{ + _data = [self dictionaryWithStringContents:contents]; +} + +- (NSDictionary *)dictionaryFromContentsOfFile:(NSString *)path +{ + NSString *fileContents = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:nil]; + return [self dictionaryFromContentsOfFile:fileContents]; +} + +- (NSDictionary *)dictionaryWithStringContents:(NSString *)contents +{ + NSMutableDictionary *settings = [NSMutableDictionary dictionary]; + + for (NSString *line in [contents componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]) { + NSArray *components = [line componentsSeparatedByString:@":"]; + if (components.count != 2) continue; + + NSString *key = components[0]; + id value = components[1]; + + if ([value isEqualToString:@"yes"] || [value isEqualToString:@"true"] ) { + value = @(YES); + } + + else if ([value isEqualToString:@"no"] || [value isEqualToString:@"false"] ) { + value = @(NO); + } + + settings[key] = value; + } + + return [NSDictionary dictionaryWithDictionary:settings]; +} + +- (id)objectForKeyedSubscript:(id )key +{ + return [self.data objectForKey:key]; +} + + +@end diff --git a/Artsy/Classes/Tooling/ARDispatchManager.h b/Artsy/Classes/Tooling/ARDispatchManager.h new file mode 100644 index 00000000000..d827e5b2ba2 --- /dev/null +++ b/Artsy/Classes/Tooling/ARDispatchManager.h @@ -0,0 +1,21 @@ +// Dispatches asyncronously unless useSyncronousDispatches is set on the shared dispatch manager +extern void ar_dispatch_async(dispatch_block_t block); + +// Dispatches to the main queue unless useSyncronousDispatches is set on the shared dispatch manager +extern void ar_dispatch_main_queue(dispatch_block_t block); + +// Dispatches to a queue unless useSyncronousDispatches is set on the shared dispatch manager +extern void ar_dispatch_on_queue(dispatch_queue_t queue, dispatch_block_t block); + +extern void ar_dispatch_after_on_queue(float seconds, dispatch_queue_t queue, dispatch_block_t block); + +extern void ar_dispatch_after(float seconds, dispatch_block_t block); + +@interface ARDispatchManager : NSObject + ++ (instancetype)sharedManager; + +// Useful in tests +@property (readwrite, nonatomic, assign) BOOL useSyncronousDispatches; + +@end \ No newline at end of file diff --git a/Artsy/Classes/Tooling/ARDispatchManager.m b/Artsy/Classes/Tooling/ARDispatchManager.m new file mode 100644 index 00000000000..22416d360b4 --- /dev/null +++ b/Artsy/Classes/Tooling/ARDispatchManager.m @@ -0,0 +1,46 @@ +void ar_dispatch_async(dispatch_block_t block) { + ar_dispatch_on_queue(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); +} + +void ar_dispatch_main_queue(dispatch_block_t block) { + ar_dispatch_on_queue(dispatch_get_main_queue(), block); +} + +void ar_dispatch_on_queue(dispatch_queue_t queue, dispatch_block_t block) { + if ([ARDispatchManager sharedManager].useSyncronousDispatches) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(queue, block); + } + } else { + dispatch_async(queue, block); + } +} + +void ar_dispatch_after_on_queue(float seconds, dispatch_queue_t queue, dispatch_block_t block) { + if ([ARDispatchManager sharedManager].useSyncronousDispatches) { + block(); + } else { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC), queue, block); + } +} + +void ar_dispatch_after(float seconds, dispatch_block_t block) { + ar_dispatch_after_on_queue(seconds, dispatch_get_main_queue(), block); +} + +@implementation ARDispatchManager + ++ (instancetype)sharedManager +{ + static ARDispatchManager *_sharedManager = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedManager = [[self alloc] init]; + }); + + return _sharedManager; +} + +@end diff --git a/Artsy/Classes/Utils/ARFeedImageLoader.h b/Artsy/Classes/Utils/ARFeedImageLoader.h new file mode 100644 index 00000000000..3f09a137501 --- /dev/null +++ b/Artsy/Classes/Utils/ARFeedImageLoader.h @@ -0,0 +1,21 @@ +#import + +@class Image; + +typedef NS_ENUM(NSInteger, ARFeedItemImageSize) { + ARFeedItemImageSizeAuto, + ARFeedItemImageSizeSmall, + ARFeedItemImageSizeMasonry, + ARFeedItemImageSizeLarge +}; + +@interface ARFeedImageLoader : NSObject + +- (void)loadImageAtAddress:(NSString *)baseImageURL desiredSize:(ARFeedItemImageSize)desiredSize forImageView:(UIImageView *)imageView customPlaceholder:(UIImage *)customPlaceholder; + ++ (UIImage *)cachedImageForPath:(NSString *)imagePath; + ++ (UIImage *)defaultPlaceholder; + ++ (UIImage *)bestAvailableCachedImageForBaseURL:(NSURL *)url; +@end diff --git a/Artsy/Classes/Utils/ARFeedImageLoader.m b/Artsy/Classes/Utils/ARFeedImageLoader.m new file mode 100644 index 00000000000..850e9244129 --- /dev/null +++ b/Artsy/Classes/Utils/ARFeedImageLoader.m @@ -0,0 +1,132 @@ +#import "ARFeedImageLoader.h" + +// From the gravity source + +//version :small, if: :is_processing_delayed? do +// process :resample => [[72, 150], 200] +// process :resize_to_limit => [200, 200] +//end +// +//version :square, if: :is_processing_delayed? do +// process :resample => [[72, 150], 230] +// process :resize_to_fill => [230, 230] +//end +// +//# this is sized to height for fillwidth rows +//# (rather than limiting to 640 square like for :large) +//# this can leave medium images larger than large +// +//version :medium, if: :is_processing_delayed? do +// process :resample => [[72, 150], 260] +// process :resize_to_height => 260 +//end +// +//# sized for width for artwork in artwork columns (shows feed item, filtering & layered search) +// +//version :tall, if: :is_processing_delayed? do +// process :resample => [[72, 150], 260] +// process :resize_to_limit => [260, 800] +//end +// +//version :large, if: :is_processing_delayed? do +// process :resample => [[72, 150], 640] +// process :resize_to_limit => [640, 640] +//end +// +//version :larger, if: :is_processing_delayed? do +// process :resample => [[72, 150], 1024] +// process :resize_to_limit => [1024, 1024] +//end + +static NSString *ARImageSizeSmall = @"small"; +static NSString *ARImageSizeLarge = @"large"; +static NSString *ARImageSizeMasonry = @"tall"; + +@interface ARFeedImageLoader () +@property (nonatomic, weak) UIImageView *imageView; +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, copy) NSString* baseImageURL; +@end + +@implementation ARFeedImageLoader + +- (void)loadImageAtAddress:(NSString *)baseImageURL desiredSize:(ARFeedItemImageSize)desiredSize forImageView:(UIImageView *)imageView customPlaceholder:(UIImage *)customPlaceholder +{ + self.imageView = imageView; + self.baseImageURL = baseImageURL; + + if (desiredSize == ARFeedItemImageSizeLarge) { + + // Check to see if we have a small image in cache first that we can use as a placeholder + + NSString *imagePath = [[self generateUrlForSize:ARFeedItemImageSizeSmall] absoluteString]; + UIImage *localImage = [self.class cachedImageForPath:imagePath]; + self.image = localImage; + } + + if (!self.image){ + self.image = customPlaceholder ?: [[self class] defaultPlaceholder]; + } + + [imageView ar_setImageWithURL:[self generateUrlForSize:desiredSize] placeholderImage:self.image]; +} + ++ (UIImage *)cachedImageForPath:(NSString *)imagePath +{ + return [SDImageCache.sharedImageCache imageFromMemoryCacheForKey:imagePath]; +} + +- (NSURL *)generateUrlForSize:(ARFeedItemImageSize)desiredSize +{ + if(!self.baseImageURL) return nil; + + NSString *size = nil; + + switch (desiredSize) { + case ARFeedItemImageSizeAuto: + NSAssert(NO, @"Shouldn't get to here with an auto size"); + case ARFeedItemImageSizeSmall: + size = ARImageSizeSmall; + break; + + case ARFeedItemImageSizeLarge: + size = ARImageSizeLarge; + break; + + case ARFeedItemImageSizeMasonry: + size = ARImageSizeMasonry; + break; + } + + return [self imageURLWithFormatName:size]; +} + +- (NSURL *)imageURLWithFormatName:(NSString *)formatName +{ + NSString *url = [self.baseImageURL stringByReplacingOccurrencesOfString:@":version" withString:formatName]; + return [NSURL URLWithString:url]; +} + ++ (UIImage *)defaultPlaceholder +{ + return [UIImage imageFromColor:[UIColor artsyLightGrey]]; +} + ++ (UIImage *)bestAvailableCachedImageForBaseURL:(NSURL *)url +{ + NSString *address = url.absoluteString; + + for (NSString *format in @[ ARImageSizeLarge, ARImageSizeMasonry, ARImageSizeSmall]) { + + NSString *cachePath = [address stringByReplacingOccurrencesOfString:@":version" withString:format]; + + UIImage *image = [self cachedImageForPath:cachePath]; + if (image) { + return image; + } + } + + return nil; +} + +@end diff --git a/Artsy/Classes/Utils/ARFileUtils.h b/Artsy/Classes/Utils/ARFileUtils.h new file mode 100644 index 00000000000..383cd8dd108 --- /dev/null +++ b/Artsy/Classes/Utils/ARFileUtils.h @@ -0,0 +1,21 @@ +// Note: these should be moved into NSFileManager categories + +@interface ARFileUtils : NSObject + +// user documents + ++ (NSString *)userDocumentsFolder; + ++ (NSString *)userDocumentsPathWithFile:(NSString *)fileName; + ++ (NSString *)userDocumentsPathWithFolder:(NSString *)folderName + filename:(NSString *)fileName; + +// caches + ++ (NSString *)cachesFolder; + ++ (NSString *)cachesPathWithFolder:(NSString *)folderName + filename:(NSString *)fileName; + +@end diff --git a/Artsy/Classes/Utils/ARFileUtils.m b/Artsy/Classes/Utils/ARFileUtils.m new file mode 100644 index 00000000000..4d32b496b4c --- /dev/null +++ b/Artsy/Classes/Utils/ARFileUtils.m @@ -0,0 +1,56 @@ +#import "ARFileUtils.h" + +static NSString *_userDocumentsDirectory; +static NSString *_cachesDirectory; + +@implementation ARFileUtils + ++ (void)initialize { + _userDocumentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject relativePath]; + _cachesDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].lastObject relativePath]; +} + ++ (NSString *)userDocumentsFolder { + if(![User currentUser]) return nil; + return [NSString stringWithFormat:@"%@/%@", _userDocumentsDirectory, [User currentUser].userID]; +} + ++ (NSString *)userDocumentsPathWithFile:(NSString *)fileName { + if(![User currentUser]) return nil; + return [self pathWithFolder:_userDocumentsDirectory folderName:[User currentUser].userID filename:fileName]; +} + ++ (NSString *)userDocumentsPathWithFolder:(NSString *)folderName + filename:(NSString *)fileName { + if(![User currentUser]) return nil; + return [self pathWithFolder:_userDocumentsDirectory folderName:NSStringWithFormat(@"%@/%@", [User currentUser].userID, folderName) filename:fileName]; +} + ++ (NSString *)cachesFolder { + return _cachesDirectory; +} + ++ (NSString *)cachesPathWithFolder:(NSString *)folderName + filename:(NSString *)fileName { + return [self pathWithFolder:_cachesDirectory folderName:folderName filename:fileName]; +} + ++ (NSString *)pathWithFolder:(NSString *)rootFolderName + folderName:(NSString *)folderName + filename:(NSString *)fileName { + + NSString *directory = [NSString stringWithFormat:@"%@/%@", rootFolderName, folderName]; + + if(![[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:nil]) { + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]; + if(error) { + ARErrorLog(@"Error creating directory at path %@/%@", rootFolderName, folderName); + ARErrorLog(@"%@", [error userInfo]); + } + } + + return [NSString stringWithFormat:@"%@/%@", directory, fileName]; +} + +@end diff --git a/Artsy/Classes/Utils/ARImageItemProvider.h b/Artsy/Classes/Utils/ARImageItemProvider.h new file mode 100644 index 00000000000..02a05babc7b --- /dev/null +++ b/Artsy/Classes/Utils/ARImageItemProvider.h @@ -0,0 +1,4 @@ +#import + +@interface ARImageItemProvider : UIActivityItemProvider +@end diff --git a/Artsy/Classes/Utils/ARImageItemProvider.m b/Artsy/Classes/Utils/ARImageItemProvider.m new file mode 100644 index 00000000000..ae058def057 --- /dev/null +++ b/Artsy/Classes/Utils/ARImageItemProvider.m @@ -0,0 +1,14 @@ +#import "ARImageItemProvider.h" + +@implementation ARImageItemProvider + +- (id)item +{ + if ([self.activityType isEqualToString:UIActivityTypeMail] || [self.activityType isEqualToString:UIActivityTypeCopyToPasteboard]) { + return self.placeholderItem; + } else { + return nil; + } +} + +@end diff --git a/Artsy/Classes/Utils/ARMessageItemProvider.h b/Artsy/Classes/Utils/ARMessageItemProvider.h new file mode 100644 index 00000000000..7f9b7c0882e --- /dev/null +++ b/Artsy/Classes/Utils/ARMessageItemProvider.h @@ -0,0 +1,5 @@ +#import + +@interface ARMessageItemProvider : UIActivityItemProvider +- (instancetype)initWithMessage:(NSString *)message path:(NSString *)path; +@end diff --git a/Artsy/Classes/Utils/ARMessageItemProvider.m b/Artsy/Classes/Utils/ARMessageItemProvider.m new file mode 100644 index 00000000000..c083e1f7629 --- /dev/null +++ b/Artsy/Classes/Utils/ARMessageItemProvider.m @@ -0,0 +1,55 @@ +#import "ARMessageItemProvider.h" + +@interface ARMessageItemProvider () +@property (nonatomic, strong, readonly) NSString *path; +@property (nonatomic, strong, readonly) NSString *message; +@property (nonatomic, strong, readonly) NSURL *url; +@end + +@implementation ARMessageItemProvider + +- (instancetype)initWithMessage:(NSString *)message path:(NSString *)path +{ + self = [self initWithPlaceholderItem:message]; + if(!self) return nil; + _path = path; + return self; +} + +- (id)item +{ + if ([self.activityType isEqualToString:UIActivityTypeMail]) { + return [NSString stringWithFormat:@"%@", self.url.absoluteString, self.message]; + } else if ([self.activityType isEqualToString:UIActivityTypeAddToReadingList]) { + return self.url; + } else if ([self.activityType isEqualToString:UIActivityTypeAirDrop]) { + return nil; // served by ARURLItemProvider + } else { + return self.message; + } +} + +- (NSString *)activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(NSString *)activityType +{ + if ([activityType isEqualToString:UIActivityTypeMail]) { + return self.message; + } else { + return nil; + } +} + +- (NSString *)message +{ + if ([self.activityType isEqualToString:UIActivityTypePostToTwitter]) { + return [NSString stringWithFormat:@"%@ on @Artsy", self.placeholderItem]; + } else { + return [NSString stringWithFormat:@"%@ on Artsy", self.placeholderItem]; + } +} + +- (NSURL *)url +{ + return [ARSwitchBoard.sharedInstance resolveRelativeUrl:self.path]; +} + +@end diff --git a/Artsy/Classes/Utils/ARNetworkErrorManager.h b/Artsy/Classes/Utils/ARNetworkErrorManager.h new file mode 100644 index 00000000000..2b93a53cdd9 --- /dev/null +++ b/Artsy/Classes/Utils/ARNetworkErrorManager.h @@ -0,0 +1,6 @@ +@interface ARNetworkErrorManager : NSObject + +/// Present full screen modal, used when a user interaction has failed ++ (void)presentActiveErrorModalWithError:(NSError *)error; + +@end diff --git a/Artsy/Classes/Utils/ARNetworkErrorManager.m b/Artsy/Classes/Utils/ARNetworkErrorManager.m new file mode 100644 index 00000000000..e0b9c97b655 --- /dev/null +++ b/Artsy/Classes/Utils/ARNetworkErrorManager.m @@ -0,0 +1,100 @@ +#import "ARShadowView.h" +#import + +@interface ARNetworkErrorManager() +@property (nonatomic, strong) ARShadowView *activeModalView; +@property (nonatomic, strong) UIView *passiveErrorView; +@property (nonatomic, strong) NSLayoutConstraint *passiveBottomContraint; +@property (nonatomic, strong) NSString *bottomAlignmentPredicate; +@end + +@implementation ARNetworkErrorManager + ++ (ARNetworkErrorManager *)sharedManager { + static ARNetworkErrorManager *_sharedManager = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedManager = [[self alloc] init]; + +// [[NSNotificationCenter defaultCenter] addObserver:_sharedManager +// selector:@selector(keyboardWasShown:) +// name:UIKeyboardDidShowNotification +// object:nil]; +// [[NSNotificationCenter defaultCenter] addObserver:_sharedManager +// selector:@selector(keyboardWillHide:) +// name:UIKeyboardWillHideNotification +// object:nil]; + }); + return _sharedManager; +} + +- (void)keyboardWasShown:(NSNotification *)notification +{ + CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; + self.bottomAlignmentPredicate = [NSString stringWithFormat:@"-%0.f", keyboardSize.height]; + + if (self.passiveBottomContraint) { + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + [UIView animateWithDuration:duration animations:^{ + self.passiveBottomContraint.constant = -keyboardSize.height; + [self.passiveErrorView.superview layoutIfNeeded]; + }]; + } +} + +- (void)keyboardWillHide:(NSNotification *)notification +{ + self.bottomAlignmentPredicate = @"0"; +} + ++ (void)presentActiveErrorModalWithError:(NSError *)error +{ + ARNetworkErrorManager *manager = [self sharedManager]; + if (manager.activeModalView) { + return; + } + + [ARAnalytics error:error]; + [manager presentActiveError:error]; +} + +- (void)presentActiveError:(NSError *)error +{ + NSArray *views = [[UINib nibWithNibName:@"ActiveErrorView" bundle:nil] instantiateWithOwner:self options:nil]; + self.activeModalView = views[0]; + + self.activeModalView.alpha = 0; + ARTopMenuViewController *topMenu = [ARTopMenuViewController sharedController]; + [topMenu.view addSubview:self.activeModalView]; + + [UIView animateWithDuration:0.15 animations:^{ + self.activeModalView.alpha = 1; + }]; +} + +- (void)removeActiveErrorModal +{ + [UIView animateWithDuration:0.25 animations:^{ + self.activeModalView.alpha = 0; + + } completion:^(BOOL finished) { + [self.activeModalView removeFromSuperview]; + self.activeModalView = nil; + }]; +} + +- (void)setActiveModalView:(ARShadowView *)activeModalView +{ + _activeModalView = activeModalView; + + UITapGestureRecognizer *removeTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(removeActiveErrorModal)]; + [self.activeModalView addGestureRecognizer:removeTapGesture]; + + UILabel *titleLabel = activeModalView.subviews[0]; + titleLabel.font = [UIFont serifFontWithSize:26]; + + [self.activeModalView createShadow]; +} + +@end diff --git a/Artsy/Classes/Utils/AROptions.h b/Artsy/Classes/Utils/AROptions.h new file mode 100644 index 00000000000..8f76134e46f --- /dev/null +++ b/Artsy/Classes/Utils/AROptions.h @@ -0,0 +1,16 @@ +// All the options as consts +extern NSString *const AROptionsLoadingScreenAlpha; +extern NSString *const AROptionsUseVCR; +extern NSString *const AROptionsSettingsMenu; +extern NSString *const AROptionsTappingPartnerSendsToPartner; + +@interface AROptions : NSObject + +/// Returns all the current options ++ (NSArray *)labsOptions; + +/// Get and set individual options ++ (BOOL)boolForOption:(NSString *)option; ++ (void)setBool:(BOOL)value forOption:(NSString *)option; + +@end diff --git a/Artsy/Classes/Utils/AROptions.m b/Artsy/Classes/Utils/AROptions.m new file mode 100644 index 00000000000..f257f85f27f --- /dev/null +++ b/Artsy/Classes/Utils/AROptions.m @@ -0,0 +1,25 @@ +NSString *const AROptionsLoadingScreenAlpha = @"Loading screens are transparent"; +NSString *const AROptionsUseVCR = @"Use offline recording"; +NSString *const AROptionsSettingsMenu = @"Enable user settings"; +NSString *const AROptionsTappingPartnerSendsToPartner = @"Partner name in feed goes to partner"; + +@implementation AROptions + ++ (NSArray *)labsOptions { + return @[ + AROptionsUseVCR, AROptionsSettingsMenu, AROptionsTappingPartnerSendsToPartner + ]; +} + ++ (BOOL)boolForOption:(NSString *)option +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:option]; +} + ++ (void)setBool:(BOOL)value forOption:(NSString *)option +{ + [[NSUserDefaults standardUserDefaults] setBool:value forKey:option]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +@end diff --git a/Artsy/Classes/Utils/ARParallaxEffect.h b/Artsy/Classes/Utils/ARParallaxEffect.h new file mode 100644 index 00000000000..6d13f6eabcd --- /dev/null +++ b/Artsy/Classes/Utils/ARParallaxEffect.h @@ -0,0 +1,7 @@ +#import + +@interface ARParallaxEffect : UIMotionEffectGroup + +- (instancetype)initWithOffset:(NSInteger)offset; +- (instancetype)initWithOffsets:(CGPoint)offsets; +@end diff --git a/Artsy/Classes/Utils/ARParallaxEffect.m b/Artsy/Classes/Utils/ARParallaxEffect.m new file mode 100644 index 00000000000..0ac5351c1e4 --- /dev/null +++ b/Artsy/Classes/Utils/ARParallaxEffect.m @@ -0,0 +1,27 @@ +#import "ARParallaxEffect.h" + +@implementation ARParallaxEffect + +- (instancetype)initWithOffset:(NSInteger)offset +{ + return [self initWithOffsets:CGPointMake(offset, offset)]; +} + +- (instancetype)initWithOffsets:(CGPoint)offsets +{ + self = [super init]; + if (self) { + UIInterpolatingMotionEffect *mx = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + mx.minimumRelativeValue = @(offsets.x); + mx.maximumRelativeValue = @(-offsets.x); + + UIInterpolatingMotionEffect *my = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; + my.minimumRelativeValue = @(offsets.y); + my.maximumRelativeValue = @(-offsets.y); + + self.motionEffects = @[mx, my]; + + } + return self; +} +@end diff --git a/Artsy/Classes/Utils/ARScrollNavigationChief.h b/Artsy/Classes/Utils/ARScrollNavigationChief.h new file mode 100644 index 00000000000..d9faaaff0bd --- /dev/null +++ b/Artsy/Classes/Utils/ARScrollNavigationChief.h @@ -0,0 +1,19 @@ +/// Psh, managers are for Java + +@protocol ARScrollNavigationChiefAwareViewController + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView; + +@end + +@interface ARScrollNavigationChief : NSObject + ++ (ARScrollNavigationChief *)chief; + +@property (readonly, nonatomic, assign) BOOL allowsMenuButtons; + +@property (readonly, nonatomic, strong) UIScrollView *currentScrollView; + +@property (readwrite, nonatomic, weak) id awareViewController; + +@end diff --git a/Artsy/Classes/Utils/ARScrollNavigationChief.m b/Artsy/Classes/Utils/ARScrollNavigationChief.m new file mode 100644 index 00000000000..f879cf5ac77 --- /dev/null +++ b/Artsy/Classes/Utils/ARScrollNavigationChief.m @@ -0,0 +1,165 @@ +typedef NS_ENUM(NSInteger, ARScrollDirection) { + ARScrollDirectionUp = -1, + ARScrollDirectionNeutral, + ARScrollDirectionDown +}; + +static ARScrollNavigationChief *instance; + +@interface ARScrollNavigationChief() + +@property (nonatomic, assign) enum ARScrollDirection lastDirection; +@property (nonatomic, assign) CGFloat lastOffset; +@property (nonatomic, assign) CGFloat initialUpwardOffset; +@property (nonatomic, strong) UIScrollView *currentScrollView; + +@property (readwrite, nonatomic, assign) BOOL allowsMenuButtons; + +@end + + +@implementation ARScrollNavigationChief + ++ (ARScrollNavigationChief *)chief +{ + static ARScrollNavigationChief *instance; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[ARScrollNavigationChief alloc] init]; + }); + + return instance; +} + +- (id)init { + self = [super init]; + if (self == nil) { + return nil; + } + + _allowsMenuButtons = YES; + + return self; +} + ++ (BOOL)automaticallyNotifiesObserversOfAllowsMenuButtons { + return NO; +} + +- (void)setAllowsMenuButtons:(BOOL)allowsMenuButtons { + if (_allowsMenuButtons == allowsMenuButtons) { + return; + } + + [self willChangeValueForKey:@keypath(self, allowsMenuButtons)]; + _allowsMenuButtons = allowsMenuButtons; + [self didChangeValueForKey:@keypath(self, allowsMenuButtons)]; +} + +- (void)setChiefsTargetScrollView:(UIScrollView *)view +{ + if (view != self.currentScrollView) { + self.currentScrollView = view; + self.lastDirection = ARScrollDirectionUp; + self.lastOffset = 0; + } +} + +- (BOOL)changedDirection:(UIScrollView *)scrollView newDirection:(ARScrollDirection *)newDirection +{ + if (self.lastDirection == ARScrollDirectionNeutral) { + *newDirection = ARScrollDirectionDown; + return YES; + } + + // If it's bouncing, we don't wanna act like it's a manual scroll + CGFloat offset = scrollView.contentOffset.y; + if (offset <= 0) { + return NO; + } + + CGFloat delta = offset - self.lastOffset; + if (delta > 0) { + *newDirection = ARScrollDirectionDown; + return self.lastDirection == ARScrollDirectionUp; + + } else { + *newDirection = ARScrollDirectionUp; + return self.lastDirection == ARScrollDirectionDown; + } +} + +static CGFloat UpwardScrollDistanceForShowing = 160; + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + CGFloat offset = scrollView.contentOffset.y; + + // No thanks phantom messages from offscreen views + if (!scrollView.superview) { + return; + } + [self setChiefsTargetScrollView:scrollView]; + + // When a scrollview *isn't* the root view on a VC + if (offset == 0) { + return; + } + + // When a scrollview subclass is the root view on a VC + if (offset == -20) { + return; + } + + // When we just can't scroll anyway + if (scrollView.contentSize.height < scrollView.bounds.size.height) { + return; + } + + ARScrollDirection newDirection; + + // Parallax scroll views won't have a negative offset. Check for topHeight instead. + BOOL isScrollingPastTopEdge = offset < 0; + + if ([self changedDirection:scrollView newDirection:&newDirection]) { + + if (newDirection == ARScrollDirectionUp) { + // If we're going up, give ourselves an initial offset for gettng a distance + self.initialUpwardOffset = scrollView.contentOffset.y; + + } else { + self.initialUpwardOffset = NSNotFound; + + // Before we hide any buttons, we want to make sure this scroll view + // can actually scroll + if (!isScrollingPastTopEdge) { + self.allowsMenuButtons = NO; + } + } + } + + if (isScrollingPastTopEdge){ + self.lastDirection = ARScrollDirectionUp; + } else if (self.lastOffset == 0) { + self.lastDirection = ARScrollDirectionDown; + + } else { + CGFloat delta = offset - self.lastOffset; + self.lastDirection = delta > 0 ? ARScrollDirectionDown : ARScrollDirectionUp; + } + + // if we do see if we've scrolled past the threshold to show + BOOL setAnOffset = self.initialUpwardOffset != NSNotFound; + // TODO rename me + BOOL whatever = !scrollView.isDecelerating && (setAnOffset && offset < self.initialUpwardOffset - UpwardScrollDistanceForShowing); + if (isScrollingPastTopEdge || whatever) { + self.allowsMenuButtons = YES; + } + + self.lastOffset = offset; + + [self.awareViewController scrollViewDidScroll:scrollView]; +} + +@end diff --git a/Artsy/Classes/Utils/ARSecureTextFieldWithPlaceholder.h b/Artsy/Classes/Utils/ARSecureTextFieldWithPlaceholder.h new file mode 100644 index 00000000000..2911aada0ec --- /dev/null +++ b/Artsy/Classes/Utils/ARSecureTextFieldWithPlaceholder.h @@ -0,0 +1,5 @@ +#import "ARTextFieldWithPlaceholder.h" + +@interface ARSecureTextFieldWithPlaceholder : ARTextFieldWithPlaceholder + +@end diff --git a/Artsy/Classes/Utils/ARSecureTextFieldWithPlaceholder.m b/Artsy/Classes/Utils/ARSecureTextFieldWithPlaceholder.m new file mode 100644 index 00000000000..da2f68c3448 --- /dev/null +++ b/Artsy/Classes/Utils/ARSecureTextFieldWithPlaceholder.m @@ -0,0 +1,72 @@ +#import "ARSecureTextFieldWithPlaceholder.h" + +@interface ARSecureTextFieldWithPlaceholder () +@property (nonatomic, strong) NSString *actualText; +@property (nonatomic) BOOL secure; +@end + +@implementation ARSecureTextFieldWithPlaceholder + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + [self setupEvents]; + } + return self; +} + +- (void)awakeFromNib +{ + [super awakeFromNib]; + [self setupEvents]; +} + +- (void)setupEvents +{ + [self addTarget:self action:@selector(editingDidBegin) forControlEvents:UIControlEventEditingDidBegin]; + [self addTarget:self action:@selector(editingDidChange) forControlEvents:UIControlEventEditingChanged]; + [self addTarget:self action:@selector(editingDidFinish) forControlEvents:UIControlEventEditingDidEnd]; +} + +- (NSString *)text +{ + if (self.editing || self.secure == NO) { + return [super text]; + } else { + return self.actualText; + } +} + + +- (void)editingDidBegin +{ + self.secureTextEntry = YES; + self.text = self.actualText; +} + +- (void)editingDidChange +{ + self.actualText = self.text; +} + +- (void)editingDidFinish +{ + self.secureTextEntry = NO; + self.actualText = self.text; + self.text = [self dotPlaceholder]; +} + +- (NSString *)dotPlaceholder +{ + int index = 0; + NSMutableString *dots = @"".mutableCopy; + while (index < self.text.length) { + [dots appendString:@"•"]; + index++; + } + return dots; +} + + +@end diff --git a/Artsy/Classes/Utils/ARSharingController.h b/Artsy/Classes/Utils/ARSharingController.h new file mode 100644 index 00000000000..7d5ec040719 --- /dev/null +++ b/Artsy/Classes/Utils/ARSharingController.h @@ -0,0 +1,13 @@ +/// Centralized sharing / tweeter / tracebooking point + +@interface ARSharingController : NSObject + ++ (void)shareObject:(id)object; ++ (void)shareObject:(id)object withThumbnailImageURL:(NSURL *)thumbnailImageURL; ++ (void)shareObject:(id)object withThumbnailImageURL:(NSURL *)thumbnailImageURL withImage:(UIImage *)image; + +- (NSString *)objectID; + +@property (nonatomic, strong, readonly) id object; + +@end diff --git a/Artsy/Classes/Utils/ARSharingController.m b/Artsy/Classes/Utils/ARSharingController.m new file mode 100644 index 00000000000..ff8eb4c281a --- /dev/null +++ b/Artsy/Classes/Utils/ARSharingController.m @@ -0,0 +1,121 @@ +#import "ARSharingController.h" +#import "ARURLItemProvider.h" +#import "ARImageItemProvider.h" +#import "ARMessageItemProvider.h" +#import + +@interface ARSharingController () +@property (nonatomic, strong) id object; +@property (nonatomic, strong) ARURLItemProvider *urlProvider; +@property (nonatomic, strong) ARImageItemProvider *imageProvider; +@property (nonatomic, strong) ARMessageItemProvider *messageProvider; +@end + +@implementation ARSharingController + ++ (void)shareObject:(id)object +{ + return [self shareObject:object withThumbnailImageURL:nil withImage:nil]; +} + ++ (void)shareObject:(id)object withThumbnailImageURL:(NSURL *)thumbnailImageURL +{ + return [self shareObject:object withThumbnailImageURL:thumbnailImageURL withImage:nil]; +} + ++ (void)shareObject:(id)object withThumbnailImageURL:(NSURL *)thumbnailImageURL withImage:(UIImage *)image +{ + ARSharingController *sharingController = [[self alloc] initWithObject:object]; + [sharingController shareWithThumbnailImageURL:thumbnailImageURL image:image]; +} + +- (instancetype)initWithObject:(id)object +{ + self = [super init]; + if (!self) { return nil; } + _object = object; + return self; +} + +- (void)shareWithThumbnailImageURL:(NSURL *)thumbnailImageURL image:(UIImage *)image +{ + _messageProvider = [[ARMessageItemProvider alloc] initWithMessage:self.message path:[self.object publicArtsyPath]]; + _urlProvider = [[ARURLItemProvider alloc] initWithMessage:self.message path:[self.object publicArtsyPath] thumbnailImageURL:thumbnailImageURL]; + _imageProvider = [[ARImageItemProvider alloc] initWithPlaceholderItem:image]; + [self presentActivityViewController]; +} + +- (void)presentActivityViewController +{ + if (ARIsRunningInDemoMode) { + [UIAlertView showWithTitle:nil message:@"Feature not enabled for this demo" cancelButtonTitle:@"OK" otherButtonTitles:nil tapBlock:nil]; + return; + } + + ARTopMenuViewController *topMenuVC = [ARTopMenuViewController sharedController]; + UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:[self activityItems] applicationActivities:nil]; + + activityVC.excludedActivityTypes = @[ + UIActivityTypePostToWeibo, + UIActivityTypePrint, + UIActivityTypeAssignToContact, + UIActivityTypeSaveToCameraRoll, + UIActivityTypePostToFlickr, + UIActivityTypePostToVimeo, + UIActivityTypePostToTencentWeibo + ]; + + activityVC.completionHandler = ^(NSString *activityType, BOOL completed) { + [self handleActivityCompletion:activityType completed:completed]; + }; + + [topMenuVC presentViewController:activityVC animated:YES completion:nil]; +} + +- (void)handleActivityCompletion:(NSString *)activityType completed:(BOOL)completed +{ + // Required for analytics +} + +// ARMessageItemProvider will add the appropriate " on Artsy:", " on Artsy", " on @Artsy", etc to message. +- (NSString *)message +{ + if (self.object.class == [Artwork class]){ + Artwork *artwork = (Artwork *)self.object; + if (artwork.artist.name.length) { + return [NSString stringWithFormat:@"\"%@\" by %@", artwork.title, artwork.artist.name]; + } else { + return [NSString stringWithFormat:@"\"%@\"", artwork.title]; + } + } else if (self.object.class == [PartnerShow class]) { + return [NSString stringWithFormat:@"See %@", self.object.name]; + } else { + return self.object.name; + } +} + +- (NSString *)objectID +{ + if (self.object.class == [Artwork class]) { + return [(Artwork *) self.object artworkID]; + } else if (self.object.class == [Artist class]) { + return [(Artist *) self.object artistID]; + } else if (self.object.class == [Gene class]) { + return [(Gene *) self.object geneID]; + } else if (self.object.class == [PartnerShow class]) { + return [(PartnerShow *) self.object showID]; + } else { + return nil; + } +} + +- (NSArray *)activityItems +{ + return @[ + self.messageProvider, + self.urlProvider, + self.imageProvider + ]; +} + +@end diff --git a/Artsy/Classes/Utils/ARSplitStackView.h b/Artsy/Classes/Utils/ARSplitStackView.h new file mode 100644 index 00000000000..0f2aec519a0 --- /dev/null +++ b/Artsy/Classes/Utils/ARSplitStackView.h @@ -0,0 +1,10 @@ +#import + +@interface ARSplitStackView : UIView + +- (instancetype)initWithLeftPredicate:(NSString *)left rightPredicate:(NSString *)right; + +@property (nonatomic, weak, readonly) ORStackView *leftStack; +@property (nonatomic, weak, readonly) ORStackView *rightStack; + +@end diff --git a/Artsy/Classes/Utils/ARSplitStackView.m b/Artsy/Classes/Utils/ARSplitStackView.m new file mode 100644 index 00000000000..5f1726de1d2 --- /dev/null +++ b/Artsy/Classes/Utils/ARSplitStackView.m @@ -0,0 +1,58 @@ +#import "ARSplitStackView.h" + +@interface ARSplitStackView () +@end + +@implementation ARSplitStackView + +- (instancetype)initWithLeftPredicate:(NSString *)left rightPredicate:(NSString *)right +{ + self = [super init]; + + ORStackView *leftStack = [[ORStackView alloc] init]; + ORStackView *rightStack = [[ORStackView alloc] init]; + + _leftStack = leftStack; + _rightStack = rightStack; + + [self addSubview:leftStack]; + [self addSubview:rightStack]; + + [_leftStack alignLeadingEdgeWithView:self predicate:nil]; + [_rightStack alignTrailingEdgeWithView:self predicate:nil]; + + [self alignAttribute:NSLayoutAttributeTop toAttribute:NSLayoutAttributeTop ofView:_rightStack predicate:nil]; + [self alignAttribute:NSLayoutAttributeTop toAttribute:NSLayoutAttributeTop ofView:_leftStack predicate:nil]; + +// [self alignAttribute:NSLayoutAttributeBottom toAttribute:NSLayoutAttributeBottom ofView:_rightStack predicate:nil]; +// [self alignAttribute:NSLayoutAttributeBottom toAttribute:NSLayoutAttributeBottom ofView:_leftStack predicate:nil]; + + [_leftStack setAutoresizingMask:UIViewAutoresizingFlexibleBottomMargin]; + [_rightStack setAutoresizingMask:UIViewAutoresizingFlexibleBottomMargin]; + [_leftStack constrainHeightToView:self predicate:@"0@250"]; + [_rightStack constrainHeightToView:self predicate:@"0@250"]; + [_leftStack constrainHeightToView:self predicate:@"<=0@1000"]; + [_rightStack constrainHeightToView:self predicate:@"<=0@1000"]; + + if (left) { + [_leftStack constrainWidth:left]; + } + + if (right) { + [_rightStack constrainWidth:right]; + } + + _leftStack.bottomMarginHeight = 0; + _rightStack.bottomMarginHeight = 0; + + return self; +} + +//- (CGSize)intrinsicContentSize +//{ +// CGSize leftSize = self.leftStack.intrinsicContentSize; +// CGSize rightSize = self.rightStack.intrinsicContentSize; +// return (leftSize.height > rightSize.height) ? leftSize : rightSize; +//} + +@end diff --git a/Artsy/Classes/Utils/ARStandardDateFormatter.h b/Artsy/Classes/Utils/ARStandardDateFormatter.h new file mode 100644 index 00000000000..67e8c7f3101 --- /dev/null +++ b/Artsy/Classes/Utils/ARStandardDateFormatter.h @@ -0,0 +1,11 @@ +#import + +@interface ARStandardDateFormatter : ISO8601DateFormatter + +/// Shared date formatter for ISO8601 text to NSDates ++ (ARStandardDateFormatter *)sharedFormatter; + +/// Transforms strings from JSON to NSDates using the ISO8601 format +@property (nonatomic, readonly) NSValueTransformer *stringTransformer; + +@end diff --git a/Artsy/Classes/Utils/ARStandardDateFormatter.m b/Artsy/Classes/Utils/ARStandardDateFormatter.m new file mode 100644 index 00000000000..acf70b42e08 --- /dev/null +++ b/Artsy/Classes/Utils/ARStandardDateFormatter.m @@ -0,0 +1,27 @@ +#import "ARStandardDateFormatter.h" + +@interface ARStandardDateFormatter() +@property (nonatomic, strong) NSValueTransformer *stringTransformer; +@end + +@implementation ARStandardDateFormatter + ++ (ARStandardDateFormatter *)sharedFormatter +{ + static ARStandardDateFormatter *_sharedFormatter = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedFormatter = [[self alloc] init]; + + _sharedFormatter.stringTransformer = [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) { + return [_sharedFormatter dateFromString:str]; + + } reverseBlock:^(NSDate *date) { + return [_sharedFormatter stringFromDate:date]; + }]; + + }); + return _sharedFormatter; +} + +@end diff --git a/Artsy/Classes/Utils/ARSwitchBoard.h b/Artsy/Classes/Utils/ARSwitchBoard.h new file mode 100644 index 00000000000..03bbe98581a --- /dev/null +++ b/Artsy/Classes/Utils/ARSwitchBoard.h @@ -0,0 +1,77 @@ +#import "ARFairAwareObject.h" + +@class ARPostFeedItem; +@class ARFollowArtistFeedItem; + +// View Controller Forward Declarations + +@class ARArtworkSetViewController; +@class ARFairShowViewController; +@class ARFairArtistViewController; +@class ARArtistViewController; +@class ARArtworkInfoViewController; +@class ARAuctionArtworkResultsViewController; +@class ARFairMapViewController; +@class ARGeneViewController; +@class ARUserSettingsViewController; + +/** + The Switchboard is the internal API for loading different native views + it does this mostly by using either an internal Sinatra like-router, or + by directly passing the message on to whichever ARNavigationContainer compliant + object we want. +*/ + +@interface ARSwitchBoard : NSObject + ++ (instancetype)sharedInstance; + +/// Provide a simple API to load an ArtworkVC from a lot of different inputs +- (ARArtworkSetViewController *)loadArtworkSet:(NSArray *)artworkSet inFair:(Fair *)fair atIndex:(NSInteger)index; +- (ARArtworkSetViewController *)loadArtwork:(Artwork *)artwork inFair:(Fair *)fair; +- (ARArtworkSetViewController *)loadArtworkWithID:(NSString *)artworkID inFair:(Fair *)fair; + +- (UIViewController *)loadBidUIForArtwork:(NSString *)artworkID inSale:(NSString *)saleID; + +/// Load the auction results for an artwork on to the stack +- (ARAuctionArtworkResultsViewController *)loadAuctionResultsForArtwork:(Artwork *)artwork; +- (ARArtworkInfoViewController *)loadMoreInfoForArtwork:(Artwork *)artwork; + +/// Load a Map VC +- (ARFairMapViewController *)loadMapInFair:(Fair *)fair; +- (ARFairMapViewController *)loadMapInFair:(Fair *)fair title:(NSString *)title selectedPartnerShows:(NSArray *)selectedPartnerShows; + +- (ARArtistViewController *)loadArtistWithID:(NSString *)artistID; +- (UIViewController *)loadArtistWithID:(NSString *)artistID inFair:(Fair *)fair; + +/// Load a Partner Page in Martsy +- (UIViewController *)loadPartnerWithID:(NSString *)partnerID; + +/// Load a Profile. Used to separate profiles with a fair from regular profiles. +- (UIViewController *)routeProfileWithID:(NSString *)profileID; + +/// Load a Gene +- (ARGeneViewController *)loadGene:(Gene *)gene; +- (ARGeneViewController *)loadGeneWithID:(NSString *)geneID; + +/// Load a fair booth +- (ARFairShowViewController *)loadShow:(PartnerShow *)show fair:(Fair *)fair; +- (ARFairShowViewController *)loadShow:(PartnerShow *)show; +- (ARFairShowViewController *)loadShowWithID:(NSString *)showID; +- (ARFairShowViewController *)loadShowWithID:(NSString *)showID fair:(Fair *)fair; + +/// Load a path relative to the baseURL through the router +- (UIViewController *)loadPath:(NSString *)path; + +/// Send an URL through the router +- (UIViewController *)loadURL:(NSURL *)url; +- (UIViewController *)loadURL:(NSURL *)url fair:(Fair *)fair; + +- (ARUserSettingsViewController *)loadUserSettings; + +- (NSURL *)resolveRelativeUrl:(NSString *)path; + +/// Buy artwork +- (UIViewController *)loadOrderUIForID:(NSString *)orderID resumeToken:(NSString *)resumeToken; + +@end diff --git a/Artsy/Classes/Utils/ARSwitchBoard.m b/Artsy/Classes/Utils/ARSwitchBoard.m new file mode 100644 index 00000000000..f81e7ae60d2 --- /dev/null +++ b/Artsy/Classes/Utils/ARSwitchBoard.m @@ -0,0 +1,391 @@ +#import "ARRouter.h" +#import +#import + +// View Controllers +#import "ARArtworkSetViewController.h" +#import "ARFairShowViewController.h" +#import "ARFairArtistViewController.h" +#import "ARGeneViewController.h" +#import "ARArtworkInfoViewController.h" +#import "ARBrowseViewController.h" +#import "ARInternalMobileWebViewController.h" +#import "ARFairGuideContainerViewController.h" +#import "ARUserSettingsViewController.h" +#import "ARArtistViewController.h" +#import "ARAuctionArtworkResultsViewController.h" +#import "ARFavoritesViewController.h" +#import "ARFairMapViewController.h" +#import "ARProfileViewController.h" + +@interface ARSwitchBoard () + +@property (readonly, nonatomic, copy) JLRoutes *routes; + +@end + +@implementation ARSwitchBoard + +#pragma mark - Lifecycle + ++ (void)load +{ + // Force a load of default routes. + [[self class] sharedInstance]; +} + ++ (instancetype)sharedInstance +{ + static ARSwitchBoard *sharedInstance; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[ARSwitchBoard alloc] init]; + }); + + return sharedInstance; +} + + +- (id)init +{ + self = [super init]; + if (!self) { return nil; } + + _routes = [[JLRoutes alloc] init]; + + @weakify(self); + [self.routes addRoute:@"/artist/:id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self) + ARArtistViewController *viewController = [self loadArtistWithID:parameters[@"id"]]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + // For artists in a gallery context, like https://artsy.net/spruth-magers/artist/astrid-klein . Until we have a native + // version of the gallery profile/context, we will use the normal native artist view instead of showing a web view on iPad. + + if ([UIDevice isPad]) { + [self.routes addRoute:@"/:profile_id/artist/:id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self) + Fair *fair = [parameters[@"fair"] isKindOfClass:Fair.class] ? parameters[@"fair"] : nil; + + ARArtistViewController *viewController = (id)[self loadArtistWithID:parameters[@"id"] inFair:fair]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + } + + [self.routes addRoute:@"/artwork/:id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self) + Fair *fair = [parameters[@"fair"] isKindOfClass:Fair.class] ? parameters[@"fair"] : nil; + ARArtworkSetViewController *viewController = [self loadArtworkWithID:parameters[@"id"] inFair:fair]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + [self.routes addRoute:@"/gene/:id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self) + ARGeneViewController *viewController = [self loadGeneWithID:parameters[@"id"]]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + + if (![UIDevice isPad]) { + [self.routes addRoute:@"/show/:id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self) + ARFairShowViewController *viewController = [self loadShowWithID:parameters[@"id"]]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + + [self.routes addRoute:@"/:profile_id/for-you" handler:^BOOL(NSDictionary *parameters) { + @strongify(self); + + id context = parameters[@"fair"]; + NSAssert(context != nil, @"Fair guide routing attempt with no context. "); + + UIViewController *viewController = [self loadFairGuideWithFair:context]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + + return YES; + }]; + + [self.routes addRoute:@"/:profile_id/browse/artist/:id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self) + + UIViewController *viewController = [self loadArtistInFairWithID:parameters[@"id"] fair:parameters[@"fair"]]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + } + + [self.routes addRoute:@"/" handler:^BOOL(NSDictionary *parameters) { + [[ARTopMenuViewController sharedController] loadFeed]; + return YES; + }]; + + [self.routes addRoute:@"/favorites" handler:^BOOL(NSDictionary *parameters) { + ARFavoritesViewController *viewController = [[ARFavoritesViewController alloc] init]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + [self.routes addRoute:@"/browse" handler:^BOOL(NSDictionary *parameters) { + ARBrowseViewController *viewController = [[ARBrowseViewController alloc] init]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + [self.routes addRoute:@"/:profile_id" handler:^BOOL(NSDictionary *parameters) { + @strongify(self); + UIViewController *viewController = [self routeProfileWithID: parameters[@"profile_id"]]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + return YES; + }]; + + return self; +} + +#pragma mark - +#pragma mark Artworks + +- (ARArtworkSetViewController *)loadArtwork:(Artwork *)artwork inFair:(Fair *)fair +{ + ARArtworkSetViewController *viewController = [[ARArtworkSetViewController alloc] initWithArtwork:artwork fair:fair]; + return viewController; +} + +- (ARArtworkSetViewController *)loadArtworkWithID:(NSString *)artworkID inFair:(Fair *)fair +{ + ARArtworkSetViewController *viewController = [[ARArtworkSetViewController alloc] initWithArtworkID:artworkID fair:fair]; + return viewController; +} + +- (ARArtworkSetViewController *)loadArtworkSet:(NSArray *)artworkSet inFair:(Fair *)fair atIndex:(NSInteger)index +{ + ARArtworkSetViewController *viewController = [[ARArtworkSetViewController alloc] initWithArtworkSet:artworkSet fair:fair atIndex:index]; + return viewController; +} + + +- (UIViewController *)loadBidUIForArtwork:(NSString *)artworkID inSale:(NSString *)saleID +{ + NSString *path = [NSString stringWithFormat:@"/feature/%@/bid/%@", saleID, artworkID]; + return [self loadURL:[NSURL URLWithString:path]]; +} + + +- (ARAuctionArtworkResultsViewController *)loadAuctionResultsForArtwork:(Artwork *)artwork +{ + ARAuctionArtworkResultsViewController *viewController = [[ARAuctionArtworkResultsViewController alloc] initWithArtwork:artwork]; + return viewController; +} + +- (ARArtworkInfoViewController *)loadMoreInfoForArtwork:(Artwork *)artwork +{ + ARArtworkInfoViewController *viewController = [[ARArtworkInfoViewController alloc] initWithArtwork:artwork]; + return viewController; +} + +- (ARFairShowViewController *)loadShow:(PartnerShow *)show fair:(Fair *)fair +{ + ARFairShowViewController *viewController = [[ARFairShowViewController alloc] initWithShow:show fair:fair]; + return viewController; +} + +- (ARFairShowViewController *)loadShow:(PartnerShow *)show +{ + return [self loadShow:show fair:nil]; +} + +- (ARFairShowViewController *)loadShowWithID:(NSString *)showID fair:(Fair *)fair +{ + ARFairShowViewController *viewController = [[ARFairShowViewController alloc] initWithShowID:showID fair:fair]; + return viewController; +} + +- (ARFairShowViewController *)loadShowWithID:(NSString *)showID +{ + return [self loadShowWithID:showID fair:nil]; +} + +#pragma mark - +#pragma mark Partner + +- (UIViewController *)loadPartnerWithID:(NSString *)partnerID +{ + return [self loadPath:partnerID]; +} + +#pragma mark - +#pragma mark Genes + +- (ARGeneViewController *)loadGene:(Gene *)gene +{ + ARGeneViewController *viewController = [[ARGeneViewController alloc] initWithGene:gene]; + return viewController; +} + + +- (ARGeneViewController *)loadGeneWithID:(NSString *)geneID +{ + ARGeneViewController *viewController = [[ARGeneViewController alloc] initWithGeneID:geneID]; + return viewController; +} + +#pragma mark - +#pragma mark Artists + +- (UIViewController *)loadArtistWithID:(NSString *)artistID inFair:(Fair *)fair +{ + if(fair){ + ARFairArtistViewController *viewController = [[ARFairArtistViewController alloc] initWithArtistID:artistID fair:fair]; + return viewController; + } else { + ARArtistViewController *viewController = [[ARArtistViewController alloc] initWithArtistID:artistID]; + return viewController; + } +} + +- (ARFairMapViewController *)loadMapInFair:(Fair *)fair +{ + ARFairMapViewController *viewController = [[ARFairMapViewController alloc] initWithFair:fair]; + return viewController; +} + +- (ARFairMapViewController *)loadMapInFair:(Fair *)fair title:(NSString *)title selectedPartnerShows:(NSArray *)selectedPartnerShows +{ + ARFairMapViewController *viewController = [[ARFairMapViewController alloc] initWithFair:fair title:title selectedPartnerShows:selectedPartnerShows]; + if (title) { + viewController.expandAnnotations = NO; + } + return viewController; +} + +- (ARArtistViewController *)loadArtistWithID:(NSString *)artistID +{ + ARArtistViewController *viewController = [[ARArtistViewController alloc] initWithArtistID:artistID]; + return viewController; +} + +- (ARFairArtistViewController *)loadArtistInFairWithID:(NSString *)artistID fair:(Fair *)fair +{ + ARFairArtistViewController *viewController = [[ARFairArtistViewController alloc] initWithArtistID:artistID fair:fair]; + return viewController; +} + +#pragma mark - +#pragma mark Urls + +- (UIViewController *)loadPath:(NSString *)path +{ + return [self loadURL:[self resolveRelativeUrl:path]]; +} + +- (UIViewController *)loadURL:(NSURL *)url +{ + return [self loadURL:url fair:nil]; +} + +- (UIViewController *)loadURL:(NSURL *)url fair:(Fair *)fair +{ + // May be nil by the end of the method + UIViewController *viewController; + + if ([ARRouter isInternalURL:url]) { + NSURL *fixedURL = [self fixHostForURL:url]; + viewController = [self routeInternalURL:fixedURL fair:fair]; + } else if ([ARRouter isWebURL:url]) { + if (ARIsRunningInDemoMode) { + [[UIApplication sharedApplication] openURL:url]; + } else { + viewController = [[ARExternalWebBrowserViewController alloc] initWithURL:url]; + } + } else { + [self openURLInExternalService:url]; + } + + return viewController; +} + +- (ARProfileViewController *)routeProfileWithID:(NSString *)profileID +{ + NSParameterAssert(profileID); + return [[ARProfileViewController alloc] initWithProfileID:profileID]; +} + +- (void)openURLInExternalService:(NSURL *)url +{ + BOOL isWebsite = [url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]; + NSString *title = isWebsite? @"Open in Safari" : @"Open with other App"; + NSString *messsage = NSStringWithFormat(@"Would you like to visit '%@'?", url.absoluteString); + messsage = [messsage stringByReplacingOccurrencesOfString:@"www." withString:@""]; + messsage = [messsage stringByReplacingOccurrencesOfString:@"http://" withString:@""]; + messsage = [messsage stringByReplacingOccurrencesOfString:@"https://" withString:@""]; + + [UIAlertView showWithTitle:title message:messsage cancelButtonTitle:@"Go back to Artsy" otherButtonTitles:@[@"Open"] tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) { + if (buttonIndex == 1) { + [[UIApplication sharedApplication] openURL:url]; + } + }]; +} + +#pragma mark - +#pragma mark Fair + +-(ARFairGuideContainerViewController *)loadFairGuideWithFair:(Fair *)fair +{ + ARFairGuideContainerViewController *viewController = [[ARFairGuideContainerViewController alloc] initWithFair:fair]; + return viewController; +} + +// use the internal router +- (ARInternalMobileWebViewController *)routeInternalURL:(NSURL *)url fair:(Fair *)fair +{ + if ([self.routes canRouteURL:url]) { + [self.routes routeURL:url withParameters:(fair? @{@"fair": fair} : nil)]; + return nil; + } + + ARInternalMobileWebViewController *viewController = [[ARInternalMobileWebViewController alloc] initWithURL:url]; + viewController.fair = fair; + return viewController; +} + +- (NSURL *)resolveRelativeUrl:(NSString *)path +{ + return [NSURL URLWithString:path relativeToURL:[ARRouter baseWebURL]]; +} + +- (NSURL *)fixHostForURL:(NSURL *)url +{ + // from applewebdata://EF86F744-3F4F-4732-8A4B-3E5E94D6D7DA/artist/marcel-duchamp + // to http://artsy.net/artist/marcel-duchamp/ + + if ([url.absoluteString hasPrefix:@"applewebdata"]) { + NSArray *components = [url.absoluteString componentsSeparatedByString:@"/"]; + NSArray *lastTwo = @[components[components.count - 2], components[components.count - 1]]; + NSString *newURLString = [NSString stringWithFormat:@"http://artsy.net/%@/%@", lastTwo[0], lastTwo[1]]; + return [NSURL URLWithString:newURLString]; + } + return url; +} + + +- (ARUserSettingsViewController *)loadUserSettings +{ + ARUserSettingsViewController *viewController = [[ARUserSettingsViewController alloc] initWithUser:[User currentUser]]; + return viewController; +} + + +- (UIViewController *)loadOrderUIForID:(NSString *)orderID resumeToken:(NSString *)resumeToken +{ + NSString *path = [NSString stringWithFormat:@"/order/%@/resume?token=%@", orderID, resumeToken]; + return [self loadPath:path]; +} + +@end diff --git a/Artsy/Classes/Utils/ARTrialController.h b/Artsy/Classes/Utils/ARTrialController.h new file mode 100644 index 00000000000..4f41936533b --- /dev/null +++ b/Artsy/Classes/Utils/ARTrialController.h @@ -0,0 +1,43 @@ +typedef NS_ENUM(NSInteger, ARTrialContext){ + ARTrialContextNotTrial, + ARTrialContextFavoriteArtwork, + ARTrialContextFavoriteArtist, + ARTrialContextFavoriteGene, + ARTrialContextShowingFavorites, + ARTrialContextContactGallery, + ARTrialContextRepresentativeInquiry, + ARTrialContextPeriodical, + ARTrialContextAuctionBid, + ARTrialContextFavoriteProfile, + ARTrialContextArtworkOrder, + ARTrialContextFairGuide +}; + +@interface ARTrialController : NSObject + +/// Shows the sign up, with an optional target / selector to re-trigger the +/// original action after we've logged in. + ++ (void)presentTrialWithContext:(enum ARTrialContext)context fromTarget:(id)target selector:(SEL)selector; + +/// Get the guest authentication token and run the completion block ++ (void)startTrialWithCompletion:(void (^)(void))completion failure:(void (^)(NSError *error))failure; + +/// No-op if you didn't have a trial account before signing up +/// otherwise will run the "favorite" artwork or whatever started the splash ++ (void)performPostSignupEvent; + +/// Adds one to the number of view controllers shown before showing the splash ++ (void)shownAViewController; + +/// When the app returns from being backgrounded we should increment said number above +/// to avoid seeing the splash after your first tap ++ (void)extendTrial; + ++ (NSString *)stringForTrialContext:(enum ARTrialContext)context; + ++ (ARTrialController *)instance; + +@property (readonly, nonatomic, assign) NSInteger threshold; + +@end diff --git a/Artsy/Classes/Utils/ARTrialController.m b/Artsy/Classes/Utils/ARTrialController.m new file mode 100644 index 00000000000..4b608daf886 --- /dev/null +++ b/Artsy/Classes/Utils/ARTrialController.m @@ -0,0 +1,150 @@ +#import "ARUserManager.h" +#import "ARAppDelegate.h" +#import + +static ARTrialController *instance; + +@interface ARTrialController () +@property (readwrite, nonatomic, assign) NSInteger threshold; +@property (readwrite, nonatomic, assign) NSInteger count; +@property (readwrite, nonatomic, assign) SEL selectorForPostSignup; +@property (readwrite, nonatomic, strong) id targetForPostSignup; +@end + +@implementation ARTrialController + ++ (ARTrialController *)instance +{ + return instance; +} + ++ (void)initialize +{ + if ([self class] == [ARTrialController class]) { + instance = [[ARTrialController alloc] init]; + } +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + [self reset]; + } + return self; +} + +- (void)reset +{ + self.count = 0; + self.threshold = [[NSUserDefaults standardUserDefaults] integerForKey:AROnboardingPromptThresholdDefault]; + if (self.threshold <= 0) { + self.threshold = 25; //just in case + } +} + ++ (void)presentTrialWithContext:(enum ARTrialContext)context fromTarget:(id)target selector:(SEL)selector +{ + [instance presentTrialWithContext:context fromTarget:target selector:selector]; +} + +- (void)presentTrialWithContext:(enum ARTrialContext)context fromTarget:(id)target selector:(SEL)selector +{ + if (ARIsRunningInDemoMode) { + [UIAlertView showWithTitle:nil message:@"Feature not enabled for this demo" cancelButtonTitle:@"OK" otherButtonTitles:nil tapBlock:nil]; + return; + } + + if ([User isTrialUser]) { + self.selectorForPostSignup = selector; + self.targetForPostSignup = target; + + ARAppDelegate *appDelegate = [ARAppDelegate sharedInstance]; + [appDelegate showTrialOnboardingWithState:ARInitialOnboardingStateInApp andContext:context]; + } +} + + ++ (NSString *)stringForTrialContext:(enum ARTrialContext)context +{ + switch (context) { + case ARTrialContextFavoriteArtist: + return @"favoriting_artist"; + case ARTrialContextFavoriteGene: + return @"favoriting_gene"; + case ARTrialContextFavoriteArtwork: + return @"favoriting_artwork"; + case ARTrialContextShowingFavorites: + return @"showing_favorites"; + case ARTrialContextPeriodical: + return @"periodical"; + case ARTrialContextRepresentativeInquiry: + return @"contact_rep"; + case ARTrialContextContactGallery: + return @"contact_gallery"; + case ARTrialContextNotTrial: + return @""; + case ARTrialContextAuctionBid: + return @"auction_bid"; + case ARTrialContextFavoriteProfile: + return @"favoriting_profile"; + case ARTrialContextArtworkOrder: + return @"artwork_order"; + case ARTrialContextFairGuide: + return @"fair_guide"; + } +} + ++ (void)performPostSignupEvent +{ + [instance performPostSignupEvent]; +} + +- (void)performPostSignupEvent +{ + if (self.selectorForPostSignup && self.targetForPostSignup) { + if ([self.targetForPostSignup respondsToSelector:self.selectorForPostSignup]) { + +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.targetForPostSignup performSelector:self.selectorForPostSignup withObject:nil]; +# pragma clang diagnostic pop + + } + } +} + + ++ (void)startTrialWithCompletion:(void (^)(void))completion failure:(void (^)(NSError *error))failure +{ + [[ARUserManager sharedManager] startTrial:completion failure:failure]; +} + ++ (void)extendTrial +{ + [instance extendTrial]; +} + +- (void)extendTrial +{ + [self reset]; +} + ++ (void)shownAViewController +{ + [instance shownAViewController]; +} + +- (void)shownAViewController +{ + self.count++; + + BOOL shouldShowSplash = self.count >= self.threshold; + if (shouldShowSplash) { + [self presentTrialWithContext:ARTrialContextPeriodical fromTarget:nil selector:nil]; + [self reset]; + } + +} + +@end diff --git a/Artsy/Classes/Utils/ARURLItemProvider.h b/Artsy/Classes/Utils/ARURLItemProvider.h new file mode 100644 index 00000000000..9acb03eb861 --- /dev/null +++ b/Artsy/Classes/Utils/ARURLItemProvider.h @@ -0,0 +1,11 @@ +#import + +@interface ARURLItemProvider : UIActivityItemProvider + +@property (nonatomic, strong, readonly) NSString *message; +@property (nonatomic, strong, readonly) NSURL *thumbnailImageURL; +@property (nonatomic, strong, readonly) UIImage *thumbnailImage; + +- (instancetype)initWithMessage:(NSString *)message path:(NSString *)path thumbnailImageURL:(NSURL *)thumbnailImageURL; + +@end diff --git a/Artsy/Classes/Utils/ARURLItemProvider.m b/Artsy/Classes/Utils/ARURLItemProvider.m new file mode 100644 index 00000000000..8fed09e8df6 --- /dev/null +++ b/Artsy/Classes/Utils/ARURLItemProvider.m @@ -0,0 +1,54 @@ +#import "ARURLItemProvider.h" +#import "ARFileUtils.h" + +@implementation ARURLItemProvider + +- (instancetype)initWithMessage:(NSString *)message path:(NSString *)path thumbnailImageURL:(NSURL *)thumbnailImageURL +{ + NSURL *shareableURL = [ARSwitchBoard.sharedInstance resolveRelativeUrl:path]; + + // sharing the URL built with URLWithString:relativeToURL via AirDrop: fails with a declined error message + self = [super initWithPlaceholderItem:[NSURL URLWithString:shareableURL.absoluteString]]; + if (!self) { return nil; } + _thumbnailImageURL = thumbnailImageURL; + _message = message; + return self; +} + +- (UIImage *)activityViewController:(UIActivityViewController *)activityViewController + thumbnailImageForActivityType:(NSString *)activityType + suggestedSize:(CGSize)size +{ + if ([self.activityType isEqualToString:UIActivityTypeAirDrop]) { + return [UIImage imageNamed:@"AppIcon_120"]; + } + + if (!self.thumbnailImage && self.thumbnailImageURL) { + NSData * imageData = [[NSData alloc] initWithContentsOfURL:self.thumbnailImageURL]; + _thumbnailImage = [UIImage imageWithData: imageData]; + } + + return self.thumbnailImage; +} + +- (id)item +{ + if ([self.activityType isEqualToString:UIActivityTypeAirDrop]) { + // https://engineering.eventbrite.com/setting-the-title-of-airdrop-shares-under-ios-7 + // replace slashes with a look-alike unicode character + NSString *safeFilename = [self.message stringByReplacingOccurrencesOfString:@"/" withString:@"\u2215"]; + // remove quotes + safeFilename = [safeFilename stringByReplacingOccurrencesOfString:@"\"" withString:@""]; + // append filename extension + safeFilename = [safeFilename stringByAppendingString:@".Artsy"]; + NSURL *filename = [NSURL fileURLWithPath:[ARFileUtils cachesPathWithFolder:@"Airdrop" filename:safeFilename]]; + id JSON = @{ @"version" : @(1), @"url" : [self.placeholderItem absoluteString] }; + NSData *data = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:nil]; + [data writeToURL:filename atomically:YES]; + return filename; + } else { + return self.placeholderItem; + } +} + +@end diff --git a/Artsy/Classes/Utils/ARValueTransformer.h b/Artsy/Classes/Utils/ARValueTransformer.h new file mode 100644 index 00000000000..ca693c925f0 --- /dev/null +++ b/Artsy/Classes/Utils/ARValueTransformer.h @@ -0,0 +1,8 @@ +#import "MTLValueTransformer.h" + +@interface ARValueTransformer : MTLValueTransformer + ++ (instancetype)enumValueTransformerWithMap:(NSDictionary *)types; ++ (instancetype)whitespaceTrimmingTransformer; + +@end diff --git a/Artsy/Classes/Utils/ARValueTransformer.m b/Artsy/Classes/Utils/ARValueTransformer.m new file mode 100644 index 00000000000..843d32730c3 --- /dev/null +++ b/Artsy/Classes/Utils/ARValueTransformer.m @@ -0,0 +1,20 @@ +#import "ARValueTransformer.h" + +@implementation ARValueTransformer + ++ (instancetype)enumValueTransformerWithMap:(NSDictionary *)types +{ + return [self.class reversibleTransformerWithForwardBlock: ^(NSString *str) { + return types[str]; + } reverseBlock: ^(NSNumber *type) { + return [types allKeysForObject:type].lastObject; + }]; +} + ++ (instancetype)whitespaceTrimmingTransformer { + return [self.class transformerWithBlock:^ id (NSString *str) { + return [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + }]; +} + +@end diff --git a/Artsy/Classes/Utils/ActiveErrorView.xib b/Artsy/Classes/Utils/ActiveErrorView.xib new file mode 100644 index 00000000000..59b7ac06bf6 --- /dev/null +++ b/Artsy/Classes/Utils/ActiveErrorView.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.h b/Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.h new file mode 100644 index 00000000000..c27c3327f65 --- /dev/null +++ b/Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.h @@ -0,0 +1,5 @@ +#import + +@interface ARHTTPRequestOperationLogger : AFHTTPRequestOperationLogger + +@end diff --git a/Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.m b/Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.m new file mode 100644 index 00000000000..afa85191fe8 --- /dev/null +++ b/Artsy/Classes/Utils/Logging/ARHTTPRequestOperationLogger.m @@ -0,0 +1,90 @@ +#import "ARHTTPRequestOperationLogger.h" +#import + +@implementation ARHTTPRequestOperationLogger + +- (id)init +{ + self = [super init]; + if (!self) { + return nil; + } + + self.level = httpLogLevel; + + return self; +} + +- (void)HTTPOperationDidStart:(NSNotification *)notification { + AFHTTPRequestOperation *operation = (AFHTTPRequestOperation *)[notification object]; + + if (![operation isKindOfClass:[AFHTTPRequestOperation class]]) { + return; + } + + objc_setAssociatedObject(operation, AFHTTPRequestOperationStartDate, [NSDate date], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (self.filterPredicate && [self.filterPredicate evaluateWithObject:operation]) { + return; + } + + switch (self.level) { + case AFLoggerLevelDebug: + [self logOperationStart:operation]; + break; + default: + break; + } +} + +- (void)logOperationStart:(AFHTTPRequestOperation *)operation +{ + NSString *body = nil; + if ([operation.request HTTPBody]) { + body = [[NSString alloc] initWithData:[operation.request HTTPBody] encoding:NSUTF8StringEncoding]; + } + + ARHTTPRequestOperationDebugLog(@"[Operation Start] %@ '%@': %@ %@", [operation.request HTTPMethod], [[operation.request URL] absoluteString], [operation.request allHTTPHeaderFields], body); +} + +static void * AFHTTPRequestOperationStartDate = &AFHTTPRequestOperationStartDate; + +- (void)HTTPOperationDidFinish:(NSNotification *)notification { + AFHTTPRequestOperation *operation = (AFHTTPRequestOperation *)[notification object]; + + if (![operation isKindOfClass:[AFHTTPRequestOperation class]]) { + return; + } + + if (self.filterPredicate && [self.filterPredicate evaluateWithObject:operation]) { + return; + } + + NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSinceDate:objc_getAssociatedObject(operation, AFHTTPRequestOperationStartDate)]; + + if (operation.error) { + switch (self.level) { + case AFLoggerLevelDebug: + case AFLoggerLevelInfo: + case AFLoggerLevelWarn: + case AFLoggerLevelError: + ARHTTPRequestOperationFailureLog(@"[Error] %@ '%@' (%ld) [%.04f s]: %@", [operation.request HTTPMethod], [[operation.response URL] absoluteString], (long)[operation.response statusCode], elapsedTime, operation.error); + default: + break; + } + } else { + switch (self.level) { + case AFLoggerLevelDebug: + ARHTTPRequestOperationSuccessLog(@"[Success] %ld '%@' [%.04f s]: %@ %@", (long)[operation.response statusCode], [[operation.response URL] absoluteString], elapsedTime, [operation.response allHeaderFields], operation.responseString); + break; + case AFLoggerLevelInfo: + ARHTTPRequestOperationSuccessLog(@"[Success] %ld '%@' [%.04f s]", (long)[operation.response statusCode], [[operation.response URL] absoluteString], elapsedTime); + break; + default: + break; + } + } +} + + +@end diff --git a/Artsy/Classes/Utils/Logging/ARLogFormatter.h b/Artsy/Classes/Utils/Logging/ARLogFormatter.h new file mode 100644 index 00000000000..c7df531631f --- /dev/null +++ b/Artsy/Classes/Utils/Logging/ARLogFormatter.h @@ -0,0 +1,5 @@ +@interface ARLogFormatter : NSObject +@property (nonatomic, assign) NSInteger loggerCount; +@property (nonatomic, strong) NSDateFormatter *dateFormatter; +@end + diff --git a/Artsy/Classes/Utils/Logging/ARLogFormatter.m b/Artsy/Classes/Utils/Logging/ARLogFormatter.m new file mode 100644 index 00000000000..5fe7fcbe717 --- /dev/null +++ b/Artsy/Classes/Utils/Logging/ARLogFormatter.m @@ -0,0 +1,51 @@ +#import "ARLogFormatter.h" + +// Human-readable context names for file loggers +static const NSDictionary *contextMap; + +@implementation ARLogFormatter + ++ (void)initialize +{ + contextMap = @{ + @(ARLogContextRequestOperation): @"Network", + @(ARLogContextAction): @"Action", + @(ARLogContextError): @"Error", + @(ARLogContextInfo): @"Info" + }; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _dateFormatter = [[NSDateFormatter alloc] init]; + [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [_dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"]; + _loggerCount = 0; + } + return self; +} + +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage +{ + NSString *dateAndTime = [self.dateFormatter stringFromDate:(logMessage->timestamp)]; + NSString *logMsg = logMessage->logMsg; + + return [NSString stringWithFormat:@"%@ [%@] | %@\n", dateAndTime, [contextMap[@(logMessage->logContext)] uppercaseString], logMsg]; + + return nil; +} + +- (void)didAddToLogger:(id)logger +{ + self.loggerCount++; + NSAssert(self.loggerCount <= 1, @"This logger isn't thread-safe"); +} + +- (void)willRemoveFromLogger:(id)logger +{ + self.loggerCount--; +} + +@end diff --git a/Artsy/Classes/Utils/Logging/ARLogger.h b/Artsy/Classes/Utils/Logging/ARLogger.h new file mode 100644 index 00000000000..c7cc5d5b3ac --- /dev/null +++ b/Artsy/Classes/Utils/Logging/ARLogger.h @@ -0,0 +1,31 @@ +// If you update this enum, update `contextMap` in the implementation too please +typedef NS_ENUM(NSInteger, ARLogContext) { + // starting at 1 because 0 is the default + ARLogContextInfo = 1, + ARLogContextAction, + ARLogContextError, + ARLogContextRequestOperation +}; + +@interface ARLogger : NSObject +/// Call this ASAP to get logging up and running +- (void)startLogging; +- (void)stopLogging; ++ (instancetype)sharedLogger; +@end + +#pragma mark - +#pragma mark Context specific macros + +// ARLogContextRequestOperation context is specifically for logging http responses directly from the +// server. To log specifically formatted text errors after a failed request, use ARLogContextNetwork +// logs. These macros are used by ARHTTPRequestOperationLogger. This logger has its own log level so +// that you may log only failed requests without affecting the global log level of ARLogger. +#define ARHTTPRequestOperationDebugLog(frmt, ...) ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_DEBUG, ARLogContextRequestOperation, frmt, ##__VA_ARGS__) +#define ARHTTPRequestOperationSuccessLog(frmt, ...) ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_INFO, ARLogContextRequestOperation, frmt, ##__VA_ARGS__) +#define ARHTTPRequestOperationFailureLog(frmt, ...) ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_ERROR, ARLogContextRequestOperation, frmt, ##__VA_ARGS__) + +// For logging human-readable Activity and Errors: +#define ARInfoLog(frmt, ...) ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_INFO, ARLogContextInfo, frmt, ##__VA_ARGS__) +#define ARActionLog(frmt, ...) ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_DEBUG, ARLogContextAction, frmt, ##__VA_ARGS__) +#define ARErrorLog(frmt, ...) ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_ERROR, ARLogContextError, frmt, ##__VA_ARGS__) diff --git a/Artsy/Classes/Utils/Logging/ARLogger.m b/Artsy/Classes/Utils/Logging/ARLogger.m new file mode 100644 index 00000000000..2382eaa805d --- /dev/null +++ b/Artsy/Classes/Utils/Logging/ARLogger.m @@ -0,0 +1,65 @@ +#import +#import +#import +#import "ARHTTPRequestOperationLogger.h" +#import "ARLogFormatter.h" + +@implementation ARLogger + ++ (instancetype)sharedLogger { + static ARLogger *_sharedLogger = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedLogger = [[self alloc] init]; + }); + + return _sharedLogger; +} + +- (void)startLogging +{ + [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor artsyLightGrey] backgroundColor:nil forFlag:(LOG_FLAG_DEBUG)]; + [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor artsyAttention] backgroundColor:nil forFlag:LOG_FLAG_INFO]; + [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor artsyRed] backgroundColor:nil forFlag:LOG_FLAG_ERROR]; + [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor colorWithHex:0x66cc4c] backgroundColor:nil forFlag:LOG_FLAG_INFO context:ARLogContextRequestOperation]; + [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor colorWithHex:0xe56633] backgroundColor:nil forFlag:LOG_FLAG_ERROR context:ARLogContextRequestOperation]; + + //Console.app + Xcode log window// We could reuse the formatter, but then our date formatter would + // need to be thread-safe + // See: https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters + [[DDASLLogger sharedInstance] setLogFormatter: [[ARLogFormatter alloc] init]]; + [[DDTTYLogger sharedInstance] setLogFormatter: [[ARLogFormatter alloc] init]]; + [DDTTYLogger sharedInstance].colorsEnabled = YES; + [DDLog addLogger:[DDASLLogger sharedInstance]]; + [DDLog addLogger:[DDTTYLogger sharedInstance]]; + [self addDDFileLogger]; + + if (![ARDeveloperOptions options][@"suppress_network_logs"]) { + [[ARHTTPRequestOperationLogger sharedLogger] startLogging]; + } +} + +- (void)dealloc +{ + [self stopLogging]; +} + +- (void)stopLogging +{ + [[ARHTTPRequestOperationLogger sharedLogger] stopLogging]; +} + +// At the moment, everything gets logged to one log. We can change this to sort based on context and to only log Errors. +- (void)addDDFileLogger +{ + DDFileLogger *logger = [[DDFileLogger alloc] init]; + logger.rollingFrequency = -1; + + // Have up to 30 1MB files + logger.maximumFileSize = 1024 * 1024; + logger.logFileManager.maximumNumberOfLogFiles = 30; + [logger setLogFormatter:[[ARLogFormatter alloc] init]]; + [DDLog addLogger:logger]; +} + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARDefaultNavigationTransition.h b/Artsy/Classes/Utils/Navigation Animations/ARDefaultNavigationTransition.h new file mode 100644 index 00000000000..e872ef13594 --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARDefaultNavigationTransition.h @@ -0,0 +1,10 @@ +#import "ARNavigationTransition.h" + +// We want to have custom transitions when you move between navigation controllers, +// and this is done via UIViewControllerAnimatedTransitioning. + +/// This Transition will shrink & fade the current view controller, and push the new view in from the left. + +@interface ARDefaultNavigationTransition : ARNavigationTransition + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARDefaultNavigationTransition.m b/Artsy/Classes/Utils/Navigation Animations/ARDefaultNavigationTransition.m new file mode 100644 index 00000000000..046a978643f --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARDefaultNavigationTransition.m @@ -0,0 +1,135 @@ +#import "UIView+OldSchoolSnapshots.h" +#import "ARDefaultNavigationTransition.h" + +#define FADE_MOVEMENT_X 8 +#define FADE_MOVEMENT_Y 16 +#define FADE_ALPHA 0.2 + +@implementation ARDefaultNavigationTransition + +- (NSTimeInterval)transitionDuration:(id )transitionContext +{ + return ARAnimationDuration; +} + +- (void)pushTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext +{ + + CGRect fullFrame = [transitionContext containerView].bounds; + CGRect offScreen = fullFrame; + + offScreen.origin.x = offScreen.size.width; + toVC.view.frame = offScreen; + + [transitionContext.containerView addSubview:fromVC.view]; + [transitionContext.containerView addSubview:toVC.view]; + + fromVC.view.alpha = 1; + fromVC.view.transform = CGAffineTransformIdentity; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0.0 + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + fromVC.view.alpha = 0.2; + fromVC.view.transform = CGAffineTransformMakeScale(0.9, 0.9); + + toVC.view.frame = fullFrame; + + } + completion:^(BOOL finished) { + fromVC.view.alpha = 1; + fromVC.view.transform = CGAffineTransformIdentity; + + [transitionContext completeTransition:YES]; + }]; +} + +- (void)popTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )context +{ + CGRect fullFrame = [context initialFrameForViewController:fromVC]; + CGRect offScreen = fullFrame; + offScreen.origin.x = offScreen.size.width; + + // To = Coming up + // From = Moving to the Side + + [context.containerView addSubview:toVC.view]; + [context.containerView addSubview:fromVC.view]; + + UIViewAnimationOptions options = UIViewAnimationOptionCurveEaseOut; + + ARNavigationController *navigationController = (ARNavigationController *)fromVC.navigationController; + + UIView *backButtonSnapshot; + + CGFloat orinalBackButtonAlpha = navigationController.backButton.alpha; + if ([context isInteractive] && [navigationController isKindOfClass:ARNavigationController.class]) { + options = UIViewAnimationOptionCurveLinear; + + // For interactive transitions, we copy snapshots of the buttons to the + // top of the context view. + + // Set the alpha to 1 so we know that there is something to take a + // snapshot of. + navigationController.backButton.alpha = 1; + + // We don't use the new iOS 7 snapshotting API because we can't wait + // for a screen update to reflect our changes. + backButtonSnapshot = [navigationController.backButton ar_snapshot]; + + // Make sure the snapshots match the original views in alpha and appear + // at the right position + backButtonSnapshot.alpha = orinalBackButtonAlpha; + + backButtonSnapshot.frame = [context.containerView convertRect:navigationController.backButton.frame fromView:navigationController.view]; + + // Restore the original alpha values + navigationController.backButton.alpha = orinalBackButtonAlpha; + + // Hide the original buttons for the duration of the animation, we'll + // revert this after the transition has finished. + navigationController.backButton.hidden = YES; + + [context.containerView addSubview:backButtonSnapshot]; + } + + toVC.view.alpha = 0.2; + toVC.view.transform = CGAffineTransformMakeScale(0.9, 0.9); + + [UIView animateWithDuration:[self transitionDuration:context] + delay:0.0 + options:options + animations:^{ + toVC.view.alpha = 1; + toVC.view.transform = CGAffineTransformIdentity; + + fromVC.view.frame = offScreen; + + backButtonSnapshot.alpha = self.backButtonTargetAlpha; + } + completion:^(BOOL finished) { + toVC.view.alpha = 1; + toVC.view.transform = CGAffineTransformIdentity; + + // Unhide the buttons + navigationController.backButton.hidden = NO; + + [backButtonSnapshot removeFromSuperview]; + + if ([context transitionWasCancelled]) { + fromVC.view.frame = fullFrame; + } + + [context completeTransition:![context transitionWasCancelled]]; + }]; +} + +#pragma mark - Properties + +- (BOOL)supportsInteractiveTransitioning +{ + return YES; +} + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransition.h b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransition.h new file mode 100644 index 00000000000..71fa841bcf8 --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransition.h @@ -0,0 +1,20 @@ +@interface ARNavigationTransition : NSObject + +@property (nonatomic, assign) enum UINavigationControllerOperation operationType; + +// For interactive transitions, set these to the required values for the +// target viewcontroller to tie the fading of the buttons to the user +// interaction. +// +// These values are currently only used by the ARDefaultTransition +@property (readwrite, nonatomic, assign) CGFloat backButtonTargetAlpha; +@property (readwrite, nonatomic, assign) CGFloat menuButtonTargetAlpha; + +// Currently, only ARDefaultTransition returns YES +@property (readonly, nonatomic, assign) BOOL supportsInteractiveTransitioning; + +- (void)pushTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext; + +- (void)popTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext; + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransition.m b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransition.m new file mode 100644 index 00000000000..50bb550333f --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransition.m @@ -0,0 +1,49 @@ +#import "ARNavigationTransition.h" + +@implementation ARNavigationTransition + +- (NSTimeInterval)transitionDuration:(id )transitionContext +{ + return 0.3; +} + +- (void)animateTransition:(id )transitionContext +{ + UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; + UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + + switch (self.operationType) { + case UINavigationControllerOperationPush: + [self pushTransitionFrom:fromVC to:toVC withContext:transitionContext]; + break; + + case UINavigationControllerOperationPop: + [self popTransitionFrom:fromVC to:toVC withContext:transitionContext]; + break; + + default:{ + CGRect endFrame = [transitionContext containerView].bounds; + toVC.view.frame = endFrame; + [transitionContext completeTransition:YES]; + break; + } + } +} + +- (void)pushTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext +{ + [transitionContext completeTransition:YES]; +} + +- (void)popTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext +{ + [transitionContext completeTransition:YES]; +} + +#pragma mark - Properties + +- (BOOL)supportsInteractiveTransitioning { + return NO; +} + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransitionController.h b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransitionController.h new file mode 100644 index 00000000000..2d739202fb8 --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransitionController.h @@ -0,0 +1,13 @@ +#import + +#import "ARNavigationTransition.h" + +@interface ARNavigationTransitionController : NSObject + +/// Get an animation transition for a navigation view controller + ++ (ARNavigationTransition *)animationControllerForOperation:(UINavigationControllerOperation)operation + fromViewController:(UIViewController *)fromVC + toViewController:(UIViewController *)toVC; + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransitionController.m b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransitionController.m new file mode 100644 index 00000000000..a4a2800648c --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARNavigationTransitionController.m @@ -0,0 +1,39 @@ +#import "ARNavigationTransitionController.h" + +#import "ARDefaultNavigationTransition.h" +#import "ARViewInRoomTransition.h" +#import "ARZoomImageTransition.h" + +#import "ARArtworkSetViewController.h" +#import "ARViewInRoomViewController.h" +#import "ARZoomArtworkImageViewController.h" + +@implementation ARNavigationTransitionController + ++ (ARNavigationTransition *)animationControllerForOperation:(UINavigationControllerOperation)operation + fromViewController:(UIViewController *)fromVC + toViewController:(UIViewController *)toVC +{ + ARNavigationTransition *transition = nil; + + if ([self objects:fromVC andSecond:toVC areTransitionsFromClass:[ARArtworkSetViewController class] andClass:[ARViewInRoomViewController class]]) { + transition = [[ARViewInRoomTransition alloc] init]; + + } else if ([self objects:fromVC andSecond:toVC areTransitionsFromClass:[ARArtworkSetViewController class] andClass:[ARZoomArtworkImageViewController class]]) { + transition = [[ARZoomImageTransition alloc] init]; + + } else { + transition = [[ARDefaultNavigationTransition alloc] init]; + } + + transition.operationType = operation; + return transition; +} + ++ (BOOL)objects:(id)first andSecond:(id)second areTransitionsFromClass:(Class)klass1 andClass:(Class)klass2 +{ + return ([first isKindOfClass:[klass1 class]] && [second isKindOfClass:[klass2 class]]) || + ([first isKindOfClass:[klass2 class]] && [second isKindOfClass:[klass1 class]]); +} + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARViewInRoomTransition.h b/Artsy/Classes/Utils/Navigation Animations/ARViewInRoomTransition.h new file mode 100644 index 00000000000..3f3e0c7fa7c --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARViewInRoomTransition.h @@ -0,0 +1,11 @@ +#import "ARNavigationTransition.h" + +/// This Transition will only work on an ArtworkSetVC and a ViewInRoomVC + +/// It works by taking the details of the current artwork image, making a new copy of the imageview, +/// adding the copy to the containr's view ( the nav controller ) and adding an animation that fades the old view +/// and resizes the imageview into the new correctly sized shape. + +@interface ARViewInRoomTransition : ARNavigationTransition + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARViewInRoomTransition.m b/Artsy/Classes/Utils/Navigation Animations/ARViewInRoomTransition.m new file mode 100644 index 00000000000..d276d5f7de1 --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARViewInRoomTransition.m @@ -0,0 +1,177 @@ +#import "ARViewInRoomTransition.h" + +#import "ARViewInRoomViewController.h" +#import "ARArtworkSetViewController.h" +#import "ARFeedImageLoader.h" +#import "ARArtworkViewController.h" + +@implementation ARViewInRoomTransition + +- (NSTimeInterval)transitionDuration:(id )transitionContext { + return 0.38; +} + +- (void)pushTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext { + ARArtworkSetViewController *artworkController = (id)fromVC; + ARViewInRoomViewController *virController = (id)toVC; + + NSAssert([artworkController isKindOfClass:[ARArtworkSetViewController class]], @"FromVC is not an ArtworkVC"); + NSAssert([virController isKindOfClass:[ARViewInRoomViewController class]], @"ToVC is not a ViewInRoomVC"); + + // The Push plan: + // Create a new Artwork Imageview on the container's view + // Set its size to be the size of the original artwork + // Add the VIRVC behind the artwork at 0 alpha + // Simultaneously fade in the VIR and move the artwork to the VIR's correct position + + UIView *originalArtworkImageView = artworkController.currentArtworkViewController.imageView; + Artwork *artwork = artworkController.currentArtworkViewController.artwork; + CGRect endFrame = [transitionContext containerView].bounds; + + // Take into account the scrolling on the artwork view + CGRect originalPositionFrame = originalArtworkImageView.frame; + originalPositionFrame.origin.y -= artworkController.currentArtworkViewController.imageViewOffset.y; + + // Create a new UIImageView that sits on the container View + // grab its image from either the imageView or the ZoomImage's background image + + UIImageView *artworkImageView = [ARViewInRoomViewController imageViewForFramedArtwork]; + artworkImageView.frame = originalPositionFrame; + artworkImageView.image = [(UIImageView *)originalArtworkImageView image]; + + if (!artworkImageView.image) { + [[ARFeedImageLoader alloc] loadImageAtAddress:[artwork baseImageURL] desiredSize:ARFeedItemImageSizeLarge + forImageView:artworkImageView customPlaceholder:nil]; + } + + // Add the controllers + [transitionContext.containerView addSubview:artworkController.view]; + [transitionContext.containerView addSubview:virController.view]; + + // Add the artwork above on the container, we can't move it to the + // VIR view here cause it'd be faded in. + [transitionContext.containerView addSubview:artworkImageView]; + + // Hide the original so the resize animation works + originalArtworkImageView.hidden = YES; + + // Put the VIR above the artwork view and make hidden + virController.view.alpha = 0; + virController.view.frame = [transitionContext containerView].bounds; + +// https://github.com/artsy/eigen/issues/1418 +// [artworkImageView setEasingFunction:QuadraticEaseOut forKeyPath:@"position"]; +// [artworkImageView setEasingFunction:QuadraticEaseOut forKeyPath:@"bounds"]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ + artworkImageView.frame = [ARViewInRoomViewController rectForImageViewWithArtwork:artwork withContainerFrame:endFrame]; + + } completion:^(BOOL finished) { + [artworkImageView removeFromSuperview]; + virController.artworkImageView = artworkImageView; + + [transitionContext completeTransition:YES]; + }]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] * .66 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + + virController.view.alpha = 1; + + } completion:nil]; + + + CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; + anim.fromValue = [NSNumber numberWithFloat:0.0]; + anim.toValue = [NSNumber numberWithFloat:0.3]; + anim.duration = [self transitionDuration:transitionContext]; + [artworkImageView.layer addAnimation:anim forKey:@"shadowOpacity"]; + artworkImageView.layer.shadowOpacity = 0.3; +} + +- (void)popTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext { + + ARArtworkSetViewController *artworkController = (id)toVC; + ARViewInRoomViewController *virController = (id)fromVC; + + NSAssert([artworkController isKindOfClass:[ARArtworkSetViewController class]], @"FromVC is not an ArtworkVC"); + NSAssert([virController isKindOfClass:[ARViewInRoomViewController class]], @"ToVC is not a ViewInRoomVC"); + + // The Pop plan: + // Create a new Artwork Imageview on the container's view + // Set its size to be the size of the VIR artwork + // Add the ArtworkView behind the VIRView at 0 alpha + // Simultaneously fade out the VIR and move the artwork to the Artwork View's correct position + + UIView *originalArtworkImageView = artworkController.currentArtworkViewController.imageView; + UIImageView *originalVIRImageView = virController.artworkImageView; + Artwork *artwork = artworkController.currentArtworkViewController.artwork; + + // Take into account the scrolling on the artwork view + CGRect originalPositionFrame = originalArtworkImageView.frame; + originalPositionFrame.origin.y -= artworkController.currentArtworkViewController.imageViewOffset.y; + + // Create a new UIImageView that sits on the container View + // grab its image from either the imageView or the ZoomImage's background image + // start it's position at the VIR size + + CGRect initialFrame = originalVIRImageView.frame; + + UIImageView *artworkImageView = [[UIImageView alloc] initWithFrame: initialFrame]; + artworkImageView.contentMode = UIViewContentModeScaleAspectFit; + artworkImageView.image = [(UIImageView *)originalArtworkImageView image]; + + // Just incase there's nothing, do it async + if (!artworkImageView.image) { + [[ARFeedImageLoader alloc] loadImageAtAddress:[artwork baseImageURL] desiredSize:ARFeedItemImageSizeLarge + forImageView:artworkImageView customPlaceholder:nil]; + } + + // Add a shadow to the artwork image + CALayer *layer = [artworkImageView layer]; + layer.shadowOffset = CGSizeMake(0, 4); + layer.shadowOpacity = 1; + layer.shadowColor = [[UIColor blackColor] CGColor]; + + // Add the controllers + [transitionContext.containerView addSubview:artworkController.view]; + [transitionContext.containerView addSubview:virController.view]; + + // Add the artwork above on the container, we can't move it to the + // VIR view here cause it'd be faded in. + [transitionContext.containerView addSubview:artworkImageView]; + originalVIRImageView.hidden = YES; + + // Hide the original so the resize animation works + originalArtworkImageView.hidden = YES; + + // Put the VIR above the artwork view and make hidden + virController.view.alpha = 1; + virController.view.frame = [transitionContext containerView].bounds; + +// https://github.com/artsy/eigen/issues/1418 +// [artworkImageView setEasingFunction:QuadraticEaseOut forKeyPath:@"position"]; +// [artworkImageView setEasingFunction:QuadraticEaseOut forKeyPath:@"bounds"]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ + + artworkImageView.frame = originalPositionFrame; + virController.view.alpha = 0; + + } completion:^(BOOL finished) { + originalVIRImageView.hidden = NO; + originalArtworkImageView.hidden = NO; + [artworkImageView removeFromSuperview]; + + [transitionContext completeTransition:YES]; + }]; + + CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; + anim.fromValue = [NSNumber numberWithFloat:0.3]; + anim.toValue = [NSNumber numberWithFloat:0.0]; + anim.duration = [self transitionDuration:transitionContext]; + [artworkImageView.layer addAnimation:anim forKey:@"shadowOpacity"]; + artworkImageView.layer.shadowOpacity = 0.0; +} + + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARZoomImageTransition.h b/Artsy/Classes/Utils/Navigation Animations/ARZoomImageTransition.h new file mode 100644 index 00000000000..ea33c9fe185 --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARZoomImageTransition.h @@ -0,0 +1,7 @@ +// Transitions in and out of the zoom image view + +#import "ARNavigationTransition.h" + +@interface ARZoomImageTransition : ARNavigationTransition + +@end diff --git a/Artsy/Classes/Utils/Navigation Animations/ARZoomImageTransition.m b/Artsy/Classes/Utils/Navigation Animations/ARZoomImageTransition.m new file mode 100644 index 00000000000..6637ce526eb --- /dev/null +++ b/Artsy/Classes/Utils/Navigation Animations/ARZoomImageTransition.m @@ -0,0 +1,115 @@ +// An interesting article on catiledlayers +// http://red-glasses.com/index.php/tutorials/catiledlayer-how-to-use-it-how-it-works-what-it-does/ + +#import "ARZoomImageTransition.h" +#import "ARZoomArtworkImageViewController.h" +#import "ARArtworkSetViewController.h" +#import "ARArtworkViewController.h" + +@implementation ARZoomImageTransition + +- (NSTimeInterval)transitionDuration:(id )transitionContext +{ + return 0.2; +} + +- (void)pushTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext +{ + ARArtworkSetViewController *artworkController = (id)fromVC; + ARZoomArtworkImageViewController *zoomController = (id)toVC; + + + NSAssert([artworkController isKindOfClass:[ARArtworkSetViewController class]], @"FromVC is not an ArtworkVC"); + NSAssert([zoomController isKindOfClass:[ARZoomArtworkImageViewController class]], @"ToVC is not a ViewInRoomVC"); + + // Add the controllers, the zoom controller is clear, so adding it will show nothing + [transitionContext.containerView addSubview:artworkController.view]; + [transitionContext.containerView addSubview:zoomController.view]; + zoomController.view.frame = [transitionContext containerView].bounds; + + UIImageView *originalArtworkImageView = (id)artworkController.currentArtworkViewController.imageView; + + CGRect endFrame = [transitionContext containerView].bounds; + CGRect originalPositionFrame = [originalArtworkImageView convertRect:originalArtworkImageView.bounds toView:transitionContext.containerView]; + + // create a zoom view, and set it to be above the preview image view + ARZoomView *artworkImageView = [[ARZoomView alloc] initWithImage:zoomController.image frame:endFrame]; + artworkImageView.backgroundImage = originalArtworkImageView.image; + [artworkImageView setMaxMinZoomScalesForSize:originalPositionFrame.size]; + artworkImageView.zoomScale = artworkImageView.minimumZoomScale; + artworkImageView.contentOffset = CGPointMake(-originalPositionFrame.origin.x, -originalPositionFrame.origin.y); + + originalArtworkImageView.alpha = 0; + + // Add the zoom view on top of the zoom controller + [transitionContext.containerView addSubview:artworkImageView]; + + [artworkImageView setMaxMinZoomScalesForCurrentFrame]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ + CGFloat zoomScale = [artworkImageView scaleForFullScreenZoomInSize:endFrame.size]; + CGPoint targetContentOffset = [artworkImageView centerContentOffsetForZoomScale:zoomScale]; + [artworkImageView performBlockWhileIgnoringContentOffsetChanges:^{ + artworkImageView.zoomScale = zoomScale; + }]; + artworkImageView.contentOffset = targetContentOffset; + } completion:^(BOOL finished) { + + // remove from animation context + [artworkImageView removeFromSuperview]; + + // let the zoom view have it + zoomController.zoomView = artworkImageView; + + [transitionContext completeTransition:YES]; + }]; +} + +- (void)popTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext +{ + ARArtworkSetViewController *artworkController = (id)toVC; + ARZoomArtworkImageViewController *zoomController = (id)fromVC; + + NSAssert([artworkController isKindOfClass:[ARArtworkSetViewController class]], @"ToVC is not an ArtworkVC"); + NSAssert([zoomController isKindOfClass:[ARZoomArtworkImageViewController class]], @"FromVC is not a ViewInRoomVC"); + + // Add the controllers, the zoom controller is clear, so adding it will show nothing + [transitionContext.containerView addSubview:artworkController.view]; + [transitionContext.containerView addSubview:zoomController.view]; + + [artworkController.view setNeedsLayout]; + + // We need to give the artworkViewController a chance to lay its view out. + // Otherwise, convertRect:toView: will return values with respect to a possibly stale layout. + ar_dispatch_main_queue(^{ + ARZoomView *artworkImageView = zoomController.zoomView; + [zoomController unconstrainZoomView]; + + artworkController.view.frame = [transitionContext containerView].bounds; + zoomController.view.frame = [transitionContext containerView].bounds; + + // Get the position to move the zoom view into + UIView *originalArtworkImageView = artworkController.currentArtworkViewController.imageView; + + CGRect originalPositionFrame = [originalArtworkImageView convertRect:originalArtworkImageView.bounds toView:transitionContext.containerView]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ + CGPoint targetContentOffset = CGPointMake(-originalPositionFrame.origin.x, -originalPositionFrame.origin.y); + [artworkImageView setMaxMinZoomScalesForSize:originalPositionFrame.size]; + [artworkImageView performBlockWhileIgnoringContentOffsetChanges:^{ + artworkImageView.zoomScale = artworkImageView.minimumZoomScale; + }]; + artworkImageView.contentOffset = targetContentOffset; + } completion:^(BOOL finished) { + + // previously the artwork was hidden + originalArtworkImageView.alpha = 1; + [zoomController.zoomView removeZoomViewForTransition]; + [zoomController.view removeFromSuperview]; + + [transitionContext completeTransition:YES]; + }]; + }); +} + +@end diff --git a/Artsy/Classes/Utils/UIApplicationStateEnum.h b/Artsy/Classes/Utils/UIApplicationStateEnum.h new file mode 100644 index 00000000000..5c01c6f094b --- /dev/null +++ b/Artsy/Classes/Utils/UIApplicationStateEnum.h @@ -0,0 +1,3 @@ +@interface UIApplicationStateEnum : NSObject ++ (NSString*)toString:(UIApplicationState)state; +@end diff --git a/Artsy/Classes/Utils/UIApplicationStateEnum.m b/Artsy/Classes/Utils/UIApplicationStateEnum.m new file mode 100644 index 00000000000..396bfbf01f4 --- /dev/null +++ b/Artsy/Classes/Utils/UIApplicationStateEnum.m @@ -0,0 +1,22 @@ +#import "UIApplicationStateEnum.h" + +@implementation UIApplicationStateEnum ++ (NSString*)toString:(UIApplicationState)state +{ + NSString *result = nil; + switch(state) { + case UIApplicationStateActive: + result = @"active"; + break; + case UIApplicationStateInactive: + result = @"inactive"; + break; + case UIApplicationStateBackground: + result = @"background"; + break; + default: + [NSException raise:NSGenericException format:@"Unexpected UIApplicationState %@", @(state)]; + } + return result; +} +@end diff --git a/Artsy/Classes/Utils/UIViewController+InnermostTopViewController.h b/Artsy/Classes/Utils/UIViewController+InnermostTopViewController.h new file mode 100644 index 00000000000..85115b76bf8 --- /dev/null +++ b/Artsy/Classes/Utils/UIViewController+InnermostTopViewController.h @@ -0,0 +1,12 @@ +#import + +@interface UIViewController (InnermostTopViewController) + +/** + * Locate the most inner child navigation view controller. + * + * @return The topViewController of the UINavigationController. + */ +- (UIViewController *)ar_innermostTopViewController; + +@end diff --git a/Artsy/Classes/Utils/UIViewController+InnermostTopViewController.m b/Artsy/Classes/Utils/UIViewController+InnermostTopViewController.m new file mode 100644 index 00000000000..ec82b5429bb --- /dev/null +++ b/Artsy/Classes/Utils/UIViewController+InnermostTopViewController.m @@ -0,0 +1,21 @@ +#import "UIViewController+InnermostTopViewController.h" + +@implementation UIViewController (InnermostTopViewController) + +- (UIViewController *)ar_innermostTopViewController +{ + for (UIViewController *childViewController in self.childViewControllers) { + UIViewController *navigationViewController = [childViewController ar_innermostTopViewController]; + if (navigationViewController) { + return navigationViewController; + } + } + + if ([self isKindOfClass:UINavigationController.class]) { + return ((UINavigationController *) self).topViewController; + } + + return nil; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARAdminSettingsViewController.h b/Artsy/Classes/View Controllers/ARAdminSettingsViewController.h new file mode 100644 index 00000000000..e2e7e06d675 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARAdminSettingsViewController.h @@ -0,0 +1,6 @@ +#import +#import + +@interface ARAdminSettingsViewController : ARGenericTableViewController + +@end diff --git a/Artsy/Classes/View Controllers/ARAdminSettingsViewController.m b/Artsy/Classes/View Controllers/ARAdminSettingsViewController.m new file mode 100644 index 00000000000..d96bc801e93 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARAdminSettingsViewController.m @@ -0,0 +1,240 @@ +#import "ARAdminSettingsViewController.h" +#import "ARGroupedTableViewCell.h" +#import "ARAnimatedTickView.h" +#import "ARAppDelegate.h" +#import "ARUserManager.h" +#import "ARFileUtils.h" +#import "ARRouter.h" + +#if DEBUG +#import +#endif + +NSString *const AROptionCell = @"OptionCell"; +NSString *const ARLabOptionCell = @"LabOptionCell"; + +@implementation ARAdminSettingsViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + ARTableViewData *tableViewData = [[ARTableViewData alloc] init]; + [self registerClass:[ARTickedTableViewCell class] forCellReuseIdentifier:ARLabOptionCell]; + [self registerClass:[ARAdminTableViewCell class] forCellReuseIdentifier:AROptionCell]; + + ARSectionData *miscSectionData = [[ARSectionData alloc] init]; + NSString *name = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]; + NSString *build = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + NSString *gitCommitRevision = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"GITCommitRev"]; + + miscSectionData.headerTitle = [NSString stringWithFormat:@"%@ v%@, build %@ (%@)", name, version, build, gitCommitRevision]; + + [miscSectionData addCellData:[self generateLogOut]]; + [miscSectionData addCellData:[self generateOnboarding]]; + [miscSectionData addCellData:[self generateEmailData]]; + [miscSectionData addCellData:[self generateRestart]]; + [miscSectionData addCellData:[self generateStagingSwitch]]; + + [tableViewData addSectionData:miscSectionData]; + + ARSectionData *labsSection = [self createLabsSection]; + [tableViewData addSectionData:labsSection]; + + ARSectionData *vcrSection = [self createVCRSection]; + [tableViewData addSectionData:vcrSection]; + + self.tableViewData = tableViewData; + self.tableView.contentInset = UIEdgeInsetsMake(88, 0, 0, 0); +} + +- (ARCellData *)generateLogOut +{ + ARCellData *onboardingData = [[ARCellData alloc] initWithIdentifier:AROptionCell]; + [onboardingData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = @"Log Out"; + }]; + + [onboardingData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + [self logout]; + }]; + return onboardingData; +} + +- (ARCellData *)generateOnboarding +{ + ARCellData *onboardingData = [[ARCellData alloc] initWithIdentifier:AROptionCell]; + [onboardingData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = @"Show Onboarding"; + }]; + + [onboardingData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + [self showSlideshow]; + }]; + return onboardingData; +} + +- (ARCellData *)generateEmailData +{ + ARCellData *emailData = [[ARCellData alloc] initWithIdentifier:AROptionCell]; + [emailData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = @"Email Artsy Developers"; + }]; + + [emailData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + [self emailTapped]; + }]; + return emailData; +} + +- (ARCellData *)generateRestart +{ + ARCellData *crashCellData = [[ARCellData alloc] initWithIdentifier:AROptionCell]; + [crashCellData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = @"Restart"; + }]; + + [crashCellData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + exit(YES); + }]; + return crashCellData; +} + +- (ARCellData *)generateStagingSwitch +{ + BOOL useStaging = [AROptions boolForOption:ARUseStagingDefault]; + NSString *title = useStaging ? @"Switch to Production (Logs out)" : @"Switch to Staging (Logs out)"; + + ARCellData *crashCellData = [[ARCellData alloc] initWithIdentifier:AROptionCell]; + [crashCellData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = title; + }]; + + [crashCellData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + [AROptions setBool:!useStaging forOption:ARUseStagingDefault]; + [self logout]; + }]; + return crashCellData; +} + +- (ARSectionData *)createLabsSection +{ + ARSectionData *labsSectionData = [[ARSectionData alloc] init]; + labsSectionData.headerTitle = @"Labs"; + + NSArray *options = [AROptions labsOptions]; + for (NSInteger index = 0; index < options.count; index++) { + NSString *title = options[index]; + + ARCellData *cellData = [[ARCellData alloc] initWithIdentifier:ARLabOptionCell]; + [cellData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = title; + cell.accessoryView = [[ARAnimatedTickView alloc] initWithSelection:[AROptions boolForOption:title]]; + }]; + + [cellData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + BOOL currentSelection = [AROptions boolForOption:title]; + [AROptions setBool:!currentSelection forOption:title]; + + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + [(ARAnimatedTickView *)cell.accessoryView setSelected:!currentSelection animated:YES]; + }]; + + [labsSectionData addCellData:cellData]; + } + return labsSectionData; +} + +- (ARSectionData *)createVCRSection +{ + ARSectionData *vcrSectionData = [[ARSectionData alloc] init]; +#if DEBUG + vcrSectionData.headerTitle = @"Offline Recording Mode (Dev)"; + + ARCellData *startCellData = [[ARCellData alloc] initWithIdentifier:ARLabOptionCell]; + [startCellData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = @"Start Recording, restarts"; + }]; + + [startCellData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + NSString *oldFilePath = [ARFileUtils cachesPathWithFolder:@"vcr" filename:@"eigen.json"]; + [[NSFileManager defaultManager] removeItemAtPath:oldFilePath error:nil]; + + [AROptions setBool:YES forOption:AROptionsUseVCR]; + exit(0); + }]; + + ARCellData *saveCellData = [[ARCellData alloc] initWithIdentifier:ARLabOptionCell]; + [saveCellData setCellConfigurationBlock:^(UITableViewCell *cell) { + cell.textLabel.text = @"Saves Recording, restarts"; + }]; + + [saveCellData setCellSelectionBlock:^(UITableView *tableView, NSIndexPath *indexPath) { + [VCR save:[ARFileUtils cachesPathWithFolder:@"vcr" filename:@"eigen.json"]]; + exit(0); + }]; + + + [vcrSectionData addCellData:startCellData]; + [vcrSectionData addCellData:saveCellData]; +#endif + + return vcrSectionData; +} + +- (void)logout +{ + [[ARUserManager sharedManager] logout]; + [ARRouter setup]; + [self showSlideshow]; +} + +- (void)showSlideshow +{ + ARAppDelegate *delegate = [ARAppDelegate sharedInstance]; + [delegate showTrialOnboardingWithState:ARInitialOnboardingStateSlideShow andContext:ARTrialContextNotTrial]; + + [self.navigationController popViewControllerAnimated:NO]; +} + +#pragma mark - +#pragma mark Email functions + +- (void)emailTapped +{ + NSString *path = [[NSBundle mainBundle] pathForResource: @"mail" ofType: @"html"]; + NSError *error = nil; + NSString *body = [NSString stringWithContentsOfFile: path encoding: NSUTF8StringEncoding error: &error]; + body = [body stringByReplacingOccurrencesOfString:@"{{Device}}" withString:[[UIDevice currentDevice] platformString]]; + body = [body stringByReplacingOccurrencesOfString:@"{{iOS Version}}" withString:[[UIDevice currentDevice] systemVersion]]; + body = [body stringByReplacingOccurrencesOfString:@"{{Version}}" withString:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]]; + [self sendMail:@[@"mobile@artsymail.com"] subject:@"Artsy Mobile Feedback" body:body]; +} + +- (void)sendMail:(NSArray *)toRecipients subject:(NSString *)subject body:(NSString*)body +{ + if ([MFMailComposeViewController canSendMail]) { + MFMailComposeViewController *controller = [[MFMailComposeViewController alloc] init]; + [controller setToRecipients:toRecipients]; + [controller setSubject:subject]; + [controller setMessageBody:body isHTML:YES]; + controller.mailComposeDelegate = self; + [self presentViewController:controller animated:YES completion:^{}]; + + } else { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"Your device is unable to send email." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; + [alert show]; + } +} + +- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error +{ + [self dismissViewControllerAnimated:YES completion:^{}]; +} + +-(BOOL)shouldAutorotate +{ + return NO; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARAppSearchViewController.h b/Artsy/Classes/View Controllers/ARAppSearchViewController.h new file mode 100644 index 00000000000..9837c20fd7d --- /dev/null +++ b/Artsy/Classes/View Controllers/ARAppSearchViewController.h @@ -0,0 +1,5 @@ +#import "ARSearchViewController.h" + +@interface ARAppSearchViewController : ARSearchViewController + +@end diff --git a/Artsy/Classes/View Controllers/ARAppSearchViewController.m b/Artsy/Classes/View Controllers/ARAppSearchViewController.m new file mode 100644 index 00000000000..eaad1593829 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARAppSearchViewController.m @@ -0,0 +1,112 @@ +#import "ARAppSearchViewController.h" +#import "ARArtworkSetViewController.h" +#import "ARArtistViewController.h" +#import "ARGeneViewController.h" +#import "ARSearchViewController+Private.h" +#import "UIView+HitTestExpansion.h" + +@interface ARAppSearchViewController () +@property(readonly, nonatomic, strong) UIButton *clearButton; +@property(readonly, nonatomic) UIView *bottomBorder; +@end + +@implementation ARAppSearchViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + self.defaultInfoLabelText = @"Search Artists, Artworks, Movements, or Medium."; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + UIButton *clearButton = [[UIButton alloc] init]; + clearButton.imageView.contentMode = UIViewContentModeScaleAspectFit; + [self.textField addSubview:clearButton]; + self.textField.clipsToBounds = NO; + [clearButton ar_extendHitTestSizeByWidth:6 andHeight:16]; + [clearButton alignTrailingEdgeWithView:self.textField predicate:nil]; + [clearButton constrainHeightToView:self.textField predicate:nil]; + [clearButton alignCenterYWithView:self.textField predicate:@"-2"]; + [clearButton alignAttribute:NSLayoutAttributeWidth toAttribute:NSLayoutAttributeHeight ofView:clearButton predicate:nil]; + [clearButton addTarget:self action:@selector(clearTapped:) forControlEvents:UIControlEventTouchUpInside]; + clearButton.hidden = YES; + + [clearButton setImage:[UIImage imageNamed:@"TextfieldClearButton"] forState:UIControlStateNormal]; + [clearButton setImage:[UIImage imageNamed:@"TextfieldClearButton"] forState:UIControlStateHighlighted]; + _clearButton = clearButton; + + // a bottom border + UIView *bottomBorder = [[UIView alloc] init]; + bottomBorder.backgroundColor = [UIColor whiteColor]; + [self.view addSubview:bottomBorder]; + _bottomBorder = bottomBorder; + [bottomBorder constrainHeight:@"1"]; + [bottomBorder alignLeadingEdgeWithView:self.searchBoxView predicate:@"-2"]; + [bottomBorder alignTrailingEdgeWithView:self.textField predicate:@"2"]; + [bottomBorder constrainTopSpaceToView:self.searchBoxView predicate:@"2"]; +} + +- (void)viewDidLayoutSubviews +{ + [self.textField bringSubviewToFront:self.clearButton]; +} + +- (void)clearTapped:(id)sender +{ + [self clearSearch]; +} + +- (void)searchText:(NSString *)text +{ + [super searchText:text]; + self.clearButton.hidden = self.textField.text.length == 0; +} + +- (AFJSONRequestOperation *)searchWithQuery:(NSString *)query success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + return [ArtsyAPI searchWithQuery:query success:success failure:failure]; +} + +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query +{ + UIViewController *controller; + if (result.model == [Artwork class]) { + controller = [[ARArtworkSetViewController alloc] initWithArtworkID:result.modelID]; + } else if (result.model == [Artist class]) { + controller = [[ARArtistViewController alloc] initWithArtistID:result.modelID]; + } else if (result.model == [Gene class]) { + controller = [[ARGeneViewController alloc] initWithGeneID:result.modelID]; + } else if (result.model == [Profile class]) { + controller = [ARSwitchBoard.sharedInstance routeProfileWithID:result.modelID]; + } else if (result.model == [SiteFeature class]) { + NSString *path = NSStringWithFormat(@"/feature/%@", result.modelID); + controller = [ARSwitchBoard.sharedInstance loadPath:path]; + } + + [self.navigationController pushViewController:controller animated:YES]; +} + +- (void)closeSearch:(id)sender +{ + [super closeSearch:sender]; + [[ARTopMenuViewController sharedController] returnToPreviousTab]; +} + +#pragma mark - ARMenuAwareViewController + +- (BOOL)hidesToolbarMenu +{ + return YES; +} + +- (BOOL)hidesBackButton +{ + return YES; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtistBiographyViewController.h b/Artsy/Classes/View Controllers/ARArtistBiographyViewController.h new file mode 100644 index 00000000000..175b34e4dd0 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtistBiographyViewController.h @@ -0,0 +1,5 @@ +@interface ARArtistBiographyViewController : UIViewController + +- (instancetype)initWithArtist:(Artist *)artist; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtistBiographyViewController.m b/Artsy/Classes/View Controllers/ARArtistBiographyViewController.m new file mode 100644 index 00000000000..2b7a5210009 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtistBiographyViewController.m @@ -0,0 +1,82 @@ +#import "ARArtistBiographyViewController.h" +#import "ARTextView.h" +#import "ORStackView+ArtsyViews.h" + + +@interface ARArtistBiographyViewController () +@property (nonatomic, strong) Artist *artist; +@property (nonatomic, strong) ORStackScrollView *view; +@end + +@implementation ARArtistBiographyViewController + +- (instancetype)initWithArtist:(Artist *)artist +{ + self = [super init]; + if (!self) { return nil; } + + _artist = artist; + + return self; +} + +- (void)loadView +{ + self.view = [[ORStackScrollView alloc] init]; + self.view.stackView.bottomMarginHeight = 20; + self.view.backgroundColor = [UIColor whiteColor]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + self.view.delegate = [ARScrollNavigationChief chief]; + + [super viewWillAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + self.view.delegate = nil; + + [super viewWillDisappear:animated]; +} + +- (void)dealloc +{ + self.view.delegate = nil; +} + +- (void)viewDidLoad +{ + [self.view.stackView addPageTitleWithString:self.artist.name]; + + ARTextView *textView = [[ARTextView alloc] init]; + textView.viewControllerDelegate = self; + [textView setMarkdownString:self.artist.blurb]; + [self.view.stackView addSubview:textView withTopMargin:@"20" sideMargin:@"40"]; + + [super viewDidLoad]; +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.artist) { + return @{ @"artist" : self.artist.artistID }; + } + + return nil; +} + +-(BOOL)shouldAutorotate +{ + return NO; +} + +#pragma mark - ARTextViewDelegate + +-(void)textView:(ARTextView *)textView shouldOpenViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:YES]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtistViewController.h b/Artsy/Classes/View Controllers/ARArtistViewController.h new file mode 100644 index 00000000000..44059d6915f --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtistViewController.h @@ -0,0 +1,10 @@ +#import "ARFairAwareObject.h" + +@interface ARArtistViewController : UIViewController + +- (instancetype)initWithArtistID:(NSString *)artistID; + +@property (readonly, nonatomic, strong) Artist *artist; +@property (nonatomic, strong) Fair *fair; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtistViewController.m b/Artsy/Classes/View Controllers/ARArtistViewController.m new file mode 100644 index 00000000000..95f30f86bfd --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtistViewController.m @@ -0,0 +1,603 @@ +#import "ARArtistViewController.h" +#import "ARNavigationButton.h" +#import "ARSwitchView.h" +#import "ARSwitchView+Artist.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARHeartButton.h" +#import "ARSharingController.h" +#import "ARArtistBiographyViewController.h" +#import "ARPostsViewController.h" +#import "ARRelatedArtistsViewController.h" +#import "AREmbeddedModelsViewController.h" +#import "ORStackView+ArtsyViews.h" +#import "ARArtworkSetViewController.h" +#import "ARTextView.h" +#import "ARArtistNetworkModel.h" + +static const NSInteger ARMinimumArtworksFor2Column = 5; + +NS_ENUM(NSInteger, ARArtistViewIndex){ + ARArtistViewArtistName = 1, + ARArtistViewArtistInfo, + ARArtistViewBioTextPad, + ARArtistViewActionButtons, + ARArtistViewArtworksToggle, + ARArtistViewArtworks, + ARArtistViewBioButtonPhone, + ARArtistViewRelatedTitle, + ARArtistViewRelatedArtists, + ARArtistViewRelatedPosts, + ARArtistViewWhitepsaceGobbler +}; + +typedef NS_ENUM(NSInteger, ARArtistArtworksDisplayMode) { + ARArtistArtworksDisplayAll, + ARArtistArtworksDisplayForSale +}; + +// TODO: Add ARFollowableNetworkModel for following status + +@interface ARArtistViewController () +@property (nonatomic, strong) ORStackScrollView *view; +@property (nonatomic, assign) enum ARArtistArtworksDisplayMode displayMode; + +@property (nonatomic, strong) ARArtistNetworkModel *networkModel; + +@property (nonatomic, assign) NSInteger allArtworksLastPage; +@property (nonatomic, assign) NSInteger forSaleArtworksLastPage; + +@property (nonatomic, assign) BOOL isGettingAllArtworks; +@property (nonatomic, assign) BOOL isGettingForSaleArtworks; + +@property (nonatomic, strong) NSMutableOrderedSet *allArtworks; +@property (nonatomic, strong) NSOrderedSet *forSaleFilteredArtworks; + +@property (nonatomic, strong) UILabel *infoLabel; +@property (nonatomic, strong) UILabel *nameLabel; +@property (nonatomic, strong) UILabel *relatedTitle; + +@property (nonatomic, strong) ARSwitchView *switchView; +@property (nonatomic, strong) AREmbeddedModelsViewController *artworkVC; +@property (nonatomic, strong) NSLayoutConstraint *artworksVCConstraint; +@property (nonatomic, strong) ARRelatedArtistsViewController *relatedArtistsVC; + +@property (nonatomic, strong) ARPostsViewController *postsVC; + +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@end + +@implementation ARArtistViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithArtistID:(NSString *)artistID +{ + self = [self init]; + + _artist = [[Artist alloc] initWithArtistID:artistID]; + _allArtworks = [[NSMutableOrderedSet alloc] init]; + _forSaleFilteredArtworks = [[NSMutableOrderedSet alloc] init]; + + return self; +} + +- (void)loadView +{ + [super loadView]; + + self.view = [[ORStackScrollView alloc] initWithStackViewClass:[ORTagBasedAutoStackView class]]; + + self.view.scrollsToTop = NO; + self.view.scrollEnabled = YES; + self.view.delegate = [ARScrollNavigationChief chief]; + self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + self.view.backgroundColor = [UIColor blackColor]; + self.view.stackView.backgroundColor = [UIColor whiteColor]; + self.view.stackView.bottomMarginHeight = 20; + + [self.view.stackView constrainHeightToView:self.view predicate:@">=-20"]; +} + +- (void)updateArtistInfo +{ + @weakify(self); + [self.networkModel getArtistInfoWithSuccess:^(Artist *artist) { + + @strongify(self); + if (!self) { return; } + + if (!artist) { + ARErrorLog(@"Failed to update artist information: missing artist"); + return; + } + + self->_artist = artist; + [self updateWithArtist]; + [self artistIsReady]; + + } failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"Could not update artist information: %@", error.localizedDescription); + [self setIsGettingArtworks:NO displayMode:self.displayMode]; + }]; +} + +- (NSString *)sideMarginString +{ + return [UIDevice isPad] ? @"100" : @"40"; +} + +- (void)viewDidLoad +{ + self.nameLabel = [self.view.stackView addPageTitleWithString:@"" tag:ARArtistViewArtistName]; + self.infoLabel = [ARThemedFactory labelForBodyText]; + self.infoLabel.textAlignment = NSTextAlignmentCenter; + [self.infoLabel constrainHeight:@"16"]; + self.infoLabel.tag = ARArtistViewArtistInfo; + [self.view.stackView addSubview:self.infoLabel withTopMargin:@"4" sideMargin:[self sideMarginString]]; + + UIView *actionsWrapper = [[UIView alloc] init]; + UIButton *shareButton = [[ARCircularActionButton alloc] initWithImageName:@"Artwork_Icon_Share"]; + [shareButton addTarget:self action:@selector(shareArtist) forControlEvents:UIControlEventTouchUpInside]; + [actionsWrapper addSubview:shareButton]; + + ARHeartButton *favoriteButton = [[ARHeartButton alloc] init]; + [favoriteButton addTarget:self action:@selector(toggleFollowingArtist:) forControlEvents:UIControlEventTouchUpInside]; + + [self.artist getFollowState:^(ARHeartStatus status) { + [favoriteButton setStatus:status animated:self.shouldAnimate]; + } failure:^(NSError *error) { + [favoriteButton setStatus:ARHeartStatusNo]; + }]; + + [actionsWrapper addSubview:favoriteButton]; + [favoriteButton alignCenterXWithView:actionsWrapper predicate:@"-30"]; + [shareButton alignCenterXWithView:actionsWrapper predicate:@"30"]; + [UIView alignTopAndBottomEdgesOfViews:@[actionsWrapper, favoriteButton, shareButton]]; + actionsWrapper.tag = ARArtistViewActionButtons; + [self.view.stackView addSubview:actionsWrapper withTopMargin:@"20" sideMargin:@"40"]; + + self.switchView = [[ARSwitchView alloc] initWithButtonTitles:[ARSwitchView artistButtonTitlesArray]]; + [self.switchView constrainWidth:@"280"]; + self.switchView.shouldAnimate = self.shouldAnimate; + self.switchView.delegate = self; + self.switchView.tag = ARArtistViewArtworksToggle; + [self.view.stackView addSubview:self.switchView withTopMargin:@"20" sideMargin:NSStringWithFormat(@">=%@", [self sideMarginString])]; + + self.artworkVC = [[AREmbeddedModelsViewController alloc] init]; + self.artworkVC.activeModule = [ARArtworkMasonryModule masonryModuleWithLayout:[self masonryLayout]]; + self.artworkVC.delegate = self; + self.artworkVC.view.tag = ARArtistViewArtworks; + [self.view.stackView addViewController:self.artworkVC toParent:self withTopMargin:@"20" sideMargin:@"0"]; + + self.relatedTitle = [ARThemedFactory labelForViewSubHeaders]; + self.relatedTitle.text = @"RELATED ARTISTS"; + self.relatedTitle.tag = ARArtistViewRelatedTitle; + self.relatedTitle.alpha = 0; + [self.view.stackView addSubview:self.relatedTitle withTopMargin:@"48" sideMargin:[self sideMarginString]]; + + self.relatedArtistsVC = [[ARRelatedArtistsViewController alloc] init]; + self.relatedArtistsVC.view.tag = ARArtistViewRelatedArtists; + [self.view.stackView addViewController:self.relatedArtistsVC toParent:self withTopMargin:@"20" sideMargin:@"0"]; + + self.postsVC = [[ARPostsViewController alloc] init]; + self.postsVC.delegate = self; + self.postsVC.view.tag = ARArtistViewRelatedPosts; + + CGFloat parentHeight = CGRectGetHeight(self.parentViewController.view.bounds) ?: CGRectGetHeight([UIScreen mainScreen].bounds); + [self.view.stackView ensureScrollingWithHeight:parentHeight tag:ARArtistViewWhitepsaceGobbler]; + + [self getRelatedArtists]; + [self getRelatedPosts]; + + [self updateArtistInfo]; + + [super viewDidLoad]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self setArtworksHeight]; +} + +- (void)updateWithArtist +{ + [self.nameLabel setText:self.artist.name]; + + self.infoLabel.text = self.artist.nationality; + + if (self.artist.nationality.length && self.artist.years.length) { + self.infoLabel.text = [NSString stringWithFormat:@"%@, %@", self.artist.nationality, self.artist.years]; + } + + NSString *artworks = [NSStringWithFormat(@"%@ %@", self.artist.publishedArtworksCount ?: @(0), @"Artworks") uppercaseString]; + NSString *artworksForSale = [NSStringWithFormat(@"%@ %@", self.artist.forSaleArtworksCount ?: @(0), @"For Sale") uppercaseString]; + + [self.switchView setTitle:artworks forButtonAtIndex:ARSwitchViewArtistButtonIndex]; + [self.switchView setTitle:artworksForSale forButtonAtIndex:ARSwitchViewForSaleButtonIndex]; + + [self.view setNeedsLayout]; +} + +- (void)artistIsReady +{ + NSInteger artworkCount = self.artist.publishedArtworksCount.integerValue; + + if (self.artist.forSaleArtworksCount.integerValue == 0) { + [self.switchView disableForSale]; + } + + if (artworkCount == 0) { + [self prepareForNoArtworks]; + } else { + + ARArtworkMasonryModule *module = (ARArtworkMasonryModule *)self.artworkVC.activeModule; + ARArtworkMasonryLayout newLayout = [self masonryLayout]; + if (newLayout != module.layout) { + ARArtworkMasonryModule *newModule = [ARArtworkMasonryModule masonryModuleWithLayout:newLayout andStyle:AREmbeddedArtworkPresentationStyleArtworkOnly]; + self.artworkVC.activeModule = newModule; + [self setArtworksHeight]; + [self.view setNeedsUpdateConstraints]; + [self.view updateConstraintsIfNeeded]; + [self.artworkVC.collectionView reloadData]; + } + [self getMoreArtworks]; + } + + if (self.artist.blurb.length > 0) { + if ([UIDevice isPad]) { + ARTextView *bioView = [[ARTextView alloc] init]; + [bioView setMarkdownString:self.artist.blurb]; + bioView.tag = ARArtistViewBioTextPad; + [self.view.stackView addSubview:bioView withTopMargin:@"20" sideMargin:[self sideMarginString]]; + } else { + ARNavigationButton *bioButton = [[ARNavigationButton alloc] initWithTitle:@"Biography"]; + [bioButton addTarget:self action:@selector(loadBioViewController) forControlEvents:UIControlEventTouchUpInside]; + bioButton.tag = ARArtistViewBioButtonPhone; + [self.view.stackView addSubview:bioButton withTopMargin:@"20" sideMargin:[self sideMarginString]]; + } + } +} + +- (void)prepareForNoArtworks +{ + self.switchView.hidden = YES; + [self.switchView constrainHeight:@"5"]; + self.artworksVCConstraint.constant = 60; + + UILabel *noWorksLabel = [ARThemedFactory labelForBodyText]; + noWorksLabel.textAlignment = NSTextAlignmentCenter; + noWorksLabel.text = [NSString stringWithFormat:@"There are no works by %@ on Artsy yet.", self.artist.name]; + [self.artworkVC.view addSubview:noWorksLabel]; + [noWorksLabel constrainWidthToView:self.artworkVC.view predicate:@"-80"]; + [noWorksLabel alignTopEdgeWithView:self.artworkVC.view predicate:@"0"]; + [noWorksLabel alignCenterXWithView:self.artworkVC.view predicate:nil]; + + [self.artworkVC ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; +} + +- (void)setArtworksHeight +{ + CGFloat height = [self.artworkVC.activeModule intrinsicSize].height; + if (!self.artworksVCConstraint) { + self.artworksVCConstraint = [[self.artworkVC.view constrainHeight:@(height).stringValue] lastObject]; + } else { + self.artworksVCConstraint.constant = height; + } +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; + [self setArtworksHeight]; +} + +- (void)toggleFollowingArtist:(ARHeartButton *)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextFavoriteArtist fromTarget:self selector:_cmd]; + return; + } + + BOOL hearted = !sender.hearted; + [sender setHearted:hearted animated:self.shouldAnimate]; + + @weakify(self); + [self.networkModel setFavoriteStatus:sender.isHearted success:^(id response) {} + failure:^(NSError *error) { + + @strongify(self); + [ARNetworkErrorManager presentActiveErrorModalWithError:error]; + [sender setHearted:!hearted animated:self.shouldAnimate]; + }]; +} + +#pragma mark - Switch Navigation + +- (void)allArtworksTapped +{ + [self switchToDisplayMode:ARArtistArtworksDisplayAll animated:self.shouldAnimate]; +} + +- (void)forSaleOnlyArtworksTapped +{ + [self switchToDisplayMode:ARArtistArtworksDisplayForSale animated:self.shouldAnimate]; +} + +- (void)switchToDisplayMode:(ARArtistArtworksDisplayMode)displayMode animated:(BOOL)animated +{ + ARArtistArtworksDisplayMode oldDisplayMode = self.displayMode; + self.displayMode = displayMode; + + + NSOrderedSet *artworks = [self artworksForDisplayMode:displayMode]; + if (![artworks isEqualToOrderedSet:[self artworksForDisplayMode:oldDisplayMode]]) { + [UIView animateTwoStepIf:self.shouldAnimate && animated duration:ARAnimationDuration * 1.5 :^{ + self.artworkVC.view.alpha = 0; + } midway:^{ + self.artworkVC.collectionView.contentOffset = CGPointZero; + if (artworks.count > 0) { + // clear the array and append the artowrks because `appendItems` does a bunch of necessary stuff that simply setting `items`. + self.artworkVC.activeModule.items = @[]; + [self.artworkVC appendItems:artworks.array]; + } else { + [self.artworkVC ar_presentIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + } + + [self getMoreArtworks]; + self.artworkVC.view.alpha = 1; + + } completion:^(BOOL finished) { + [self.artworkVC ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + [self.view setNeedsUpdateConstraints]; + }]; + } +} + +- (void)getMoreArtworks +{ + if (![self shouldFetchArtworks]) { return; } + + BOOL displayMode = self.displayMode; + [self setIsGettingArtworks:YES displayMode:displayMode]; + + BOOL showingForSale = (displayMode == ARArtistArtworksDisplayForSale); + NSInteger lastPage = (showingForSale) ? self.forSaleArtworksLastPage : self.allArtworksLastPage; + NSDictionary *params = (showingForSale) ? @{ @"filter[]" : @"for_sale" } : nil; + + @weakify(self); + [self.networkModel getArtistArtworksAtPage:lastPage + 1 params:params success:^(NSArray *artworks) { + @strongify(self); + [self.artworkVC ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + [self handleFetchedArtworks:artworks displayMode:self.displayMode]; + } failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"Could not get Artist Artworks: %@", error.localizedDescription); + [self.artworkVC ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + [self setIsGettingArtworks:NO displayMode:displayMode]; + }]; +} + +- (void)setIsGettingArtworks:(BOOL)isGetting displayMode:(ARArtistArtworksDisplayMode)displayMode +{ + if (displayMode == ARArtistArtworksDisplayAll) { + self.isGettingAllArtworks = isGetting; + + } else if (displayMode == ARArtistArtworksDisplayForSale) { + self.isGettingForSaleArtworks = isGetting; + } +} + +- (void)handleFetchedArtworks:(NSArray *)artworks displayMode:(ARArtistArtworksDisplayMode)displayMode +{ + [self setIsGettingArtworks:NO displayMode:displayMode]; + + if(!artworks.count) { + return; + } + + if (displayMode == ARArtistArtworksDisplayAll) { + self.allArtworksLastPage++; + + } else if (displayMode == ARArtistArtworksDisplayForSale) { + self.forSaleArtworksLastPage++; + } + + [self.allArtworks addObjectsFromArray:artworks]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"forSale == YES"]; + NSOrderedSet *filteredResults = [self.allArtworks filteredOrderedSetUsingPredicate:predicate]; + + self.forSaleFilteredArtworks = filteredResults; + [self.artworkVC appendItems:[[self artworksForDisplayMode:self.displayMode] array]]; + + [self.view layoutSubviews]; +} + +#pragma mark - datasource + +- (NSOrderedSet *)artworksForDisplayMode:(ARArtistArtworksDisplayMode)displayMode +{ + switch (displayMode) { + case ARArtistArtworksDisplayAll: + return self.allArtworks; + + case ARArtistArtworksDisplayForSale: + return self.forSaleFilteredArtworks; + } + + return nil; +} + +- (BOOL)shouldFetchArtworks +{ + BOOL displayMode = self.displayMode; + + if ([self isFetchingDataForDisplayMode:displayMode]) { + return NO; + } + + // At this point we have an artist stub, so grab with the + // artist ID. + if (self.artist.name == nil) { + return YES; + } + + NSOrderedSet *artworks = [self artworksForDisplayMode:displayMode]; + NSInteger maxCount = 0; + + if (displayMode == ARArtistArtworksDisplayAll) { + maxCount = [self.artist.publishedArtworksCount integerValue]; + + } else if (displayMode == ARArtistArtworksDisplayForSale) { + maxCount = [self.artist.forSaleArtworksCount integerValue]; + } + + return ([artworks count] < maxCount); +} + +- (BOOL)isFetchingDataForDisplayMode:(ARArtistArtworksDisplayMode)displayMode +{ + BOOL fetching = NO; + if (displayMode == ARArtistArtworksDisplayAll) { + fetching = self.isGettingAllArtworks; + } else if (displayMode == ARArtistArtworksDisplayForSale) { + fetching = self.isGettingForSaleArtworks; + } + + return fetching; +} + +- (NSInteger)lastPageForDisplayMode:(ARArtistArtworksDisplayMode)displayMode +{ + if (displayMode == ARArtistArtworksDisplayAll) { + return self.allArtworksLastPage; + } else if (displayMode == ARArtistArtworksDisplayForSale) { + return self.forSaleArtworksLastPage; + } + + return 0; +} + +- (void)shareArtist +{ + [ARSharingController shareObject:self.artist withThumbnailImageURL:self.artist.smallImageURL]; +} + +- (void)loadBioViewController +{ + ARArtistBiographyViewController * artistBioVC = [[ARArtistBiographyViewController alloc] initWithArtist:self.artist]; + [self.navigationController pushViewController:artistBioVC animated:self.shouldAnimate]; +} + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.artist) { + return @{ @"artist" : self.artist.artistID, @"type" : @"artist" }; + } + + return nil; +} + +- (BOOL)artworksShouldBeSingleRow +{ + // in case we have some artworks but no artist info + NSInteger count = MAX([self.artist.publishedArtworksCount integerValue], [self artworksForDisplayMode:self.displayMode].count); + return count < ARMinimumArtworksFor2Column; +} + +- (ARArtworkMasonryLayout)masonryLayout +{ + if ([self artworksShouldBeSingleRow]) { + return ARArtworkMasonryLayout1Row; + } else { + return ARArtworkMasonryLayout2Row; + } +} + +- (void)getRelatedArtists +{ + @weakify(self); + [self.artist getRelatedArtists:^(NSArray *artists) { + @strongify(self); + if (artists.count > 0 ) { + [UIView animateIf:self.shouldAnimate duration:ARAnimationDuration :^{ + self.relatedTitle.alpha = 1; + }]; + artists = [artists filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"publishedArtworksCount != 0"]]; + self.relatedArtistsVC.relatedArtists = artists; + } + }]; +} + +- (void)getRelatedPosts +{ + @weakify(self); + [self.artist getRelatedPosts:^(NSArray *posts) { + @strongify(self); + if (posts.count > 0) { + self.postsVC.posts = posts; + [self.view.stackView addSubview:self.postsVC.view withTopMargin:@"20" sideMargin:@"40"]; + } + }]; +} + +#pragma mark - AREmbeddedModelsDelegate + +-(void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:self.artworkVC.items inFair:nil atIndex:index]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)embeddedModelsViewControllerDidScrollPastEdge:(AREmbeddedModelsViewController *)controller +{ + [self getMoreArtworks]; +} + +#pragma mark - ARPostsViewControllerDelegate + +-(void)postViewController:(ARPostsViewController *)postViewController shouldShowViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +#pragma mark - ARSwitchViewDelegate + + +- (void)switchView:(ARSwitchView *)switchView didPressButtonAtIndex:(NSInteger)buttonIndex animated:(BOOL)animated +{ + if (buttonIndex == ARSwitchViewArtistButtonIndex) { + [self allArtworksTapped]; + } else if (buttonIndex == ARSwitchViewForSaleButtonIndex) { + [self forSaleOnlyArtworksTapped]; + } +} + +#pragma mark - DI + +- (ARArtistNetworkModel *)networkModel +{ + return _networkModel ?: [[ARArtistNetworkModel alloc] initWithArtist:self.artist]; +} + + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkDetailView.h b/Artsy/Classes/View Controllers/ARArtworkDetailView.h new file mode 100644 index 00000000000..ffc91fcecf5 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkDetailView.h @@ -0,0 +1,23 @@ +/** + The ARArtworkDetailView is a view for showing artwork metadata + such such as artist name, artwork name, description, materials, + size, price and partner. + + Has no intrinsic height as it will generate it based off the content at runtime. + */ + +@class ARArtworkDetailView; + +@protocol ARArtworkDetailViewDelegate +- (void)artworkDetailView:(ARArtworkDetailView *)detailView shouldPresentViewController:(UIViewController *)viewController; +- (void)didUpdateArtworkDetailView:(ARArtworkDetailView *)detailView; +@end + +@interface ARArtworkDetailView : ORTagBasedAutoStackView +@property (nonatomic, weak) id delegate; +@property(readonly, nonatomic, strong) Artwork *artwork; +@property(readonly, nonatomic, strong) Fair *fair; +- (instancetype)initWithArtwork:(Artwork *)artwork andFair:(Fair *)fair; +- (void)updateWithFair:(Fair *)fair; +- (void)updateWithArtwork:(Artwork *)artwork; +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkDetailView.m b/Artsy/Classes/View Controllers/ARArtworkDetailView.m new file mode 100644 index 00000000000..36d632e5977 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkDetailView.m @@ -0,0 +1,288 @@ +#import "ARArtworkDetailView.h" +#import "ARTextView.h" + +NS_ENUM(NSInteger, ARDetailSubViewOrder){ + ARDetailArtistName = 1, + ARDetailArtworkTitle, + ARDetailArtworkMedium, + ARDetailDimensionInches, + ARDetailDimensionCM, + ARDetailCollectionInstitution, + ARDetailImageRights, + ARDetailPartner, + ARDetailArtworkAuctionEstimate, + ARDetailFair, + ARDetailFairDescription +}; + +@interface ARArtworkDetailView () + +@end + +@implementation ARArtworkDetailView + +- (instancetype)initWithArtwork:(Artwork *)artwork andFair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + + _artwork = artwork; + _fair = fair; + self.backgroundColor = [UIColor whiteColor]; + self.bottomMarginHeight = 0; + return self; +} + +- (void)setDelegate:(id)delegate +{ + _delegate = delegate; + @weakify(self); + [self.artwork onArtworkUpdate:^{ + @strongify(self); + [self updateWithArtwork:self.artwork]; + } failure:nil]; + [self updateWithArtwork:self.artwork]; + + [self.artwork onSaleArtworkUpdate:^(SaleArtwork *saleArtwork) { + @strongify(self); + [self updateWithSaleArtwork:saleArtwork]; + } failure: nil]; +} + +// Either create the view or find it in the hierarchy + +- (id)viewFor:(enum ARDetailSubViewOrder)viewType +{ + UIView *view = [self viewWithTag:viewType]; + if (view) { + return view; + } + + switch (viewType) { + + case ARDetailArtworkTitle: { + view = [[ARArtworkTitleLabel alloc] init]; + break; + } + + case ARDetailArtistName: { + view = [[ARSansSerifLabelWithChevron alloc] init]; + break; + } + + case ARDetailArtworkMedium: { + ARSerifLineHeightLabel *mediumLabel = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:3]; + mediumLabel.numberOfLines = 0; + view = mediumLabel; + break; + } + + case ARDetailDimensionInches: { + view = [[ARSerifLabel alloc] init]; + break; + } + + case ARDetailDimensionCM: { + view = [[ARSerifLabel alloc] init]; + break; + } + + case ARDetailImageRights: { + ARSerifLineHeightLabel *imageRightsLabel = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:6]; + imageRightsLabel.font = [UIFont serifFontWithSize:12]; + view = imageRightsLabel; + break; + } + + case ARDetailPartner: { + view = [[ARSerifLabelWithChevron alloc] init]; + break; + } + + case ARDetailFair: { + view = [[ARSerifLabel alloc] init]; + break; + } + + case ARDetailFairDescription: { + ARSerifLabel *fairDescriptionLabel = [[ARSerifLabel alloc] init]; + fairDescriptionLabel.font = [UIFont serifFontWithSize:14]; + fairDescriptionLabel.textColor = [UIColor blackColor]; + view = fairDescriptionLabel; + break; + } + + case ARDetailArtworkAuctionEstimate: { + view = [[ARSerifLabel alloc] init]; + break; + } + + case ARDetailCollectionInstitution: { + ARTextView *collectionInstitutionTextView = [[ARTextView alloc] init]; + collectionInstitutionTextView.viewControllerDelegate = self; + collectionInstitutionTextView.expectsSingleLine = YES; + collectionInstitutionTextView.plainLinks = YES; + collectionInstitutionTextView.font = [UIFont serifFontWithSize:14]; + collectionInstitutionTextView.viewControllerDelegate = self; + view = collectionInstitutionTextView; + break; + } + + default: + NSAssert(FALSE, @"Not found a view for the view type %@", @(viewType)); + return nil; + } + + view.tag = viewType; + return view; +} + +- (void)updateWithArtwork:(Artwork *)artwork +{ + _artwork = artwork; + BOOL hasArtist = artwork.artist && artwork.artist.name.length; + if (hasArtist) { + ARItalicsSerifLabelWithChevron *artistNameLabel = [self viewFor:ARDetailArtistName]; + artistNameLabel.text = artwork.artist.name.uppercaseString; + + artistNameLabel.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openArtworkArtist:)]; + [artistNameLabel addGestureRecognizer:tapGesture]; + + [artistNameLabel constrainHeight:@"50"]; + [self addSubview:artistNameLabel withTopMargin:@"0" sideMargin:@"0"]; + } + + if (artwork.title.length) { + NSString *topMargin = hasArtist ? @"4" : @"16"; + ARArtworkTitleLabel *titleLabel = [self viewFor:ARDetailArtworkTitle]; + titleLabel.font = [titleLabel.font fontWithSize:16]; + [titleLabel setTitle:artwork.title date:artwork.date]; + [self addSubview:titleLabel withTopMargin:topMargin sideMargin:@"0"]; + } + + if (artwork.medium.length) { + ARSerifLineHeightLabel *mediumLabel = [self viewFor:ARDetailArtworkMedium]; + mediumLabel.text = artwork.medium; + [self addSubview:mediumLabel withTopMargin:@"4" sideMargin:@"0"]; + } + + if (artwork.dimensionsInches.length) { + ARSerifLabel *dimensionInchesLabel = [self viewFor:ARDetailDimensionInches]; + dimensionInchesLabel.text = artwork.dimensionsInches; + + [self addSubview:dimensionInchesLabel withTopMargin:@"4" sideMargin:@"0"]; + } + + if (artwork.dimensionsCM.length) { + ARSerifLabel *dimensionCMLabel = [self viewFor:ARDetailDimensionCM]; + dimensionCMLabel.text = artwork.dimensionsCM; + + [self addSubview:dimensionCMLabel withTopMargin:@"4" sideMargin:@"0"]; + } + + if (artwork.imageRights.length) { + ARSerifLabel *imageRightsLabel = [self viewFor:ARDetailImageRights]; + imageRightsLabel.text = [artwork.imageRights stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; + + [self addSubview:imageRightsLabel withTopMargin:@"12" sideMargin:@"0"]; + } + + if (artwork.collectingInstitution.length) { + ARTextView *collectionInstitionTextView = [self viewFor:ARDetailCollectionInstitution]; + [collectionInstitionTextView setMarkdownString:artwork.collectingInstitution]; + + [self addSubview:collectionInstitionTextView withTopMargin:@"4"]; + [collectionInstitionTextView alignLeadingEdgeWithView:self predicate:@"-4"]; + [collectionInstitionTextView constrainWidthToView:self predicate:@"-2"]; + + } else if (artwork.partner && artwork.partner.name.length) { + + ARSerifLabelWithChevron *partnerLabel = [self viewFor:ARDetailPartner]; + partnerLabel.text = artwork.partner.name; + partnerLabel.userInteractionEnabled = YES; + + BOOL showChevron = (artwork.partner.website.length || artwork.partner.defaultProfilePublic); + partnerLabel.chevronHidden = !showChevron; + + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openArtworkPartner:)]; + [partnerLabel addGestureRecognizer:tapGesture]; + + // Treat it different visually if it looks like a button. + NSString *topMargin = (showChevron) ? @"0" : @"20"; + if (showChevron) { + [partnerLabel constrainHeight:@"40"]; + } + + [self addSubview:partnerLabel withTopMargin:topMargin sideMargin:@"0"]; + } + + [self.delegate didUpdateArtworkDetailView:self]; + [self updateFairLabels]; +} + + +- (void)updateWithFair:(Fair *)fair +{ + _fair = fair; + [self updateFairLabels]; +} + +- (void)updateFairLabels +{ + if (self.artwork.partner && self.fair) { + ARSerifLabelWithChevron *fairName = [self viewFor:ARDetailFair]; + fairName.text = self.fair.name; + fairName.userInteractionEnabled = YES; + [self addSubview:fairName withTopMargin:@"20" sideMargin:@"0"]; + [fairName addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openArtworkFair:)]]; + + + PartnerShow *booth = [self.fair findShowForPartner:self.artwork.partner]; + NSString *fairCopy = (booth) ? booth.ausstellungsdauerAndLocation : self.fair.ausstellungsdauer; + ARSerifLabel *fairDescription = [self viewFor:ARDetailFairDescription]; + fairDescription.text = fairCopy; + fairDescription.userInteractionEnabled = YES; + [self addSubview:fairDescription withTopMargin:@"4" sideMargin:@"0"]; + [fairDescription addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openArtworkFair:)]]; + + [self.delegate didUpdateArtworkDetailView:self]; + } +} + +- (void)updateWithSaleArtwork:(SaleArtwork *)saleArtwork +{ + if (saleArtwork.hasEstimate) { + ARSerifLabel *auctionLabel = [self viewFor:ARDetailArtworkAuctionEstimate]; + auctionLabel.text = saleArtwork.estimateString; + [self addSubview:auctionLabel withTopMargin:@"12" sideMargin:@"0"]; + [self.delegate didUpdateArtworkDetailView:self]; + } +} + +- (void)openArtworkArtist:(UIGestureRecognizer *)tapGesture +{ + // This will pass the message up the responder chain + [[UIApplication sharedApplication] sendAction:@selector(tappedOpenArtworkArtist:) to:nil from:self forEvent:nil]; +} + +- (void)openArtworkFair:(UIGestureRecognizer *)tapGesture +{ + // This will pass the message up the responder chain + [[UIApplication sharedApplication] sendAction:@selector(tappedOpenFair:) to:nil from:self forEvent:nil]; +} + +- (void)openArtworkPartner:(UIGestureRecognizer *)tapGesture +{ + // This will pass the message up the responder chain + [[UIApplication sharedApplication] sendAction:@selector(tappedOpenArtworkPartner:) to:nil from:self forEvent:nil]; +} + +#pragma mark - ARTextViewDelegate + +-(void)textView:(ARTextView *)textView shouldOpenViewController:(UIViewController *)viewController +{ + [self.delegate artworkDetailView:self shouldPresentViewController:viewController]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkFlowModule.h b/Artsy/Classes/View Controllers/ARArtworkFlowModule.h new file mode 100644 index 00000000000..bda8f3fcf94 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkFlowModule.h @@ -0,0 +1,23 @@ +#import "ARModelCollectionViewModule.h" + +typedef NS_ENUM(NSInteger, ARArtworkFlowLayout){ + ARArtworkFlowLayoutSingleRow, + ARArtworkFlowLayoutDoubleRow, + ARArtworkFlowLayoutPagingCarousel, + ARArtworkFlowLayoutDoubleColumn +}; + +/// Handles the layout and styling for Carousel & Single image +/// layouts in an AREmbeddedModelsViewController +@interface ARArtworkFlowModule : ARModelCollectionViewModule + +/// Create a flow based module ++ (instancetype)flowModuleWithLayout:(enum ARArtworkFlowLayout)layout andStyle:(enum AREmbeddedArtworkPresentationStyle)style; + +/// Gets the size for the view with a style & items ++ (CGSize)intrinsicSizeWithlayout:(enum ARArtworkFlowLayout)layout andArtworks:(NSArray *)items; + +/// The artsy specific styles for the layout +@property (nonatomic, assign, readonly) enum AREmbeddedArtworkPresentationStyle style; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkFlowModule.m b/Artsy/Classes/View Controllers/ARArtworkFlowModule.m new file mode 100644 index 00000000000..7f565a1899a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkFlowModule.m @@ -0,0 +1,158 @@ +#import "ARArtworkFlowModule.h" +#import "ARItemThumbnailViewCell.h" +#import "ARArtworkWithMetadataThumbnailCell.h" + +/// Note: a purposeful lack of constants in here. YOLO. + +@interface ARArtworkFlowModule () +@property (nonatomic, strong) UICollectionViewFlowLayout *moduleLayout; +@property (nonatomic, assign) enum ARArtworkFlowLayout layout; +@property (nonatomic, assign) enum AREmbeddedArtworkPresentationStyle style; +@end + +@implementation ARArtworkFlowModule + ++ (instancetype)flowModuleWithLayout:(enum ARArtworkFlowLayout)layout andStyle:(enum AREmbeddedArtworkPresentationStyle)style +{ + ARArtworkFlowModule *module = [[self alloc] init]; + module.layout = layout; + module.style = style; + + module.moduleLayout = [self createFlowLayoutForModule:module]; + + return module; +} + ++ (UICollectionViewFlowLayout *)createFlowLayoutForModule:(ARArtworkFlowModule *)module +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + + switch (module.layout) { + default: + layout.minimumInteritemSpacing = 20; + layout.itemSize = [module itemSizeWithLayout:module.layout andArtworks:module.items]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.sectionInset = UIEdgeInsetsMake(0, 20, 0, 20); + break; + } + + return layout; +} + + +- (CGSize)intrinsicSize +{ + return [self.class intrinsicSizeWithlayout:self.layout andArtworks:self.items]; +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + // Add paging +} + +#pragma mark Flow Based Sizing + +- (CGSize)itemSizeWithLayout:(enum ARArtworkFlowLayout)layout andArtworks:(NSArray *)items +{ + switch (layout) { + case ARArtworkFlowLayoutDoubleRow: + return CGSizeMake(100, 120); + + case ARArtworkFlowLayoutDoubleColumn: + return CGSizeMake(160, 130); + + default: + return CGSizeMake(300, 300); + } +} + ++ (CGSize)intrinsicSizeWithlayout:(enum ARArtworkFlowLayout)layout andArtworks:(NSArray *)items +{ +// if (items.count == 0) { +// size = CGSizeMake(320, 320); +// +// } else if (items.count == 1) { +// size = [self sizeForSingleItem:items.first]; +// +// } else { +// CGFloat height = [self sizeForSingleItem:items.first].height; +// size = CGSizeMake(320, height); +// } + + switch (layout) { + case ARArtworkFlowLayoutDoubleRow: + return CGSizeMake(300, 300); + break; + + default: + return CGSizeMake(300, 300); + } +} + ++ (CGSize)sizeForSingleItem:(id)item +{ + CGFloat aspectRatio = item.aspectRatio; + CGFloat height, width; + CGFloat ARFeedCarouselThresholdRatio = 3.0/2.0; + + CGSize sizeWithMaxHeight = [self sizeForSingleItem:item withHeight:280]; + + if (aspectRatio) { + if (aspectRatio < ARFeedCarouselThresholdRatio) { + + // Fit inside ARFeedWidth x ARFeedWidth Square + if (aspectRatio <= 1) { + return sizeWithMaxHeight; + + } else { + width = 280; + height = 280/aspectRatio; + } + + } else { + // Contraints : height <= ARFeedWidth and width <= 2 * ARFeedWidth + + CGFloat maxWidth = 2 * 280; + if (sizeWithMaxHeight.width > maxWidth) { + return CGSizeMake(maxWidth, maxWidth/aspectRatio); + } else { + return sizeWithMaxHeight; + } + } + + } else { + // force it to square + height = 280; + width = 280; + } + + return CGSizeMake(width, height); +} + ++ (CGSize)sizeForSingleItem:(id)item withHeight:(CGFloat)height +{ + CGFloat aspectRatio = item.aspectRatio; + CGFloat width; + + if (aspectRatio) { + width = height * aspectRatio; + } else { + // force it to square + width = height; + } + + return CGSizeMake(width, height); +} + +- (Class)classForCell +{ + switch (self.style) { + case AREmbeddedArtworkPresentationStyleArtworkOnly: + return [ARItemThumbnailViewCell class]; + + case AREmbeddedArtworkPresentationStyleArtworkMetadata: + return [ARArtworkWithMetadataThumbnailCell class]; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkInfoViewController.h b/Artsy/Classes/View Controllers/ARArtworkInfoViewController.h new file mode 100644 index 00000000000..a7d2cc98c26 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkInfoViewController.h @@ -0,0 +1,8 @@ +#import + +@interface ARArtworkInfoViewController : UIViewController + +- (instancetype)initWithArtwork:(Artwork *)artwork; + +@end + diff --git a/Artsy/Classes/View Controllers/ARArtworkInfoViewController.m b/Artsy/Classes/View Controllers/ARArtworkInfoViewController.m new file mode 100644 index 00000000000..51777786c9a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkInfoViewController.m @@ -0,0 +1,82 @@ +#import "ARArtworkInfoViewController.h" +#import "ORStackView+ArtsyViews.h" +#import "ARTextView.h" + +@interface ARArtworkInfoViewController () +@property (nonatomic, strong)Artwork *artwork; +@property (nonatomic, strong) ORStackScrollView *view; +@end + +@implementation ARArtworkInfoViewController + +- (instancetype)initWithArtwork:(Artwork *)artwork +{ + self = [super init]; + if (!self) { return nil; } + _artwork = artwork; + return self; +} + +- (void)loadView +{ + self.view = [[ORStackScrollView alloc] init]; + self.view.stackView.bottomMarginHeight = 20; + self.view.backgroundColor = [UIColor whiteColor]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + self.view.delegate = [ARScrollNavigationChief chief]; + [super viewWillAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + self.view.delegate = nil; + + [super viewWillDisappear:animated]; +} + +- (void)dealloc +{ + self.view.delegate = nil; +} + +- (void)viewDidLoad +{ + [self.view.stackView addPageTitleWithString:@"More Info"]; + [self addSectionWithTitle:@"Provenance" andText:self.artwork.provenance]; + [self addSectionWithTitle:@"Signature" andText:self.artwork.signature]; + [self addSectionWithTitle:@"Additional Information" andText:self.artwork.additionalInfo]; + [self addSectionWithTitle:@"Literature" andText:self.artwork.literature]; + + [super viewDidLoad]; +} + +- (void)addSectionWithTitle:(NSString *)title andText:(NSString *)text +{ + if (![text length]) { return; } + ARLabel *label = [[ARSansSerifLabel alloc] init]; + label.font = [label.font fontWithSize:14]; + label.text = title; + [self.view.stackView addSubview:label withTopMargin:@"20" sideMargin:@"40"]; + + UITextView *textView = [[ARTextView alloc] init]; + textView.text = text; + [self.view.stackView addSubview:textView withTopMargin:@"0" sideMargin:@"30"]; +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.artwork) { + return @{ @"artwork" : self.artwork.artworkID }; + } + return nil; +} + +-(BOOL)shouldAutorotate +{ + return NO; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkMasonryModule.h b/Artsy/Classes/View Controllers/ARArtworkMasonryModule.h new file mode 100644 index 00000000000..6ffb6bd3024 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkMasonryModule.h @@ -0,0 +1,48 @@ +#import "ARModelCollectionViewModule.h" +#import "ARCollectionViewMasonryLayout.h" + +typedef NS_ENUM(NSInteger, ARArtworkMasonryLayout){ + ARArtworkMasonryLayout1Row, + ARArtworkMasonryLayout2Row, + ARArtworkMasonryLayout1Column, + ARArtworkMasonryLayout2Column, + ARArtworkMasonryLayout3Column, + ARArtworkMasonryLayout4Column +}; + +@class ARArtworkMasonryModule; + +// A protocol to standardize the way we provide different layout styles to the module. +@protocol ARArtworkMasonryLayoutProvider +- (enum ARArtworkMasonryLayout)masonryLayoutForPadWithOrientation:(UIInterfaceOrientation)orientation; +@end + +@interface ARArtworkMasonryModule : ARModelCollectionViewModule + +/// Create a masonry based module, uses the presentation style of artwork only ++ (instancetype)masonryModuleWithLayout:(enum ARArtworkMasonryLayout)layout; + +/// Designated initializer. Creates a masonry module with a specific artwork style ++ (instancetype)masonryModuleWithLayout:(enum ARArtworkMasonryLayout)layout andStyle:(enum AREmbeddedArtworkPresentationStyle)style; + +/// Get the height of the collection view for a horizontal layout ++ (CGFloat)intrinsicHeightForHorizontalLayout:(ARArtworkMasonryLayout)layout; + +/// Gets the intrinsic property from an existing instance +- (CGSize)intrinsicSize; + +/// The module layout provided and managed by this class +/// for use in a AREmbeddedModelsViewController +@property (nonatomic, readonly) ARCollectionViewMasonryLayout *moduleLayout; + +/// The specific layout +@property (nonatomic, assign, readwrite) enum ARArtworkMasonryLayout layout; + +@property (nonatomic, weak, readwrite) id layoutProvider; + +/// The artwork presentation style +@property (nonatomic, assign, readonly) enum AREmbeddedArtworkPresentationStyle style; + +@property (nonatomic, assign) BOOL showTrailingLoadingIndicator; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkMasonryModule.m b/Artsy/Classes/View Controllers/ARArtworkMasonryModule.m new file mode 100644 index 00000000000..4f9d9e81b09 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkMasonryModule.m @@ -0,0 +1,323 @@ +#import "ARArtworkMasonryModule.h" +#import "ARReusableLoadingView.h" +#import "ARItemThumbnailViewCell.h" +#import "ARArtworkWithMetadataThumbnailCell.h" + + +@interface ARArtworkMasonryModule() +@property (nonatomic, strong) ARCollectionViewMasonryLayout *moduleLayout; +@property (nonatomic, assign) enum AREmbeddedArtworkPresentationStyle style; +@property (nonatomic, assign, getter = isHorizontal) BOOL horizontalOrientation; +@property (nonatomic, readonly, strong) ARReusableLoadingView *loadingView; +@end + +@implementation ARArtworkMasonryModule + ++ (instancetype)masonryModuleWithLayout:(enum ARArtworkMasonryLayout)layout andStyle:(enum AREmbeddedArtworkPresentationStyle)style; +{ + ARArtworkMasonryModule *module = [[self alloc] init]; + + module.layout = layout; + module.style = style; + if ([UIDevice isPad]) { + [[NSNotificationCenter defaultCenter] addObserver:module selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil]; + } + return module; +} + + ++ (instancetype)masonryModuleWithLayout:(enum ARArtworkMasonryLayout)layout; +{ + return [self masonryModuleWithLayout:layout andStyle:AREmbeddedArtworkPresentationStyleArtworkOnly]; +} + ++ (CGFloat)intrinsicHeightForHorizontalLayout:(ARArtworkMasonryLayout)layout +{ + NSAssert([self.class layoutIsHorizontal:layout], @"intrinsicHeightForHorizontalLayout must be given a horizontal layout."); + CGFloat height = [self dimensionForlayout:layout]; + CGFloat margin = [self itemMarginsforLayout:layout].height; + CGFloat rank = [self rankForlayout:layout]; + height = (rank * height) + ((rank - 1) * margin); + return height; +} + ++ (BOOL)layoutIsHorizontal:(ARArtworkMasonryLayout)layout +{ + switch (layout) { + case ARArtworkMasonryLayout1Column: + case ARArtworkMasonryLayout2Column: + case ARArtworkMasonryLayout3Column: + case ARArtworkMasonryLayout4Column: + return NO; + + case ARArtworkMasonryLayout2Row: + case ARArtworkMasonryLayout1Row: + return YES; + } +} + ++ (NSInteger)rankForlayout:(ARArtworkMasonryLayout)layout +{ + switch (layout) { + case ARArtworkMasonryLayout4Column: + return 4; + + case ARArtworkMasonryLayout3Column: + return 3; + + case ARArtworkMasonryLayout2Column: + case ARArtworkMasonryLayout2Row: + return 2; + + case ARArtworkMasonryLayout1Column: + case ARArtworkMasonryLayout1Row: + return 1; + } +} + ++ (CGFloat)dimensionForlayout:(ARArtworkMasonryLayout)layout +{ + switch (layout) { + case ARArtworkMasonryLayout1Column: + return 280; + + case ARArtworkMasonryLayout2Column: + // On iPad, the 2-column layout is only used in portrait mode. + return [UIDevice isPad] ? 315 : 130; + + case ARArtworkMasonryLayout3Column: + // The 3-column layout is only used on iPad. + NSAssert([UIDevice isPad], @"ARARtworkMasonryLayout3Column is intended for use with iPad"); + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + if (UIInterfaceOrientationIsLandscape(orientation)) { + return 280; + } else { + return 200; + } + + case ARArtworkMasonryLayout4Column: + // The 4-column layout is only used in iPad landscape mode. + NSAssert([UIDevice isPad], @"ARARtworkMasonryLayout4Column is intended for use with iPad"); + return 210; + + case ARArtworkMasonryLayout2Row: + if ([UIDevice isPad]) { + if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) { + return 134; + } else { + return 184; + } + } else { + return 120; + } + + case ARArtworkMasonryLayout1Row: + if ([UIDevice isPad]) { + if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) { + return 400; + } else { + return 400; + } + } else { + return 240; + } + } +} + ++ (UIEdgeInsets)edgeInsetsForlayout:(ARArtworkMasonryLayout)layout { + CGFloat inset = [UIDevice isPad] ? 50 : 20; + switch (layout) { + case ARArtworkMasonryLayout1Column: + case ARArtworkMasonryLayout2Column: + case ARArtworkMasonryLayout3Column: + case ARArtworkMasonryLayout4Column: + return (UIEdgeInsets){ 20, 0, 20, 0 }; + + case ARArtworkMasonryLayout1Row: + case ARArtworkMasonryLayout2Row: + return (UIEdgeInsets){ 0, inset, 0, inset }; + } +} + + +- (void)orientationChanged:(id)sender +{ + // Only called if current device is an iPad + if (self.layoutProvider) { + self.layout = [self.layoutProvider masonryLayoutForPadWithOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; + } else { + [self setup]; + } +} + +- (void)setLayout:(enum ARArtworkMasonryLayout)layout +{ + _layout = layout; + [self setup]; +} + +- (CGSize)intrinsicSize +{ + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + CGSize screenSize = [[UIScreen mainScreen] bounds].size; + CGFloat width = UIInterfaceOrientationIsLandscape(orientation) ? screenSize.height : screenSize.width; + CGFloat height; + if ([self.class layoutIsHorizontal:self.layout]) { + height = [self.class intrinsicHeightForHorizontalLayout:self.layout]; + } else { + NSMutableArray *lengths = [NSMutableArray array]; + for (Artwork *artwork in self.items) { + CGFloat itemLength = [self.class variableDimensionForItem:artwork style:self.style layout:self.layout]; + [lengths addObject:@( itemLength )]; + } + + height = [self.moduleLayout longestDimensionWithLengths:lengths withOppositeDimension:width]; + } + return (CGSize){ width, height }; +} + +- (Class)classForCell +{ + switch (self.style) { + case AREmbeddedArtworkPresentationStyleArtworkOnly: + return [ARItemThumbnailViewCell class]; + + case AREmbeddedArtworkPresentationStyleArtworkMetadata: + return [ARArtworkWithMetadataThumbnailCell class]; + } +} + +- (void)setup +{ + self.horizontalOrientation = [self.class layoutIsHorizontal:self.layout]; + + if (!self.moduleLayout) { + self.moduleLayout = [[ARCollectionViewMasonryLayout alloc] initWithDirection:self.direction]; + } + + self.moduleLayout.itemMargins = [self.class itemMarginsforLayout:self.layout]; + self.moduleLayout.rank = [self.class rankForlayout:self.layout]; + self.moduleLayout.dimensionLength = [self.class dimensionForlayout:self.layout]; + self.moduleLayout.contentInset = [self.class edgeInsetsForlayout:self.layout]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(ARCollectionViewMasonryLayout *)collectionViewLayout variableDimensionForItemAtIndexPath:(NSIndexPath *)indexPath +{ + Artwork *item = self.items[indexPath.row]; + return [[self class] variableDimensionForItem:item style:self.style layout:self.layout]; +} + ++ (CGFloat)variableDimensionForItem:(Artwork *)item style:(AREmbeddedArtworkPresentationStyle)style layout:(ARArtworkMasonryLayout)layout +{ + CGFloat staticDimension = [self.class dimensionForlayout:layout]; + CGFloat variableDimension = 0; + + // Set the artwork height/width for the artwork based on the layout + BOOL isHorizontal = [[self class] layoutIsHorizontal:layout]; + if (style == AREmbeddedArtworkPresentationStyleArtworkMetadata && isHorizontal) { + staticDimension -= [ARArtworkWithMetadataThumbnailCell heightForMetaData]; + } + switch (layout) { + case ARArtworkMasonryLayout1Column: + case ARArtworkMasonryLayout2Column: + case ARArtworkMasonryLayout3Column: + case ARArtworkMasonryLayout4Column: + variableDimension = staticDimension / item.aspectRatio; + break; + + case ARArtworkMasonryLayout1Row: + case ARArtworkMasonryLayout2Row: + variableDimension = staticDimension * item.aspectRatio; + break; + } + + // Apply sizing offsets for the style + switch (style) { + case AREmbeddedArtworkPresentationStyleArtworkMetadata: + if (!isHorizontal) { + return variableDimension + [ARArtworkWithMetadataThumbnailCell heightForMetaData]; + } + break; + case AREmbeddedArtworkPresentationStyleArtworkOnly: + break; + } + return variableDimension; + +} + ++ (CGSize)itemMarginsforLayout:(ARArtworkMasonryLayout)layout +{ + switch (layout) { + case ARArtworkMasonryLayout1Column: + case ARArtworkMasonryLayout2Column: + if ([UIDevice isPad]) { + return (CGSize){ 38, 38 }; + } + case ARArtworkMasonryLayout3Column: + if ([UIDevice isPad]) { + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + if (UIInterfaceOrientationIsLandscape(orientation)) { + return (CGSize){ 42, 42 }; + } else { + return (CGSize){ 34, 34 }; + } + } + case ARArtworkMasonryLayout4Column: + if ([UIDevice isPad]) { + return (CGSize){ 28, 28 }; + } + + case ARArtworkMasonryLayout2Row: + case ARArtworkMasonryLayout1Row: + return (CGSize){ 15, 15 }; + } + +} + +- (ARCollectionViewMasonryLayoutDirection)direction { + return (self.isHorizontal) ? + ARCollectionViewMasonryLayoutDirectionHorizontal + : ARCollectionViewMasonryLayoutDirectionVertical; +} + +- (ARFeedItemImageSize)imageSize +{ + if ([UIDevice isPad] || self.layout == ARArtworkMasonryLayout1Column) { + return ARFeedItemImageSizeLarge; + } else if (self.isHorizontal) { + return ARFeedItemImageSizeAuto; + } else { + return ARFeedItemImageSizeMasonry; + } +} + +- (void)setShowTrailingLoadingIndicator:(BOOL)showTrailingLoadingIndicator +{ + _showTrailingLoadingIndicator = showTrailingLoadingIndicator; + if (showTrailingLoadingIndicator) { + [self.loadingView startIndeterminateAnimated:YES]; + } else { + [self.loadingView stopIndeterminateAnimated:YES]; + } +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(ARCollectionViewMasonryLayout *)collectionViewLayout dimensionForFooterAtIndexPath:(NSIndexPath *)indexPath +{ + return self.showTrailingLoadingIndicator ? 40 : 0; +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if (!self.loadingView) { + _loadingView = [[ARReusableLoadingView alloc] init]; + [self.loadingView startIndeterminateAnimated:YES]; + } + return self.loadingView; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkPreviewActionsView.h b/Artsy/Classes/View Controllers/ARArtworkPreviewActionsView.h new file mode 100644 index 00000000000..c0683fac6e8 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkPreviewActionsView.h @@ -0,0 +1,24 @@ +@class ARHeartButton, ARArtworkInfoButton; + +/// Presents buttons for an artwork + +@interface ARArtworkPreviewActionsView : UIView + +/// Creates an instance, does not retain artwork but registers for updates +- (instancetype)initWithArtwork:(Artwork *)artwork andFair:(Fair *)fair; + +/// The button for indicating you're favoriting a work +@property (readonly, nonatomic, strong) ARHeartButton *favoriteButton; + +/// The button for sharing a work over airplay / twitter / fb +@property (readonly, nonatomic, strong) ARCircularActionButton *shareButton; + +/// The button for viewing a room, initially hidden, only available +/// if the Artwork can be viewed in a room. +@property (readonly, nonatomic, strong) ARCircularActionButton *viewInRoomButton; + +/// The button for showing the map, initially hidden, only available +/// if in a fair context +@property (readonly, nonatomic, strong) ARCircularActionButton *viewInMapButton; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkPreviewActionsView.m b/Artsy/Classes/View Controllers/ARArtworkPreviewActionsView.m new file mode 100644 index 00000000000..a4cbfa34022 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkPreviewActionsView.m @@ -0,0 +1,105 @@ +#import "ARArtworkPreviewActionsView.h" +#import "ARHeartButton.h" + +@interface ARArtworkPreviewActionsView() +@property (readwrite, nonatomic, strong) NSLayoutConstraint *mapButtonConstraint; +@end + +@implementation ARArtworkPreviewActionsView + +- (instancetype)initWithArtwork:(Artwork *)artwork andFair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + + _shareButton = [[ARCircularActionButton alloc] initWithImageName:@"Artwork_Icon_Share"]; + _favoriteButton = [[ARHeartButton alloc] init]; + _viewInRoomButton = [[ARCircularActionButton alloc] initWithImageName:@"Artwork_Icon_VIR"]; + _viewInMapButton = [[ARCircularActionButton alloc] initWithImageName:@"MapButtonAction"]; + + [@[_viewInMapButton, _viewInRoomButton] each:^(UIButton *button) { + button.alpha = 0; + button.enabled = NO; + }]; + + self.shareButton.accessibilityIdentifier = @"Share Artwork"; + self.favoriteButton.accessibilityIdentifier = @"Favorite Artwork"; + self.viewInRoomButton.accessibilityIdentifier = @"View Artwork in Room"; + self.viewInMapButton.accessibilityIdentifier = @"Show the map"; + + [self addSubview:self.shareButton]; + [self addSubview:self.favoriteButton]; + + ARThemeLayoutVendor *layout = [ARTheme themeNamed:@"Artwork"].layout; + + if ([UIDevice isPad]) { + [self.favoriteButton alignCenterXWithView:self predicate:@"-32"]; + [self.shareButton alignCenterXWithView:self predicate:@"32"]; + } else { + // right aligned with VIR + + [self addSubview:self.viewInRoomButton]; + + [self.shareButton alignTrailingEdgeWithView:self predicate:@"0"]; + [self.favoriteButton alignAttribute:NSLayoutAttributeTrailing toAttribute:NSLayoutAttributeLeading ofView:_shareButton predicate:layout[@"ButtonMargin"]]; + [self.viewInRoomButton alignAttribute:NSLayoutAttributeTrailing toAttribute:NSLayoutAttributeLeading ofView:_favoriteButton predicate:layout[@"ButtonMargin"]]; + + [self addSubview:self.viewInMapButton]; + + self.mapButtonConstraint = [[_viewInMapButton alignAttribute:NSLayoutAttributeTrailing toAttribute:NSLayoutAttributeLeading ofView:_viewInRoomButton predicate:layout[@"ButtonMargin"]] firstObject]; + } + + + for (UIView *view in @[self.shareButton, self.favoriteButton, self.viewInRoomButton, self.viewInMapButton]) { + [view alignTopEdgeWithView:self predicate:@"0"]; + } + + [self.shareButton addTarget:nil action:@selector(tappedArtworkShare:) forControlEvents:UIControlEventTouchUpInside]; + [self.favoriteButton addTarget:nil action:@selector(tappedArtworkFavorite:) forControlEvents:UIControlEventTouchUpInside]; + [self.viewInRoomButton addTarget:nil action:@selector(tappedArtworkViewInRoom:) forControlEvents:UIControlEventTouchUpInside]; + [self.viewInMapButton addTarget:nil action:@selector(tappedArtworkViewInMap:) forControlEvents:UIControlEventTouchUpInside]; + + @weakify(self); + [artwork getFavoriteStatus:^(ARHeartStatus status) { + @strongify(self); + [self.favoriteButton setStatus:status animated:YES]; + } failure:^(NSError *error) { + @strongify(self); + [self.favoriteButton setStatus:ARHeartStatusNo animated:YES]; + }]; + + [artwork onArtworkUpdate:^{ + @strongify(self); + [self updateWithArtwork:artwork andFair:fair]; + } failure:nil]; + + return self; +} + +- (void)updateWithArtwork:(Artwork *)artwork andFair:(Fair *)fair +{ + if (artwork.canViewInRoom) { + [UIView animateWithDuration:ARAnimationQuickDuration animations:^{ + self.viewInRoomButton.enabled = YES; + self.viewInRoomButton.alpha = 1; + }]; + } + + if (fair) { + self.mapButtonConstraint.constant = artwork.canViewInRoom ? -8 : 48; + + [ArtsyAPI getShowsForArtworkID:artwork.artworkID inFairID:fair.fairID success:^(NSArray *shows) { + [UIView animateWithDuration:ARAnimationQuickDuration animations:^{ + self.viewInMapButton.enabled = YES; + self.viewInMapButton.alpha = 1; + }]; + } failure:nil]; + } +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize) { UIViewNoIntrinsicMetric, 48 }; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkPreviewImageView.h b/Artsy/Classes/View Controllers/ARArtworkPreviewImageView.h new file mode 100644 index 00000000000..9ca2726d6d9 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkPreviewImageView.h @@ -0,0 +1,14 @@ +/// Handles presenting an artwork and dealing with some of the simpler interactions + +extern const CGFloat ARiPadPreviewImageWidth; +extern const CGFloat AconstRiPhonePreviewImageWidth; + +@interface ARArtworkPreviewImageView : UIImageView + +/// The artwork for showing, triggers loading the image and invalidates layout +@property (nonatomic, strong) Artwork *artwork; + +/// Provide an idea of the size this view will want to set its intrinsic size from +@property (nonatomic, assign) CGSize outerBounds; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkPreviewImageView.m b/Artsy/Classes/View Controllers/ARArtworkPreviewImageView.m new file mode 100644 index 00000000000..a92e49629f1 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkPreviewImageView.m @@ -0,0 +1,137 @@ +#import "ARArtworkPreviewImageView.h" +#import "ARFeedImageLoader.h" + +@interface ARArtworkPreviewImageView() +@end + +@implementation ARArtworkPreviewImageView + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + [self setContentHuggingPriority:1000 forAxis:UILayoutConstraintAxisVertical]; + self.userInteractionEnabled = YES; + self.backgroundColor = [UIColor artsyLightGrey]; + + // This in practice can occasionally crop by a pixel or so, which is acceptable + self.contentMode = UIViewContentModeScaleAspectFit; + + // We cannot trust the aspect ratio from the server. If it's wrong it would overlap the buttons, thus we crop. + self.clipsToBounds = YES; + + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(zoomTap:)]; + [self addGestureRecognizer:tapGesture]; + + UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(zoomPinch:)]; + [self addGestureRecognizer:pinchGesture]; + + return self; +} + +-(void)setArtwork:(Artwork *)artwork +{ + _artwork = artwork; + [self updateWithArtwork:artwork]; + @weakify(self); + [artwork onArtworkUpdate:^{ + @strongify(self); + [self updateWithArtwork:artwork]; + } failure:nil]; +} + +- (void)zoomTap:(UIGestureRecognizer *)gesture +{ + [self goToFullScreen]; +} + +- (void)zoomPinch:(UIPinchGestureRecognizer *)gesture +{ + if (gesture.state != UIGestureRecognizerStateBegan) { + return; + } + + self.userInteractionEnabled = NO; + [self goToFullScreen]; +} + +- (void)goToFullScreen +{ + if ([self.artwork.defaultImage needsTiles]) { + + // Let the ArtworkVC decide what to do, pass via responder chain + [[UIApplication sharedApplication] sendAction:@selector(tappedTileableImagePreview:) to:nil from:self forEvent:nil]; + + } else { + // Do a small bounce when tapping an artwork which doesn't go full screen + + CGAffineTransform original = self.transform; + [UIView animateKeyframesWithDuration:.25 delay:0 options:0 animations:^{ + [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:.49 animations:^{ + self.transform = CGAffineTransformScale(original, original.a * 1.05, original.d * 1.05); + }]; + [UIView addKeyframeWithRelativeStartTime:.5 relativeDuration:.5 animations:^{ + self.transform = original; + }]; + } completion:nil]; + } +} + +- (void)updateWithArtwork:(Artwork *)artwork +{ + NSURL *detailURL = artwork.defaultImage.urlForDetailImage; + UIImage *placeholderImage = [self placeholderImageForArtwork:artwork]; + [self ar_setImageWithURL:detailURL placeholderImage:placeholderImage]; +} + +- (UIImage *)placeholderImageForArtwork:(Artwork *)artwork +{ + NSURL *imageURL = [NSURL URLWithString:artwork.defaultImage.baseImageURL]; + return [ARFeedImageLoader bestAvailableCachedImageForBaseURL:imageURL]; +} + +- (void)setImage:(UIImage *)image +{ + + if (image) { + self.backgroundColor = [UIColor whiteColor]; + [self setAspectRatioConstraintWithImage:image]; + [super setImage:image]; + } +} + +- (void)setAspectRatioConstraintWithImage:(UIImage *)image +{ + [self removeConstraints:self.constraints]; + CGFloat ratio = image.size.height/image.size.width; + NSLayoutConstraint *constraint1 = [NSLayoutConstraint + constraintWithItem:self + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeWidth + multiplier:ratio + constant:0]; + constraint1.priority = 750; + + NSLayoutConstraint *constraint2 = [NSLayoutConstraint + constraintWithItem:self + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationLessThanOrEqual + toItem:self + attribute:NSLayoutAttributeWidth + multiplier:ratio + constant:0]; + constraint2.priority = 1000; + + [self addConstraint:constraint1]; + [self addConstraint:constraint2]; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkSetViewController.h b/Artsy/Classes/View Controllers/ARArtworkSetViewController.h new file mode 100644 index 00000000000..d208df83a52 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkSetViewController.h @@ -0,0 +1,22 @@ +@class ARArtworkViewController; + +@interface ARArtworkSetViewController : UIPageViewController + +- (instancetype)initWithArtworkID:(NSString *)artworkID; +- (instancetype)initWithArtworkID:(NSString *)artworkID fair:(Fair *)fair; + +- (instancetype)initWithArtwork:(Artwork *)artwork; +- (instancetype)initWithArtwork:(Artwork *)artwork fair:(Fair *)fair; + +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet; +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet fair:(Fair *)fair; + +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet atIndex:(NSInteger)index; +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet fair:(Fair *)fair atIndex:(NSInteger)index; + +- (ARArtworkViewController *)currentArtworkViewController; + +@property (nonatomic, assign, readonly) NSInteger index; +@property (nonatomic, strong) Fair *fair; + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkSetViewController.m b/Artsy/Classes/View Controllers/ARArtworkSetViewController.m new file mode 100644 index 00000000000..2112d9d96ba --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkSetViewController.m @@ -0,0 +1,217 @@ +#import "ARArtworkSetViewController.h" +#import "ARViewInRoomViewController.h" +#import "ARArtworkViewController.h" + +@interface ARArtworkSetViewController () + +// Private Properties +@property (nonatomic, strong) NSArray *artworks; +@property (nonatomic, assign) NSInteger index; + +@end + +@implementation ARArtworkSetViewController + +- (instancetype)initWithArtworkID:(NSString *)artworkID +{ + return [self initWithArtworkID:artworkID fair:nil]; +} + +- (instancetype)initWithArtworkID:(NSString *)artworkID fair:(Fair *)fair +{ + Artwork *artwork = [[Artwork alloc] initWithArtworkID:artworkID]; + return [self initWithArtwork:artwork fair:fair]; +} + +- (instancetype)initWithArtwork:(Artwork *)artwork +{ + return [self initWithArtwork:artwork fair:nil]; +} + +- (instancetype)initWithArtwork:(Artwork *)artwork fair:(Fair *)fair +{ + return [self initWithArtworkSet:@[artwork] fair:fair]; +} + +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet +{ + return [self initWithArtworkSet:artworkSet fair:nil atIndex:0]; +} + +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet fair:(Fair *)fair +{ + return [self initWithArtworkSet:artworkSet fair:fair atIndex:0]; +} + +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet atIndex:(NSInteger)index +{ + return [self initWithArtworkSet:artworkSet fair:nil atIndex:index]; +} + +- (instancetype)initWithArtworkSet:(NSArray *)artworkSet fair:(Fair *)fair atIndex:(NSInteger)index +{ + self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; + + if (!self) { return nil; } + + self.fair = fair; + self.delegate = self; + self.dataSource = self; + self.automaticallyAdjustsScrollViewInsets = NO; + + self.artworks = artworkSet; + self.index = [self isValidArtworkIndex:index] ? index : 0; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil]; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + ARArtworkViewController *artworkVC = [self viewControllerForIndex:self.index]; + [self setViewControllers:@[artworkVC] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; +} + +- (BOOL)isValidArtworkIndex:(NSInteger)index +{ + if (index < 0 || index >= self.artworks.count) { + return NO; + } + return YES; +} + +- (ARArtworkViewController *)viewControllerForIndex:(NSInteger)index +{ + if (![self isValidArtworkIndex:index]) return nil; + + ARArtworkViewController *artworkViewController = [[ARArtworkViewController alloc] initWithArtwork:self.artworks[index] fair:self.fair]; + artworkViewController.index = index; + + return artworkViewController; +} + +#pragma mark - +#pragma mark Page view controller data source + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(ARArtworkViewController *)viewController +{ + if (self.artworks.count == 1) { + return nil; + } + + NSInteger newIndex = viewController.index - 1; + if (newIndex < 0) { + newIndex = self.artworks.count - 1; + } + return [self viewControllerForIndex:newIndex]; +} + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(ARArtworkViewController *)viewController +{ + if (self.artworks.count == 1) { + return nil; + } + + NSInteger newIndex = (viewController.index + 1) % self.artworks.count; + return [self viewControllerForIndex:newIndex]; +} + +#pragma mark - +#pragma mark Page view controller delegate + + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + if (self.navigationController) { + UIGestureRecognizer *gesture = self.navigationController.interactivePopGestureRecognizer; + [self.pagingScrollView.panGestureRecognizer requireGestureRecognizerToFail:gesture]; + } + + [self.currentArtworkViewController setHasFinishedScrolling]; +} + +- (void)orientationChanged:(NSNotification *)notification +{ + UIDevice *device = [notification object]; + UIDeviceOrientation orientation = [device orientation]; + Artwork *artwork = self.currentArtworkViewController.artwork; + + BOOL isTopViewController = self.navigationController.topViewController == self; + BOOL isShowingModalViewController = [ARTopMenuViewController sharedController].presentedViewController != nil; + BOOL canShowInRoom = self.currentArtworkViewController.artwork.canViewInRoom; + + if (![UIDevice isPad] && canShowInRoom && !isShowingModalViewController && isTopViewController) { + + if (UIInterfaceOrientationIsLandscape(orientation)) { + ARViewInRoomViewController *viewInRoomVC = [[ARViewInRoomViewController alloc] initWithArtwork:artwork]; + viewInRoomVC.popOnRotation = YES; + viewInRoomVC.rotationDelegate = self; + + [self.navigationController pushViewController:viewInRoomVC animated:YES]; + } + } + + if (![UIDevice isPad]) { + self.view.bounds = [UIScreen mainScreen].bounds; + } +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; +} + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad] ? YES : self.currentArtworkViewController.artwork.canViewInRoom; +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation +{ + return UIInterfaceOrientationPortrait; +} + + +- (NSUInteger)pageViewControllerSupportedInterfaceOrientations:(UIPageViewController *)pageViewController +{ + return [self supportedInterfaceOrientations]; +} + +- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed; +{ + if (completed) { + [self.currentArtworkViewController setHasFinishedScrolling]; + } +} + +- (UIScrollView *)pagingScrollView +{ + return self.view.subviews.firstObject; +} + +- (ARArtworkViewController *)currentArtworkViewController +{ + return self.viewControllers.lastObject; +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.currentArtworkViewController.artwork) { + return @{ @"artwork" : self.currentArtworkViewController.artwork.artworkID, @"type" : @"artwork" }; + } + + return nil; +} + + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkViewController+ButtonActions.h b/Artsy/Classes/View Controllers/ARArtworkViewController+ButtonActions.h new file mode 100644 index 00000000000..95e68a06995 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkViewController+ButtonActions.h @@ -0,0 +1,5 @@ +#import "ARArtworkViewController.h" + +@interface ARArtworkViewController (ButtonActions) + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkViewController+ButtonActions.m b/Artsy/Classes/View Controllers/ARArtworkViewController+ButtonActions.m new file mode 100644 index 00000000000..9593ac6cae7 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkViewController+ButtonActions.m @@ -0,0 +1,191 @@ +#import "ARArtworkViewController+ButtonActions.h" +#import "ARZoomArtworkImageViewController.h" +#import "ARArtworkInfoViewController.h" +#import "ARAuctionArtworkResultsViewController.h" +#import "ARViewInRoomViewController.h" +#import "ARSharingController.h" +#import "ARArtworkPreviewImageView.h" +#import "ARFairShowViewController.h" +#import "ARHeartButton.h" +#import "ARFairViewController.h" +#import +#import "ARRouter.h" +#import "ARInternalMobileWebViewController.h" +#import "ARFairMapViewController.h" +#import "ARBidButton.h" + +@implementation ARArtworkViewController (ButtonActions) + +- (void)tappedTileableImagePreview:(ARArtworkPreviewImageView *)sender +{ + ARZoomArtworkImageViewController *zoomImgeVC = [[ARZoomArtworkImageViewController alloc] initWithImage:self.artwork.defaultImage]; + zoomImgeVC.suppressZoomViewCreation = (self.fair == nil); + [self.navigationController pushViewController:zoomImgeVC animated:YES]; +} + +- (void)tappedArtworkFavorite:(ARHeartButton *)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextFavoriteArtwork fromTarget:self selector:_cmd]; + return; + } + + BOOL hearted = !sender.hearted; + [sender setHearted:hearted animated:YES]; + + [self.artwork setFollowState:sender.isHearted success:^(id json) { + [NSNotificationCenter.defaultCenter postNotificationName:ARFairRefreshFavoritesNotification object:nil]; + } failure:^(NSError *error) { + [ARNetworkErrorManager presentActiveErrorModalWithError:error]; + [sender setHearted:!hearted animated:YES]; + }]; +} + +- (void)tappedArtworkShare:(id)sender +{ + if (self.artwork.defaultImage.downloadable) { + [ARSharingController shareObject:self.artwork withThumbnailImageURL:self.artwork.defaultImage.urlForThumbnailImage withImage:self.imageView.image]; + } else if (self.artwork.canShareImage) { + [ARSharingController shareObject:self.artwork withThumbnailImageURL:self.artwork.defaultImage.urlForThumbnailImage]; + } else { + [ARSharingController shareObject:self.artwork]; + } +} + + +- (void)tappedArtworkViewInRoom:(id)sender +{ + ARViewInRoomViewController *viewInRoomVC = [[ARViewInRoomViewController alloc] initWithArtwork:self.artwork]; + [self.navigationController pushViewController:viewInRoomVC animated:YES]; +} + +- (void)tappedArtworkViewInMap:(id)sender +{ + [ArtsyAPI getShowsForArtworkID:self.artwork.artworkID inFairID:self.fair.fairID success:^(NSArray *shows) { + if (shows.count > 0) { + ARFairMapViewController *viewController = [[ARSwitchBoard sharedInstance] loadMapInFair:self.fair title:self.artwork.partner.name selectedPartnerShows:shows]; + [self.navigationController pushViewController:viewController animated:YES]; + } + } failure:^(NSError *error) { + // ignore + }]; +} + +- (void)tappedBidButton:(ARBidButton *)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextAuctionBid fromTarget:self selector:_cmd]; + return; + } + [self.artwork onSaleArtworkUpdate:^(SaleArtwork *saleArtwork) { + [self bidCompelted:saleArtwork]; + } failure:^(NSError *error) { + ARErrorLog(@"Can't get sale to bid for artwork %@. Error: %@", self.artwork.artworkID, error.localizedDescription); + }]; +} + +- (void)bidCompelted:(SaleArtwork *)saleArtwork +{ + [ARAnalytics setUserProperty:@"has_started_bid" toValue:@"true"]; + + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadBidUIForArtwork:self.artwork.artworkID + inSale:saleArtwork.auction.saleID]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)tappedBuyButton:(ARButton *)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextAuctionBid fromTarget:self selector:_cmd]; + return; + } + + // create a new order + NSURLRequest *request = [ARRouter newPendingOrderWithArtworkID:self.artwork.artworkID]; + + @weakify(self); + AFJSONRequestOperation *op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request + success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) { + NSString *orderID = [JSON valueForKey:@"id"]; + NSString *resumeToken = [JSON valueForKey:@"token"]; + ARInfoLog(@"Created order %@", orderID); + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadOrderUIForID:orderID resumeToken:resumeToken]; + [self.navigationController pushViewController:controller animated:YES]; + } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { + @strongify(self); + ARErrorLog(@"Creating a new order failed. Error: %@,\nJSON: %@", error.localizedDescription, JSON); + [self tappedContactGallery:sender]; + }]; + + [op start]; + +} + +- (void)tappedContactGallery:(ARButton *)sender +{ + ARInquireForArtworkViewController *inquireVC = [[ARInquireForArtworkViewController alloc] initWithPartnerInquiryForArtwork:self.artwork fair:self.fair]; + [inquireVC presentFormWithInquiryURLRepresentation:[self inquiryURLRepresentation]]; +} + +- (void)tappedContactRepresentative:(ARButton *)sender +{ + ARInquireForArtworkViewController *inquireVC = [[ARInquireForArtworkViewController alloc] initWithAdminInquiryForArtwork:self.artwork fair:self.fair]; + [inquireVC presentFormWithInquiryURLRepresentation:[self inquiryURLRepresentation]]; +} + +- (void)tappedOpenArtworkPartner:(id)sender +{ + Partner *partner = self.artwork.partner; + if (self.fair) { + [ArtsyAPI getShowsForArtworkID:self.artwork.artworkID inFairID:self.fair.fairID success:^(NSArray *shows) { + if (shows.count > 0) { + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadShow:shows.firstObject fair:self.fair]; + [self.navigationController pushViewController:viewController animated:YES]; + } + } failure:^(NSError *error) { + // ignore + }]; + } else if (partner.defaultProfilePublic) { + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPartnerWithID:partner.profileID]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + } + } else if(partner.website.length) { + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadURL:[NSURL URLWithString:partner.website]]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + } + } +} + +- (void)tappedOpenFair:(id)sender +{ + Fair *fair = self.fair? : self.artwork.fair; + UIViewController *viewController = [ARSwitchBoard.sharedInstance routeProfileWithID:fair.organizer.profileID]; + [self.navigationController pushViewController:viewController animated:YES]; +} +- (void)tappedOpenArtworkArtist:(id)sender +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadArtistWithID:self.artwork.artist.artistID inFair:self.fair]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)tappedAuctionResults:(id)sender +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadAuctionResultsForArtwork:self.artwork]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)tappedMoreInfo:(id)sender +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadMoreInfoForArtwork:self.artwork]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)tappedAuctionInfo:(id)sender +{ + ARInternalMobileWebViewController *viewController = [[ARInternalMobileWebViewController alloc] initWithURL:[NSURL URLWithString:@"/auction-info"]]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkViewController.h b/Artsy/Classes/View Controllers/ARArtworkViewController.h new file mode 100644 index 00000000000..c4a783bf7c6 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkViewController.h @@ -0,0 +1,29 @@ +#import "ARInquireForArtworkViewController.h" + +@interface ARArtworkViewController : UIViewController + +/// Designated initializer +- (instancetype)initWithArtworkID:(NSString *)artworkID fair:(Fair *)fair; +- (instancetype)initWithArtwork:(Artwork *)artwork fair:(Fair *)fair; + +/// The artwork this VC represents +@property (nonatomic, strong, readonly) Artwork *artwork; +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +/// The index in the current set of artworks +@property (nonatomic, assign) NSInteger index; + +/// The imageview for the artwork preview, used in transitions +- (UIImageView *)imageView; + +/// The current offset that should be applied to the imageview +- (CGPoint)imageViewOffset; + +// Triggers actions based on when scrolling has settled +- (void)setHasFinishedScrolling; + +- (NSString *)inquiryURLRepresentation; + + +@end diff --git a/Artsy/Classes/View Controllers/ARArtworkViewController.m b/Artsy/Classes/View Controllers/ARArtworkViewController.m new file mode 100644 index 00000000000..75c382590dd --- /dev/null +++ b/Artsy/Classes/View Controllers/ARArtworkViewController.m @@ -0,0 +1,229 @@ +#import "ARArtworkViewController.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARArtworkRelatedArtworksView.h" +#import "ARArtworkBlurbView.h" +#import "ARArtworkMetadataView.h" +#import "ARPostsViewController.h" +#import "ARArtworkView.h" +#import "ARArtworkViewController+ButtonActions.h" + +@interface ARArtworkViewController() + +@property (nonatomic, strong) ARArtworkView *view; +@property (nonatomic, strong, readonly) ARPostsViewController *postsVC; +@end + +@implementation ARArtworkViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithArtworkID:(NSString *)artworkID fair:(Fair *)fair +{ + Artwork *artwork = [[Artwork alloc] initWithArtworkID:artworkID]; + return [self initWithArtwork:artwork fair:fair]; +} + +- (instancetype)initWithArtwork:(Artwork *)artwork fair:(Fair *)fair +{ + self = [self init]; + if (!self) { return nil; } + + _artwork = artwork; + _fair = fair; + + return self; +} + +- (void)loadView +{ + self.view = [[ARArtworkView alloc] initWithArtwork:self.artwork fair:self.fair andParentViewController:self]; + self.view.delegate = self; + self.view.metadataView.delegate = self; + self.view.artworkBlurbView.delegate = self; + self.view.relatedArtworksView.parentViewController = self; + + // Adding the posts view separately because we must add its View Controller to self. + _postsVC = [[ARPostsViewController alloc] init]; + self.postsVC.view.tag = ARArtworkRelatedPosts; + self.postsVC.delegate = self; + self.postsVC.view.alpha = 0; + [self.view.stackView addViewController:self.postsVC toParent:self withTopMargin:nil sideMargin:@"20"]; +} + +- (void)viewDidLoad +{ + if (self.artwork.title == nil){ + [self ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + } + + @weakify(self); + + void (^completion)(void) = ^{ + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + }; + + [self.artwork onArtworkUpdate:^{ + completion(); + } failure:^(NSError *error) { + completion(); + }]; + + [super viewDidLoad]; +} + +- (UIImageView *)imageView +{ + return self.view.metadataView.imageView; +} + +- (void)viewDidAppear:(BOOL)animated +{ + // When we get back from zoom / VIR allow the preview to do trigger zoom + self.view.metadataView.userInteractionEnabled = YES; + [super viewDidAppear:self.shouldAnimate && animated]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [self.view.relatedArtworksView cancel]; + self.view.scrollsToTop = NO; + + [super viewDidDisappear:self.shouldAnimate && animated]; +} + +- (void)setHasFinishedScrolling +{ + // Get the full artwork details once scroll is settled. + // This is only called on the primary artwork view + self.view.scrollsToTop = YES; + + [self.artwork updateArtwork]; + [self.artwork updateSaleArtwork]; + [self.artwork updateFair]; + [self.view.relatedArtworksView updateWithArtwork:self.artwork]; + if (!self.postsVC.posts.count){ + [self getRelatedPosts]; + } +} + +- (void)getRelatedPosts +{ + @weakify(self); + [self.artwork getRelatedPosts:^(NSArray *posts) { + @strongify(self); + [self updateWithRelatedPosts:posts]; + }]; +} + +- (void)updateWithRelatedPosts:(NSArray *)posts +{ + if (posts.count > 0) { + self.postsVC.posts = posts; + [UIView animateIf:self.shouldAnimate duration:ARAnimationDuration :^{ + self.postsVC.view.alpha = 1; + }]; + } else { + [self.view.stackView removeSubview:self.postsVC.view]; + _postsVC = nil; + } +} + +- (NSString *)inquiryURLRepresentation +{ + return [NSString stringWithFormat:@"http://artsy.net/artwork/%@", self.artwork.artworkID]; +} + +#pragma mark - ScrollView delegate methods + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + [[ARScrollNavigationChief chief] scrollViewDidScroll:scrollView]; +} + +#pragma mark - VIR / Zoom Transition + +- (CGPoint)imageViewOffset +{ + return (CGPoint){ 0, [self.view contentOffset].y }; +} + +#pragma mark - Tapping on buttons +#pragma mark Moved to ARArtworkViewController+ButtonActions.m + +- (BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +#pragma mark rotation + +- (NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskPortrait; +} + +#pragma mark - ARArtworkBlurViewDelegate + +-(void)artworkBlurView:(ARArtworkBlurbView *)blurbView shouldPresentViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +#pragma mark - ARArtworkMetadataViewDelegate + +-(void)artworkMetadataView:(ARArtworkMetadataView *)metadataView shouldPresentViewController:(UIViewController *)viewController { + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)artworkMetadataView:(ARArtworkMetadataView *)metadataView didUpdateArtworkDetailView:(id)detailView +{ + [self.view.stackView setNeedsLayout]; + [self.view.stackView layoutIfNeeded]; +} + +- (void)artworkMetadataView:(ARArtworkMetadataView *)metadataView didUpdateArtworkActionsView:(ARArtworkActionsView *)actionsView +{ + [metadataView layoutIfNeeded]; + + [UIView animateTwoStepIf:self.shouldAnimate + duration:ARAnimationDuration * 2 :^{ + [self.view.stackView layoutIfNeeded]; + } midway:^{ + actionsView.alpha = 1; + } completion:nil]; +} + +#pragma mark - ARPostsViewControllerDelegate + +-(void)postViewController:(ARPostsViewController *)postViewController shouldShowViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +#pragma mark - ARArtworkRelatedArtworksViewParentViewController + +- (void)relatedArtworksView:(ARArtworkRelatedArtworksView *)view shouldShowViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)didUpdateRelatedArtworksView:(ARArtworkRelatedArtworksView *)relatedArtworksView +{ + [UIView animateTwoStepIf:self.shouldAnimate + duration:ARAnimationDuration * 2 :^{ + [self.view.stackView setNeedsLayout]; + [self.view.stackView layoutIfNeeded]; + } midway:^{ + relatedArtworksView.alpha = 1; + [self.view flashScrollIndicators]; + } completion:nil]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARAuctionArtworkResultsViewController.h b/Artsy/Classes/View Controllers/ARAuctionArtworkResultsViewController.h new file mode 100644 index 00000000000..de880511eaa --- /dev/null +++ b/Artsy/Classes/View Controllers/ARAuctionArtworkResultsViewController.h @@ -0,0 +1,10 @@ +/// Presents the related auction results for an artwork +@interface ARAuctionArtworkResultsViewController : UITableViewController + +/// Designated initializer +- (instancetype)initWithArtwork:(Artwork *)artwork; + +/// Artwork that the auction results controller represents +@property (nonatomic, strong, readonly) Artwork *artwork; + +@end diff --git a/Artsy/Classes/View Controllers/ARAuctionArtworkResultsViewController.m b/Artsy/Classes/View Controllers/ARAuctionArtworkResultsViewController.m new file mode 100644 index 00000000000..7179d967b16 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARAuctionArtworkResultsViewController.m @@ -0,0 +1,141 @@ +#import "ARAuctionArtworkResultsViewController.h" +#import "ARAuctionArtworkTableViewCell.h" +#import "ARPageSubtitleView.h" +#import "ARFeedStatusIndicatorTableViewCell.h" + +static NSString *ARAuctionTableViewCellIdentifier = @"ARAuctionTableViewCellIdentifier"; +static NSString *ARAuctionTableViewHeaderIdentifier = @"ARAuctionTableViewHeaderIdentifier"; + +static const NSInteger ARArtworkIndex = 0; + +@interface ARAuctionArtworkResultsViewController () +@property (nonatomic, copy) NSArray *auctionResults; +@end + +@implementation ARAuctionArtworkResultsViewController + +- (instancetype)initWithArtwork:(Artwork *)artwork +{ + self = [super initWithStyle:UITableViewStyleGrouped]; + if (!self) { return nil; } + + _artwork = artwork; + + @weakify(self); + [_artwork getRelatedAuctionResults:^(NSArray *auctionResults) { + @strongify(self); + self.auctionResults = auctionResults; + }]; + + [self.tableView registerClass:[ARAuctionArtworkTableViewCell class] forCellReuseIdentifier:ARAuctionTableViewCellIdentifier]; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.tableView.allowsSelection = NO; + self.tableView.backgroundColor = [UIColor whiteColor]; + self.tableView.tableHeaderView = [self createWarningView]; + + return self; +} + +- (UIView *)createWarningView +{ + CGFloat bottomMargin = 12; + UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 88 + bottomMargin)]; + UILabel *warning = [[ARSerifLabel alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 88)]; + warning.textAlignment = NSTextAlignmentCenter; + warning.backgroundColor = [UIColor artsyAttention]; + warning.text = @"Note: Auction results are an \nexperimental feature with\n limited data."; + [container addSubview:warning]; + [warning alignToView:container]; + + return container; +} + +- (void)setAuctionResults:(NSArray *)auctionResults +{ + _auctionResults = auctionResults; + [self.tableView reloadData]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 2; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + NSString *title = (section == ARArtworkIndex)? @"COMPARABLE AUCTION RESULTS FOR" : @"MOST SIMILAR RESULTS"; + return [[ARPageSubTitleView alloc] initWithTitle:title]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return 40; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section +{ + return 0; +} + +- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == ARArtworkIndex) { + CGFloat width = CGRectGetWidth(self.view.bounds); + return [ARAuctionArtworkTableViewCell heightWithArtwork:self.artwork withWidth:width]; + } + + if (self.auctionResults.count) { + return [ARAuctionArtworkTableViewCell estimatedHeightWithAuctionLot:self.auctionResults[indexPath.row]]; + } else { + return [ARFeedStatusIndicatorTableViewCell heightForFeedItemWithState:ARFeedStatusStateLoading]; + } +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat width = CGRectGetWidth(self.view.bounds); + if (indexPath.section == ARArtworkIndex) { + return [ARAuctionArtworkTableViewCell heightWithArtwork:self.artwork withWidth:width]; + } + + if (self.auctionResults.count) { + return [ARAuctionArtworkTableViewCell heightWithAuctionLot:self.auctionResults[indexPath.row] withWidth:width]; + } else { + return [ARFeedStatusIndicatorTableViewCell heightForFeedItemWithState:ARFeedStatusStateLoading]; + } +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == ARArtworkIndex) { + return 1; + } + + return MAX(self.auctionResults.count, 1); +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ARAuctionArtworkTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:ARAuctionTableViewCellIdentifier]; + + if (indexPath.section == ARArtworkIndex) { + [cell updateWithArtwork:self.artwork]; + } else { + if (self.auctionResults.count) { + [cell updateWithAuctionResult:self.auctionResults[indexPath.row]]; + + } else { + return [ARFeedStatusIndicatorTableViewCell cellWithInitialState:ARFeedStatusStateLoading]; + } + } + + return cell; +} + + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARBrowseViewController.h b/Artsy/Classes/View Controllers/ARBrowseViewController.h new file mode 100644 index 00000000000..2ca89f742bc --- /dev/null +++ b/Artsy/Classes/View Controllers/ARBrowseViewController.h @@ -0,0 +1,5 @@ +#import + +@interface ARBrowseViewController : UIViewController + +@end diff --git a/Artsy/Classes/View Controllers/ARBrowseViewController.m b/Artsy/Classes/View Controllers/ARBrowseViewController.m new file mode 100644 index 00000000000..4afb0855ae1 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARBrowseViewController.m @@ -0,0 +1,119 @@ +#import "ARBrowseViewController.h" +#import "ARBrowseFeaturedLinksCollectionView.h" +#import "UIViewController+FullScreenLoading.h" +#import "ORStackView+ArtsyViews.h" + +@interface ARBrowseViewController () + +@property (nonatomic, strong) ORStackScrollView *view; +@property (nonatomic, strong) NSMutableArray *collectionViews; +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@end + +@implementation ARBrowseViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +#pragma mark - UIViewController + +- (void)loadView +{ + self.view = [[ORStackScrollView alloc] init]; + self.view.stackView.bottomMarginHeight = 20; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.delegate = [ARScrollNavigationChief chief]; +} + +- (void)viewDidLoad +{ + [self.view.stackView addPageTitleWithString:@"Featured Categories"]; + self.collectionViews = [NSMutableArray array]; + + ARBrowseFeaturedLinksCollectionView *featureCollectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSingleRow]; + [self.view.stackView addSubview:featureCollectionView withTopMargin:@"30" sideMargin:@"0"]; + [self.collectionViews addObject: featureCollectionView]; + featureCollectionView.selectionDelegate = self; + + [ArtsyAPI getFeaturedLinksForGenesWithSuccess:^(NSArray *genes) { + featureCollectionView.featuredLinks = genes; + [self ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + + } failure:^(NSError *error) { + [self ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + }]; + + [ArtsyAPI getFeaturedLinkCategoriesForGenesWithSuccess:^(NSArray *orderedSets) { + for (OrderedSet *orderedSet in orderedSets) { + [self createCollectionViewWithOrderedSet:orderedSet]; + } + } failure:^(NSError *error) { + NSLog(@"error"); + }]; + + [self ar_presentIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + [super viewDidLoad]; +} + +- (void)createCollectionViewWithOrderedSet:(OrderedSet *)orderedSet +{ + [self.view.stackView addPageSubtitleWithString:orderedSet.name]; + + ARBrowseFeaturedLinksCollectionView *categoryCollectionView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutDoubleRow]; + categoryCollectionView.selectionDelegate = self; + [self.view.stackView addSubview:categoryCollectionView withTopMargin:@"20" sideMargin:@"0"]; + [self.collectionViews addObject:categoryCollectionView]; + + [orderedSet getItems:^(NSArray *items) { + categoryCollectionView.featuredLinks = items; + }]; +} + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +- (void)invalidateCollectionViews +{ + for (ARBrowseFeaturedLinksCollectionView *collectionView in self.collectionViews) { + [collectionView.collectionViewLayout invalidateLayout]; + [collectionView invalidateIntrinsicContentSize]; + } +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + [self invalidateCollectionViews]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + self.view.delegate = [ARScrollNavigationChief chief]; + [self invalidateCollectionViews]; + [super viewWillAppear:animated && self.shouldAnimate]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + self.view.delegate = nil; + + [super viewWillDisappear:animated && self.shouldAnimate]; +} + +#pragma mark - ARBrowseFeaturedLinksCollectionViewDelegate + +- (void)didSelectFeaturedLink:(FeaturedLink *)featuredLink +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:featuredLink.href]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARCollectorStatusViewController.h b/Artsy/Classes/View Controllers/ARCollectorStatusViewController.h new file mode 100644 index 00000000000..25d7970a28e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARCollectorStatusViewController.h @@ -0,0 +1,9 @@ +@class AROnboardingViewController; + +@interface ARCollectorStatusViewController : UIViewController + +@property (nonatomic, weak) AROnboardingViewController *delegate; + ++ (NSString *)stringFromCollectorLevel:(enum ARCollectorLevel)level; + +@end diff --git a/Artsy/Classes/View Controllers/ARCollectorStatusViewController.m b/Artsy/Classes/View Controllers/ARCollectorStatusViewController.m new file mode 100644 index 00000000000..d847c533dd4 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARCollectorStatusViewController.m @@ -0,0 +1,112 @@ +#import "ARCollectorStatusViewController.h" +#import "AROnboardingTableViewCell.h" +#import "AROnboardingViewController.h" + +@interface ARCollectorStatusViewController() +@property (nonatomic) ARSerifLineHeightLabel *label; +@property (nonatomic) UITableView *tableView; +@end + +@implementation ARCollectorStatusViewController + + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.backgroundColor = [UIColor clearColor]; + + CGSize screenSize = self.view.bounds.size; + + self.label = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:6]; + self.label.backgroundColor = [UIColor clearColor]; + self.label.opaque = NO; + self.label.frame = CGRectMake(20, 30, 280, 120); + self.label.font = [UIFont serifFontWithSize:24]; + self.label.textColor = [UIColor whiteColor]; + self.label.numberOfLines = 0; + [self.view addSubview:self.label]; + self.label.text = @"To give you better\nrecommendations we would\nlike to know a few things\nabout you."; + + self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, screenSize.height - 250, screenSize.width, 230) + style:UITableViewStylePlain]; + [self.tableView registerClass:[AROnboardingTableViewCell class] forCellReuseIdentifier:@"StatusCell"]; + self.tableView.backgroundColor = [UIColor clearColor]; + self.tableView.dataSource = self; + self.tableView.delegate = self; + self.tableView.scrollEnabled = NO; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + [self.view addSubview:self.tableView]; +} + + +#pragma mark - +#pragma mark Table view + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StatusCell"]; + cell.textLabel.text = @[@"Yes, I buy art", @"Interested in starting", @"Just looking and learning"][indexPath.row]; + return cell; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return 3; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 44 + (5 * 2); +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + UILabel *header = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 280, 30)]; + header.textColor = [UIColor whiteColor]; + header.font = [UIFont serifFontWithSize:24]; + header.text = @"Do you buy art?"; + + UIView *wrapper = [[UIView alloc] init]; + [wrapper addSubview:header]; + + CGRect frame = header.frame; + frame.size.height += 20; + wrapper.frame = frame; + [wrapper addSubview:header]; + + CALayer *separator = [CALayer layer]; + separator.frame = CGRectMake(15, wrapper.bounds.size.height - .5, 290, .5); + separator.backgroundColor = [UIColor artsyHeavyGrey].CGColor; + [wrapper.layer addSublayer:separator]; + return wrapper; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return 50; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + ARCollectorLevel level = (ARCollectorLevel)(3 - indexPath.row); + [self.delegate collectorLevelDone:level]; +} + ++ (NSString *)stringFromCollectorLevel:(enum ARCollectorLevel)level +{ + switch (level) { + case ARCollectorLevelCollector: + return @"collector"; + case ARCollectorLevelInterested: + return @"interested"; + case ARCollectorLevelNo: + return @"no"; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARContentViewControllers.h b/Artsy/Classes/View Controllers/ARContentViewControllers.h new file mode 100644 index 00000000000..710e04a8139 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARContentViewControllers.h @@ -0,0 +1,14 @@ +#import "ARArtworkSetViewController.h" +#import "ARArtistViewController.h" +#import "ARShowFeedViewController.h" +#import "ARAdminSettingsViewController.h" +#import "ARUserSettingsViewController.h" +#import "ARInternalMobileWebViewController.h" +#import "ARExternalWebBrowserViewController.h" +#import "ARViewInRoomViewController.h" +#import "ARFavoritesViewController.h" +#import "ARBrowseViewController.h" +#import "ARGeneViewController.h" +#import "ARAuctionArtworkResultsViewController.h" +#import "ARArtworkInfoViewController.h" +#import "ARQuicksilverViewController.h" diff --git a/Artsy/Classes/View Controllers/ARCreateAccountViewController.h b/Artsy/Classes/View Controllers/ARCreateAccountViewController.h new file mode 100644 index 00000000000..2757af7a157 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARCreateAccountViewController.h @@ -0,0 +1,7 @@ +@class AROnboardingViewController; + +@interface ARCreateAccountViewController : UIViewController + +@property (nonatomic, weak) AROnboardingViewController *delegate; + +@end diff --git a/Artsy/Classes/View Controllers/ARCreateAccountViewController.m b/Artsy/Classes/View Controllers/ARCreateAccountViewController.m new file mode 100644 index 00000000000..ec4593e3e16 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARCreateAccountViewController.m @@ -0,0 +1,309 @@ +#import "ARCreateAccountViewController.h" +#import "AROnboardingViewController.h" +#import "AROnboardingNavBarView.h" +#import "ARTextFieldWithPlaceholder.h" +#import "ARSecureTextFieldWithPlaceholder.h" +#import "ARUserManager.h" +#import "ARSpinner.h" +#import +#import "ARAnalyticsConstants.h" +#import "UIView+HitTestExpansion.h" + +//sigh +#define EMAIL_TAG 111 +#define SOCIAL_TAG 222 + +@interface ARCreateAccountViewController () +@property (nonatomic) AROnboardingNavBarView *navbar; +@property (nonatomic) ARTextFieldWithPlaceholder *name, *email, *password; +@property (nonatomic) ARSpinner *loadingSpinner; +@property (nonatomic, strong) UIView *containerView; +@property (nonatomic, strong) NSLayoutConstraint *keyboardConstraint; + +@end + +@implementation ARCreateAccountViewController + +- (void)viewDidLoad +{ + self.navbar = [[AROnboardingNavBarView alloc] init]; + + [self.view addSubview:self.navbar]; + [self.navbar.title setText:@"Create Account"]; + [self.navbar.back addTarget:self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; + + [self.navbar.forward setTitle:@"JOIN" forState:UIControlStateNormal]; + [self.navbar.forward addTarget:self action:@selector(submit:) forControlEvents:UIControlEventTouchUpInside]; + + UITapGestureRecognizer *keyboardCancelTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard)]; + [self.view addGestureRecognizer:keyboardCancelTap]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillShow:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; + + self.name = [[ARTextFieldWithPlaceholder alloc] init]; + self.name.placeholder = @"Full Name"; + self.name.autocapitalizationType = UITextAutocapitalizationTypeWords; + self.name.autocorrectionType = UITextAutocorrectionTypeNo; + self.name.returnKeyType = UIReturnKeyNext; + self.name.keyboardAppearance = UIKeyboardAppearanceDark; + + self.email = [[ARTextFieldWithPlaceholder alloc] init]; + self.email.placeholder = @"Email"; + self.email.keyboardType = UIKeyboardTypeEmailAddress; + self.email.autocorrectionType = UITextAutocorrectionTypeNo; + self.email.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.email.returnKeyType = UIReturnKeyNext; + self.email.keyboardAppearance = UIKeyboardAppearanceDark; + + self.password = [[ARSecureTextFieldWithPlaceholder alloc] init]; + self.password.placeholder = @"Password"; + self.password.secureTextEntry = YES; + self.password.returnKeyType = UIReturnKeyJoin; + self.password.keyboardAppearance = UIKeyboardAppearanceDark; + + self.containerView = [[UIView alloc] init]; + [self.view addSubview:self.containerView]; + [self.containerView alignCenterXWithView:self.view predicate:nil]; + NSString *centerYOffset = [UIDevice isPad] ? @"0" : @"-30"; + [self.containerView alignCenterYWithView:self.view predicate: NSStringWithFormat(@"%@@750", centerYOffset)]; + self.keyboardConstraint = [[self.containerView alignBottomEdgeWithView:self.view predicate:@"<=0@1000"] lastObject]; + [self.containerView constrainWidth:@"280"]; + + [@[self.name, self.email, self.password] each:^(ARTextFieldWithPlaceholder *textField) { + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + textField.delegate = self; + [self.containerView addSubview:textField]; + [textField constrainWidth:@"280" height:@"30"]; + [textField alignLeading:@"0" trailing:@"0" toView:self.containerView]; + [textField ar_extendHitTestSizeByWidth:0 andHeight:10]; + }]; + [self.name alignTopEdgeWithView:self.containerView predicate:@"0"]; + [self.email constrainTopSpaceToView:self.name predicate:@"20"]; + [self.password constrainTopSpaceToView:self.email predicate:@"20"]; + [self.password alignBottomEdgeWithView:self.containerView predicate:@"0"]; + + ARSpinner *spinner = [[ARSpinner alloc] initWithFrame:CGRectMake(0, 0, 44, 44)]; + spinner.alpha = 0; + spinner.center = self.view.center; + spinner.spinnerColor = [UIColor whiteColor]; + self.loadingSpinner = spinner; + [self.view addSubview:spinner]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextFieldTextDidChangeNotification object:nil]; + + [super viewDidLoad]; +} + +- (void)keyboardWillShow:(NSNotification *)notification +{ + CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + // In iOS 7 in Landscape orientation, the keyboard's length and width values are as though the orientation was Portrait. + // This is fixed in iOS 8, but we must account for both possibilities. We will therefore assume the actual height to be the smaller of the two dimensions. + // See http://stackoverflow.com/questions/24314222/change-in-metrics-for-the-new-ios-simulator-in-xcode-6 + CGFloat height = MIN(keyboardSize.width, keyboardSize.height); + + self.keyboardConstraint.constant = -height - ([UIDevice isPad] ? 20 : 10); + [UIView animateIf:YES duration:duration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)keyboardWillHide:(NSNotification *)notification +{ + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + self.keyboardConstraint.constant = 0; + [UIView animateIf:YES duration:duration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [self.name becomeFirstResponder]; + [super viewDidAppear:animated]; +} + +- (void)hideKeyboard +{ + [self.view endEditing:YES]; +} + +- (BOOL)canSubmit +{ + return self.email.text.length + && self.name.text.length + && [self.email.text containsString:@"@"] + && self.password.text.length >= 6; +} + + +- (void)back:(id)sender +{ + [self.delegate popViewControllerAnimated:YES]; +} + +- (void)setFormEnabled:(BOOL)enabled +{ + [@[self.name, self.email, self.password] each:^(ARTextFieldWithPlaceholder *textField) { + textField.enabled = enabled; + textField.alpha = enabled ? 1 : 0.3; + }]; + + [self.navbar.forward setEnabled:enabled animated:YES];; + + if (enabled) { + [self.loadingSpinner fadeOutAnimated:YES]; + } else { + [self.loadingSpinner fadeInAnimated:YES]; + } +} + +- (NSString *)existingAccountSource:(NSDictionary *)JSON +{ + NSArray *providers = JSON[@"providers"]; + if (providers) { + return [providers.first lowercaseString]; + } + NSString *message = JSON[@"text"]; + if (!message) { + return @"email"; + } + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"A user with this email has already signed up with ([A-Za-z]+)." options:0 error:nil]; + NSTextCheckingResult *match = [regex firstMatchInString:message options:0 range:NSMakeRange(0, message.length)]; + if ([match numberOfRanges] != 2) { + return @"email"; + } + NSString *provider = [message substringWithRange:[match rangeAtIndex:1]]; + return [provider lowercaseString]; +} + +- (void)submit:(id)sender +{ + [self setFormEnabled:NO]; + + NSString *username = self.email.text; + NSString *password = self.password.text; + + @weakify(self); + [[ARUserManager sharedManager] createUserWithName:self.name.text email:username password:password success:^(User *user) { + @strongify(self); + [self loginWithUserCredentials]; + } failure:^(NSError *error, id JSON) { + @strongify(self); + if (JSON + && [JSON isKindOfClass:[NSDictionary class]] + && ([JSON[@"error"] isEqualToString:@"User Already Exists"] + || [JSON[@"error"] isEqualToString:@"User Already Invited"])) { + NSString *source = [self existingAccountSource:JSON]; + [self accountExists:source]; + [ARAnalytics event:ARAnalyticsUserAlreadyExistedAtSignUp]; + + } else { + [self setFormEnabled:YES]; + + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn’t create your account" message:@"Please check your email address & password" delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil]; + [alert show]; + + [self.email becomeFirstResponder]; + } + }]; +} + +- (void)loginWithUserCredentials +{ + NSString *username = self.email.text; + NSString *password = self.password.text; + + @weakify(self); + [[ARUserManager sharedManager] loginWithUsername:username password:password successWithCredentials:nil + gotUser:^(User *currentUser) { + @strongify(self); + [self.delegate signupDone]; + [ARAnalytics event:ARAnalyticsUserSignedIn withProperties:@{ @"context" : ARAnalyticsUserContextEmail }]; + + } authenticationFailure:^(NSError *error) { + @strongify(self); + [self setFormEnabled:YES]; + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn’t Log In" message:@"Please check your email and password." delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil]; + [alert show]; + + } networkFailure:^(NSError *error) { + @strongify(self); + [self setFormEnabled:YES]; + [self performSelector:_cmd withObject:self afterDelay:3]; + [ARNetworkErrorManager presentActiveErrorModalWithError:error]; + }]; +} + +- (void)accountExists:(NSString *)source +{ + NSString *message; + NSInteger tag; + if ([source isEqualToString:@"email"]) { + message= [NSString stringWithFormat:@"An account already exists for the email address \"%@\".", self.email.text]; + tag = EMAIL_TAG; + } else { + message= [NSString stringWithFormat:@"An account already exists for the email address \"%@\". Please log in via %@.", + self.email.text, + [source capitalizedString]]; + tag = SOCIAL_TAG; + } + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Account Already Exists" message:message delegate:self cancelButtonTitle:@"Log In" otherButtonTitles:nil]; + alert.tag = tag; + [alert show]; +} + + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UITextFieldTextDidChangeNotification + object:nil]; +} + +#pragma mark - +#pragma mark UITextField + +- (void)textChanged:(NSNotification *)n +{ + [self.navbar.forward setEnabled:[self canSubmit] animated:YES]; +} + +#pragma mark - delegate +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + if (textField == self.name) { + [self.email becomeFirstResponder]; + return YES; + } else if (textField == self.email) { + [self.password becomeFirstResponder]; + return YES; + } else if ([self canSubmit]) { + [self submit:nil]; + return YES; + } + return NO; +} + +#pragma mark - UIAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + NSString *email = nil; + if (alertView.tag == EMAIL_TAG) { + email = self.email.text; + } + [self.delegate logInWithEmail:email]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARCrossfadingImageView.h b/Artsy/Classes/View Controllers/ARCrossfadingImageView.h new file mode 100644 index 00000000000..f00169a4ea1 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARCrossfadingImageView.h @@ -0,0 +1,18 @@ +@interface ARCrossfadingImageView : UIImageView + +@property (nonatomic, readwrite) BOOL shouldLoopImages; + +@property (nonatomic) NSInteger currentIndex; + +/// Setting images resets currentIndex to 0 +@property (nonatomic, copy) NSArray *images; + +/// Interpolate between image[currentIndex] and +/// image[currentIndex + 1] with 0.f <= t <= 1.f +- (void)up:(CGFloat)t; + +/// Interpolate between image[currentIndex] and +/// image[currentIndex - 1] with 0.f <= t <= 1.f +- (void)down:(CGFloat)t; + +@end diff --git a/Artsy/Classes/View Controllers/ARCrossfadingImageView.m b/Artsy/Classes/View Controllers/ARCrossfadingImageView.m new file mode 100644 index 00000000000..5a74d4d371e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARCrossfadingImageView.m @@ -0,0 +1,127 @@ +#import "ARCrossfadingImageView.h" + +typedef NS_ENUM(NSInteger, ARDirection) { + ARUp = 1, + ARDown = -1 +}; + +@interface ARCrossfadingImageView() +@property (nonatomic) UIImageView *topView; +@property (nonatomic) ARDirection dir; +@end + +@implementation ARCrossfadingImageView + +- (instancetype)init +{ + self = [super init]; + if (self) { + _topView = [[UIImageView alloc] initWithFrame:self.bounds]; + _currentIndex = NSNotFound; + self.contentMode = UIViewContentModeScaleAspectFill; + [self insertSubview:_topView atIndex:0]; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + _topView.frame = self.bounds; +} + +- (void)setBounds:(CGRect)bounds +{ + [super setBounds:bounds]; + _topView.frame = bounds; +} + +- (void)setImages:(NSArray *)images +{ + _images = images; + self.currentIndex = 0; + self.dir = ARUp; + +} + +- (void)setDir:(ARDirection)dir +{ + if (_dir == dir) { + return; + } + _dir = dir; + [self setTop]; +} + +- (void)setTop +{ + switch (self.dir){ + case ARUp: + if (self.currentIndex == self.images.count - 1) { + if (self.shouldLoopImages) { + self.topView.image = [self.images firstObject]; + } else { + self.topView.image = nil; + } + } else { + self.topView.image = self.images[self.currentIndex + 1]; + } + break; + case ARDown: + if (self.currentIndex == 0) { + if (self.shouldLoopImages) { + self.topView.image = [self.images lastObject]; + } else { + self.topView.image = nil; + } + } else { + self.topView.image = self.images[self.currentIndex - 1]; + } + } +} + +- (void)setCurrentIndex:(NSInteger)currentIndex +{ + NSInteger imageCount = self.images.count; + if (currentIndex == self.currentIndex || imageCount == 0) { + return; + } + if (currentIndex < 0 || currentIndex >= imageCount) { + ARErrorLog(@"Index %@ out of bounds in crossfading image view %@ with %@ images", + @(currentIndex), self, @(imageCount)); + return; + } + _currentIndex = currentIndex; + + self.image = self.images[_currentIndex]; + self.topView.alpha = 0; + [self setTop]; +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + [super setContentMode:contentMode]; + [_topView setContentMode:contentMode]; +} + +- (void)up:(CGFloat)t +{ + if (!self.shouldLoopImages && self.currentIndex == self.images.count - 1) { + return; + } + self.dir = ARUp; + t = fmaxf(fminf(1.f, t), 0.f); + self.topView.alpha = t; +} + +- (void)down:(CGFloat)t +{ + if (!self.shouldLoopImages && self.currentIndex == 0) { + return; + } + self.dir = ARDown; + t = fmaxf(fminf(1.f, t), 0.f); + self.topView.alpha = t; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARDemoSplashViewController.h b/Artsy/Classes/View Controllers/ARDemoSplashViewController.h new file mode 100644 index 00000000000..faa3a6c89e5 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARDemoSplashViewController.h @@ -0,0 +1,3 @@ +@interface ARDemoSplashViewController : UIViewController + +@end diff --git a/Artsy/Classes/View Controllers/ARDemoSplashViewController.m b/Artsy/Classes/View Controllers/ARDemoSplashViewController.m new file mode 100644 index 00000000000..e6803f60b44 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARDemoSplashViewController.m @@ -0,0 +1,20 @@ +#import "ARDemoSplashViewController.h" + + +@interface ARDemoSplashViewController () +@property (weak, nonatomic) IBOutlet UILabel *infoLabel; +@property (weak, nonatomic) IBOutlet UIImageView *backgroundImage; +@end + +@implementation ARDemoSplashViewController + +- (void)viewDidLoad +{ + self.backgroundImage.image = [UIImage imageNamed:@"Default"]; + self.backgroundImage.center = self.view.center; + self.infoLabel.font = [UIFont serifFontWithSize:16]; + + [super viewDidLoad]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARDemoSplashViewController.xib b/Artsy/Classes/View Controllers/ARDemoSplashViewController.xib new file mode 100644 index 00000000000..361c5de4094 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARDemoSplashViewController.xib @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Artsy/Classes/View Controllers/AREmbeddedModelsViewController.h b/Artsy/Classes/View Controllers/AREmbeddedModelsViewController.h new file mode 100644 index 00000000000..d785c653aad --- /dev/null +++ b/Artsy/Classes/View Controllers/AREmbeddedModelsViewController.h @@ -0,0 +1,62 @@ +#import "ARModelCollectionViewModule.h" +#import "ARArtworkFlowModule.h" +#import "ARArtworkMasonryModule.h" + +@class AREmbeddedModelsViewController; + +@protocol AREmbeddedModelsDelegate + +-(void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController; + +/// Allows the host view controller to act on an item tap, will +/// default to ARSwitchboard if selector not valid +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index; + +@optional + +/// This message gets passed if the edge is reached. Currently +/// unimplemented, may be moved to a block property. +- (void)embeddedModelsViewControllerDidScrollPastEdge:(AREmbeddedModelsViewController *)controller; + +@end + + +/// The AREmbedded Models View Controller is a layout agnostic way to +/// present thumbnailable items like artworks with optional metadata. + +@interface AREmbeddedModelsViewController : UIViewController + +/// An optional delegate for actions +@property (nonatomic, weak) id delegate; + +/// The items shown by the embedded models VC +@property (nonatomic, copy, readonly) NSArray *items; + +/// Appends items and inserts the collectionview items +- (void)appendItems:(NSArray *)items; + +/// The module that controls the UICollectionViewLayout +@property (nonatomic, strong) ARModelCollectionViewModule *activeModule; + +/// To provide extra customization to the collection view +@property (nonatomic, strong, readonly) UICollectionView *collectionView; + +/// Update the height constraint when new items are added, defaults OFF +/// useful when dealing with vertical layouts. +@property (nonatomic, assign) BOOL constrainHeightAutomatically; + +/// Header View for when the view controller is basically another VCs view. +@property (nonatomic, strong) UIView *headerView; +@property (nonatomic, assign) CGFloat headerHeight; + +/// Shows a progress indicator, only works in masonry +@property (nonatomic, assign) BOOL showTrailingLoadingIndicator; + +/// Recieves UIScrollViewDelegate methods +@property (nonatomic, strong) id scrollDelegate; + +@property (nonatomic, strong, readonly) Fair *fair; + +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +@end diff --git a/Artsy/Classes/View Controllers/AREmbeddedModelsViewController.m b/Artsy/Classes/View Controllers/AREmbeddedModelsViewController.m new file mode 100644 index 00000000000..b505c7c061d --- /dev/null +++ b/Artsy/Classes/View Controllers/AREmbeddedModelsViewController.m @@ -0,0 +1,287 @@ +#import "AREmbeddedModelsViewController.h" +#import "ARItemThumbnailViewCell.h" +#import "ARReusableLoadingView.h" + +@interface AREmbeddedModelsViewController() + +@property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, strong) NSLayoutConstraint *heightConstraint; + +// Private Accessors +@property (nonatomic, strong, readwrite) Fair *fair; + +@end + +@implementation AREmbeddedModelsViewController + +- (void)viewDidLoad +{ + self.collectionView = [self createCollectionView]; + self.collectionView.backgroundColor = [UIColor whiteColor]; + self.collectionView.dataSource = self; + self.collectionView.delegate = self; + self.collectionView.scrollsToTop = NO; + self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + self.collectionView.showsHorizontalScrollIndicator = NO; + self.collectionView.showsVerticalScrollIndicator = NO; + + [self.collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:UICollectionElementKindSectionHeader]; + [self.collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:UICollectionElementKindSectionFooter]; + [self.view addSubview:self.collectionView]; + + [super viewDidLoad]; +} + +- (void)viewDidLayoutSubviews +{ + self.collectionView.frame = self.view.bounds; +} + +- (UICollectionView *)createCollectionView +{ + // Because the collection view is lazily created at view will appear + // there we can't guarantee that the activemodule is set already. + + UICollectionView *collectionView = nil; + if (!self.activeModule) { + self.activeModule = [ARArtworkFlowModule flowModuleWithLayout:ARArtworkFlowLayoutSingleRow + andStyle:AREmbeddedArtworkPresentationStyleArtworkOnly]; + } + + collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.activeModule.moduleLayout]; + return collectionView; +} + +- (void)setCollectionView:(UICollectionView *)collectionView +{ + _collectionView = collectionView; + if (self.activeModule) { + [self.collectionView registerClass:self.activeModule.classForCell + forCellWithReuseIdentifier:NSStringFromClass(self.activeModule.classForCell)]; + self.collectionView.collectionViewLayout = self.activeModule.moduleLayout; + } + +} + +- (void)setActiveModule:(ARModelCollectionViewModule *)activeModule +{ + if (self.collectionView && activeModule) { + // Must be done in this order, otherwise inexplicable crashes happen inside Apple code. + self.collectionView.collectionViewLayout = activeModule.moduleLayout; + [self.collectionView registerClass:activeModule.classForCell + forCellWithReuseIdentifier:NSStringFromClass(activeModule.classForCell)]; + _activeModule = activeModule; + [self.collectionView reloadData]; + [self.collectionView layoutIfNeeded]; + } else { + _activeModule = activeModule; + } +} + +#pragma mark - Sizing + +- (CGSize)preferredContentSize +{ + return [self.activeModule intrinsicSize]; +} + +#pragma mark - Reloading + +- (NSArray *)items +{ + return [self.activeModule items]; +} + +- (void)appendItems:(NSArray *)items +{ + if (!self && !self.collectionView) { + return; + } + + self.activeModule.items = [self.activeModule.items arrayByAddingObjectsFromArray:items]; + [self.collectionView reloadData]; + + // This can crash when you have hundreds of works, but is the right way to do it + // TODO: Figure this out + +// NSInteger artworkCount = [self collectionView:self.collectionView numberOfItemsInSection:0]; +// +// NSInteger start = artworkCount - artworks.count - 1; +// NSMutableArray *indexPaths = [NSMutableArray array]; +// +// for (NSInteger i = 0; i < artworks.count; i++) { +// NSIndexPath *path = [NSIndexPath indexPathForItem:start + i inSection:0]; +// [indexPaths addObject:path]; +// } +// +// if (self.collectionView.scrollEnabled) { +// [CATransaction begin]; +// [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; +// } +// +// [self.collectionView insertItemsAtIndexPaths:indexPaths]; +// +// if (self.collectionView.scrollEnabled) { +// [CATransaction commit]; +// } + + [self updateViewConstraints]; + [self.view.superview setNeedsUpdateConstraints]; +} + +- (void)setConstrainHeightAutomatically:(BOOL)constrainHeightAutomatically +{ + _constrainHeightAutomatically = constrainHeightAutomatically; + + if (constrainHeightAutomatically) { + self.heightConstraint = [[self.view constrainHeight:@"260"] lastObject]; + self.collectionView.scrollEnabled = NO; + } else { + [self.view removeConstraint:self.heightConstraint]; + self.collectionView.scrollEnabled = YES; + [self.view setNeedsUpdateConstraints]; + } +} + +- (void)updateViewConstraints +{ + [super updateViewConstraints]; + + if (self.heightConstraint) { + if (self.collectionView.contentSize.height != 0) { + self.heightConstraint.constant = self.collectionView.contentSize.height; + } else { + self.heightConstraint.constant = self.activeModule.intrinsicSize.height; + } + } +} + +- (void)setHeaderHeight:(CGFloat)headerHeight +{ + _headerHeight = headerHeight; + [self.collectionView.collectionViewLayout invalidateLayout]; +} + +#pragma mark - UIScrollViewDelegate methods + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + if (self.scrollDelegate && [self.scrollDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { + [self.scrollDelegate scrollViewDidScroll:scrollView]; + } + + if(self.delegate && (scrollView.contentSize.height - scrollView.contentOffset.y) < scrollView.bounds.size.height) { + if([self.delegate respondsToSelector:@selector(embeddedModelsViewControllerDidScrollPastEdge:)]) { + [self.delegate embeddedModelsViewControllerDidScrollPastEdge:self]; + } + } + + if(_delegate && (scrollView.contentSize.width - scrollView.contentOffset.x) < scrollView.bounds.size.width) { + if([self.delegate respondsToSelector:@selector(embeddedModelsViewControllerDidScrollPastEdge:)]) { + [self.delegate embeddedModelsViewControllerDidScrollPastEdge:self]; + } + } +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + SEL scrollViewWillEndDraggingSelector = @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:); + if (self.activeModule && [self.activeModule respondsToSelector:scrollViewWillEndDraggingSelector]) { + [self.activeModule scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + } + + if (self.scrollDelegate && [self.scrollDelegate respondsToSelector:scrollViewWillEndDraggingSelector]) { + [self.scrollDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView +{ + if ([self.scrollDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) { + [self.scrollDelegate scrollViewDidEndDecelerating:scrollView]; + } +} + +#pragma mark - UICollectionViewDelegate methods + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate embeddedModelsViewController:self didTapItemAtIndex:indexPath.row]; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return self.items.count; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + id cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass(self.activeModule.classForCell) forIndexPath:indexPath]; + id item = self.items[indexPath.row]; + + if ([cell respondsToSelector:@selector(setImageSize:)]) { + [cell setImageSize:self.activeModule.imageSize]; + } + + if ([cell respondsToSelector:@selector(setupWithRepresentedObject:)]) { + [cell setupWithRepresentedObject:item]; + + } else { + ARErrorLog(@"Could not set up cell %@", cell); + } + + return cell; +} + +#pragma mark ARCollectionViewDelegateFlowLayout + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kind forIndexPath:indexPath]; + if (view.subviews.count == 0) { + [view addSubview:self.headerView]; + [self.headerView alignTop:@"0" leading:@"0" bottom:nil trailing:@"0" toView:view]; + } + return view; + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { + UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kind forIndexPath:indexPath]; + if (view.subviews.count == 0) { + ARReusableLoadingView *loadingView = [[ARReusableLoadingView alloc] init]; + [view addSubview:loadingView]; + [loadingView startIndeterminateAnimated:self.shouldAnimate]; + [loadingView alignTop:@"0" leading:@"0" bottom:nil trailing:@"0" toView:view]; + } + return view; + } else { + return nil; + } +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section +{ + return self.headerView ? CGSizeMake(CGRectGetWidth(self.collectionView.bounds), self.headerHeight) : CGSizeZero; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section +{ + return self.showTrailingLoadingIndicator ? CGSizeMake(CGRectGetWidth(self.collectionView.bounds), 44) : CGSizeZero; +} + +#pragma mark ARCollectionViewMasonryLayoutDelegate Methods + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(ARCollectionViewMasonryLayout *)collectionViewLayout variableDimensionForItemAtIndexPath:(NSIndexPath *)indexPath +{ + if ([self.activeModule conformsToProtocol:@protocol(ARCollectionViewMasonryLayoutDelegate)]){ + return [(id)self.activeModule collectionView:collectionView layout:collectionViewLayout variableDimensionForItemAtIndexPath:indexPath]; + } else { + return 0; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARExternalWebBrowserViewController.h b/Artsy/Classes/View Controllers/ARExternalWebBrowserViewController.h new file mode 100644 index 00000000000..9ebafa7817e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARExternalWebBrowserViewController.h @@ -0,0 +1,7 @@ +#import "TSMiniWebBrowser.h" + +@interface ARExternalWebBrowserViewController : TSMiniWebBrowser + +@property (readonly, nonatomic, strong) UIScrollView *scrollView; + +@end diff --git a/Artsy/Classes/View Controllers/ARExternalWebBrowserViewController.m b/Artsy/Classes/View Controllers/ARExternalWebBrowserViewController.m new file mode 100644 index 00000000000..4fbebf601ad --- /dev/null +++ b/Artsy/Classes/View Controllers/ARExternalWebBrowserViewController.m @@ -0,0 +1,95 @@ +#import "ARExternalWebBrowserViewController.h" +#import + +@interface TSMiniWebBrowser (Private) +@property(nonatomic, readonly, strong) UIWebView *webView; +@end + +@interface ARExternalWebBrowserViewController() +@property(nonatomic, readonly, strong) UIGestureRecognizer *gesture; +@end + +@implementation ARExternalWebBrowserViewController + +- (instancetype)initWithURL:(NSURL *)url +{ + self = [super initWithURL:url]; + if (!self) { return nil; } + + self.showNavigationBar = NO; + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + self.webView.frame = self.view.bounds; + self.webView.scrollView.delegate = [ARScrollNavigationChief chief]; + self.webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal; + + [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + if ([self.navigationController isKindOfClass:[ARNavigationController class]]) { + UIGestureRecognizer *gesture = self.navigationController.interactivePopGestureRecognizer; + + [self.webView.scrollView.panGestureRecognizer requireGestureRecognizerToFail:gesture]; + _gesture = gesture; + } +} + +- (void)viewWillDisappear:(BOOL)animated +{ + self.gesture.delegate = nil; + [super viewWillDisappear:animated]; + [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; +} + +#pragma mark - Properties + +- (UIScrollView *)scrollView +{ + return self.webView.scrollView; +} + +#pragma mark UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + return YES; +} + +#pragma mark UIWebViewDelegate + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + if (navigationType == UIWebViewNavigationTypeLinkClicked) { + if ([JLRoutes canRouteURL:request.URL]) { + [JLRoutes routeURL:request.URL]; + return NO; + } + } + + return YES; +} + +- (BOOL)shouldAutorotate +{ + return NO; +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.currentURL) { + return @{ @"url" : self.currentURL.absoluteString, @"type" : @"url" }; + } + + return nil; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairArtistViewController.h b/Artsy/Classes/View Controllers/ARFairArtistViewController.h new file mode 100644 index 00000000000..09b922ba461 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairArtistViewController.h @@ -0,0 +1,12 @@ +@protocol ARFairAwareObject; + +@interface ARFairArtistViewController : UIViewController + +- (id)initWithArtistID:(NSString *)artistID fair:(Fair *)fair; + +@property (readonly, nonatomic, strong) Artist *artist; +@property (readonly, nonatomic, strong) Fair *fair; + +- (BOOL)isFollowingArtist; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairArtistViewController.m b/Artsy/Classes/View Controllers/ARFairArtistViewController.m new file mode 100644 index 00000000000..b6333ab02f3 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairArtistViewController.m @@ -0,0 +1,245 @@ +#import "ARFairArtistViewController.h" +#import "ORStackView+ArtsyViews.h" +#import "ARNavigationButton.h" +#import "ARFairShowViewController.h" +#import "AREmbeddedModelsViewController.h" +#import "ARFollowableNetworkModel.h" +#import "ARFollowableButton.h" +#import "ARFairMapViewController.h" +#import "ARFairShowMapper.h" +#import "ARFairMapPreview.h" +#import "ARArtworkSetViewController.h" + +NS_ENUM(NSInteger, ARFairArtistViewIndex){ + ARFairArtistTitle = 1, + ARFairArtistHeader, + ARFairArtistMapPreview, + ARFairArtistFollow, + ARFairArtistShows, + ARFairArtistOnArtsy = ARFairArtistShows + 3 * 42, // we don't expect more than 42 shows + ARFairArtistWhitespaceGobbler +}; + +@interface ARFairArtistViewController () +@property (nonatomic, strong, readonly) ORStackScrollView *view; +@property (nonatomic, strong, readonly) ARFollowableNetworkModel *followableNetwork; +@property (nonatomic, strong, readonly) NSArray *partnerShows; +@property (nonatomic, strong, readwrite) Fair *fair; +@property (nonatomic, strong, readonly) NSString *header; +@property (nonatomic, strong, readonly) UIView *headerView; +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; +@end + +@implementation ARFairArtistViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithArtistID:(NSString *)artistID fair:(Fair *)fair +{ + self = [self init]; + _fair = fair; + _artist = [[Artist alloc] initWithArtistID:artistID]; + return self; +} + +- (void)loadView +{ + self.view = [[ORStackScrollView alloc] initWithStackViewClass:[ORTagBasedAutoStackView class]]; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.stackView.bottomMarginHeight = 20; + self.view.delegate = [ARScrollNavigationChief chief]; + + @weakify(self); + [ArtsyAPI getArtistForArtistID:self.artist.artistID success:^(Artist *artist) { + @strongify(self); + if (!self) { return; } + self->_artist = artist; + [self artistDidLoad]; + } failure:^(NSError *error) { + @strongify(self); + [self artistDidLoad]; + }]; +} + +- (BOOL)shouldAutorotate +{ + return NO; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; +} + +- (void)artistDidLoad +{ + _header = NSStringWithFormat(@"%@ at %@", self.artist.name, self.fair.name); + + [self.view.stackView addPageTitleWithString:self.header tag:ARFairArtistTitle]; + + UIView *headerView = [[UIView alloc] init]; + headerView.tag = ARFairArtistHeader; + [self.view.stackView addSubview:headerView withTopMargin:@"20" sideMargin:@"40"]; + [headerView constrainHeight:@"80"]; + _headerView = headerView; + + [self addTitle]; + + @weakify(self); + [ArtsyAPI getShowsForArtistID:self.artist.artistID inFairID:self.fair.fairID success:^(NSArray *shows) { + @strongify(self); + if (!self) { return; } + + self->_partnerShows = shows; + + [self addMapAndMapButton]; + [self addFollowButton]; + [self addArtistOnArtsyButton]; + + [shows eachWithIndex:^(PartnerShow *show, NSUInteger index) { + [self.view.stackView addGenericSeparatorWithSideMargin:@"40" tag:ARFairArtistShows + index * 3]; + [self addNavigationButtonForShowToStack:show tag:ARFairArtistShows + index * 3 + 1]; + [self addArtworksForShowToStack:show tag:ARFairArtistShows + index * 3 + 2]; + }]; + } failure:nil]; + + CGFloat parentHeight = CGRectGetHeight(self.parentViewController.view.bounds) ?: CGRectGetHeight([UIScreen mainScreen].bounds); + [self.view.stackView ensureScrollingWithHeight:parentHeight tag:ARFairArtistWhitespaceGobbler]; +} + +- (void)addArtistOnArtsyButton +{ + NSString *title = NSStringWithFormat(@"%@ on Artsy", self.artist.name); + ARNavigationButton *button = [[ARNavigationButton alloc] initWithTitle:title]; + button.tag = ARFairArtistOnArtsy; + button.onTap = ^(UIButton *tappedButton){ + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadArtistWithID:self.artist.artistID inFair:nil]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; + }; + [self.view.stackView addSubview:button withTopMargin:@"20" sideMargin:@"40"]; +} + +- (void)addArtworksForShowToStack:(PartnerShow *)show tag:(NSInteger)tag +{ + ARArtworkMasonryLayout layout = show.artworks.count > 1 ? ARArtworkMasonryLayout2Column : ARArtworkMasonryLayout1Column; + ARArtworkMasonryModule *module = [ARArtworkMasonryModule masonryModuleWithLayout:layout andStyle:AREmbeddedArtworkPresentationStyleArtworkMetadata]; + AREmbeddedModelsViewController *artworkController = [[AREmbeddedModelsViewController alloc] init]; + artworkController.delegate = self; + artworkController.activeModule = module; + [artworkController appendItems:show.artworks]; + artworkController.constrainHeightAutomatically = YES; + artworkController.view.tag = tag; + + [self.view.stackView addViewController:artworkController toParent:self withTopMargin:@"0" sideMargin:@"0"]; +} + +- (void)addNavigationButtonForShowToStack:(PartnerShow *)show tag:(NSInteger)tag +{ + ARNavigationButton *button = [[ARSerifNavigationButton alloc] initWithTitle:show.partner.name andSubtitle:show.locationInFair withBorder:0]; + button.tag = tag; + button.onTap = ^(UIButton *tappedButton){ + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadShow:show fair:self.fair]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; + }; + [self.view.stackView addSubview:button withTopMargin:@"0" sideMargin:@"40"]; +} + +- (void)addTitle +{ + if (self.artist.nationality.length && self.artist.years.length) { + UILabel *titleLabel = [[ARSerifLabel alloc] init]; + titleLabel.textColor = [UIColor blackColor]; + titleLabel.preferredMaxLayoutWidth = 220; + titleLabel.text = [NSString stringWithFormat:@"%@, %@", self.artist.nationality, self.artist.years]; + [self.headerView addSubview:titleLabel]; + [titleLabel alignCenterYWithView:self.headerView predicate:nil]; + [titleLabel alignLeadingEdgeWithView:self.headerView predicate:nil]; + } +} + +- (void)addMapAndMapButton +{ + @weakify(self); + [self.fair getFairMaps:^(NSArray *maps) { + @strongify(self); + + Map *map = maps.firstObject; + if (!map) { return; } + + ARCircularActionButton *mapButton = [[ARCircularActionButton alloc] initWithImageName:@"MapButtonAction"]; + [mapButton addTarget:self action:@selector(mapButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.headerView addSubview:mapButton]; + [mapButton alignCenterYWithView:self.headerView predicate:nil]; + [mapButton alignAttribute:NSLayoutAttributeTrailing toAttribute:NSLayoutAttributeTrailing ofView:self.headerView predicate:nil]; + + UIButton *mapViewContainer = [[UIButton alloc] init]; + mapViewContainer.tag = ARFairArtistMapPreview; + [mapViewContainer constrainHeight:@"150"]; + CGRect frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 150); + ARFairMapPreview *mapPreview = [[ARFairMapPreview alloc] initWithFairMap:map andFrame:frame]; + [mapViewContainer addSubview:mapPreview]; + [mapPreview alignToView:mapViewContainer]; + [mapPreview setZoomScale:mapPreview.minimumZoomScale animated:self.shouldAnimate]; + [mapPreview addShows:self.partnerShows animated:self.shouldAnimate]; + [mapViewContainer addTarget:self action:@selector(mapButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.view.stackView addSubview:mapViewContainer withTopMargin:@"0" sideMargin:@"20"]; + }]; +} + +- (void)mapButtonTapped:(id)mapButtonTapped +{ + @weakify(self); + [self.fair getFairMaps:^(NSArray * maps) { + @strongify(self); + ARFairMapViewController *viewController = [[ARSwitchBoard sharedInstance] loadMapInFair:self.fair title:self.header selectedPartnerShows:self.partnerShows]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; + }]; +} + +- (void)addFollowButton +{ + ARFollowableButton *followButton = [[ARFollowableButton alloc] init]; + followButton.tag = ARFairArtistFollow; + followButton.toFollowTitle = @"Follow Artist"; + followButton.toUnfollowTitle = @"Following Artist"; + [self.view.stackView addSubview:followButton withTopMargin:@"10" sideMargin:@"40"]; + [followButton addTarget:self action:@selector(toggleFollowArtist:) forControlEvents:UIControlEventTouchUpInside]; + + _followableNetwork = [[ARFollowableNetworkModel alloc] initWithFollowableObject:self.artist]; + [followButton setupKVOOnNetworkModel:self.followableNetwork]; +} + +- (void)toggleFollowArtist:(id)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextFavoriteArtist fromTarget:self selector:_cmd]; + return; + } + self.followableNetwork.following = !self.followableNetwork.following; +} + +#pragma mark - Public Methods + +- (BOOL)isFollowingArtist +{ + return self.followableNetwork.following; +} + +-(void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtwork:controller.items[index] inFair:self.fair]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairGuideContainerViewController.h b/Artsy/Classes/View Controllers/ARFairGuideContainerViewController.h new file mode 100644 index 00000000000..8406f3df3df --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairGuideContainerViewController.h @@ -0,0 +1,11 @@ +#import + +@interface ARFairGuideContainerViewController : UIViewController + +- (instancetype)initWithFair:(Fair *)fair __attribute((objc_designated_initializer)); +- (instancetype)init __attribute__((unavailable("Designated Initializer initWithFair: must be used."))); + +@property (nonatomic, strong) Fair *fair; +@property (nonatomic, assign) BOOL animatedTransitions; //Defaults to YES + +@end diff --git a/Artsy/Classes/View Controllers/ARFairGuideContainerViewController.m b/Artsy/Classes/View Controllers/ARFairGuideContainerViewController.m new file mode 100644 index 00000000000..499d90847c7 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairGuideContainerViewController.m @@ -0,0 +1,376 @@ +#import "ARFairGuideContainerViewController.h" +#import "ARFairGuideViewController.h" +#import "ARFairMapViewController.h" + +#import "UIViewController+SimpleChildren.h" +#import "UIViewController+FullScreenLoading.h" + +#import "UIView+HitTestExpansion.h" + +@interface ARFairGuideContainerViewController () + +@property (nonatomic, readonly) ARNavigationController *parentViewController; + +@property (nonatomic, strong) UIButton *backButton; + +@property (nonatomic, assign) BOOL mapsLoaded; +@property (nonatomic, assign) BOOL fairLoaded; + +@property (nonatomic, strong) ARFairGuideViewController *fairGuideViewController; +@property (nonatomic, strong) ARFairMapViewController *fairMapViewController; +@property (nonatomic, strong) UIView *fairGuideBackgroundView; + +@property (nonatomic, strong) UIView *clickInterceptorView; + +@property (nonatomic, assign) BOOL mapCollapsed; +@property (nonatomic, strong) NSMutableArray *subviewsConstraintsArray; +@property (nonatomic, weak) NSLayoutConstraint *fairGuideTopLayoutConstraint; +@property (nonatomic, weak) NSLayoutConstraint *fairBackgroundViewTopLayoutConstraint; +@property (readwrite, nonatomic, assign) CGFloat topHeight; + +@end + +@implementation ARFairGuideContainerViewController + +const CGFloat kClosedMapHeight = 180.0f; + +- (instancetype)initWithFair:(Fair *)fair +{ + self = [super init]; + if (!self) return nil; + + _fair = fair; + _mapCollapsed = YES; + _subviewsConstraintsArray = [NSMutableArray array]; + _animatedTransitions = YES; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view. + + self.view.backgroundColor = [UIColor whiteColor]; + + @weakify(self); + [[[self rac_signalForSelector:@selector(viewWillAppear:)] take:1] subscribeNext:^(id _) { + @strongify(self); + [self downloadContent]; + }]; + + [[RACSignal combineLatest:@[RACObserve(self, mapsLoaded), RACObserve(self, fairLoaded)]] subscribeNext:^(id x) { + @strongify(self); + [self checkForDataLoaded]; + }]; + + // Every time the mapCollapsed property changes, we want to setup the constraints. + // We skip the first time because we don't want to fire immediately. + [[[RACObserve(self, mapCollapsed) skip:1] distinctUntilChanged] subscribeNext:^(id _) { + @strongify(self); + + CGFloat height = 0; + if (self.mapCollapsed) { + height = kClosedMapHeight; + } + [self.fairMapViewController centerMap:0.5 inFrameOfHeight:height animated:YES]; + + BOOL parentButtonsVisible = (self.mapCollapsed == YES); + if (parentButtonsVisible == NO) { + // We want to swap the visibility of our back button and the parent one + // immediately if it's disappearing, but after the animation if it's appearing + [self updateBackButtonAlpha]; + } + + [UIView animateIf:self.animatedTransitions + duration:0.3f :^{ + self.fairMapViewController.titleHidden = self.mapCollapsed; + [self setupContraints]; + [self setBackButtonRotation]; + + } completion:^(BOOL finished) { + if (parentButtonsVisible) { + [self updateBackButtonAlpha]; + } + }]; + }]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + [[ARScrollNavigationChief chief] setAwareViewController:self]; +} + +- (BOOL)shouldAutorotate +{ + return NO; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return UIInterfaceOrientationMaskPortrait; +} + +#pragma mark - User Interaction + +- (void)back:(id)sender +{ + self.mapCollapsed = YES; +} + +#pragma mark - Private Methods + +- (void)downloadContent +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + + [self.fair updateFair:^{ + self.fairLoaded = YES; + }]; + + [self.fair getFairMaps:^(NSArray *maps) { + self.mapsLoaded = YES; + }]; +} + +- (void)updateBackButtonAlpha +{ + self.backButton.alpha = self.mapCollapsed ? 0.0f : 1.0f; + [self.parentViewController showBackButton:self.mapCollapsed animated:NO]; +} + +- (void)setBackButtonRotation +{ + if (self.mapCollapsed) { + self.backButton.transform = CGAffineTransformIdentity; + } else { + self.backButton.transform = CGAffineTransformMakeRotation(M_PI_2); + } +} + +- (void)checkForDataLoaded +{ + if (self.mapsLoaded && self.fairLoaded) { + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + + [self ar_addModernChildViewController:self.fairMapViewController]; + [self.view addSubview:self.clickInterceptorView]; + [self.view addSubview:self.fairGuideBackgroundView]; + [self ar_addModernChildViewController:self.fairGuideViewController]; + [self.fairMapViewController centerMap:0.5 inFrameOfHeight:kClosedMapHeight animated:NO]; + + [self setupContraints]; + + [self.view addSubview:self.backButton]; + + NSNumber *topNumber = @(12 + CGRectGetHeight([[UIApplication sharedApplication] statusBarFrame])); + NSString *top = topNumber.stringValue; + [self.backButton alignTop:top leading:@"12" toView:self.view]; + + [self setBackButtonRotation]; + [self updateBackButtonAlpha]; + self.fairMapViewController.titleHidden = self.mapCollapsed; + + [self.fairGuideViewController fairDidLoad]; + + [self updateParallaxConstraints]; + } +} + +- (void)setupContraints +{ + [self.view removeConstraints:self.subviewsConstraintsArray]; + [self.subviewsConstraintsArray removeAllObjects]; + + CGFloat mapTopMargin = self.topLayoutGuide.length; + + NSString *topLayoutGuide = @(self.topLayoutGuide.length).stringValue; + + if (self.mapCollapsed) { + if (self.hasMap) { + mapTopMargin += kClosedMapHeight; + } + + self.topHeight = mapTopMargin; + + NSString *mapBottomString = @(CGRectGetHeight(self.view.bounds) - mapTopMargin).stringValue; + NSString *mapTopString = @(mapTopMargin).stringValue; + + // @"-64" is statusbar + nav bar, to hide the map VC's title view + [self alignViewToSelf:self.fairMapViewController.view top:@"-64" leading:@"0" bottom:mapBottomString trailing:@"0"]; + [self alignViewToSelf:self.clickInterceptorView top:@"0" leading:@"0" bottom:mapBottomString trailing:@"0"]; + [self alignViewToSelf:self.fairGuideViewController.view top:nil leading:@"0" bottom:@"0" trailing:@"0"]; + [self alignViewToSelf:self.fairGuideBackgroundView top:nil leading:@"0" bottom:@"0" trailing:@"0"]; + + NSArray *constraints = [self.fairGuideViewController.view alignTopEdgeWithView:self.view predicate:mapTopString]; + [self.subviewsConstraintsArray addObjectsFromArray:constraints]; + self.fairGuideTopLayoutConstraint = constraints.firstObject; + + constraints = [self.fairGuideBackgroundView alignTopEdgeWithView:self.view predicate:mapTopString]; + [self.subviewsConstraintsArray addObjectsFromArray:constraints]; + self.fairBackgroundViewTopLayoutConstraint = constraints.firstObject; + + } else { + NSString *screenHeight = @(CGRectGetHeight(self.view.bounds)).stringValue; + [self alignViewToSelf:self.fairMapViewController.view top:topLayoutGuide leading:@"0" bottom:@"0" trailing:@"0"]; + [self alignViewToSelf:self.clickInterceptorView top:screenHeight leading:@"0" bottom:nil trailing:@"0"]; + [self alignViewToSelf:self.fairGuideViewController.view top:screenHeight leading:@"0" bottom:nil trailing:@"0"]; + [self alignViewToSelf:self.fairGuideBackgroundView top:screenHeight leading:@"0" bottom:nil trailing:@"0"]; + } + + [self.view layoutIfNeeded]; +} + +- (void)updateParallaxConstraints +{ + UIScrollView *scrollView = (UIScrollView *)self.fairGuideViewController.view; + + CGPoint contentOffset = scrollView.contentOffset; + CGFloat oldTopHeight = self.topHeight; + self.topHeight -= contentOffset.y; + + if (contentOffset.y < -86) { + self.mapCollapsed = NO; + return; + } + + dispatch_block_t updateConstraints = ^{ + self.fairGuideTopLayoutConstraint.constant = self.topHeight; + [self.view setNeedsLayout]; + }; + + // Collapsing + if (oldTopHeight > 0 && self.topHeight < kClosedMapHeight) { + CGFloat heightRatio = ((1.0 - self.topHeight/kClosedMapHeight) + 1.0) / 2.0; + [self.fairMapViewController centerMap:heightRatio inFrameOfHeight:kClosedMapHeight animated:NO]; + self.fairBackgroundViewTopLayoutConstraint.constant = self.topHeight; + updateConstraints(); + scrollView.contentOffset = contentOffset.y > 0 ? CGPointZero : contentOffset; + + // Expanding + } else if (contentOffset.y < 0) { + CGFloat heightRatio = ((contentOffset.y + kClosedMapHeight)/kClosedMapHeight) / 2.0; + [self.fairMapViewController centerMap:heightRatio inFrameOfHeight:kClosedMapHeight animated:NO]; + self.fairBackgroundViewTopLayoutConstraint.constant = fabsf(contentOffset.y) + kClosedMapHeight; + updateConstraints(); + } + } + +- (void)alignViewToSelf:(UIView *)view top:(NSString *)top leading:(NSString *)leading bottom:(NSString *)bottom trailing:(NSString *)trailing +{ + [self.subviewsConstraintsArray addObjectsFromArray:[view alignTop:top leading:leading bottom:bottom trailing:trailing toView:self.view]]; +} + +- (BOOL)hasMap +{ + return self.fair.maps.count != 0; +} + +#pragma mark - UIGestureRecognizer + +- (void)didReceiveTap:(UITapGestureRecognizer *)recognizer +{ + self.mapCollapsed = !self.mapCollapsed; +} + +#pragma mark - Overridden Properties + +- (UIView *)clickInterceptorView +{ + if (_clickInterceptorView == nil) { + _clickInterceptorView = [[UIView alloc] init]; + UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didReceiveTap:)]; + [_clickInterceptorView addGestureRecognizer:tapRecognizer]; + } + + return _clickInterceptorView; +} + +- (ARFairGuideViewController *)fairGuideViewController +{ + if (_fairGuideViewController == nil) { + _fairGuideViewController = [[ARFairGuideViewController alloc] initWithFair:self.fair]; + _fairGuideViewController.delegate = self; + _fairGuideViewController.showTopBorder = self.hasMap; + _fairGuideViewController.view.backgroundColor = [UIColor clearColor]; + } + + return _fairGuideViewController; +} + +- (ARFairMapViewController *)fairMapViewController +{ + if (_fairMapViewController == nil) { + NSString *title = [User currentUser].name ? NSStringWithFormat(@"%@'s Guide", [User currentUser].name) : @"Your Personal Guide"; + _fairMapViewController = [[ARFairMapViewController alloc] initWithFair:self.fair title:title selectedPartnerShows:nil]; + } + + return _fairMapViewController; +} + +- (UIView *)fairGuideBackgroundView +{ + if (_fairGuideBackgroundView == nil) { + _fairGuideBackgroundView = [[UIView alloc] init]; + _fairGuideBackgroundView.backgroundColor = [UIColor whiteColor]; + } + + return _fairGuideBackgroundView; +} + +- (UIButton *)backButton +{ + if (_backButton == nil) { + _backButton = [[ARMenuButton alloc] init]; + [_backButton ar_extendHitTestSizeByWidth:10 andHeight:10]; + [_backButton setImage:[UIImage imageNamed:@"BackArrow"] forState:UIControlStateNormal]; + [_backButton addTarget:self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; + _backButton.accessibilityIdentifier = @"Back"; + } + + return _backButton; +} + +- (void)setTopHeight:(CGFloat)topHeight +{ + _topHeight = round(MAX(0, MIN(kClosedMapHeight, topHeight))); +} + +#pragma mark - ARScrollNavigationChiefAwareViewController + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + if ([scrollView isDescendantOfView:self.view] == NO) { return; } + if (self.hasMap == NO) { return; } + + [self updateParallaxConstraints]; + + // We move the scroll view indicator as we do the parallax, so we trick a little. + CGFloat top = -self.topHeight * 1.5 + 20 + kClosedMapHeight/2; + scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(top, 0, 0, 0); +} + +#pragma mark - ARFairGuideViewControllerDelegate + +- (void)fairGuideViewControllerDidChangeTab:(ARFairGuideViewController *)controller +{ + if (controller.contentIsOverstretched) { + UIScrollView *scrollView = (ORStackScrollView *)controller.view; + scrollView.contentOffset = CGPointZero; + scrollView.clipsToBounds = NO; + [self setupContraints]; + [self.parentViewController showBackButton:YES animated:NO]; + } +} + +- (void)fairGuideViewControllerDidChangeUser:(ARFairGuideViewController *)controller +{ + self.fairLoaded = NO; + self.mapsLoaded = NO; + [self downloadContent]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairGuideViewController.h b/Artsy/Classes/View Controllers/ARFairGuideViewController.h new file mode 100644 index 00000000000..a3e470b159f --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairGuideViewController.h @@ -0,0 +1,23 @@ +@class ARFairGuideViewController; + +@protocol ARFairGuideViewControllerDelegate + +- (void)fairGuideViewControllerDidChangeTab:(ARFairGuideViewController *)controller; +- (void)fairGuideViewControllerDidChangeUser:(ARFairGuideViewController *)controller; + +@end + +@interface ARFairGuideViewController : UIViewController + +- (instancetype)initWithFair:(Fair *)fair __attribute((objc_designated_initializer)); +- (instancetype)init __attribute__((unavailable("Designated Initializer initWithFair: must be used."))); + +- (void)fairDidLoad; + +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, assign, readonly) BOOL contentIsOverstretched; +@property (nonatomic, assign, readwrite) BOOL showTopBorder; + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairGuideViewController.m b/Artsy/Classes/View Controllers/ARFairGuideViewController.m new file mode 100644 index 00000000000..ef1332f89b4 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairGuideViewController.m @@ -0,0 +1,306 @@ +#import "ARFairGuideViewController.h" +#import "ARNavigationButtonsViewController.h" +#import "ARSwitchView.h" +#import "ARSwitchView+FairGuide.h" +#import "ORStackView+ArtsyViews.h" +#import "ARFairFavoritesNetworkModel.h" + +// Switch view width should be divisible by the number of items (in this case 3) for consistent rendering. +static CGFloat const ARFairGuideSwitchviewWidth = 279; + +NS_ENUM(NSInteger, ARFairGuideViewOrder) { + ARFairGuideViewTitle, + ARFairGuideViewSubtitle, + ARFairGuideViewSignupForArtsyButton, + ARFairGuideViewTabs, + ARFairGuideViewNavigationSeparator, + ARFairGuideViewNavigationButtons, + ARFairGuideViewShowsToFollowTitle, + ARFairGuideViewShowsToFollowSeparator, + ARFairGuideViewShowsToFollow, + ARFairGuideViewAllExhibitors, + ARFairGuideViewWhitespace +}; + +typedef NS_ENUM(NSInteger, ARFairGuideSelectedTab) { + ARFairGuideSelectedTabUndefined = -1, + ARFairGuideSelectedTabWork = 0, + ARFairGuideSelectedTabExhibitors, + ARFairGuideSelectedTabArtists +}; + +@interface ARFairGuideViewController() + +@property (nonatomic, strong, readwrite) Fair *fair; + +@property (nonatomic, strong) ARNavigationButtonsViewController *currentViewController; +@property (nonatomic, strong) ARNavigationButtonsViewController *workViewController; +@property (nonatomic, strong) ARNavigationButtonsViewController *exhibitorsViewController; +@property (nonatomic, strong) ARNavigationButtonsViewController *artistsViewController; +@property (nonatomic, strong) ARFairFavoritesNetworkModel *fairFavorites; +@property (nonatomic, assign) ARFairGuideSelectedTab selectedTabIndex; + +@property (nonatomic, strong) ORStackScrollView *view; +@property (nonatomic, strong) User *currentUser; +@end + +@implementation ARFairGuideViewController + +#pragma mark - Lifecyce + +- (instancetype)initWithFair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + + self.fair = fair; + + // We'll set this later in viewDidLoad, triggering the overridden setter + _selectedTabIndex = ARFairGuideSelectedTabUndefined; + + return self; +} + +#pragma mark - UIViewController + +- (void)loadView +{ + self.view = [[ORStackScrollView alloc] initWithStackViewClass:ORTagBasedAutoStackView.class]; + self.view.stackView.bottomMarginHeight = 15; + self.view.showsVerticalScrollIndicator = ![User isTrialUser]; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.delegate = [ARScrollNavigationChief chief]; + self.view.alwaysBounceVertical = YES; +} + +-(BOOL)shouldAutorotate +{ + return NO; +} + +-(NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; +} + +#pragma mark - Overridden Properties + +- (void)setSelectedTabIndex:(ARFairGuideSelectedTab)selectedTabIndex +{ + if (selectedTabIndex != _selectedTabIndex) { + [self.currentViewController willMoveToParentViewController:nil]; + [self.currentViewController removeFromParentViewController]; + [self.view.stackView removeSubview:self.currentViewController.view]; + + switch (selectedTabIndex) { + case ARFairGuideSelectedTabUndefined: + self.currentViewController = nil; + break; + case ARFairGuideSelectedTabExhibitors: + self.currentViewController = self.exhibitorsViewController; + break; + case ARFairGuideSelectedTabArtists: + self.currentViewController = self.artistsViewController; + break; + case ARFairGuideSelectedTabWork: + self.currentViewController = self.workViewController; + break; + } + + [self.currentViewController willMoveToParentViewController:self]; + [self addChildViewController:self.currentViewController]; + [self.view.stackView addSubview:self.currentViewController.view withTopMargin:@"12" sideMargin:@"20"]; + [self.currentViewController didMoveToParentViewController:self]; + } + + _selectedTabIndex = selectedTabIndex; + + // Need to dispatch to the next invocation of the run loop so that the child VC + // has a chance to set its content + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate fairGuideViewControllerDidChangeTab:self]; + }); +} + +- (ARNavigationButtonsViewController *)workViewController +{ + if (!_workViewController) { + _workViewController = [[ARNavigationButtonsViewController alloc] init]; + _workViewController.view.tag = ARFairGuideViewNavigationButtons; + } + + return _workViewController; +} + +- (ARNavigationButtonsViewController *)exhibitorsViewController +{ + if (!_exhibitorsViewController) { + _exhibitorsViewController = [[ARNavigationButtonsViewController alloc] init]; + _exhibitorsViewController.view.tag = ARFairGuideViewNavigationButtons; + } + + return _exhibitorsViewController; +} + +- (ARNavigationButtonsViewController *)artistsViewController +{ + if (!_artistsViewController) { + _artistsViewController = [[ARNavigationButtonsViewController alloc] init]; + _artistsViewController.view.tag = ARFairGuideViewNavigationButtons; + } + + return _artistsViewController; +} + +- (ARFairFavoritesNetworkModel *)fairFavorites +{ + // Lazy-loading property + if (!_fairFavorites) { + _fairFavorites = [[ARFairFavoritesNetworkModel alloc] init]; + _fairFavorites.delegate = self; + } + + return _fairFavorites; +} + +- (BOOL)contentIsOverstretched +{ + return self.view.contentSize.height < CGRectGetHeight(self.view.frame); +} + +#pragma mark - Public Methods + +- (void)fairDidLoad +{ + for (UIView *childView in [self.view.stackView.subviews copy]) { + [self.view.stackView removeSubview:childView]; + } + + if (self.showTopBorder) { + [self addTopBorder]; + } + + if (self.hasCurrentUser) { + [self addPersonalLabel]; + [self addTabView]; + [self addUserContent]; + } else { + [self addTrialLabel]; + } + + self.selectedTabIndex = ARFairGuideSelectedTabWork; + + CGFloat parentHeight = CGRectGetHeight(self.parentViewController.view.bounds) ?: CGRectGetHeight([UIScreen mainScreen].bounds); + [self.view.stackView ensureScrollingWithHeight:parentHeight tag:ARFairGuideViewWhitespace]; +} + +#pragma mark - Private Methods + +- (void)addTopBorder +{ + UIView *view = [[UIView alloc] initWithFrame:CGRectZero]; + view.backgroundColor = [UIColor artsyMediumGrey]; + [view constrainHeight:@"2"]; + [self.view.stackView addSubview:view withTopMargin:@"0" sideMargin:@"0"]; +} + +- (void)addPersonalLabel +{ + UILabel *titleLabel = [ARThemedFactory labelForSerifHeaders]; + titleLabel.text = self.currentUser.name ? NSStringWithFormat(@"%@'s Guide", self.currentUser.name) : @"Your Personal Guide"; + titleLabel.userInteractionEnabled = YES; + titleLabel.tag = ARFairGuideViewTitle; + [self.view.stackView addSubview:titleLabel withTopMargin:@"12" sideMargin:@"80"]; +} + +- (void)addTrialLabel +{ + UILabel *titleLabel = [ARThemedFactory labelForSerifHeaders]; + titleLabel.text = @"Discover Your Personal Guide"; + titleLabel.userInteractionEnabled = YES; + titleLabel.tag = ARFairGuideViewTitle; + [self.view.stackView addSubview:titleLabel withTopMargin:@"18" sideMargin:@"80"]; + [titleLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(signupForArtsy:)]]; + + UILabel *subtitleLabel = [ARThemedFactory labelForSerifSubHeaders]; + subtitleLabel.text = @"To view recommendations\nsign up for Artsy"; + subtitleLabel.tag = ARFairGuideViewSubtitle; + subtitleLabel.userInteractionEnabled = YES; + [subtitleLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(signupForArtsy:)]]; + [self.view.stackView addSubview:subtitleLabel withTopMargin:@"44" sideMargin:@"40"]; + + ARBlackFlatButton *signupForArtsy = [[ARBlackFlatButton alloc] init]; + signupForArtsy.tag = ARFairGuideViewSignupForArtsyButton; + [signupForArtsy setTitle:@"Sign up for Artsy" forState:UIControlStateNormal]; + [signupForArtsy addTarget:self action:@selector(signupForArtsy:) forControlEvents:UIControlEventTouchUpInside]; + [self.view.stackView addSubview:signupForArtsy withTopMargin:@"20" sideMargin:@"40"]; +} + +- (void)signupForArtsy:(id)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextFairGuide fromTarget:self selector:@selector(userDidSignUp)]; + } +} + +- (void)userDidSignUp +{ + [self.delegate fairGuideViewControllerDidChangeUser:self]; + _selectedTabIndex = ARFairGuideSelectedTabUndefined; +} + +- (void)addUserContent +{ + [self.fairFavorites getFavoritesForNavigationsButtonsForFair:self.fair + artwork:^(NSArray *workArray) { + [self.workViewController addButtonDescriptions:workArray unique:YES]; + } exhibitors:^(NSArray *exhibitorsArray) { + [self.exhibitorsViewController addButtonDescriptions:exhibitorsArray unique:YES]; + } artists:^(NSArray *artistsArray) { + [self.artistsViewController addButtonDescriptions:artistsArray unique:YES]; + } failure:^(NSError *error) { + //ignore + } + ]; +} + +- (void)addTabView +{ + ARSwitchView *switchView = [[ARSwitchView alloc] initWithButtonTitles:[ARSwitchView fairGuideButtonTitleArray]]; + switchView.delegate = self; + switchView.tag = ARFairGuideViewTabs; + + [self.view.stackView addSubview:switchView withTopMargin:@"20" sideMargin:@"40"]; + [switchView constrainWidth:NSStringWithFormat(@"%@", @(ARFairGuideSwitchviewWidth))]; +} + +#pragma mark - ARSwitchViewDelegate + + +- (void)switchView:(ARSwitchView *)switchView didPressButtonAtIndex:(NSInteger)buttonIndex animated:(BOOL)animated +{ + if (buttonIndex == ARSwitchViewWorkButtonIndex) { + self.selectedTabIndex = ARFairGuideSelectedTabWork; + } else if (buttonIndex == ARSwitchViewArtistsButtonIndex) { + self.selectedTabIndex = ARFairGuideSelectedTabArtists; + } else if (buttonIndex == ARSwitchViewExhibitorsButtonIndex) { + self.selectedTabIndex = ARFairGuideSelectedTabExhibitors; + } +} + +- (void)fairFavoritesNetworkModel:(ARFairFavoritesNetworkModel *)fairFavoritesNetworkModel shouldPresentViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (User *)currentUser +{ + return _currentUser ?: [User currentUser]; +} + +- (BOOL)hasCurrentUser +{ + return self.currentUser && (self.currentUser != (id)[NSNull null]); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapAnnotation.h b/Artsy/Classes/View Controllers/ARFairMapAnnotation.h new file mode 100644 index 00000000000..604d729044c --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapAnnotation.h @@ -0,0 +1,15 @@ +#import + +@interface ARFairMapAnnotation : NAAnnotation + +-(id)initWithPoint:(CGPoint)point representedObject:(id)representedObject; + +@property (nonatomic, strong) id representedObject; +@property (nonatomic, readonly) enum ARMapFeatureType featureType; +@property (nonatomic, readonly) NSString *href; +@property (nonatomic, readonly) NSString *title; +@property (nonatomic, readonly) NSString *subTitle; +@property (nonatomic, assign) BOOL saved; +@property (nonatomic, assign) BOOL highlighted; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapAnnotation.m b/Artsy/Classes/View Controllers/ARFairMapAnnotation.m new file mode 100644 index 00000000000..0fd7146788a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapAnnotation.m @@ -0,0 +1,132 @@ +#import "ARFairMapAnnotation.h" +#import "ARFairMapAnnotationView.h" +#import "UIView+HitTestExpansion.h" + +@interface ARFairMapAnnotation() +@property(nonatomic, readonly) ARFairMapAnnotationView *view; +@end + +@implementation ARFairMapAnnotation + +- (id)initWithPoint:(CGPoint)point representedObject:(id)representedObject +{ + self = [super initWithPoint:point]; + if (self) { + _representedObject = representedObject; + _saved = NO; + _highlighted = NO; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + return [[ARFairMapAnnotation allocWithZone:zone] initWithPoint:self.point representedObject:self.representedObject]; +} + +- (BOOL)isEqual:(id)object +{ + if([object isKindOfClass:self.class]){ + return CGPointEqualToPoint(self.point , [object point]) && [self.representedObject isEqual:[object representedObject]]; + } + return [super isEqual:object]; +} + +- (UIView *)createViewOnMapView:(NAMapView *)mapView +{ + ARFairMapAnnotationView *view = [[ARFairMapAnnotationView alloc] initWithMapView:mapView forAnnotation:self]; + enum ARMapFeatureType featureType = self.featureType; + view.mapFeatureType = featureType; + view.href = self.href; + view.displayTitle = self.title; + + self.mapViewDelegate = mapView.mapViewDelegate; + + if (featureType == ARMapFeatureTypeDefault) { + [view reduceToPoint]; + } + + [view ar_extendHitTestSizeByWidth:0 andHeight:5]; + [view addTarget:self action:@selector(tappedOnAnnotation:) forControlEvents:UIControlEventTouchUpInside]; + + return view; +} + +- (enum ARMapFeatureType)featureType +{ + if ([self.representedObject isKindOfClass:PartnerShow.class]) { + if (self.highlighted) { + return ARMapFeatureTypeHighlighted; + } else if (self.saved) { + return ARMapFeatureTypeSaved; + } else { + return ARMapFeatureTypeDefault; + } + } else if([self.representedObject isKindOfClass:MapFeature.class]) { + return [self.representedObject featureType]; + } else { + return ARMapFeatureTypeGenericEvent; + } +} + +- (NSString *)href +{ + if ([self.representedObject isKindOfClass:MapFeature.class]) { + return [self.representedObject href]; + } + + return nil; +} + +- (NSString *)title +{ + if ([self.representedObject isKindOfClass:PartnerShow.class]) { + PartnerShow *partnerShow = self.representedObject; + return partnerShow.partner.shortName ?: partnerShow.partner.name; + } else if([self.representedObject isKindOfClass:MapFeature.class]) { + MapFeature *mapFeature = self.representedObject; + return mapFeature.name; + } else { + return @""; + } +} + +- (NSString *)subTitle +{ + if ([self.representedObject isKindOfClass:PartnerShow.class]) { + return [self.representedObject locationInFair]; + } else if([self.representedObject isKindOfClass:MapFeature.class]) { + return @""; + } else { + return @""; + } +} + +- (void)tappedOnAnnotation:(id)sender +{ + [self.mapViewDelegate mapView:self.mapView tappedOnAnnotation:self]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + _highlighted = highlighted; + [self updateFeatureView]; +} + +- (void)setSaved:(BOOL)saved +{ + _saved = saved; + [self updateFeatureView]; +} + +- (void)updateFeatureView +{ + self.view.mapFeatureType = self.featureType; +} + +- (void)updatePosition +{ + [self.view updatePosition]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapAnnotationView.h b/Artsy/Classes/View Controllers/ARFairMapAnnotationView.h new file mode 100644 index 00000000000..6d72f885d22 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapAnnotationView.h @@ -0,0 +1,19 @@ +#import "MapFeature.h" +#import "ARFairMapAnnotation.h" + +@interface ARFairMapAnnotationView : UIButton + +- (id)initWithMapView:(NAMapView *)mapView forAnnotation:(ARFairMapAnnotation *)annotation; +- (CGRect)boundingFrame; +- (void)reduceToPoint; +- (void)expandToFull; +- (void)updatePosition; + +@property(nonatomic, assign) enum ARMapFeatureType mapFeatureType; +@property(nonatomic, readwrite, copy) NSString *href; +@property(nonatomic, readonly, assign) BOOL reducedToPoint; +@property(nonatomic, readonly, getter=isUserInteractionAlwaysEnabled) BOOL userInteractionAlwaysEnabled; +@property(nonatomic, readonly) ARFairMapAnnotation *annotation; +@property(nonatomic, readonly) NAMapView *mapView; +@property(nonatomic, readwrite, strong) NSString *displayTitle; +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapAnnotationView.m b/Artsy/Classes/View Controllers/ARFairMapAnnotationView.m new file mode 100644 index 00000000000..ab3e5413dfc --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapAnnotationView.m @@ -0,0 +1,150 @@ +#import "ARFairMapAnnotationView.h" + +@interface ARFairMapAnnotationView () +@property (nonatomic, weak) UIImageView *mapFeatureView; +@property (nonatomic, strong) UILabel *primaryTitleLabel; +@property (nonatomic, strong) UIView *borderView; +@property (nonatomic, assign) BOOL reducedToPoint; +@property (nonatomic, assign) CGPoint mapPositioningPoint; +@end + +@implementation ARFairMapAnnotationView + +static CGFloat ARHorizontalOffsetFromIcon = 4; + +- (instancetype) initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + _reducedToPoint = NO; + } + return self; +} + +- (id)initWithMapView:(NAMapView *)mapView forAnnotation:(ARFairMapAnnotation *)annotation +{ + self = [super init]; + if (self) { + _mapView = mapView; + _annotation = annotation; + } + return self; +} + + +- (void)setDisplayTitle:(NSString *)title +{ + if(! self.primaryTitleLabel){ + UILabel *primaryTitleLabel = [[UILabel alloc] init]; + primaryTitleLabel.font = [UIFont sansSerifFontWithSize:8]; + primaryTitleLabel.preferredMaxLayoutWidth = 90; + self.primaryTitleLabel = primaryTitleLabel; + [self addSubview:primaryTitleLabel]; + [primaryTitleLabel alignTopEdgeWithView:self predicate:nil]; + [primaryTitleLabel alignAttribute:NSLayoutAttributeLeading toAttribute:NSLayoutAttributeTrailing ofView:self.mapFeatureView predicate:NSStringWithFormat(@"%f", ARHorizontalOffsetFromIcon)]; + [primaryTitleLabel constrainHeightToView:self.mapFeatureView predicate:nil]; + } + + self.primaryTitleLabel.text = self.hasLabel ? [title uppercaseString] : @""; + _displayTitle = title; +} + +// draw a red border around the current view +- (void)addBorder +{ + if (!self.borderView) { + CGRect primaryRect = CGRectUnion(self.bounds, self.primaryTitleLabel.frame); + UIView *borderView = [[UIView alloc] initWithFrame:primaryRect]; + borderView.layer.borderColor = [UIColor redColor].CGColor; + borderView.layer.borderWidth = 1.0f; + [self addSubview:borderView]; + _borderView = borderView; + } +} + +- (void)setMapFeatureType:(enum ARMapFeatureType)mapFeatureType +{ + _mapFeatureType = mapFeatureType; + + if(! self.mapFeatureView) { + UIImageView *mapFeatureView = [[UIImageView alloc] init]; + mapFeatureView.contentMode = UIViewContentModeScaleAspectFit; + [self addSubview:mapFeatureView]; + [mapFeatureView alignTopEdgeWithView:self predicate:nil]; + [mapFeatureView alignLeadingEdgeWithView:self predicate:nil]; + self.mapFeatureView = mapFeatureView; + self.clipsToBounds = NO; + } + + NSString *mapFeatureTypeString = NSStringFromARMapFeatureType(mapFeatureType) ?: @"GenericEvent"; + self.mapFeatureView.image = [UIImage imageNamed:NSStringWithFormat(@"MapAnnotation_%@", mapFeatureTypeString)]; + + CGFloat dimension = self.mapFeatureView.image.size.height / 2; + self.mapPositioningPoint = CGPointMake(dimension, dimension); +} + +- (void)updatePosition +{ + CGPoint point = [self.mapView zoomRelativePoint:self.annotation.point]; + point.x -= self.mapPositioningPoint.x; + point.y -= self.mapPositioningPoint.x; + self.frame = CGRectMake(point.x, point.y, self.boundingFrame.size.width, self.boundingFrame.size.height); +} + +- (CGRect)boundingFrame +{ + CGRect primaryRect = CGRectNull; + if ([self hasLabel]) { + primaryRect = CGRectUnion(self.bounds, self.primaryTitleLabel.frame); + } else { + primaryRect = self.bounds; + } + return (CGRect){ + .origin = self.frame.origin, + .size = CGSizeMake(primaryRect.size.width, primaryRect.size.height) + }; +} + +- (void)reduceToPoint +{ + if (self.mapFeatureType == ARMapFeatureTypeHighlighted) { + return; + } + + if (self.mapFeatureType != ARMapFeatureTypeDefault) { + self.primaryTitleLabel.hidden = YES; + } else { + self.hidden = YES; + } + + _reducedToPoint = YES; +} + +- (void)expandToFull +{ + self.frame = self.boundingFrame; + if (self.mapFeatureType && self.mapFeatureType != ARMapFeatureTypeDefault) { + self.primaryTitleLabel.hidden = NO; + } else { + self.hidden = NO; + } + + _reducedToPoint = NO; +} + +- (BOOL)hasLabel +{ + return self.mapFeatureType != ARMapFeatureTypeEntrance + && self.mapFeatureType != ARMapFeatureTypeCoatCheck + && self.mapFeatureType != ARMapFeatureTypeExit + && self.mapFeatureType != ARMapFeatureTypeTicket; +} + +- (BOOL)isUserInteractionAlwaysEnabled +{ + return self.mapFeatureType == ARMapFeatureTypeHighlighted + || self.mapFeatureType == ARMapFeatureTypeSaved + || self.mapFeatureType == ARMapFeatureTypeArtsy; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapPreview.h b/Artsy/Classes/View Controllers/ARFairMapPreview.h new file mode 100644 index 00000000000..a6507c5992b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapPreview.h @@ -0,0 +1,7 @@ +#import + +@interface ARFairMapPreview : NATiledImageMapView +- (id)initWithFairMap:(Map *)map andFrame:(CGRect)frame; +- (void)addShow:(PartnerShow *)show animated:(BOOL)animated; +- (void)addShows:(NSArray *)shows animated:(BOOL)animated; +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapPreview.m b/Artsy/Classes/View Controllers/ARFairMapPreview.m new file mode 100644 index 00000000000..7cf64befb89 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapPreview.m @@ -0,0 +1,52 @@ +#import "ARFairMapPreview.h" +#import "ARTiledImageDataSourceWithImage.h" +#import "ARFairShowMapper.h" +#import "ARFairMapZoomManager.h" + +@interface ARFairMapPreview () +@property (nonatomic, weak, readonly) Map *map; +@property (nonatomic, strong) ARFairShowMapper *showMapper; +@property (nonatomic, strong) ARFairMapZoomManager *mapZoomManager; + +@end + +@implementation ARFairMapPreview + +- (instancetype) initWithFairMap:(Map *)map andFrame:(CGRect)frame +{ + ARTiledImageDataSourceWithImage *ds = [[ARTiledImageDataSourceWithImage alloc] initWithImage:map.image]; + self = [super initWithFrame:frame tiledImageDataSource:ds]; + if (!self) { return nil; } + + self.zoomStep = 2.5; + self.showsVerticalScrollIndicator = NO; + self.showsHorizontalScrollIndicator = NO; + self.backgroundImageURL = [ds.image urlForThumbnailImage]; + self.backgroundColor = [UIColor colorWithHex:0xf6f6f6]; + self.userInteractionEnabled = NO; + + _showMapper = [[ARFairShowMapper alloc] initWithMapView:self map:map imageSize:[ds imageSizeForImageView:nil]]; + + _mapZoomManager = [[ARFairMapZoomManager alloc] initWithMap:self dataSource:ds]; + [self.mapZoomManager setMaxMinZoomScalesForCurrentBounds]; + + return self; +} + +- (void)addShows:(NSArray *)shows animated:(BOOL)animated +{ + [self.showMapper addShows:[NSSet setWithArray:shows]]; + [self.showMapper selectPartnerShows:shows animated:animated]; +} + +- (void)addShow:(PartnerShow *)show animated:(BOOL)animated +{ + [self addShows:[NSArray arrayWithObject:show] animated:animated]; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, 150); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapViewController.h b/Artsy/Classes/View Controllers/ARFairMapViewController.h new file mode 100644 index 00000000000..01369406a12 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapViewController.h @@ -0,0 +1,21 @@ +#import "ARMenuAwareViewController.h" + +@class ARTiledImageDataSourceWithImage, ARFairMapZoomManager, ARFairShowMapper; + +@interface ARFairMapViewController : UIViewController + +- (id)initWithFair:(Fair *)fair; +- (id)initWithFair:(Fair *)fair title:(NSString *)title selectedPartnerShows:(NSArray *)selectedPartnerShows; + +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, strong, readonly) ARTiledImageDataSourceWithImage *mapDataSource; +@property (nonatomic, strong, readonly) ARFairMapZoomManager *mapZoomManager; +@property (nonatomic, strong, readonly) ARFairShowMapper *mapShowMapper; + +@property (readwrite, nonatomic, assign) BOOL expandAnnotations; // defaults to YES + +@property (nonatomic, assign) BOOL titleHidden; + +- (void)centerMap:(CGFloat)heightRatio inFrameOfHeight:(CGFloat)height animated:(BOOL)animated; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairMapViewController.m b/Artsy/Classes/View Controllers/ARFairMapViewController.m new file mode 100644 index 00000000000..44e7d584a7b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairMapViewController.m @@ -0,0 +1,333 @@ +#import "ARFairMapViewController.h" +#import "ARTiledImageDataSourceWithImage.h" +#import "ARFairMapZoomManager.h" +#import "ARFairShowMapper.h" +#import "ARFairShowViewController.h" +#import "ARFairSearchViewController.h" +#import "UIViewController+SimpleChildren.h" +#import "ARFairMapAnnotationCallOutView.h" +#import "ARArtworkSetViewController.h" +#import "ARGeneViewController.h" +#import "ARSearchFieldButton.h" + +@interface ARFairMapViewController () +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong, readwrite) NATiledImageMapView *mapView; +@property (nonatomic, strong) ARSearchFieldButton *searchButton; +@property (nonatomic, strong, readwrite) ARTiledImageDataSourceWithImage *mapDataSource; +@property (nonatomic, strong, readwrite) ARFairSearchViewController *searchVC; +@property (nonatomic, readonly, assign) BOOL calloutAnnotationHighlighted; +@property (nonatomic, readonly, strong) ARFairMapAnnotationCallOutView *calloutView; +@property (nonatomic, readonly, strong) NSArray *selectedPartnerShows; +@property (nonatomic, readonly, strong) NSString *selectedTitle; +@end + +@implementation ARFairMapViewController + +- (id)initWithFair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + + _fair = fair; + + Map *map = _fair.maps.firstObject; + _mapDataSource = [[ARTiledImageDataSourceWithImage alloc] initWithImage:map.image]; + _expandAnnotations = YES; + + return self; +} + +- (id)initWithFair:(Fair *)fair title:(NSString *)title selectedPartnerShows:(NSArray *)selectedPartnerShows +{ + self = [self initWithFair:fair]; + if (!self) { return nil; } + + _selectedTitle = title; + _selectedPartnerShows = selectedPartnerShows; + + return self; +} + +- (void)dealloc +{ + if (_mapShowMapper) + { + [_fair removeObserver:_mapShowMapper forKeyPath:@keypath(Fair.new, shows)]; + [_fair removeObserver:self forKeyPath:@keypath(Fair.new, shows)]; + } +} + +- (void)viewDidLoad +{ + NATiledImageMapView *mapView = [[NATiledImageMapView alloc] initWithFrame:self.view.frame tiledImageDataSource:self.mapDataSource]; + mapView.mapViewDelegate = self; + mapView.zoomStep = 2.5; + mapView.showsVerticalScrollIndicator = NO; + mapView.showsHorizontalScrollIndicator = NO; + mapView.backgroundImageURL = [self.mapDataSource.image urlForThumbnailImage]; + mapView.backgroundColor = [UIColor colorWithHex:0xf6f6f6]; + [self.view addSubview:mapView]; + _mapView = mapView; + + _calloutView = [[ARFairMapAnnotationCallOutView alloc] initOnMapView:self.mapView fair:self.fair]; + self.calloutView.hidden = YES; + [self.mapView addSubview:self.calloutView]; + + // Prioritise the double tap gesture over the single tap for buttons + self.mapView.doubleTapGesture.delaysTouchesBegan = YES; + + // We don't want to trigger the load view early so these get set up after + _mapZoomManager = [[ARFairMapZoomManager alloc] initWithMap:self.mapView dataSource:self.mapDataSource]; + + _mapShowMapper = [[ARFairShowMapper alloc] initWithMapView:mapView map:self.fair.maps.firstObject imageSize:[self.mapDataSource imageSizeForImageView:nil]]; + [self.fair addObserver:self.mapShowMapper forKeyPath:@keypath(Fair.new, shows) options:NSKeyValueObservingOptionNew context:nil]; + [self.fair addObserver:self forKeyPath:@keypath(Fair.new, shows) options:NSKeyValueObservingOptionNew context:nil]; + + [self.mapZoomManager setMaxMinZoomScalesForCurrentBounds]; + [self.mapZoomManager zoomToFitAnimated:NO]; + [self.mapShowMapper setupMapFeatures]; + + if (!self.selectedTitle) { + self.searchButton = [[ARSearchFieldButton alloc] init]; + self.searchButton.delegate = self; + [self.view addSubview:self.searchButton]; + [self.searchButton constrainTopSpaceToView:(UIView *)self.topLayoutGuide predicate:@"17"]; + [self.searchButton alignTrailingEdgeWithView:self.view predicate:@"-20"]; + [self.searchButton constrainWidth:@"240"]; + + } else { + self.titleLabel = [[ARSansSerifHeaderLabel alloc] init]; + self.titleLabel.text = [self.selectedTitle uppercaseString]; + self.titleLabel.backgroundColor = [UIColor clearColor]; + self.titleLabel.numberOfLines = 0; + self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + + [self.view addSubview:self.titleLabel]; + NSString *top = [@(10 + ([self.parentViewController isKindOfClass:[UINavigationController class]] ? 20 : 0)) stringValue]; + [self.titleLabel alignTop:top leading:@"60" bottom:nil trailing:@"-60" toView:self.view]; + [self.titleLabel constrainHeight:@">=44"]; + } + + RAC(self.mapShowMapper, expandAnnotations) = RACObserve(self, expandAnnotations); + + // Due to a problem in the custom UIViewController transitions API (when the VC's view is a scrollview subclass) + @weakify(self); + [[self rac_signalForSelector:@selector(viewWillDisappear:)] subscribeNext:^(id x) { + @strongify(self); + + CGPoint contentOffset = self.mapView.contentOffset; + + [[[self rac_signalForSelector:@selector(viewWillAppear:)] take:1] subscribeNext:^(id x) { + [self.mapView setContentOffset:contentOffset animated:NO]; + }]; + }]; + + [super viewDidLoad]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self.mapZoomManager setMaxMinZoomScalesForCurrentBounds]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + if (self.selectedPartnerShows) { + NSSet *selectedPartnerShowsSet = [NSSet setWithArray:self.selectedPartnerShows]; + [self.mapShowMapper addShows:selectedPartnerShowsSet]; + [self.mapShowMapper selectPartnerShows:self.selectedPartnerShows animated:YES]; + _selectedPartnerShows = nil; + } else { + [self.fair downloadShows]; + } + + // Required since, before the view appears, only the annotations *positions* are correct, not their sizes + [self.mapView updatePositions]; + + [super viewDidAppear:animated]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.mapZoomManager setMaxMinZoomScalesForCurrentBounds]; +} + +- (void)setTitleHidden:(BOOL)titleHidden +{ + _titleHidden = titleHidden; + + self.titleLabel.alpha = (titleHidden ? 0.0 : 1.0); +} + +- (void)centerMap:(CGFloat)heightRatio inFrameOfHeight:(CGFloat)height animated:(BOOL)animated +{ + CGFloat x = self.mapView.contentOffset.x + (self.mapView.frame.size.width / 2.0f); + CGFloat y = heightRatio * height + (self.mapView.contentSize.height / 2.0f); + [self.mapView updateContentOffsetToCenterPoint:CGPointMake(x, y) animated:animated]; +} + +- (void)mapView:(NAMapView *)imageView tappedOnAnnotation:(ARFairMapAnnotation *)annotation +{ + [self hideCallOut]; + [self showCalloutForAnnotation:annotation animated:YES]; +} + +- (void)mapView:(NAMapView *)imageView hasChangedZoomLevel:(CGFloat)level +{ + [self hideCallOut]; + [self.mapShowMapper mapZoomLevelChanged:level]; +} + ++ (NSSet *)keyPathsForValuesAffectingHidesBackButton +{ + return [NSSet setWithObjects:@"searchVC.menuState", nil]; +} + + +- (BOOL)hidesBackButton +{ + if (self.searchVC) { + return YES; + } else { + return NO; + } +} + +- (BOOL)hidesToolbarMenu +{ + return YES; +} + +- (void)showCalloutForAnnotation:(ARFairMapAnnotation *)annotation animated:(BOOL)animated +{ + [self hideCallOut]; + + [self.mapView centerOnPoint:annotation.point animated:animated]; + + if (!annotation.title) { + return; + } + + self.calloutView.annotation = annotation; + _calloutAnnotationHighlighted = annotation.highlighted; + annotation.highlighted = YES; + + self.calloutView.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.4f, 0.4f); + [self.mapView bringSubviewToFront:self.calloutView]; + self.calloutView.hidden = NO; + + [UIView animateIf:animated duration:0.1f :^{ + self.calloutView.transform = CGAffineTransformIdentity; + }]; +} + +- (void)hideCallOut +{ + self.calloutView.annotation.highlighted = self.calloutAnnotationHighlighted; + ARFairMapAnnotationView *annotationView = (ARFairMapAnnotationView *)self.calloutView.annotation.view; + [annotationView reduceToPoint]; + self.calloutView.hidden = YES; +} + +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query +{ + if (result.model == [Artwork class]) { + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadArtworkWithID:result.modelID inFair:self.fair]; + [self.navigationController pushViewController:controller animated:YES]; + } else if (result.model == [Artist class]) { + Artist *artist = [[Artist alloc] initWithArtistID:result.modelID]; + [self selectedArtist:artist]; + } else if (result.model == [Gene class]) { + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadGeneWithID:result.modelID]; + [self.navigationController pushViewController:controller animated:YES]; + } else if (result.model == [Profile class]) { + UIViewController *controller = [ARSwitchBoard.sharedInstance routeProfileWithID:result.modelID]; + [self.navigationController pushViewController:controller animated:YES]; + } else if (result.model == [SiteFeature class]) { + NSString *path = NSStringWithFormat(@"/feature/%@", result.modelID); + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadPath:path]; + [self.navigationController pushViewController:controller animated:YES]; + } else if (result.model == [PartnerShow class]) { + PartnerShow *partnerShow = [[PartnerShow alloc] initWithShowID:result.modelID]; + [self selectedPartnerShow:partnerShow]; + } +} + +- (void)hideScreenContents +{ + [self cancelledSearch:self.searchVC]; + [self hideCallOut]; +} + +- (void)selectedPartnerShow:(PartnerShow *)partnerShow +{ + [self hideScreenContents]; + + RACCommand *completionCommand = [[[ARTopMenuViewController sharedController] rootNavigationController] presentPendingOperationLayover]; + + [ArtsyAPI getShowInfo:partnerShow success:^(PartnerShow *partnerShow) { + [[completionCommand execute:nil] subscribeCompleted:^{ + [self.mapShowMapper selectPartnerShow:partnerShow animated:YES]; + }]; + } failure:^(NSError *error) { + [[completionCommand execute:nil] subscribeCompleted:^{ + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadShowWithID:partnerShow.showID fair:self.fair]; + [self.navigationController pushViewController:controller animated:YES]; + }]; + }]; +} + +- (void)selectedArtist:(Artist *)artist +{ + [self hideScreenContents]; + + RACCommand *completionCommand = [[[ARTopMenuViewController sharedController] rootNavigationController] presentPendingOperationLayover]; + + [ArtsyAPI getShowsForArtistID:artist.artistID inFairID:self.fair.fairID success:^(NSArray *shows) { + [[completionCommand execute:nil] subscribeCompleted:^{ + [self.mapShowMapper selectPartnerShows:shows animated:YES]; + }]; + } failure:^(NSError *error) { + [[completionCommand execute:nil] subscribeCompleted:^{ + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadArtistWithID:artist.artistID inFair:self.fair]; + [self.navigationController pushViewController:controller animated:YES]; + }]; + }]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(Fair *)fair change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@keypath(Fair.new, shows)]) { + // perform the selection on map once we have shows downloaded or loaded from cache + if (self.selectedPartnerShows) { + [self.mapShowMapper selectPartnerShows:self.selectedPartnerShows animated:YES]; + _selectedPartnerShows = nil; + } + } +} + +#pragma mark - ARSearchFieldButtonDelegate + +- (void)searchFieldButtonWasPressed:(ARSearchFieldButton *)sender +{ + NSAssert(self.searchVC == nil, @"Trying to replace existing search view controller. "); + + self.searchVC = [[ARFairSearchViewController alloc] initWithFair:self.fair]; + self.searchVC.delegate = self; + self.searchVC.view.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8]; + self.searchButton.hidden = YES; + [self ar_addModernChildViewController:self.searchVC]; + [self.searchVC.view alignToView:self.navigationController.view]; +} + +- (void)cancelledSearch:(ARFairSearchViewController *)controller +{ + [self ar_removeChildViewController:self.searchVC]; + self.searchVC = nil; + self.searchButton.hidden = NO; +} + + +@end diff --git a/Artsy/Classes/View Controllers/ARFairPostsViewController.h b/Artsy/Classes/View Controllers/ARFairPostsViewController.h new file mode 100644 index 00000000000..5a518933d21 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairPostsViewController.h @@ -0,0 +1,14 @@ +#import "ARFairSectionViewController.h" +#import "ARPostFeedItemLinkView.h" + +@protocol ARFairPostsViewControllerDelegate +@required +- (void)didSelectPost:(NSString *)postURL; +@end + +@interface ARFairPostsViewController : ARFairSectionViewController + +@property (nonatomic, strong, readonly) ARFeedTimeline *feedTimeline; +@property (nonatomic, strong) id selectionDelegate; +@end + diff --git a/Artsy/Classes/View Controllers/ARFairPostsViewController.m b/Artsy/Classes/View Controllers/ARFairPostsViewController.m new file mode 100644 index 00000000000..aaab16b458b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairPostsViewController.m @@ -0,0 +1,51 @@ +#import "ARFairPostsViewController.h" +#import "ARPostFeedItem.h" +#import "ORStackView+ArtsyViews.h" + +@implementation ARFairPostsViewController + +- (instancetype)initWithFair:(Fair *)fair +{ + self = [super initWithFair:fair]; + if (!self) { return nil; } + + @weakify(self); + [fair getPosts:^(ARFeedTimeline *feedTimeline) { + @strongify(self); + [self setFeedTimeline:feedTimeline]; + }]; + + return self; +} + +-(void)setFeedTimeline:(ARFeedTimeline *)feedTimeline +{ + _feedTimeline = feedTimeline; + [self reloadData]; +} + +-(void)reloadData +{ + if ([[self feedTimeline] numberOfItems] > 0) { + [self addPageTitleWithString:@"Posts"]; + [(ORStackView *)self.view addGenericSeparatorWithSideMargin: @"20"]; + for (NSInteger i = 0; i < [[self feedTimeline] numberOfItems]; i++) { + ARPostFeedItem *postFeedItem = (ARPostFeedItem *) [[self feedTimeline] itemAtIndex:i]; + ARPostFeedItemLinkView * postLinkView = [[ARPostFeedItemLinkView alloc] init]; + [postLinkView updateWithPostFeedItem:postFeedItem]; + [self addSubview:postLinkView withTopMargin:nil sideMargin:nil]; + } + } +} + +-(void)tappedPostFeedItemLinkView:(ARPostFeedItemLinkView *)sender +{ + if (self.selectionDelegate) { + [self.selectionDelegate didSelectPost:sender.targetPath]; + } else { + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:sender.targetPath]; + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairProfileViewController.m b/Artsy/Classes/View Controllers/ARFairProfileViewController.m new file mode 100644 index 00000000000..6859819bc9a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairProfileViewController.m @@ -0,0 +1,219 @@ +#import "ARFairProfileViewController.h" + +NSString * const ARFairRefreshFavoritesNotification = @"ARFairRefreshFavoritesNotification"; +NSString * const ARFairHighlightArtworkIDKey = @"ARFairHighlightArtworkIDKey"; +NSString * const ARFairHighlightArtistIDKey = @"ARFairHighlightArtistIDKey"; +NSString * const ARFairHighlightShowsKey = @"ARFairHighlightShowsKey"; +NSString * const ARFairHighlightPartnersKey = @"ARFairHighlightPartnersKey"; +NSString * const ARFairHighlightFocusMapKey = @"ARFairHighlightFocusMapKey"; +NSString * const ARFairMapSetFavoritePartnersKey = @"ARFairMapSetFavoritePartnersKey"; +NSString * const ARFairHighlightFavoritePartnersKey = @"ARFairHighlightFavoritePartnersKey"; + +@interface ARFairProfileViewController() + +// Taken from the ARParallaxScrollViewController. +@property (readonly, nonatomic, strong) UIViewController *topViewController; +@property (readonly, nonatomic, strong) UIViewController *bottomViewController; + +@property (readonly, nonatomic) ARFairNavigationController *innerNavigationController; +@property (readonly, nonatomic) ARFairViewController *fairVC; + +@property (readonly, nonatomic, strong) ARFairFavoritesNetworkModel *favoritesNetworkModel; + +@property (nonatomic, copy) NSString *lazyProfileID; +@property (readonly, nonatomic) ARFairSearchViewController *searchVC; + +@property (readwrite, nonatomic, assign) BOOL enableMenuButtons; + +@property (readwrite, nonatomic, strong) AFJSONRequestOperation *favoritesOperation; + +// The top-most view controller in the inner navigation controller. +// This is the on that decides if the back or menu buttons should be visible or +// not and everything rotation related. +@property (readwrite, nonatomic, strong) UIViewController *mainViewController; + +@end + +@implementation ARFairProfileViewController + +- (id)initWithProfile:(Profile *)profile +{ + NSAssert([profile.owner isKindOfClass:[FairOrganizer class]], @"Expected a profile owned by a FairOrganizer."); + + self = [super init]; + if (!self) { return nil; } + _profile = profile; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _searchVC = [[ARFairSearchViewController alloc] init]; + + self.view.clipsToBounds = YES; + + [self ar_presentIndeterminateLoadingIndicator]; + [self setupProfile]; +} + +- (void) setFair:(Fair *)fair +{ + [_fair removeObserver:self forKeyPath:@keypath(Fair.new, shows)]; + + _fair = fair; + + [self.fair addObserver:self forKeyPath:@keypath(Fair.new, shows) options:NSKeyValueObservingOptionNew context:nil]; +} + +- (void)setupProfile +{ + NSString * defaultFairID = ((FairOrganizer *) _profile.owner).defaultFairID; + + self.fair = [[Fair alloc] initWithFairID:defaultFairID]; + + ARFairViewController *fairViewController = [[ARFairViewController alloc] initWithFair:self.fair andProfile:self.profile]; + _fairVC = fairViewController; + _innerNavigationController = [[ARFairNavigationController alloc] initWithRootViewController:self.fairVC]; + self.innerNavigationController.navigationBarHidden = YES; + self.innerNavigationController.delegate = self; + + // make us the delegate, this also enableds transitions with the navigation bar hidden. + self.innerNavigationController.interactivePopGestureRecognizer.delegate = self; + + [self ar_addModernChildViewController:self.innerNavigationController]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + NSNotificationCenter *dc = NSNotificationCenter.defaultCenter; + [dc addObserver:self selector:@selector(didReceiveRefreshNotification:) name:ARFairRefreshFavoritesNotification object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + NSNotificationCenter *dc = NSNotificationCenter.defaultCenter; + [dc removeObserver:self name:ARFairRefreshFavoritesNotification object:nil]; +} + +- (void)refreshFavorites +{ + if (User.isTrialUser) { + return; + } + + if(!_favoritesNetworkModel) { + _favoritesNetworkModel = [[ARFairFavoritesNetworkModel alloc] init]; + } + + if(self.favoritesNetworkModel.isDownloading) return; + [self.favoritesNetworkModel getFavoritesForNavigationsButtonsForFair:self.fair navigation:nil success:^(NSArray *relatedPartners) { + } failure:nil]; +} + +- (void)setupSearchView +{ + // TODO: https://github.com/orta/ORStackView/issues/9 + [self ar_addModernChildViewController:self.searchVC]; +} + +- (void)didReceiveRefreshNotification:(NSNotification *)notification +{ + [self refreshFavorites]; +} + +#pragma mark - UIViewController + +- (BOOL)shouldAutorotate +{ + return self.mainViewController.shouldAutorotate; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return self.mainViewController.supportedInterfaceOrientations ?: ([UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown); +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.mainViewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.mainViewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + +#pragma mark - ARMenuAwareViewController + ++ (NSSet *)keyPathsForValuesAffectingHidesBackButton +{ + return [NSSet setWithObjects:@"mainViewController.hidesBackButton", @"searchVC.menuState", nil]; +} + +- (BOOL)hidesBackButton +{ + BOOL isRootView = (self.navigationController.viewControllers.count <= 1); + if (isRootView) { + return YES; + } + + if (self.searchVC.menuState == ARMenuStateExpanded) { + return YES; + } + + if ([self.mainViewController conformsToProtocol:@protocol(ARMenuAwareViewController)]) { + return [(id)self.mainViewController hidesBackButton]; + } + + return NO; +} + ++ (NSSet *)keyPathsForValuesAffectingHidesMenuButton +{ + return [NSSet setWithObjects:@"mainViewController.hidesMenuButton", @"searchVC.menuState", nil]; +} + +- (BOOL)hidesMenuButton +{ + if (self.searchVC.menuState==ARMenuStateExpanded) { + return YES; + } + + if ([self.mainViewController conformsToProtocol:@protocol(ARMenuAwareViewController)]) { + return [(id)self.mainViewController hidesMenuButton]; + } + + return NO; +} + +#pragma mark - UINavigationControllerDelegate + +- (void)navigationController:(ARFairNavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + self.enableMenuButtons = NO; + self.mainViewController = viewController; +} + +- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + self.enableMenuButtons = YES; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(Fair *)fair change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@keypath(Fair.new, shows)]) { + [self refreshFavorites]; + } +} + +- (void)dealloc +{ + // remove observer + self.fair = nil; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairSearchViewController.h b/Artsy/Classes/View Controllers/ARFairSearchViewController.h new file mode 100644 index 00000000000..b895b2015fa --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairSearchViewController.h @@ -0,0 +1,21 @@ +#import "ARSearchViewController.h" + +@class ARFairSearchViewController; + +@protocol ARFairSearchViewControllerDelegate +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query; + +@optional +- (void)cancelledSearch:(ARFairSearchViewController *)controller; +@end + +@interface ARFairSearchViewController : ARSearchViewController + +- (instancetype)initWithFair:(Fair *)fair; + +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, weak, readwrite) id delegate; + +- (NSArray *)searchPartners:(NSString *)query; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairSearchViewController.m b/Artsy/Classes/View Controllers/ARFairSearchViewController.m new file mode 100644 index 00000000000..0910019441a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairSearchViewController.m @@ -0,0 +1,118 @@ +#import "ARFairSearchViewController.h" +#import "ARSearchViewController+Private.h" + +@implementation ARFairSearchViewController + +- (instancetype)initWithFair:(Fair *)fair +{ + self = [super init]; + + if (!self) { return nil; } + + _fair = fair; + + return self; +} + +- (void)viewDidLoad +{ + self.defaultInfoLabelText = @"Find Exhibitors & Artists"; + self.searchIconImageName = @"SearchIcon_HeavyGrey"; + + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor whiteColor]; + self.textField.textColor = [UIColor artsyHeavyGrey]; + self.textField.tintColor = [UIColor artsyHeavyGrey]; + self.textField.clearButtonMode = UITextFieldViewModeWhileEditing; + [self.closeButton setTitleColor:[UIColor artsyHeavyGrey] forState:UIControlStateNormal]; + + self.searchDataSource.textColor = [UIColor artsyHeavyGrey]; + self.searchDataSource.placeholderImage = [UIImage imageNamed:@"SearchThumb_HeavyGrey"]; + + // fair search is a solid grey background + UIView *searchBox = [[UIView alloc] init]; + searchBox.backgroundColor = [UIColor colorWithHex:0xf2f2f2]; + [self.view insertSubview:searchBox atIndex:0]; + [searchBox alignLeadingEdgeWithView:self.searchIcon predicate:@"-4"]; + [searchBox alignTrailingEdgeWithView:self.textField predicate:@"4"]; + [searchBox alignTop:@"-8" bottom:@"8" toView:self.textField]; +} + +- (void)fetchSearchResults:(NSString *)text replace:(BOOL)replaceResults +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + NSArray *partnerSearchResults = [self searchPartners:text]; + dispatch_sync(dispatch_get_main_queue(), ^{ + [self addResults:partnerSearchResults replace:replaceResults]; + [super fetchSearchResults:text replace:NO]; + }); + }); +} + +- (NSArray *)searchPartners:(NSString *)query +{ + query = query.lowercaseString; + NSMutableArray *results = [NSMutableArray array]; + NSSet *allShows = self.fair.shows; + for (PartnerShow *show in allShows) { + // boths are usually some text that ends with the booth number + if ([show.locationInFair.lowercaseString hasSuffix:query]) { + [results addObject:[SearchResult modelWithJSON:@{ + @"id" : show.showID, + @"display" : show.locationInFair, + @"model" : @"partnershow", + @"label" : show.partner.partnerID, + @"published" : @YES + }]]; + } else { + // partner names match by prefix and within each word of the partner name + NSString *partnerName = show.partner.name.lowercaseString; + NSString *partnerShortName = show.partner.shortName.lowercaseString; + if ([partnerName hasPrefix:query] + || [[partnerName componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] includes:query] + || [partnerShortName hasPrefix:query] + || [[partnerShortName componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] includes:query] + ) { + [results addObject:[SearchResult modelWithJSON:@{ + @"id" : show.showID, + @"display" : show.partner.name, + @"model" : @"partnershow", + @"label" : show.partner.partnerID, + @"published" : @YES + }]]; + } + } + + if (results.count == 5) { + break; + } + } + return results; +} + +- (AFJSONRequestOperation *)searchWithQuery:(NSString *)query success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + return [ArtsyAPI searchWithFairID:self.fair.fairID andQuery:query success:^(NSArray *searchResults) { + success([searchResults select:^BOOL(SearchResult *searchResult) { + // we have local search results for shows + return ! [searchResult.model isEqual:[PartnerShow class]]; + }]); + } failure:failure]; +} + +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query +{ + [self.delegate selectedResult:result ofType:type fromQuery:query]; +} + +- (void)closeSearch:(id)sender +{ + if ([self.delegate respondsToSelector:@selector(cancelledSearch:)]) { + [self.delegate cancelledSearch:self]; + } else { + [super closeSearch:sender]; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairSectionViewController.h b/Artsy/Classes/View Controllers/ARFairSectionViewController.h new file mode 100644 index 00000000000..6ab08266d71 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairSectionViewController.h @@ -0,0 +1,13 @@ +#import + +@interface ARFairSectionViewController : UIViewController + +- (instancetype)initWithFair:(Fair *)fair; +- (UILabel *)addPageTitleWithString:(NSString *)title; +- (void)addSubview:(UIView *)view withTopMargin:(NSString *)margin; +- (void)addSubview:(UIView *)view withTopMargin:(NSString *)topMargin sideMargin:(NSString *)sideMargin; +- (void)addGenericSeparatorWithSideMargin:(NSString *)sideMargin; +@property (nonatomic, strong, readonly) Fair *fair; + +@end + diff --git a/Artsy/Classes/View Controllers/ARFairSectionViewController.m b/Artsy/Classes/View Controllers/ARFairSectionViewController.m new file mode 100644 index 00000000000..2baed15952d --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairSectionViewController.m @@ -0,0 +1,55 @@ +#import "ARFairSectionViewController.h" +#import "ORStackView+ArtsyViews.h" + +@interface ARFairSectionViewController () +@property (nonatomic, strong) ORStackView *view; +@end + +@implementation ARFairSectionViewController + +- (instancetype)initWithFair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + + _fair = fair; + + return self; +} + +- (void)loadView +{ + self.view = [[ORStackView alloc] init]; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.bottomMarginHeight = 0; +} + +- (UILabel *)addPageTitleWithString:(NSString *)title +{ + return [self.view addPageTitleWithString:title]; +} + +- (void)addSubview:(UIView *)view withTopMargin:(NSString *)margin +{ + [self.view addSubview:view withTopMargin:margin]; +} + +- (void)addSubview:(UIView *)view withTopMargin:(NSString *)topMargin sideMargin:(NSString *)sideMargin +{ + [self.view addSubview:view withTopMargin:topMargin sideMargin:sideMargin]; +} + +- (void)addGenericSeparatorWithSideMargin:(NSString *)margin +{ + [self.view addGenericSeparatorWithSideMargin:margin]; +} + +- (CGSize)preferredContentSize +{ + return (CGSize){ + .width = CGRectGetWidth(self.parentViewController.view.bounds), + .height = CGRectGetWidth(self.view.bounds) + }; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairShowViewController.h b/Artsy/Classes/View Controllers/ARFairShowViewController.h new file mode 100644 index 00000000000..b7992aeecde --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairShowViewController.h @@ -0,0 +1,12 @@ +@interface ARFairShowViewController : UIViewController + +- (id)initWithShow:(PartnerShow *)show fair:(Fair *)fair; +- (id)initWithShowID:(NSString *)showID fair:(Fair *)fair; + +@property (nonatomic, strong, readonly) PartnerShow *show; +@property (nonatomic, strong, readonly) Fair *fair; + +- (NSDictionary *)dictionaryForAnalytics; +- (BOOL)isFollowing; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairShowViewController.m b/Artsy/Classes/View Controllers/ARFairShowViewController.m new file mode 100644 index 00000000000..a067af26dde --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairShowViewController.m @@ -0,0 +1,506 @@ +#import "ARFairShowViewController.h" +#import "ARImagePageViewController.h" +#import "ARFollowableNetworkModel.h" +#import "ARFollowableButton.h" +#import "AREmbeddedModelsViewController.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARActionButtonsView.h" +#import "ARSharingController.h" +#import "ARWhitespaceGobbler.h" +#import "ARFairMapViewController.h" +#import "ARFairMapPreview.h" +#import "ARArtworkSetViewController.h" +#import "ARShowNetworkModel.h" + +NS_ENUM(NSInteger, ARFairShowViewIndex){ + ARFairShowViewHeader = 1, + ARFairShowViewActionButtons, + ARFairShowViewPartnerLabel, + ARFairShowViewPartnerLabelFollowButton, + ARFairShowViewPartnerName, + ARFairShowViewBoothLocation, + ARFairShowViewMapPreview, + ARFairShowViewFollowPartner, + ARFairShowViewWhitespaceAboveArtworks, + ARFairShowViewArtworks +}; + +static const NSInteger ARFairShowMaximumNumberOfHeadlineImages = 5; + +@interface ARFairShowViewController () +@property (nonatomic, strong, readonly) ORStackScrollView *view; +@property (nonatomic, strong, readonly) ARImagePageViewController *imagePageViewController; +@property (nonatomic, strong, readonly) ARFollowableNetworkModel *followableNetwork; +@property (nonatomic, strong, readonly) AREmbeddedModelsViewController *showArtworksViewController; +@property (nonatomic, strong, readonly) ARActionButtonsView *actionButtonsView; +@property (nonatomic, strong, readwrite) Fair *fair; +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +@property (nonatomic, strong) NSLayoutConstraint *followButtonWidthConstraint; +@property (nonatomic, strong) NSLayoutConstraint *headerImageHeightConstraint; +@property (nonatomic, strong) ARShowNetworkModel *showNetworkModel; + +@end + +@implementation ARFairShowViewController + ++ (CGFloat)followButtonWidthForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { + return 315; + } else { + return 281; + } +} + ++ (CGFloat)headerImageHeightForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ([UIDevice isPhone]) { + return 213; + } else { + if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { + return 413; + } else { + return 511; + } + } +} + +-(instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithShowID:(NSString *)showID fair:(Fair *)fair +{ + PartnerShow *show = [[PartnerShow alloc] initWithShowID:showID]; + return [self initWithShow:show fair:fair]; +} + +- (instancetype)initWithShow:(PartnerShow *)show fair:(Fair *)fair +{ + self = [self init]; + + _show = show; + _fair = fair ? fair : show.fair; + + return self; +} + +- (NSString *)sideMarginPredicate +{ + return [UIDevice isPad] ? @"100" : @"40"; +} + +- (void)loadView +{ + self.view = [[ORStackScrollView alloc] initWithStackViewClass:[ORTagBasedAutoStackView class]]; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.delegate = [ARScrollNavigationChief chief]; +} + +- (void)fairDidLoad +{ + [self addImagePagingViewToStack]; + [self getShowHeaderImages]; + [self addActionButtonsToStack]; + [self addPartnerLabelAndFollowButtonToStack]; + [self addPartnerMetadataToStack]; + [self addMapPreview]; + [self addFairArtworksToStack]; + + [self ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + + // Create a "be full screen with a low priority" constraint + CGFloat height = CGRectGetHeight(self.parentViewController.parentViewController.view.bounds); + NSString *heightConstraint = [NSString stringWithFormat:@">=%.0f@800", height -19]; + [self.view.stackView constrainHeight:heightConstraint]; + + [self setConstraintConstantsForOrientation:[UIApplication sharedApplication].statusBarOrientation]; +} + +- (void)addActionButtonsToStack +{ + _actionButtonsView = [[ARActionButtonsView alloc] init]; + self.actionButtonsView.tag = ARFairShowViewActionButtons; + [self.view.stackView addSubview:self.actionButtonsView withTopMargin:@"20" sideMargin:[self sideMarginPredicate]]; + + NSMutableArray *descriptions = [NSMutableArray array]; + + [descriptions addObject:@{ + ARActionButtonImageKey: @"Artwork_Icon_Share", + ARActionButtonHandlerKey: ^(ARCircularActionButton *sender) { + NSURL *imageURL = nil; + if (self.imagePageViewController.images.count) { + imageURL = [(Image *)self.imagePageViewController.images[0] urlForThumbnailImage]; + } + [ARSharingController shareObject:self.show withThumbnailImageURL:imageURL]; + } + }]; + + if (self.show.hasMapLocation && self.fair) { + [descriptions addObject:self.descriptionForMapButton]; + } + + self.actionButtonsView.actionButtonDescriptions = descriptions; +} + +- (NSDictionary *)descriptionForMapButton +{ + @weakify(self); + return @{ + ARActionButtonImageKey: @"MapButtonAction", + ARActionButtonHandlerKey: ^(ARCircularActionButton *sender) { + @strongify(self); + [self handleMapButtonPress:sender]; + } + }; +} + +- (void)handleMapButtonPress:(ARCircularActionButton *)sender +{ + @weakify(self); + [self.showNetworkModel getFairMaps:^(NSArray *maps) { + @strongify(self); + ARFairMapViewController *viewController = [[ARSwitchBoard sharedInstance] loadMapInFair:self.fair title:self.show.title selectedPartnerShows:@[self.show]]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; + }]; +} + +- (void)addPartnerLabelAndFollowButtonToStack +{ + UIView *partnerLabel = [self partnerLabel]; + UIView *followButton = [self followButton]; + + if ([UIDevice isPhone]) { + [self.view.stackView addSubview:partnerLabel withTopMargin:@"0" sideMargin:[self sideMarginPredicate]]; + if (followButton) { + [self.view.stackView addSubview:followButton withTopMargin:@"32" sideMargin:[self sideMarginPredicate]]; + } + } else { + UIView *containerView = [[UIView alloc] init]; + containerView.tag = ARFairShowViewPartnerLabelFollowButton; + + + [containerView addSubview:partnerLabel]; + [containerView addSubview:followButton]; + + [containerView constrainHeight:@"40"]; + [partnerLabel alignTop:@"0" bottom:@"0" toView:containerView]; + if (followButton) { + [partnerLabel alignLeading:@"0" trailing:nil toView:containerView]; + [followButton alignLeading:nil trailing:@"0" toView:containerView]; + [followButton alignTop:@"0" bottom:@"0" toView:containerView]; + [UIView alignAttribute:NSLayoutAttributeRight ofViews:@[partnerLabel] toAttribute:NSLayoutAttributeLeft ofViews:@[followButton] predicate:nil]; + CGFloat followButtonWidth = [[self class] followButtonWidthForInterfaceOrientation:self.interfaceOrientation]; + self.followButtonWidthConstraint = [[followButton constrainWidth:@(followButtonWidth).stringValue] firstObject]; + } else { + [partnerLabel alignLeading:@"0" trailing:@"0" toView:containerView]; + } + + [self.view.stackView addSubview:containerView withTopMargin:@"20" sideMargin:[self sideMarginPredicate]]; + } +} + +- (void)viewDidLoad{ + [super viewDidLoad]; + + [self ar_presentIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + + @weakify(self); + [self.showNetworkModel getShowInfo:^(PartnerShow *show) { + @strongify(self); + if (!self) { return; } + + [self.show mergeValuesForKeysFromModel:show]; + + if (!self.fair) { + self->_fair = show.fair; + } + + [self fairDidLoad]; + } failure:^(NSError *error) { + @strongify(self); + + [self fairDidLoad]; + }]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + [self setConstraintConstantsForOrientation:toInterfaceOrientation]; +} + +- (void)setConstraintConstantsForOrientation:(UIInterfaceOrientation)orientation +{ + self.followButtonWidthConstraint.constant = [[self class] followButtonWidthForInterfaceOrientation:orientation]; + self.headerImageHeightConstraint.constant = [[self class] headerImageHeightForInterfaceOrientation:orientation]; +} + +- (UILabel *)partnerLabel +{ + ARItalicsSerifLabelWithChevron *partnerLabel = [[ARItalicsSerifLabelWithChevron alloc] init]; + partnerLabel.font = [UIFont sansSerifFontWithSize:16]; + partnerLabel.tag = ARFairShowViewPartnerLabel; + BOOL showChevron = (self.show.partner.profileID && self.show.partner.defaultProfilePublic); + + partnerLabel.chevronDelta = 6; + partnerLabel.text = self.show.partner.name.uppercaseString; + partnerLabel.chevronHidden = !showChevron; + + if (showChevron) { + partnerLabel.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openArtworkPartner:)]; + [partnerLabel addGestureRecognizer:tapGesture]; + } + + [partnerLabel constrainHeight:@"40"]; + return partnerLabel; +} + +- (void)addPartnerMetadataToStack +{ + ARItalicsSerifLabelWithChevron *partnerName = [[ARItalicsSerifLabelWithChevron alloc] init]; + partnerName.tag = ARFairShowViewPartnerName; + partnerName.font = [UIFont serifFontWithSize:16]; + partnerName.text = self.show.fair ? self.show.fair.name : self.show.name; + [self.view.stackView addSubview:partnerName withTopMargin:@"12" sideMargin:[self sideMarginPredicate]]; + + if (self.show.fair && !self.fair){ + partnerName.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openShowFair:)]; + [partnerName addGestureRecognizer:tapGesture]; + partnerName.chevronHidden = NO; + } else { + partnerName.chevronHidden = YES; + } + + ARSerifLabel *boothLocation = [[ARSerifLabel alloc] init]; + boothLocation.tag = ARFairShowViewBoothLocation; + boothLocation.textColor = [UIColor blackColor]; + boothLocation.font = [UIFont serifFontWithSize:14]; + boothLocation.text = self.show.ausstellungsdauerAndLocation; + [self.view.stackView addSubview:boothLocation withTopMargin:@"6" sideMargin:[self sideMarginPredicate]]; +} + +- (void)openShowFair:(id)sender +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance routeProfileWithID:self.show.fair.organizer.profileID]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)addFairArtworksToStack +{ + // We use a view that takes up the required whitespace to hit the full height + + // TODO: this should only load more artworks on scroll (like favorites) + + __block ARWhitespaceGobbler *whitespaceGobbler = [[ARWhitespaceGobbler alloc] init]; + whitespaceGobbler.backgroundColor = [UIColor whiteColor]; + whitespaceGobbler.tag = ARFairShowViewWhitespaceAboveArtworks; + [self.view.stackView addSubview:whitespaceGobbler withTopMargin:nil sideMargin:nil]; + + ARArtworkMasonryLayout layout; + if ([UIDevice isPad]) { + layout = [self masonryLayoutForPadWithOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; + } else { + layout = ARArtworkMasonryLayout2Column; + } + + ARArtworkMasonryModule *module = [ARArtworkMasonryModule masonryModuleWithLayout:layout andStyle:AREmbeddedArtworkPresentationStyleArtworkMetadata]; + module.layoutProvider = self; + _showArtworksViewController = [[AREmbeddedModelsViewController alloc] init]; + self.showArtworksViewController.view.tag = ARFairShowViewArtworks; + self.showArtworksViewController.delegate = self; + self.showArtworksViewController.activeModule = module; + self.showArtworksViewController.constrainHeightAutomatically = YES; + self.showArtworksViewController.showTrailingLoadingIndicator = YES; + [self.view.stackView addSubview:self.showArtworksViewController.view withTopMargin:@"0" sideMargin:nil]; + + @weakify(self); + [self getArtworksAtPage:1 onArtworks:^(NSArray * artworks) { + @strongify(self); + if (artworks.count > 0) { + if (whitespaceGobbler) { + [self.view.stackView removeSubview:whitespaceGobbler]; + [self.showArtworksViewController appendItems:artworks]; + whitespaceGobbler = nil; + } else { + [self.showArtworksViewController appendItems:artworks]; + } + } else { + self.showArtworksViewController.showTrailingLoadingIndicator = NO; + [self.showArtworksViewController.view invalidateIntrinsicContentSize]; + } + }]; +} + +- (void)getArtworksAtPage:(NSInteger)page onArtworks:(void (^)(NSArray *))onArtworks +{ + NSParameterAssert(onArtworks); + + @weakify(self); + [self.showNetworkModel getArtworksAtPage:page success:^(NSArray *artworks) { + @strongify(self); + onArtworks(artworks); + if (artworks.count > 0) { + [self getArtworksAtPage:page + 1 onArtworks:onArtworks]; + } + } failure:nil]; +} + +- (UIView *)followButton +{ + if(!self.show.partner.defaultProfilePublic) return nil; + + ARFollowableButton *followButton = [[ARFollowableButton alloc] init]; + followButton.tag = ARFairShowViewFollowPartner; + followButton.toFollowTitle = @"Follow Gallery"; + followButton.toUnfollowTitle = @"Following Gallery"; + [followButton addTarget:self action:@selector(toggleFollowShow:) forControlEvents:UIControlEventTouchUpInside]; + + Profile *profile = [[Profile alloc] initWithProfileID:self.show.partner.profileID]; + _followableNetwork = [[ARFollowableNetworkModel alloc] initWithFollowableObject:profile]; + [followButton setupKVOOnNetworkModel:self.followableNetwork]; + + return followButton; +} + +- (void)addMapPreview +{ + @weakify(self); + [self.showNetworkModel getFairMaps:^(NSArray *maps) { + @strongify(self); + + Map *map = maps.firstObject; + if (!map) { return; } + + UIButton *mapViewContainer = [[UIButton alloc] init]; + mapViewContainer.tag = ARFairShowViewMapPreview; + CGRect frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 150); + + ARFairMapPreview *mapPreview = [[ARFairMapPreview alloc] initWithFairMap:map andFrame:frame]; + [mapViewContainer addSubview:mapPreview]; + [mapPreview alignToView:mapViewContainer]; + [mapViewContainer constrainHeight:@"150"]; + + [mapPreview setZoomScale:mapPreview.minimumZoomScale animated:self.shouldAnimate]; + [mapPreview addShow:self.show animated:self.shouldAnimate]; + [mapViewContainer addTarget:self action:@selector(handleMapButtonPress:) forControlEvents:UIControlEventTouchUpInside]; + [self.view.stackView addSubview:mapViewContainer withTopMargin:@"20" sideMargin:@"20"]; + }]; +} + +- (void)toggleFollowShow:(id)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextFavoriteProfile fromTarget:self selector:_cmd]; + return; + } + + self.followableNetwork.following = !self.followableNetwork.following; +} + +- (void)addImagePagingViewToStack +{ + _imagePageViewController = [[ARImagePageViewController alloc] init]; + self.imagePageViewController.imageContentMode = UIViewContentModeScaleAspectFill; + self.imagePageViewController.view.tag = ARFairShowViewHeader; + [self.view.stackView addSubview:self.imagePageViewController.view withTopMargin:@"0" sideMargin:@"0"]; + CGFloat headerImageHeight = [[self class] headerImageHeightForInterfaceOrientation:self.interfaceOrientation]; + self.headerImageHeightConstraint = [[self.imagePageViewController.view constrainHeight:@(headerImageHeight).stringValue] firstObject]; +} + +- (void)getShowHeaderImages +{ + [self.showNetworkModel getFairBoothArtworksAndInstallShots:self.show gotInstallImages:^(NSArray *images) { + if (images.count == 1) { + [self setSingleInstallShot:images.firstObject]; + } else { + self.imagePageViewController.images = [images take:ARFairShowMaximumNumberOfHeadlineImages]; + } + + } gotArtworks:^(NSArray *images) { + if (self.imagePageViewController.images.count) return; + [self setSingleInstallShot:images.firstObject]; + + } noImages:^{ + Image *blankImage = [Image modelWithJSON:@{ @"image_url" : @"" }]; + [self setSingleInstallShot:blankImage]; + }]; +} + +- (void)setSingleInstallShot:(Image *)image +{ + self.imagePageViewController.images = @[image]; + self.imagePageViewController.view.userInteractionEnabled = NO; + [self.imagePageViewController setHidesPageIndicators:YES]; +} + +- (void)openArtworkPartner:(UITapGestureRecognizer *)gestureRecognizer +{ + Partner *partner = self.show.partner; + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPartnerWithID:partner.profileID]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +-(NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; +} + +#pragma mark - DI + +- (ARShowNetworkModel *)showNetworkModel +{ + if (_showNetworkModel == nil) { + _showNetworkModel = [[ARShowNetworkModel alloc] initWithFair:self.fair show:self.show]; + } + + return _showNetworkModel; +} + +#pragma mark - AREmbeddedModelsDelegate + +-(void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController { + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:self.showArtworksViewController.items inFair:self.fair atIndex:index]; + [self.navigationController pushViewController:viewController animated:self.shouldAnimate]; +} + +#pragma mark - Public Methods + +- (BOOL)isFollowing +{ + return self.followableNetwork.isFollowing; +} + +- (NSDictionary *)dictionaryForAnalytics { + return @{ + @"partner_show_id" : self.show.showID ?: @"", + @"partner_id" : self.show.partner.partnerID ?: @"", + @"profile_id" : self.show.partner.profileID ?: @"", + @"fair_id" : self.fair.fairID ?: @"" + }; +} + +#pragma mark - ARArtworkMasonryLayoutProvider +- (ARArtworkMasonryLayout)masonryLayoutForPadWithOrientation:(UIInterfaceOrientation)orientation +{ + return UIInterfaceOrientationIsLandscape(orientation) ? ARArtworkMasonryLayout3Column : ARArtworkMasonryLayout2Column; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFairViewController.h b/Artsy/Classes/View Controllers/ARFairViewController.h new file mode 100644 index 00000000000..c02cdf6b041 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairViewController.h @@ -0,0 +1,25 @@ +#import "ARMenuAwareViewController.h" + +@class Fair; +@class ARNavigationButtonsViewController; + +extern NSString * const ARFairRefreshFavoritesNotification; +extern NSString * const ARFairHighlightArtworkIDKey; +extern NSString * const ARFairHighlightArtistIDKey; +extern NSString * const ARFairHighlightShowsKey; +extern NSString * const ARFairHighlightPartnersKey; +extern NSString * const ARFairHighlightFocusMapKey; +extern NSString * const ARFairMapSetFavoritePartnersKey; +extern NSString * const ARFairHighlightFavoritePartnersKey; + +@interface ARFairViewController : UIViewController + +- (id)initWithFair:(Fair *)fair; +- (id)initWithFair:(Fair *)fair andProfile:(Profile *)profile; + +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, strong, readonly) Profile *fairProfile; + +@property (nonatomic, assign) BOOL animatesSearchBehavior; + +@end diff --git a/Artsy/Classes/View Controllers/ARFairViewController.m b/Artsy/Classes/View Controllers/ARFairViewController.m new file mode 100644 index 00000000000..3dbc6056179 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFairViewController.m @@ -0,0 +1,346 @@ +#import "ARFairViewController.h" +#import "ORStackView+ArtsyViews.h" +#import "ARNavigationButtonsViewController.h" +#import "ARFairPostsViewController.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARFairMapViewController.h" +#import "ARButtonWithImage.h" +#import "ARButtonWithCircularImage.h" +#import "ARBrowseFeaturedLinksCollectionView.h" +#import "ARFairSearchViewController.h" +#import "ARSearchFieldButton.h" +#import "UIViewController+SimpleChildren.h" +#import "ARFairShowViewController.h" +#import "ARArtworkSetViewController.h" +#import "ARGeneViewController.h" +#import "ARParallaxHeaderViewController.h" + +NSString * const ARFairRefreshFavoritesNotification = @"ARFairRefreshFavoritesNotification"; +NSString * const ARFairHighlightArtworkIDKey = @"ARFairHighlightArtworkIDKey"; +NSString * const ARFairHighlightArtistIDKey = @"ARFairHighlightArtistIDKey"; +NSString * const ARFairHighlightShowsKey = @"ARFairHighlightShowsKey"; +NSString * const ARFairHighlightPartnersKey = @"ARFairHighlightPartnersKey"; +NSString * const ARFairHighlightFocusMapKey = @"ARFairHighlightFocusMapKey"; +NSString * const ARFairMapSetFavoritePartnersKey = @"ARFairMapSetFavoritePartnersKey"; +NSString * const ARFairHighlightFavoritePartnersKey = @"ARFairHighlightFavoritePartnersKey"; + +@interface ARFairViewController () + +@property (nonatomic, strong) ORStackScrollView *stackView; +@property (nonatomic, strong) ARParallaxHeaderViewController *headerViewController; +@property (nonatomic, strong) ARSearchFieldButton *searchButton; +@property (nonatomic, strong) ARFairSearchViewController *searchVC; +@property (nonatomic, strong) ARNavigationButtonsViewController *primaryNavigationVC; +@property (nonatomic, strong) ARFairSectionViewController *categoryCollectionVC; +@property (nonatomic, strong) ARNavigationButtonsViewController *curatorVC; +@property (nonatomic, strong) ARFairPostsViewController *fairPostsVC; +@property (nonatomic, strong) NSLayoutConstraint *searchConstraint; + +@property (nonatomic, assign) BOOL hasMap; +@property (nonatomic, assign) BOOL displayingSearch; + +@property (nonatomic, assign) BOOL hidesBackButton; +@property (nonatomic, assign) BOOL hidesToolbarMenu; + +@end + +@implementation ARFairViewController + +- (instancetype)initWithFair:(Fair *)fair +{ + return [self initWithFair:fair andProfile:nil]; +} + +- (instancetype)initWithFair:(Fair *)fair andProfile:(Profile *)profile +{ + self = [super init]; + if (!self) { return nil; } + + _fair = fair; + _fairProfile = profile; + _animatesSearchBehavior = YES; + + RAC(self, hasMap) = [RACObserve(_fair, maps) map:^id(NSArray *maps) { + return @(maps.count > 0); + }]; + RAC(self, displayingSearch) = [RACObserve(self, searchVC) map:^id(id viewController) { + return @(viewController != nil); + }]; + RAC(self, hidesBackButton) = RACObserve(self, displayingSearch); + RAC(self, hidesToolbarMenu) = RACObserve(self, displayingSearch); + + return self; +} + +- (void)viewDidLoad +{ + self.stackView = [[ORStackScrollView alloc] init]; + [self.view addSubview:self.stackView]; + [self.stackView alignToView:self.view]; + self.stackView.backgroundColor = [UIColor whiteColor]; + self.stackView.delegate = [ARScrollNavigationChief chief]; + + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + @weakify(self); + + [_fair updateFair:^{ + @strongify(self); + [self fairDidLoad]; + }]; + + [super viewDidLoad]; +} + +-(BOOL)shouldAutorotate +{ + return NO; +} + +-(NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; +} + +- (void)fairDidLoad +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + + if ([self hasSufficientDataForParallaxHeader]) { + self.headerViewController = [[ARParallaxHeaderViewController alloc] initWithContainingScrollView:self.stackView fair:self.fair profile:self.fairProfile]; + [self.stackView.stackView addSubview:self.headerViewController.view withTopMargin:nil sideMargin:nil]; + } else { + [self.stackView.stackView addSerifPageTitle:self.fair.name subtitle:self.fair.ausstellungsdauer]; + [self.stackView.stackView addGenericSeparatorWithSideMargin: @"40"]; + } + + self.searchButton = [[ARSearchFieldButton alloc] init]; + self.searchButton.delegate = self; + [self.stackView.stackView addSubview:self.searchButton withTopMargin:@"24" sideMargin:@"40"]; + + self.primaryNavigationVC = [[ARNavigationButtonsViewController alloc] init]; + self.primaryNavigationVC.view.hidden = YES; + [self.stackView.stackView addSubview:self.primaryNavigationVC.view withTopMargin:@"12" sideMargin:@"20"]; + + self.categoryCollectionVC = [[ARFairSectionViewController alloc] initWithFair:_fair]; + ARBrowseFeaturedLinksCollectionView *categoryCollectionBrowseView = [[ARBrowseFeaturedLinksCollectionView alloc] initWithStyle:ARFeaturedLinkLayoutSinglePaging]; + categoryCollectionBrowseView.selectionDelegate = self; + [self.stackView.stackView addSubview:self.categoryCollectionVC.view withTopMargin:@"60" sideMargin:@"20"]; + + self.curatorVC = [[ARNavigationButtonsViewController alloc] init]; + [(ORStackView *)self.curatorVC.view addPageTitleWithString:@"Insider's Picks"]; + [(ORStackView *)self.curatorVC.view addGenericSeparatorWithSideMargin: @"20"]; + self.curatorVC.view.hidden = YES; + [self.stackView.stackView addSubview:self.curatorVC.view withTopMargin:@"60" sideMargin:@"20"]; + + BOOL (^displayOnMobile)(FeaturedLink *) = ^(FeaturedLink *link) { + return link.displayOnMobile; + }; + + @weakify(self) + [self.fair getFairMaps:^(NSArray *maps) { + [self.fair getOrderedSets:^(NSMutableDictionary *orderedSets) { + for (OrderedSet *primarySet in orderedSets[@"primary"]) { + [primarySet getItems:^(NSArray *items) { + @strongify(self); + NSArray *buttonDescriptions = [[items ?: @[] + select:displayOnMobile] + map:^(FeaturedLink *link) { + return [self buttonDescriptionForFeaturedLink:link buttonClass:[ARButtonWithImage class]]; + }]; + + if (self.hasMap) { + NSMutableArray *mutableArray = [buttonDescriptions mutableCopy]; + if (mutableArray.count > 0) { + mutableArray[1] = [self buttonDescriptionForMapLink]; + } else { + mutableArray[0] = [self buttonDescriptionForMapLink]; + } + buttonDescriptions = [NSArray arrayWithArray:mutableArray]; + } + + self.primaryNavigationVC.buttonDescriptions = buttonDescriptions; + self.primaryNavigationVC.view.hidden = NO; + }]; + + break; + }; + + for (OrderedSet *exploreGenesSet in orderedSets[@"explore"]) { + [exploreGenesSet getItems:^(NSArray *items) { + @strongify(self); + [self.categoryCollectionVC addPageTitleWithString:exploreGenesSet.name]; + [self.categoryCollectionVC addGenericSeparatorWithSideMargin:@"20"]; + [self.categoryCollectionVC addSubview:categoryCollectionBrowseView withTopMargin:@"20" sideMargin:@"-20"]; + categoryCollectionBrowseView.featuredLinks = [items select:displayOnMobile]; + }]; + break; + } + + for (OrderedSet *curatorSet in orderedSets[@"curator"]) { + [curatorSet getItems:^(NSArray *items) { + @strongify(self); + self.curatorVC.buttonDescriptions = [[items select:displayOnMobile] map:^(FeaturedLink *link) { + return [self buttonDescriptionForFeaturedLink:link buttonClass:[ARButtonWithCircularImage class]]; + }]; + self.curatorVC.view.hidden = NO; + if (self.curatorVC.buttonDescriptions.count == 0) { + [self.stackView.stackView removeSubview:self.curatorVC.view]; + } + }]; + + break; + } + }]; + }]; + + if (self.fair.organizer) { + self.fairPostsVC = [[ARFairPostsViewController alloc] initWithFair:[self fair]]; + self.fairPostsVC.selectionDelegate = self; + [self.stackView.stackView addSubview:self.fairPostsVC.view withTopMargin:@"60" sideMargin:@"20"]; + } + + [self.stackView.stackView addWhiteSpaceWithHeight:@"20"]; + [self viewDidLayoutSubviews]; +} + +#pragma mark - Private + +- (BOOL)hasSufficientDataForParallaxHeader +{ + return self.fair.bannerAddress.length > 0 || self.fairProfile.iconURL.length > 0; +} + +- (NSDictionary *)buttonDescriptionForMapLink +{ + @weakify(self); + return @{ + ARNavigationButtonClassKey: [ARButtonWithImage class], + ARNavigationButtonPropertiesKey: @{ + @keypath(ARButtonWithImage.new, title): @"Map", + @keypath(ARButtonWithImage.new, subtitle): @"Find your way", + @keypath(ARButtonWithImage.new, image): [UIImage imageNamed:@"MapIcon"] + }, + ARNavigationButtonHandlerKey: ^(ARButtonWithImage *button) { + @strongify(self); + ARFairMapViewController *viewController = [[ARSwitchBoard sharedInstance] loadMapInFair:self.fair]; + [self.navigationController pushViewController:viewController animated:YES]; + } + }; +} + +- (NSDictionary *)buttonDescriptionForFeaturedLink:(FeaturedLink *)featuredLink buttonClass:(Class)buttonClass +{ + @weakify(self); + return @{ + ARNavigationButtonClassKey: buttonClass, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARButtonWithImage.new, title): featuredLink.title ?: NSNull.null, + @keypath(ARButtonWithImage.new, subtitle): featuredLink.subtitle ?: NSNull.null, + @keypath(ARButtonWithImage.new, imageURL): featuredLink.smallSquareImageURL ?: NSNull.null, + @keypath(ARButtonWithImage.new, targetURL): [NSURL URLWithString:featuredLink.href] ?: NSNull.null + }, + ARNavigationButtonHandlerKey: ^(UIButton *button) { + @strongify(self); + if ([button isKindOfClass:[ARButtonWithImage class]]) { + ARButtonWithImage *buttonWithImage = (ARButtonWithImage *) button; + [self buttonPressed:buttonWithImage]; + } else { + ARActionLog(@"Clicked %@", button); + } + } + }; +} + +- (void)buttonPressed:(ARButtonWithImage *)buttonWithImage +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadURL:buttonWithImage.targetURL fair:self.fair]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +#pragma mark ARBrowseFeaturedLinksCollectionViewDelegate + +- (void)didSelectFeaturedLink:(FeaturedLink *)featuredLink +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:featuredLink.href]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +#pragma mark ARFairPostsViewControllerDelegate + +- (void)didSelectPost:(NSString *)postURL +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:postURL]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +#pragma mark - ARSearchFieldButtonDelegate + +- (void)searchFieldButtonWasPressed:(ARSearchFieldButton *)sender +{ + NSAssert(self.searchVC == nil, @"Trying to replace existing search view controller. "); + + self.searchVC = [[ARFairSearchViewController alloc] initWithFair:self.fair]; + self.searchVC.delegate = self; + self.searchVC.view.alpha = 0.0f; + [self ar_addModernChildViewController:self.searchVC]; + [self.searchVC.view constrainWidthToView:self.view predicate:@"0@1000"]; + [self.searchVC.view constrainHeightToView:self.view predicate:@"0@1000"]; + [self.searchVC.view alignCenterWithView:self.view]; + self.stackView.scrollEnabled = NO; + + [UIView animateIf:self.animatesSearchBehavior duration:ARAnimationDuration :^{ + self.searchVC.view.alpha = 1.0; + }]; +} + +#pragma mark - ARFairSearchViewControllerDelegate + +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query +{ + ARSwitchBoard *switchBoard = [ARSwitchBoard sharedInstance]; + if (result.model == [Artwork class]) { + UIViewController *controller = [switchBoard loadArtworkWithID:result.modelID inFair:self.fair]; + [self.navigationController pushViewController:controller animated:YES]; + + } else if (result.model == [Artist class]) { + Artist *artist = [[Artist alloc] initWithArtistID:result.modelID]; + UIViewController *controller = [switchBoard loadArtistWithID:artist.artistID inFair:self.fair]; + [self.navigationController pushViewController:controller animated:YES]; + + } else if (result.model == [Gene class]) { + UIViewController *controller = [switchBoard loadGeneWithID:result.modelID]; + [self.navigationController pushViewController:controller animated:YES]; + + } else if (result.model == [Profile class]) { + UIViewController *controller = [ARSwitchBoard.sharedInstance routeProfileWithID:result.modelID]; + [self.navigationController pushViewController:controller animated:YES]; + + } else if (result.model == [SiteFeature class]) { + NSString *path = NSStringWithFormat(@"/feature/%@", result.modelID); + UIViewController *controller = [[ARSwitchBoard sharedInstance] loadPath:path]; + [self.navigationController pushViewController:controller animated:YES]; + + } else if (result.model == [PartnerShow class]) { + PartnerShow *partnerShow = [[PartnerShow alloc] initWithShowID:result.modelID]; + UIViewController *controller = [switchBoard loadShowWithID:partnerShow.showID fair:self.fair]; + [self.navigationController pushViewController:controller animated:YES]; + } +} + +- (void)cancelledSearch:(ARFairSearchViewController *)controller +{ + self.stackView.scrollEnabled = YES; + [UIView animateIf:self.animatesSearchBehavior duration:ARAnimationDuration :^{ + self.searchVC.view.alpha = 0.0; + } completion:^(BOOL finished) { + [self ar_removeChildViewController:self.searchVC], self.searchVC = nil; + }]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFavoriteItemModule.h b/Artsy/Classes/View Controllers/ARFavoriteItemModule.h new file mode 100644 index 00000000000..7629b924e91 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFavoriteItemModule.h @@ -0,0 +1,5 @@ +#import "ARModelCollectionViewModule.h" + +@interface ARFavoriteItemModule : ARModelCollectionViewModule +@property (nonatomic, strong) UICollectionViewFlowLayout *moduleLayout; +@end diff --git a/Artsy/Classes/View Controllers/ARFavoriteItemModule.m b/Artsy/Classes/View Controllers/ARFavoriteItemModule.m new file mode 100644 index 00000000000..059ff3d46a3 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFavoriteItemModule.m @@ -0,0 +1,31 @@ +#import "ARFavoriteItemModule.h" +#import "ARFavoriteItemViewCell.h" + +@implementation ARFavoriteItemModule + +- (instancetype)init +{ + self = [super init]; + if (!self) {return nil;} + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + CGFloat sideMargin = [UIDevice isPad] ? 50 : 20; + layout.sectionInset = UIEdgeInsetsMake(20, sideMargin, 20, sideMargin); + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + CGFloat width = [ARFavoriteItemViewCell widthForCellWithOrientation:orientation]; + CGFloat height = [ARFavoriteItemViewCell heightForCellWithOrientation:orientation]; + layout.itemSize = (CGSize){ width, height }; + _moduleLayout = layout; + return self; +} + +- (Class)classForCell +{ + return [ARFavoriteItemViewCell class]; +} + +- (ARFeedItemImageSize)imageSize +{ + return ARFeedItemImageSizeLarge; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFavoritesViewController.h b/Artsy/Classes/View Controllers/ARFavoritesViewController.h new file mode 100644 index 00000000000..f7f0a48aacd --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFavoritesViewController.h @@ -0,0 +1,9 @@ +typedef NS_ENUM(NSInteger, ARFavoritesDisplayMode) { + ARFavoritesDisplayModeArtworks, + ARFavoritesDisplayModeArtists, + ARFavoritesDisplayModeGenes +}; + +@interface ARFavoritesViewController : UIViewController +@property (nonatomic, assign, readwrite) ARFavoritesDisplayMode displayMode; +@end diff --git a/Artsy/Classes/View Controllers/ARFavoritesViewController.m b/Artsy/Classes/View Controllers/ARFavoritesViewController.m new file mode 100644 index 00000000000..01d7c57d65b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFavoritesViewController.m @@ -0,0 +1,334 @@ +#import "ARFavoritesViewController.h" +#import "ARArtworkMasonryModule.h" +#import "UIViewController+SimpleChildren.h" +#import "AREmbeddedModelsViewController.h" +#import "ORStackView+ArtsyViews.h" +#import "ARHeartButton.h" +#import "ARArtworkSetViewController.h" +#import "ARArtworkFavoritesNetworkModel.h" +#import "ARGeneFavoritesNetworkModel.h" +#import "ARArtistFavoritesNetworkModel.h" +#import "ARSwitchView+Favorites.h" +#import "ARFavoriteItemModule.h" +#import "ARArtistViewController.h" +#import "ARGeneViewController.h" +#import "ARFavoriteItemViewCell.h" + +@interface ARFavoritesViewController() + +@property (nonatomic, strong, readonly) AREmbeddedModelsViewController *embeddedItemsVC; +@property (nonatomic, strong, readonly) UILabel *noFavoritesInfoLabel; +@property (nonatomic, strong, readonly) ORStackView *headerContainerView; +@property (nonatomic, weak) UIView *emptyStateView; + +@property (nonatomic, strong, readwrite) ARFavoritesNetworkModel *activeNetworkModel; + +@property (nonatomic, strong, readonly) ARArtworkFavoritesNetworkModel *artworkFavoritesNetworkModel; +@property (nonatomic, strong, readonly) ARArtworkMasonryModule *artworksModule; +@property (nonatomic, strong, readonly) ARArtistFavoritesNetworkModel *artistFavoritesNetworkModel; +@property (nonatomic, strong, readonly) ARFavoriteItemModule *artistsModule; +@property (nonatomic, strong, readonly) ARGeneFavoritesNetworkModel *geneFavoritesNetworkModel; +@property (nonatomic, strong, readonly) ARFavoriteItemModule *genesModule; + +@property (nonatomic, strong) dispatch_queue_t artworkPageQueue; +@property (nonatomic, assign) BOOL artworkPageQueueSuspended; + +@end + +@implementation ARFavoritesViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + _embeddedItemsVC = [[AREmbeddedModelsViewController alloc] init]; + return self; +} + +- (void)dealloc +{ + [self.embeddedItemsVC.collectionView setDelegate:nil]; + [self resumePageQueue]; +} + +- (void)resumePageQueue +{ + if (self.artworkPageQueue && self.artworkPageQueueSuspended) { + dispatch_resume(self.artworkPageQueue); + _artworkPageQueueSuspended = NO; + } +} + +- (void)suspendPageQueue +{ + if (self.artworkPageQueue && ! self.artworkPageQueueSuspended) { + dispatch_suspend(self.artworkPageQueue); + _artworkPageQueueSuspended = YES; + } +} + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + self.embeddedItemsVC.delegate = self; + self.embeddedItemsVC.showTrailingLoadingIndicator = YES; + + _headerContainerView = [[ORStackView alloc] init]; + [self.headerContainerView addPageTitleWithString:@"FAVORITES"]; + [self.headerContainerView addWhiteSpaceWithHeight:[UIDevice isPad] ? @"42" : @"28"]; + self.headerContainerView.bottomMarginHeight = 23; + ARSwitchView *switchView = [[ARSwitchView alloc] initWithButtonTitles:[ARSwitchView favoritesButtonsTitlesArray]]; + switchView.delegate = self; + + // Switch view width should be divisible by the number of items (in this case 3) for consistent rendering. + + NSString *switchViewWidth = [UIDevice isPad] ? @"399" : @"279"; + [switchView constrainWidth:switchViewWidth]; + + [self.headerContainerView addSubview:switchView withTopMargin:@"0"]; + [switchView alignCenterXWithView:self.headerContainerView predicate:0]; + + self.embeddedItemsVC.headerView = self.headerContainerView; + self.embeddedItemsVC.headerHeight = self.headerHeight; + + [self ar_addModernChildViewController:self.embeddedItemsVC]; + [self.embeddedItemsVC.view alignToView:self.view]; + + self.embeddedItemsVC.collectionView.showsVerticalScrollIndicator = YES; + + if (!self.artworkPageQueue) { + self.artworkPageQueue = dispatch_queue_create("Favorite Artworks Pages", NULL); + } else { + [self resumePageQueue]; + } + + _artworkFavoritesNetworkModel = [[ARArtworkFavoritesNetworkModel alloc] init]; + _artistFavoritesNetworkModel = [[ARArtistFavoritesNetworkModel alloc] init]; + _geneFavoritesNetworkModel = [[ARGeneFavoritesNetworkModel alloc] init]; + + ARArtworkMasonryLayout layout = [UIDevice isPad] ? [self masonryLayoutForPadWithOrientation:[[UIApplication sharedApplication] statusBarOrientation]] : ARArtworkMasonryLayout2Column; + _artworksModule = [ARArtworkMasonryModule masonryModuleWithLayout:layout andStyle:AREmbeddedArtworkPresentationStyleArtworkMetadata]; + _artworksModule.layoutProvider = self; + _artistsModule = [[ARFavoriteItemModule alloc] init]; + _genesModule = [[ARFavoriteItemModule alloc] init]; + + self.displayMode = ARFavoritesDisplayModeArtworks; + + self.embeddedItemsVC.scrollDelegate = [ARScrollNavigationChief chief]; + + [self setModuleItemSizesForOrientation:[UIApplication sharedApplication].statusBarOrientation]; + [self.embeddedItemsVC.headerView updateConstraints]; + self.embeddedItemsVC.collectionView.scrollsToTop = YES; + + [super viewDidLoad]; +} + +- (CGFloat)headerHeight +{ + return [UIDevice isPad] ? 193 : 127; +} + +- (ARArtworkMasonryLayout)masonryLayoutForPadWithOrientation:(UIInterfaceOrientation)orientation +{ + return UIInterfaceOrientationIsLandscape(orientation) ? ARArtworkMasonryLayout4Column : ARArtworkMasonryLayout3Column; +} + +- (void)setModuleItemSizesForOrientation:(UIInterfaceOrientation)orientation +{ + CGFloat width = [ARFavoriteItemViewCell widthForCellWithOrientation:orientation]; + CGFloat height = [ARFavoriteItemViewCell heightForCellWithOrientation:orientation]; + + self.genesModule.moduleLayout.itemSize = (CGSize){ width, height }; + self.artistsModule.moduleLayout.itemSize = (CGSize){ width, height }; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + [self setModuleItemSizesForOrientation:(UIInterfaceOrientation)toInterfaceOrientation]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [self suspendPageQueue]; + self.embeddedItemsVC.scrollDelegate = nil; + + [super viewWillDisappear:animated]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.view layoutIfNeeded]; + [self updateView]; +} + +- (void)hideEmptyState +{ + if (self.emptyStateView) { + self.emptyStateView.hidden = YES; + self.embeddedItemsVC.headerHeight = self.headerHeight; + } +} + +- (void)showEmptyState +{ + if (self.emptyStateView) { + self.emptyStateView.hidden = NO; + } else { + + CGFloat emptyStateWidth = 270; + UIView *emptyStateView = [[UIView alloc] init]; + _emptyStateView = emptyStateView; + + UILabel *infoLabel = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:4]; + infoLabel.font = [UIFont serifFontWithSize:20]; + infoLabel.textAlignment = NSTextAlignmentCenter; + infoLabel.numberOfLines = 3; + infoLabel.text = @"Favorite artworks, artists and categories by tapping the heart icon throughout Artsy."; + [emptyStateView addSubview:infoLabel]; + _noFavoritesInfoLabel = infoLabel; + [infoLabel alignTopEdgeWithView:emptyStateView predicate:[UIDevice isPad] ? @"40" : @"0"]; + [infoLabel alignLeading:@"0" trailing:@"0" toView:emptyStateView]; + [infoLabel constrainWidthToView:emptyStateView predicate:@"0"]; + infoLabel.preferredMaxLayoutWidth = emptyStateWidth; + + ARHeartButton *heartButton = [[ARHeartButton alloc] init]; + [emptyStateView addSubview:heartButton]; + [heartButton constrainTopSpaceToView:infoLabel predicate:@"20"]; + [heartButton alignCenterXWithView:emptyStateView predicate:nil]; + [heartButton addTarget:self action:@selector(tappedNoArtworksHeart:) forControlEvents:UIControlEventTouchUpInside]; + [heartButton setStatus:ARHeartStatusNo]; + [emptyStateView alignBottomEdgeWithView:heartButton predicate:@"0"]; + + [self.headerContainerView addSubview:emptyStateView withTopMargin:@"40"]; + [emptyStateView constrainWidth:@(emptyStateWidth).stringValue]; + [emptyStateView alignCenterXWithView:self.headerContainerView predicate:@"0"]; + } + + [self.headerContainerView layoutIfNeeded]; + self.embeddedItemsVC.headerHeight = CGRectGetHeight(self.headerContainerView.frame); +} + +- (void)tappedNoArtworksHeart:(ARHeartButton *)button +{ + if (button.hearted) { + self.noFavoritesInfoLabel.text = @"Favorite artworks, artists and categories by tapping the heart icon throughout Artsy."; + } else { + self.noFavoritesInfoLabel.text = @"You got it!\nNow go favorite some artworks, artists and categories."; + } + + BOOL hearted = !button.hearted; + [button setHearted:hearted animated:YES]; +} + +- (void)setDisplayMode:(ARFavoritesDisplayMode)displayMode +{ + _displayMode = displayMode; + + if (displayMode == ARFavoritesDisplayModeArtworks) { + self.embeddedItemsVC.activeModule = self.artworksModule; + self.activeNetworkModel = self.artworkFavoritesNetworkModel; + } else if (displayMode == ARFavoritesDisplayModeArtists) { + self.embeddedItemsVC.activeModule = self.artistsModule; + self.activeNetworkModel = self.artistFavoritesNetworkModel; + } else if (displayMode == ARFavoritesDisplayModeGenes) { + self.embeddedItemsVC.activeModule = self.genesModule; + self.activeNetworkModel = self.geneFavoritesNetworkModel; + } +} + +- (void)updateView +{ + BOOL allDownloaded = self.activeNetworkModel.allDownloaded; + self.embeddedItemsVC.showTrailingLoadingIndicator = !allDownloaded; + [self.embeddedItemsVC.collectionView reloadData]; + if (!allDownloaded) { + [self checkContentSize]; + } else if (self.embeddedItemsVC.activeModule.items.count <= 0){ + [self showEmptyState]; + } +} + +- (void)checkContentSize +{ + CGFloat contentHeight = self.embeddedItemsVC.collectionView.contentSize.height; + CGFloat frameHeight = self.embeddedItemsVC.collectionView.frame.size.height; + // This will only be true the first time a tab is loaded. Get enough items to fill the height of the view. + if (contentHeight < frameHeight){ + [self getNextItemSet]; + } +} + +- (void)getNextItemSet +{ + ARFavoritesNetworkModel *networkModel = self.activeNetworkModel; + ARModelCollectionViewModule *module = self.embeddedItemsVC.activeModule; + if (networkModel.allDownloaded) { return; }; + @weakify(self); + dispatch_async(self.artworkPageQueue, ^{ + [self.activeNetworkModel getFavorites:^(NSArray *items){ + @strongify(self); + [self addItems:items toModule:module]; + } failure:nil]; + }); +} + +- (void)addItems:(NSArray *)items toModule:(ARModelCollectionViewModule *)module +{ + if (items.count > 0) { module.items = [module.items arrayByAddingObjectsFromArray:items]; } + if (module == self.embeddedItemsVC.activeModule) { [self updateView]; } +} + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +#pragma mark - AREmbeddedModelsDelegate + +-(void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + if (self.displayMode == ARFavoritesDisplayModeArtworks) { + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:self.embeddedItemsVC.items inFair:nil atIndex:index]; + [self.navigationController pushViewController:viewController animated:YES]; + } else if (self.displayMode == ARFavoritesDisplayModeArtists) { + Artist *artist = self.embeddedItemsVC.items[index]; + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadArtistWithID:artist.artistID]; + [self.navigationController pushViewController:viewController animated:YES]; + } else if (self.displayMode == ARFavoritesDisplayModeGenes) { + Gene *gene = self.embeddedItemsVC.items[index]; + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadGene:gene]; + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +- (void)embeddedModelsViewControllerDidScrollPastEdge:(AREmbeddedModelsViewController *)embeddedModelsViewController +{ + [self getNextItemSet]; +} + +#pragma mark - ARSwitchViewDelegate + + +- (void)switchView:(ARSwitchView *)switchView didPressButtonAtIndex:(NSInteger)buttonIndex animated:(BOOL)animated +{ + if (buttonIndex == ARSwitchViewFavoriteArtworksIndex) { + self.displayMode = ARFavoritesDisplayModeArtworks; + } else if (buttonIndex == ARSwitchViewFavoriteArtistsIndex) { + self.displayMode = ARFavoritesDisplayModeArtists; + } else if (buttonIndex == ARSwitchViewFavoriteCategoriesIndex) { + self.displayMode = ARFavoritesDisplayModeGenes; + } + [self hideEmptyState]; + [self updateView]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFeaturedArtworksViewController.h b/Artsy/Classes/View Controllers/ARFeaturedArtworksViewController.h new file mode 100644 index 00000000000..84d7e0cf128 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFeaturedArtworksViewController.h @@ -0,0 +1,5 @@ +#import + +@interface ARFeaturedArtworksViewController : UICollectionViewController +- (void)setArtworks:(NSArray *)artworks; +@end diff --git a/Artsy/Classes/View Controllers/ARFeaturedArtworksViewController.m b/Artsy/Classes/View Controllers/ARFeaturedArtworksViewController.m new file mode 100644 index 00000000000..fdef605b6ad --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFeaturedArtworksViewController.m @@ -0,0 +1,81 @@ +#import "ARFeaturedArtworksViewController.h" +#import "ARArtworkWithMetadataThumbnailCell.h" +#import "ARArtworkSetViewController.h" + +static NSString *FeaturedArtworkCellID = @"FeaturedArtworkCellID"; +static CGFloat ARFeaturedArtworksCellHeight = 260; + +@interface ARFeaturedArtworksViewController () +@property (nonatomic, strong, readwrite) NSArray *artworks; +@end + +@implementation ARFeaturedArtworksViewController + +- (instancetype)init +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.minimumInteritemSpacing = 20; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + self = [super initWithCollectionViewLayout:layout]; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [self.collectionView registerClass:[ARArtworkWithMetadataThumbnailCell class] forCellWithReuseIdentifier:FeaturedArtworkCellID]; + + [self.view constrainHeight:NSStringWithFormat(@"%f", [self preferredContentSize].height)]; + + self.collectionView.backgroundColor = [UIColor whiteColor]; + self.collectionView.dataSource = self; + self.collectionView.delegate = self; + self.collectionView.showsHorizontalScrollIndicator = NO; + self.collectionView.scrollsToTop = NO; + self.collectionView.contentInset = UIEdgeInsetsMake(0, [UIDevice isPad] ? 50 : 20, 0, 0); +} + +- (void)setArtworks:(NSArray *)artworks +{ + if ([artworks isEqualToArray:_artworks]) { + return; + } + _artworks = artworks; + [self.collectionView reloadData]; + [self.collectionView.collectionViewLayout invalidateLayout]; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return self.artworks.count; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + CGFloat imageHeight = ARFeaturedArtworksCellHeight - [ARArtworkWithMetadataThumbnailCell heightForMetaData]; + return CGSizeMake(imageHeight, ARFeaturedArtworksCellHeight); +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ARArtworkWithMetadataThumbnailCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:FeaturedArtworkCellID forIndexPath:indexPath]; + Artwork *artwork = _artworks[indexPath.row]; + [cell setupWithRepresentedObject:artwork]; + return cell; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:_artworks inFair:nil atIndex:indexPath.row]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (CGSize)preferredContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, ARFeaturedArtworksCellHeight); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFeedLinkUnitViewController.h b/Artsy/Classes/View Controllers/ARFeedLinkUnitViewController.h new file mode 100644 index 00000000000..b9e39930c77 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFeedLinkUnitViewController.h @@ -0,0 +1,5 @@ +#import "ARNavigationButtonsViewController.h" + +@interface ARFeedLinkUnitViewController : ARNavigationButtonsViewController +- (void) fetchLinks:(void (^)(void))completion; +@end diff --git a/Artsy/Classes/View Controllers/ARFeedLinkUnitViewController.m b/Artsy/Classes/View Controllers/ARFeedLinkUnitViewController.m new file mode 100644 index 00000000000..95f1f95143a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFeedLinkUnitViewController.m @@ -0,0 +1,92 @@ +#import "ARFeedLinkUnitViewController.h" +#import "ARNavigationButton.h" + +@implementation ARFeedLinkUnitViewController + +- (void)fetchLinks:(void (^)(void))completion +{ + if (ARIsRunningInDemoMode) { + FeaturedLink *link = [self defaultFeedLink]; + [self addButtonDescriptions:[self phoneNavigationForFeaturedLinks:@[link]]]; + + if (completion) { + completion(); + } + return; + } + + __block BOOL feedLinks = NO; + __block BOOL betaFeedLinks = NO; + + void (^completionCheck)() = ^(){ + if (feedLinks && betaFeedLinks) { + completion(); + } + }; + + @weakify(self); + + // edit set here: http://admin.artsy.net/set/52277573c9dc24da5b00020c + [ArtsyAPI getOrderedSetItemsWithKey:@"eigen:feed-links" success:^(NSArray *items) { + @strongify(self); + [self addButtonDescriptions:[self phoneNavigationForFeaturedLinks:items]]; + + feedLinks = YES; + completionCheck(); + } failure:^(NSError *error) { + feedLinks = YES; + completionCheck(); + }]; + + NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; + if ([bundleID containsString:@".dev"] || [bundleID containsString:@".beta"]) { + // edit set here: http://admin.artsy.net/set/5308e7be9c18db75fd000343 + [ArtsyAPI getOrderedSetItemsWithKey:@"eigen:beta-feed-links" success:^(NSArray *items) { + @strongify(self); + [self addButtonDescriptions:[self phoneNavigationForFeaturedLinks:items]]; + betaFeedLinks = YES; + completionCheck(); + } failure:^(NSError *error) { + betaFeedLinks = YES; + completionCheck(); + }]; + } else { + betaFeedLinks = YES; + completionCheck(); + } +} + +- (NSArray *)phoneNavigationForFeaturedLinks:(NSArray *)featuredLinks +{ + @weakify(self); + NSMutableArray *phoneNavigation = [NSMutableArray array]; + for(FeaturedLink *featuredLink in featuredLinks) { + [phoneNavigation addObject:@{ + ARNavigationButtonClassKey: ARSerifNavigationButton.class, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARSerifNavigationButton.new, title): featuredLink.title, + @keypath(ARSerifNavigationButton.new, subtitle): featuredLink.subtitle + }, + ARNavigationButtonHandlerKey: ^(UIButton *sender) { + @strongify(self); + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:featuredLink.href]; + [self.navigationController pushViewController:viewController animated:YES]; + } + }]; + } + return phoneNavigation; +} + +- (FeaturedLink *)defaultFeedLink +{ + return [FeaturedLink modelWithJSON:@{ + @"id": @"52277695c9dc2405b000022b", + @"image_url": @"http://static1.artsy.net/featured_links/52277695c9dc2405b000022b/:version.jpg", + @"title": @"Featured Works For Sale", + @"subtitle" : @"", + @"href": @"http://m.artsy.net/home/featured_works", + @"display_on_mobile": @YES + }]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARFeedViewController.h b/Artsy/Classes/View Controllers/ARFeedViewController.h new file mode 100644 index 00000000000..c3eff9fe72b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFeedViewController.h @@ -0,0 +1,30 @@ +#import "ARFeedConstants.h" +#import "ARFeedTimeline.h" + +/** + The ARFeedVC shows items in a feed using an ARFeedTimeline, a feed + is generally a collection of ARFeedItem subclasses that you can optionally filter. + + It is expected that people would subclass it and can use the + tableViewHeader/tableViewFooter to provide additional meta-data around the feed +*/ + +@interface ARFeedViewController : UIViewController < UITableViewDataSource, UITableViewDelegate, UIViewControllerRestoration> + +/// The designated initializer +- (instancetype)initWithFeedTimeline:(ARFeedTimeline *)feedTimeline; + + +- (void)refreshFeed; +- (void)loadNextFeedPage; + +- (void)presentLoadingView; +- (void)hideLoadingView; + +/// Overriding this in subclasses allows you to make changes to the subclasses +- (void)setupTableView; + +@property (nonatomic, strong, readonly) ARFeedTimeline *feedTimeline; +@property (nonatomic, strong, readonly) UITableView *tableView; +@property (nonatomic, readonly, assign, getter=isOnboarding) BOOL onboarding; +@end diff --git a/Artsy/Classes/View Controllers/ARFeedViewController.m b/Artsy/Classes/View Controllers/ARFeedViewController.m new file mode 100644 index 00000000000..eb895adeece --- /dev/null +++ b/Artsy/Classes/View Controllers/ARFeedViewController.m @@ -0,0 +1,284 @@ +// If we choose to add a pull to refresh +// : http://stackoverflow.com/a/14148118/385754 + +#import "ARFeedViewController.h" +#import "ARFeedItem.h" + +#import "ARReusableLoadingView.h" +#import "ARFeedStatusIndicatorTableViewCell.h" +#import "UIViewController+ARStateRestoration.h" +#import "ARModernPartnerShowTableViewCell.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARtsyAPI+Private.h" + +@interface ARFeedViewController() +@property (nonatomic, strong) ARReusableLoadingView *loadingView; +@property (nonatomic, strong) ARFeedTimeline *feedTimeline; +@property (nonatomic, strong) UITableView *tableView; +@property (nonatomic, assign) enum ARFeedStatusState footerState; +@property (nonatomic, readonly) NSDate *refreshDateTime; +@property (nonatomic, readonly) BOOL shouldRefreshFeed; +@property (nonatomic, readwrite, assign) NSInteger refreshFeedInterval; +@property (nonatomic, readonly, assign) BOOL loading; +@end + +@implementation ARFeedViewController + +- (instancetype)initWithFeedTimeline:(ARFeedTimeline *)feedTimeline +{ + self = [super init]; + if(!self) return nil; + + _feedTimeline = feedTimeline; + _footerState = ARFeedStatusStateLoading; + _refreshFeedInterval = 60 * 60 * 2; + _loading = NO; + + return self; +} + +- (void)viewDidLoad +{ + self.view.backgroundColor = [UIColor blackColor]; + + [self setupTableView]; + [self.tableView registerClass:[ARModernPartnerShowTableViewCell class] forCellReuseIdentifier:@"PartnerShowCellIdentifier"]; + + [super viewDidLoad]; +} + +- (void)setupTableView +{ + UITableView *tableView = [[[self classForTableView] alloc] init]; + tableView.separatorStyle = UITableViewCellSelectionStyleNone; + tableView.dataSource = self; + tableView.delegate = self; + tableView.backgroundColor = [UIColor whiteColor]; + tableView.restorationIdentifier = @"ARFeedTableViewRID"; + + [self.view addSubview:tableView]; + + [tableView alignToView:self.view]; + self.tableView = tableView; +} + +- (Class)classForTableView +{ + return [UITableView class]; +} + +// refresh feed every 2 hours by default, otherwise just keep loading more items +- (BOOL)shouldRefreshFeed +{ + return self.refreshDateTime && [[NSDate date] timeIntervalSinceDate:self.refreshDateTime] > self.refreshFeedInterval; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + if (self.shouldRefreshFeed) { + [self refreshFeedItems]; + } +} + +- (BOOL)prefersStatusBarHidden +{ + return NO; +} + +// during onboarding the feed is shown, so don't display it again +- (BOOL)isOnboarding +{ + return ![User currentUser] && ![User isTrialUser]; +} + +- (void)refreshFeed +{ + if (self.loading) { + return; + } + + _loading = YES; + [self refreshFeedItems]; + + _refreshDateTime = [NSDate date]; + _loading = NO; +} + +- (void)refreshFeedItems +{ + @weakify(self) + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + [self.feedTimeline getNewItems:^{ + @strongify(self); + [self hideLoadingView]; + [self.tableView reloadData]; + } failure:^(NSError *error) { + ARErrorLog(@"There was an error getting newest items for the feed: %@", error.localizedDescription); + [self performSelector:@selector(refreshFeed) withObject:nil afterDelay:3]; + }]; + }]; +} + +#pragma mark - Table view data source + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if(indexPath.row == self.feedTimeline.numberOfItems) { + return [ARFeedStatusIndicatorTableViewCell heightForFeedItemWithState:_footerState]; + } else { + ARFeedItem *item = [self.feedTimeline itemAtIndex:indexPath.row]; + return [ARModernPartnerShowTableViewCell heightForItem:item]; + } +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.feedTimeline.numberOfItems + 1; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if(indexPath.row == self.feedTimeline.numberOfItems) { + return [ARFeedStatusIndicatorTableViewCell cellWithInitialState:_footerState]; + } else { + ARFeedItem *feedItem = [self.feedTimeline itemAtIndex:indexPath.row]; + ARModernPartnerShowTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:feedItem.cellIdentifier forIndexPath:indexPath]; + cell.delegate = self; + [cell configureWithFeedItem:feedItem]; + return cell; + } +} + +- (void)loadNextFeedPage +{ + if (self.loading || ! self.feedTimeline.hasNext || self.feedTimeline.loading) { + return; + } + + _loading = YES; + [self setFooterStatus:ARFeedStatusStateLoading]; + + @weakify(self) + NSInteger oldCount = self.feedTimeline.numberOfItems; + + [self.feedTimeline getNextPage:^{ + @strongify(self); + if (!self) { return; } + + NSMutableArray *indexPaths = [NSMutableArray array]; + NSInteger newCount = self.feedTimeline.numberOfItems; + for (NSInteger i = oldCount; i < newCount; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; + } + [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + self->_loading = NO; + } + failure:^(NSError *error) { + @strongify(self); + if (!self) { return; } + + // add a "network error, retry?" state to footer + ARErrorLog(@"There was an error getting next feed page: %@", error.localizedDescription); + [self setFooterStatus:ARFeedStatusStateNetworkError]; + self->_loading = NO; + } + completion:^{ + @strongify(self); + if (!self) { return; } + + [self setFooterStatus:ARFeedStatusStateEndOfFeed]; + self->_loading = NO; + }]; +} + +- (void)setFooterStatus:(ARFeedStatusState)state +{ + _footerState = state; + + NSIndexPath *lastItem = [NSIndexPath indexPathForRow:self.feedTimeline.numberOfItems inSection:0]; + id cell = [self.tableView cellForRowAtIndexPath:lastItem]; + + if ([cell isKindOfClass:[ARFeedStatusIndicatorTableViewCell class]]) { + // Animate the height change + [self.tableView beginUpdates]; + + [cell setState:state]; + [self.tableView reloadRowsAtIndexPaths:@[lastItem] withRowAnimation:UITableViewRowAnimationFade]; + + [self.tableView endUpdates]; + [self.tableView scrollToRowAtIndexPath:lastItem atScrollPosition:UITableViewScrollPositionBottom animated:YES]; + } +} + +/// These are for the initial first load, show a centered progress indicator + +- (void)presentLoadingView +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; +} + +- (void)hideLoadingView +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; +} + +#pragma mark - Table view delegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ +} + +#pragma mark - Scroll view delegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + // nav transitions wanna send us scroll events after the transition and we are all like + // nuh-uh + + if (self.navigationController.topViewController == self && scrollView == self.tableView) { + [[ARScrollNavigationChief chief] scrollViewDidScroll:scrollView]; + } + + if((scrollView.contentSize.height - scrollView.contentOffset.y) < scrollView.bounds.size.height) { + [self loadNextFeedPage]; + } +} + +#pragma mark - +#pragma mark State Restoration + ++ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder +{ + ARFeedViewController *mainFeedViewController = [[ARFeedViewController alloc] init]; + [mainFeedViewController setupRestorationIdentifierAndClass]; + + return mainFeedViewController; +} + +#pragma mark - Orientation + +-(BOOL)shouldAutorotate +{ + return NO; +} + +-(NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown; +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation +{ + return UIInterfaceOrientationPortrait; +} + +#pragma mark - ARModernPartnerShowTableViewCellDelegate + +-(void)modernPartnerShowTableViewCell:(ARModernPartnerShowTableViewCell *)cell shouldShowViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:YES]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARHeroUnitViewController.h b/Artsy/Classes/View Controllers/ARHeroUnitViewController.h new file mode 100644 index 00000000000..b8c54c537a6 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARHeroUnitViewController.h @@ -0,0 +1,9 @@ +@class ARHeroUnitsNetworkModel; + +@interface ARHeroUnitViewController : UIViewController + +@property (nonatomic, strong) ARHeroUnitsNetworkModel *heroUnitNetworkModel; + +- (void)fetchHeroUnits; + +@end diff --git a/Artsy/Classes/View Controllers/ARHeroUnitViewController.m b/Artsy/Classes/View Controllers/ARHeroUnitViewController.m new file mode 100644 index 00000000000..92d7b659c9b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARHeroUnitViewController.m @@ -0,0 +1,234 @@ +#import "ARHeroUnitViewController.h" +#import "ARSiteHeroUnitView.h" +#import "ARHeroUnitsNetworkModel.h" + +const static CGFloat ARHeroUnitDotsHeight = 30; +const static CGFloat ARCarouselDelay = 10; + +@interface ARHeroUnitViewController () +@property (nonatomic, strong) UIPageControl *pageControl; +@property (nonatomic) CAGradientLayer *shadowLayer; +@property (nonatomic, strong) UIPageViewController *pageViewController; +@property (nonatomic, strong) NSTimer *timer; +@end + +@interface ARSiteHeroUnitViewController : UIViewController +- (instancetype)initWithHeroUnit:(SiteHeroUnit *)heroUnit andIndex:(NSInteger)index; +@property (nonatomic, assign) NSInteger index; +@property (nonatomic, assign) SiteHeroUnit *heroUnit; +@end + +@implementation ARHeroUnitViewController + ++ (CGFloat)heroUnitHeight +{ + return [UIDevice isPad] ? 380 : 232; +} + +- (void)loadView +{ + [super loadView]; + [self.view constrainHeight:@([self.class heroUnitHeight]).stringValue]; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.userInteractionEnabled = NO; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; + self.pageViewController.dataSource = self; + self.pageViewController.delegate = self; + [self addChildViewController:self.pageViewController]; + [self.view addSubview:self.pageViewController.view]; + [self.pageViewController didMoveToParentViewController:self]; + + self.pageControl = [[UIPageControl alloc] init]; + self.pageControl.frame = CGRectMake(0, [self.class heroUnitHeight] - ARHeroUnitDotsHeight, CGRectGetWidth(self.view.bounds), ARHeroUnitDotsHeight); + self.pageControl.hidesForSinglePage = YES; + self.pageControl.pageIndicatorTintColor = [UIColor colorWithWhite:1 alpha:0.5]; + self.pageControl.currentPageIndicatorTintColor = [UIColor whiteColor]; + + CAGradientLayer *shadowLayer = [CAGradientLayer layer]; + shadowLayer.colors = @[(id)[UIColor colorWithWhite:0 alpha:.4].CGColor, + (id)[UIColor colorWithWhite:0 alpha:.12].CGColor, + (id)[UIColor colorWithWhite:0 alpha:0].CGColor]; + + shadowLayer.startPoint = CGPointMake(0, 1); + shadowLayer.endPoint = CGPointMake(0, 0.8); + shadowLayer.shouldRasterize = YES; + + [self.view.layer insertSublayer:shadowLayer below:self.pageControl.layer]; + self.shadowLayer = shadowLayer; + + [self.view insertSubview:self.pageControl aboveSubview:self.pageViewController.view]; +} + +- (void)viewDidLayoutSubviews +{ + self.shadowLayer.frame = self.view.bounds; + [super viewDidLayoutSubviews]; +} + +- (void)fetchHeroUnits +{ + @weakify(self); + [self.heroUnitNetworkModel getHeroUnitsWithSuccess:^{ + @strongify(self); + + // Should never be false in production, but will cause problems in development if false on staging. + NSArray *heroUnits = self.heroUnitNetworkModel.heroUnits; + BOOL hasHeroUnits = heroUnits.count >= 1; + if (!hasHeroUnits) { return; } + + self.view.userInteractionEnabled = YES; + [self updateViewWithHeroUnits:heroUnits]; + [self startTimer]; + + } failure:^(NSError *error) { + [self performSelector:_cmd withObject:nil afterDelay:3]; + }]; +} + +- (void)updateViewWithHeroUnits:(NSArray *)heroUnits +{ + self.pageControl.numberOfPages = heroUnits.count; + ARSiteHeroUnitViewController *initialVC = [self viewControllerForIndex:0]; + if (!initialVC) { return; } + NSArray *initialVCs = @[initialVC]; + [self.pageViewController setViewControllers:initialVCs direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self fetchHeroUnits]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + [self cancelTimer]; +} + +- (CGSize)preferredContentSize +{ + return (CGSize){UIViewNoIntrinsicMetric, [self.class heroUnitHeight]}; +} + +- (ARSiteHeroUnitViewController *)currentViewController +{ + return self.pageViewController.viewControllers.count > 0 ? self.pageViewController.viewControllers[0] : nil; +} + +- (void)startTimer +{ + if (self.heroUnitNetworkModel.heroUnits.count <= 1) { return; } + [self cancelTimer]; + self.timer = [NSTimer scheduledTimerWithTimeInterval:ARCarouselDelay target:self selector:@selector(goToNextHeroUnit) userInfo:nil repeats:YES]; +} + +- (void)cancelTimer +{ + if (self.timer) { + [self.timer invalidate]; + self.timer = nil; + } +} + +-(void)goToNextHeroUnit +{ + self.pageViewController.view.userInteractionEnabled = NO; + UIViewController *nextVC = [self pageViewController:self.pageViewController viewControllerAfterViewController:[self currentViewController]]; + @weakify(self); + [self.pageViewController setViewControllers:@[nextVC] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished) { + @strongify(self); + [self.pageControl setCurrentPage:[self currentViewController].index]; + self.pageViewController.view.userInteractionEnabled = YES; + + }]; +} + +#pragma mark - UIPageViewControllerDataSource + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(ARSiteHeroUnitViewController *)viewController +{ + if (self.heroUnitNetworkModel.heroUnits.count == 1) { return nil; } + + NSInteger newIndex = viewController.index - 1; + if (newIndex < 0) { + newIndex = self.heroUnitNetworkModel.heroUnits.count - 1; + } + + return [self viewControllerForIndex:newIndex ?: 0]; +} + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(ARSiteHeroUnitViewController *)viewController +{ + if (self.heroUnitNetworkModel.heroUnits.count == 1) { return nil; } + + NSInteger newIndex = (viewController.index + 1) % self.heroUnitNetworkModel.heroUnits.count; + return [self viewControllerForIndex:newIndex ?: 0]; +} + +- (ARSiteHeroUnitViewController *)viewControllerForIndex:(NSInteger)index +{ + if (index < 0 || index >= self.heroUnitNetworkModel.heroUnits.count) { return nil; } + + SiteHeroUnit *heroUnit = self.heroUnitNetworkModel.heroUnits[index]; + ARSiteHeroUnitViewController *viewController = [[ARSiteHeroUnitViewController alloc] initWithHeroUnit:heroUnit andIndex:index]; + return viewController; +} + +#pragma mark - UIPageViewControllerDelegate + +- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers +{ + [self cancelTimer]; +} + +- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed +{ + if (completed) { + [self.pageControl setCurrentPage:[self currentViewController].index]; + } + [self startTimer]; +} + +@end + +@implementation ARSiteHeroUnitViewController + +- (instancetype)initWithHeroUnit:(SiteHeroUnit *)heroUnit andIndex:(NSInteger)index +{ + self = [super init]; + if (!self) { return nil; } + _heroUnit = heroUnit; + _index = index; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedUnit:)]; + tapGesture.cancelsTouchesInView = NO; + [self.view addGestureRecognizer:tapGesture]; + + ARSiteHeroUnitView *heroUnitView = [[ARSiteHeroUnitView alloc] initWithFrame:self.view.bounds unit:self.heroUnit]; + // We can't use autoresizing masks in a view controller contained in a UIPageViewController + // See: http://stackoverflow.com/questions/17729336/uipageviewcontroller-auto-layout-rotation-issue + heroUnitView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:heroUnitView]; +} + +- (void)tappedUnit:(id)sender +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadPath:self.heroUnit.link]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +@end \ No newline at end of file diff --git a/Artsy/Classes/View Controllers/ARImagePageViewController.h b/Artsy/Classes/View Controllers/ARImagePageViewController.h new file mode 100644 index 00000000000..a61bb50bcb3 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARImagePageViewController.h @@ -0,0 +1,12 @@ +#import + +@interface ARImagePageViewController : UIPageViewController + +- (id)initWithTransitionStyle:(UIPageViewControllerTransitionStyle)style navigationOrientation:(UIPageViewControllerNavigationOrientation)navigationOrientation options:(NSDictionary *)options __attribute__((unavailable("Please use init."))); + +@property (nonatomic, copy) NSArray *images; +@property (nonatomic, assign) UIViewContentMode imageContentMode; + +- (void)setHidesPageIndicators:(BOOL)hidden; + +@end diff --git a/Artsy/Classes/View Controllers/ARImagePageViewController.m b/Artsy/Classes/View Controllers/ARImagePageViewController.m new file mode 100644 index 00000000000..32b8c81d70f --- /dev/null +++ b/Artsy/Classes/View Controllers/ARImagePageViewController.m @@ -0,0 +1,133 @@ +#import "ARImagePageViewController.h" + +@interface ARImageViewController : UIViewController + +- (instancetype)initWithImageURL:(NSURL *)imageURL contentMode:(UIViewContentMode)contentMode index:(NSInteger)index; + +@property (nonatomic, assign) NSInteger index; +@property (nonatomic, assign) UIViewContentMode contentMode; +@property (nonatomic, strong) NSURL *imageURL; + +@end + +@interface ARImagePageViewController () +@end + +@implementation ARImagePageViewController + +- (id)init +{ + self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; + if(!self) return nil; + + self.dataSource = self; + _images = [NSArray array]; + _imageContentMode = UIViewContentModeScaleAspectFit; + + UIPageControl *pageControl = [UIPageControl appearanceWhenContainedIn:[ARImagePageViewController class], nil]; + pageControl.pageIndicatorTintColor = [UIColor artsyMediumGrey]; + pageControl.currentPageIndicatorTintColor = [UIColor blackColor]; + + return self; +} + +- (void)setImages:(NSArray *)images +{ + if(_images.count == 0 && images.count > 0){ + _images = images.copy; + + NSArray *initialVCs = @[[self viewControllerForIndex:0]]; + [self setViewControllers:initialVCs direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; + + } else { + _images = images.copy; + } +} + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(ARImageViewController *)viewController +{ + if (self.images.count == 1) { + return nil; + } + + NSInteger newIndex = viewController.index - 1; + if (newIndex < 0) { + newIndex = self.images.count - 1; + } + return [self viewControllerForIndex:newIndex]; +} + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(ARImageViewController *)viewController +{ + if (self.images.count == 1) { + return nil; + } + + NSInteger newIndex = (viewController.index + 1) % self.images.count; + return [self viewControllerForIndex:newIndex]; +} + +- (ARImageViewController *)viewControllerForIndex:(NSInteger)index +{ + if (index < 0 || index >= self.images.count) { + return nil; + } + + Image *image = self.images[index]; + ARImageViewController *viewController = [[ARImageViewController alloc] initWithImageURL:image.urlForThumbnailImage contentMode:self.imageContentMode index:index]; + + return viewController; +} + +- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController +{ + return self.images.count; +} + +- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController +{ + return [pageViewController.viewControllers.firstObject index]; +} + +- (void)setHidesPageIndicators:(BOOL)hidden +{ + UIPageControl *pageControl = [[self.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(class = %@)", [UIPageControl class]]] lastObject]; + pageControl.hidden = hidden; +} + +@end + +@implementation ARImageViewController + +- (instancetype)initWithImageURL:(NSURL *)imageURL contentMode:(UIViewContentMode)contentMode index:(NSInteger)index { + self = [super initWithNibName:nil bundle:nil]; + if (!self) { return nil; } + + self.imageURL = imageURL; + self.contentMode = contentMode; + self.index = index; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + UIImageView *imageView = [[UIImageView alloc] init]; + imageView.contentMode = self.contentMode; + imageView.frame = self.view.bounds; + + if ([self.imageURL isFileURL]) { + imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:self.imageURL]]; + } else { + [imageView ar_setImageWithURL:self.imageURL]; + } + + // We can't use autoresizing masks in a view controller contained in a UIPageViewController + // See: http://stackoverflow.com/questions/17729336/uipageviewcontroller-auto-layout-rotation-issue + imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:imageView]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARInquireForArtworkViewController.h b/Artsy/Classes/View Controllers/ARInquireForArtworkViewController.h new file mode 100644 index 00000000000..054761a3615 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARInquireForArtworkViewController.h @@ -0,0 +1,28 @@ +/// The InquireForArtworkVC is a modal view which allows +/// the user to contact either the gallery or their representative + +typedef NS_ENUM(NSInteger, ARInquireState) { + ARInquireStateRepresentative, + ARInquireStatePartner +}; + +@interface ARInquireForArtworkViewController : UIViewController + +/// Create a Inquire form for contacting an artsy specialist +- (instancetype)initWithAdminInquiryForArtwork:(Artwork *)artwork fair:(Fair *)fair; + +/// Create a Inquire form for contacting the gallery +- (instancetype)initWithPartnerInquiryForArtwork:(Artwork *)artwork fair:(Fair *)fair; + +@property (nonatomic, strong, readonly) Fair *fair; +@property (nonatomic, strong, readonly) Artwork *artwork; +@property (nonatomic, assign, readonly) enum ARInquireState state; + +@property (nonatomic, strong, readonly) NSString *inquiryURLRepresentation; + +/// The Inquiry form will present itself modally over the given view controllers +- (void)presentFormWithInquiryURLRepresentation:(NSString *)inquiryURLRepresentation; + +- (NSString *)body; + +@end diff --git a/Artsy/Classes/View Controllers/ARInquireForArtworkViewController.m b/Artsy/Classes/View Controllers/ARInquireForArtworkViewController.m new file mode 100644 index 00000000000..4d2e9f0fbe1 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARInquireForArtworkViewController.m @@ -0,0 +1,872 @@ +#import "ARInquireForArtworkViewController.h" +#import "ARUserManager.h" +#import "UIViewController+ScreenSize.h" +#import "UIView+HittestExpansion.h" +#import "ARAppDelegate.h" +#import + +#define USE_LIVE_DATA 1 +// Future TODO: Don't use image alpha on contact image, use grayscale'd image. + +// The state of the form, for enabling / disabling +typedef NS_ENUM(NSInteger, ARInquireFormState) { + ARInquireFormStateSendable, + ARInquireFormStateSending, + ARInquireFormStateSent, + ARInquireFormStateCancelled +}; + +@interface ARInquireForArtworkViewController() + +@property (nonatomic, assign) enum ARInquireState state; +@property (nonatomic, strong) Artwork *artwork; + +@property (nonatomic, strong) UIViewController *hostController; +@property (nonatomic, strong) NSString *inquiryURLRepresentation; + +@property (nonatomic, strong, readonly) UIView *inquiryFormView; +@property (nonatomic, strong, readonly) UIView *topMenuView; +@property (nonatomic, strong, readonly) UIView *specialistView; +@property (nonatomic, strong, readonly) UIView *artworkView; +@property (nonatomic, strong, readonly) UIView *contentSeparator; +@property (nonatomic, strong, readonly) UIView *nameEmailForm; +@property (nonatomic, strong, readonly) UITextField *nameInput; +@property (nonatomic, strong, readonly) UITextField *emailInput; +@property (nonatomic, strong, readonly) ALPValidator *emailValidator; +@property (nonatomic, strong, readonly) UIScrollView *contentView; +@property (nonatomic, strong, readonly) UIView *backgroundView; +@property (nonatomic, strong, readonly) UIView *topContainer; + +@property (nonatomic, strong, readonly) ARModalMenuButton *cancelButton; +@property (nonatomic, strong, readonly) ARModalMenuButton *sendButton; + +@property (nonatomic, strong, readonly) UILabel *messageTitleLabel; +@property (nonatomic, strong, readonly) UILabel *messageBodyLabel; +@property (nonatomic, strong, readonly) UIButton *failureDismissButton; +@property (nonatomic, strong, readonly) UIButton *failureTryAgainButton; + +@property (nonatomic, strong, readonly) UITextView *textView; +@property (nonatomic, strong, readonly) UILabel *userSignature; + +@property (nonatomic, strong, readonly) UIImageView *specialistHeadImage; +@property (nonatomic, strong, readonly) UILabel *specialistNameLabel; + +@property (nonatomic, strong, readonly) UIView *viewBehindKeyboard; +@property (nonatomic, strong, readonly) NSLayoutConstraint *keyboardHeightConstraint; +@property (nonatomic, strong, readonly) NSLayoutConstraint *keyboardPositionConstraint; +@property (nonatomic, strong, readonly) NSLayoutConstraint *inquiryHeightConstraint; +@property (nonatomic, strong, readonly) NSLayoutConstraint *hideInquiryConstraint; +// Private Access +@property (nonatomic, strong, readwrite) Fair *fair; + +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +@end + +@implementation ARInquireForArtworkViewController + +-(instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithAdminInquiryForArtwork:(Artwork *)artwork fair:(Fair *)fair +{ + self = [self init]; + + _state = ARInquireStateRepresentative; + _artwork = artwork; + self.fair = fair; + + [self getCurrentAdmin]; + + return self; +} + +- (instancetype)initWithPartnerInquiryForArtwork:(Artwork *)artwork fair:(Fair *)fair +{ + self = [self init]; + + _state = ARInquireStatePartner; + _artwork = artwork; + self.fair = fair; + + return self; +} + +- (void)presentFormWithInquiryURLRepresentation:(NSString *)inquiryURLRepresentation +{ + NSParameterAssert(inquiryURLRepresentation); + UIViewController *hostController = [ARTopMenuViewController sharedController]; + self.inquiryURLRepresentation = inquiryURLRepresentation; + self.modalPresentationCapturesStatusBarAppearance = YES; + + hostController.modalPresentationStyle = UIModalPresentationCurrentContext; + self.modalPresentationStyle = UIModalPresentationOverCurrentContext; + [hostController presentViewController:self animated:NO completion:nil]; +} + +- (void)removeFromHostViewController +{ + [[UIApplication sharedApplication] setStatusBarHidden:NO]; + [self hideMessage]; + [self hideFailureButtons]; + [self fadeBackgroundVisible:NO]; + [self performSelector:@selector(dismissSelf) withObject:nil afterDelay:ARAnimationDuration]; +} + +- (void)dismissSelf +{ + [self.presentingViewController dismissViewControllerAnimated:NO completion:nil]; +} + +- (BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +- (void)viewDidLoad +{ + self.view.backgroundColor = [UIColor clearColor]; + + [[UIApplication sharedApplication] setStatusBarHidden:self.smallScreen]; + self.view.backgroundColor = [UIColor clearColor]; + _backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:self.backgroundView]; + + // Make the background view a square equal to the longest dimension of self.view, regardless of orientation. + // This is to ensure that the background view completely coveres the view behind it when rotating. + [self.backgroundView constrainWidthToView:self.view predicate:@">=0"]; + [self.backgroundView constrainHeightToView:self.view predicate:@">=0"]; + [self.backgroundView alignAttribute:NSLayoutAttributeHeight toAttribute:NSLayoutAttributeWidth ofView:self.backgroundView predicate:nil]; + + [self.backgroundView alignCenterWithView:self.view]; + self.backgroundView.layer.backgroundColor = [UIColor blackColor].CGColor; + self.backgroundView.layer.opacity = 0.0; + self.view.backgroundColor = [UIColor clearColor]; + [self createBackground]; + [self createMessages]; + [self createTopMenu]; + + if (![ARUserManager sharedManager].currentUser) { + [self createNameEmailForm]; + } + + if (!self.smallScreen && self.state == ARInquireStateRepresentative) { + [self createArtsySpecialistSection]; + } + + [self createContentScrollView]; + [self createTextInputArea]; + [self createUserSignature]; + [self createContentSeparator]; + + if (!self.smallScreen || self.state == ARInquireStatePartner) { + [self createArtworkSection]; + [self.artworkView alignBottomEdgeWithView:self.inquiryFormView predicate:@"-10"]; + } else if (self.state == ARInquireStateRepresentative) { + [self createArtsySpecialistSection]; + [self.specialistView alignBottomEdgeWithView:self.inquiryFormView predicate:@"-10"]; + } + + // ensure all the layout is done before we animate in the view controller + [self.view layoutIfNeeded]; + + [super viewDidLoad]; +} + +- (void)createMessages +{ + ARTheme *theme = [ARTheme themeNamed:@"InquireForm"]; + + UILabel *titleLabel = [[UILabel alloc] init]; + titleLabel.font = theme.fonts[@"BackgroundTitle"]; + titleLabel.textAlignment = NSTextAlignmentCenter; + titleLabel.textColor = [UIColor whiteColor]; + [self.view addSubview:titleLabel]; + [titleLabel alignAttribute:NSLayoutAttributeTop toAttribute:NSLayoutAttributeCenterY ofView:self.view predicate:@"*.8"]; + [titleLabel alignCenterXWithView:self.view predicate:nil]; + _messageTitleLabel = titleLabel; + + UILabel *bodyTextLabel = [ARThemedFactory labelForBodyText]; + bodyTextLabel.textAlignment = NSTextAlignmentCenter; + bodyTextLabel.textColor = [UIColor whiteColor]; + bodyTextLabel.backgroundColor = [UIColor clearColor]; + bodyTextLabel.numberOfLines = 0; + [self.view addSubview:bodyTextLabel]; + [bodyTextLabel constrainTopSpaceToView:titleLabel predicate:@"8"]; + [bodyTextLabel alignCenterXWithView:self.view predicate:nil]; + _messageBodyLabel = bodyTextLabel; + + ARWhiteFlatButton *failureTryAgainButton = [[ARWhiteFlatButton alloc] initWithFrame:CGRectZero]; + [failureTryAgainButton constrainHeight:@"50"]; + [failureTryAgainButton setTitle:@"Send Again" forState:UIControlStateNormal]; + [self.view addSubview:failureTryAgainButton]; + [failureTryAgainButton constrainTopSpaceToView:self.messageBodyLabel predicate:@"20"]; + [failureTryAgainButton alignCenterXWithView:self.view predicate:nil]; + [failureTryAgainButton addTarget:self action:@selector(sendButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + _failureTryAgainButton = failureTryAgainButton; + + ARClearFlatButton *failureDismissButton = [[ARClearFlatButton alloc] initWithFrame:CGRectZero]; + [failureDismissButton constrainHeight:@"50"]; + [failureDismissButton setTitle:@"Cancel" forState:UIControlStateNormal]; + [self.view addSubview:failureDismissButton]; + [failureDismissButton constrainTopSpaceToView:self.failureTryAgainButton predicate:@"10"]; + [failureDismissButton alignCenterXWithView:self.view predicate:nil]; + [failureDismissButton addTarget:self action:@selector(cancelButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + _failureDismissButton = failureDismissButton; + + [self hideMessage]; + [self hideFailureButtons]; + +} + +- (void)setStatusWithTitle:(NSString *)title body:(NSString *)body +{ + self.messageTitleLabel.text = title.uppercaseString; + self.messageBodyLabel.text = body; + + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; +} + +- (void)fadeBackgroundVisible:(BOOL)makeVisible +{ + float fromValue = makeVisible ? 0.0 : 0.5; + float toValue = makeVisible ? 0.5 : 0.0; + if (self.shouldAnimate) { + CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fade.duration = ARAnimationDuration; + fade.fromValue = [NSNumber numberWithFloat:fromValue]; + fade.toValue = [NSNumber numberWithFloat:toValue]; + fade.fillMode = kCAFillModeBoth; + [self.backgroundView.layer addAnimation:fade forKey:@"backgroundOpacity"]; + + } + self.backgroundView.layer.opacity = toValue; +} + +- (void)createBackground +{ + UIView *topContainer = [[UIView alloc] init]; + [self.view addSubview:topContainer]; + [topContainer alignLeading:@"0" trailing:@"0" toView:self.view]; + topContainer.backgroundColor = [UIColor clearColor]; + _inquiryHeightConstraint = [[topContainer constrainHeightToView:self.view predicate:nil] lastObject]; + + // We will toggle the priority of the second constraint to determine whether or not it should take + // precedence over the first in order to show/hide the form. + [topContainer constrainTopSpaceToView:(UIView *)self.topLayoutGuide predicate:@"0@500"]; + _hideInquiryConstraint = [[topContainer constrainTopSpaceToView:(UIView *)self.bottomLayoutGuide predicate:@"0@999"] lastObject]; + + _topContainer = topContainer; + UIView *inquiryFormView = [[UIView alloc] init]; + [topContainer addSubview:inquiryFormView]; + + inquiryFormView.clipsToBounds = YES; + + + UIView *viewBehindKeyboard = [[UIView alloc] initWithFrame:CGRectZero]; + viewBehindKeyboard.backgroundColor = [UIColor blackColor]; + [self.view addSubview:viewBehindKeyboard]; + [viewBehindKeyboard alignLeading: @"0" trailing:@"0" toView:self.view]; + _keyboardPositionConstraint = [[viewBehindKeyboard alignAttribute:NSLayoutAttributeTop toAttribute:NSLayoutAttributeBottom ofView:(UIView *)self.bottomLayoutGuide predicate:nil] lastObject]; + + if ([UIDevice isPad]) { + [inquiryFormView constrainWidth:@"600"]; + [inquiryFormView alignTopEdgeWithView:topContainer predicate:@"50"]; + [inquiryFormView alignBottomEdgeWithView:topContainer predicate:@"0"]; + [inquiryFormView alignCenterXWithView:topContainer predicate:nil]; + } else { + [inquiryFormView alignToView:topContainer]; + } + _inquiryFormView = inquiryFormView; + _viewBehindKeyboard = viewBehindKeyboard; +} + +- (void)keyboardWillShow:(NSNotification *)notification +{ + CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + // In iOS 7 in Landscape orientation, the keyboard's length and width values are as though the orientation was Portrait. + // This is fixed in iOS 8, but we must account for both possibilities. We will therefore assume the actual height to be the smaller of the two dimensions. + // See http://stackoverflow.com/questions/24314222/change-in-metrics-for-the-new-ios-simulator-in-xcode-6 + CGFloat height = MIN(keyboardSize.width, keyboardSize.height); + + self.inquiryHeightConstraint.constant = -height - self.topLayoutGuide.length; + + if (!self.keyboardHeightConstraint) { + _keyboardHeightConstraint = [[self.viewBehindKeyboard constrainHeight:@(height).stringValue] lastObject]; + } else { + self.keyboardHeightConstraint.constant = height; + } + [self.view layoutIfNeeded]; + + // Show Keyboard + self.keyboardPositionConstraint.constant = -height; + [UIView animateIf:self.shouldAnimate duration:duration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)keyboardWillHide:(NSNotification *)notification +{ + self.keyboardPositionConstraint.constant = 0; + + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + [UIView animateIf:self.shouldAnimate duration:duration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)createTopMenu +{ + UIView *topMenuView = [[UIView alloc] init]; + [self.inquiryFormView addSubview:topMenuView]; + [topMenuView alignTop:@"0" leading:@"20" bottom:nil trailing:@"-20" toView:self.inquiryFormView]; + [topMenuView constrainHeight:@"60"]; + + ARModalMenuButton *cancelButton = [[ARModalMenuButton alloc] init]; + [cancelButton ar_extendHitTestSizeByWidth:10 andHeight:10]; + cancelButton.titleLabel.textAlignment = NSTextAlignmentLeft; + [cancelButton setTitle:@"Cancel" forState:UIControlStateNormal]; + [cancelButton setTitleColor:[UIColor artsyHeavyGrey] forState:UIControlStateNormal]; + [cancelButton addTarget:self action:@selector(cancelButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + _cancelButton = cancelButton; + [topMenuView addSubview:cancelButton]; + [cancelButton alignLeadingEdgeWithView:topMenuView predicate:@"0"]; + + UILabel *titleLabel = [[UILabel alloc] init]; + titleLabel.font = [UIFont sansSerifFontWithSize:12]; + titleLabel.textAlignment = NSTextAlignmentCenter; + if (self.state == ARInquireStatePartner) { + if (self.artwork.partner.type == ARPartnerTypeGallery) { + titleLabel.text = NSLocalizedString(@"CONTACT GALLERY", @"CONTACT GALLERY"); + } else { + titleLabel.text = NSLocalizedString(@"CONTACT SELLER", @"CONTACT SELLER"); + } + } else if (self.state == ARInquireStateRepresentative) { + titleLabel.text = NSLocalizedString(@"CONTACT SPECIALIST", @"CONTACT SPECIALIST"); + } + [topMenuView addSubview:titleLabel]; + [titleLabel alignCenterXWithView:topMenuView predicate:@"0"]; + + ARModalMenuButton *sendButton = [[ARModalMenuButton alloc] init]; + [sendButton ar_extendHitTestSizeByWidth:10 andHeight:10]; + sendButton.titleLabel.textAlignment = NSTextAlignmentRight; + [sendButton setTitle:@"Send" forState:UIControlStateNormal]; + [sendButton setTitleColor:[UIColor artsyPurple] forState:UIControlStateNormal]; + [sendButton setTitleColor:[UIColor artsyPurpleWithAlpha:0.3f] forState:UIControlStateHighlighted]; + [sendButton setTitleColor:[UIColor artsyPurpleWithAlpha:0.3f] forState:UIControlStateDisabled]; + [sendButton addTarget:self action:@selector(sendButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + _sendButton = sendButton; + [topMenuView addSubview:sendButton]; + [sendButton alignTrailingEdgeWithView:topMenuView predicate:@"0"]; + + ARSeparatorView *separator = [[ARSeparatorView alloc] init]; + [topMenuView addSubview:separator]; + [separator alignLeading:@"0" trailing:@"0" toView:topMenuView]; + [separator alignBottomEdgeWithView:topMenuView predicate:@"0"]; + + [titleLabel alignTop:@"15" bottom:@"-15" toView:topMenuView]; + [UIView alignTopAndBottomEdgesOfViews:@[cancelButton, titleLabel, sendButton]]; + + _topMenuView = topMenuView; +} + +- (void)createNameEmailForm +{ + UIView *nameEmailForm = [[UIView alloc] init]; + [self.inquiryFormView addSubview:nameEmailForm]; + [nameEmailForm constrainTopSpaceToView:self.topMenuView predicate:@"0"]; + [nameEmailForm alignLeading:@"20" trailing:@"-20" toView:self.inquiryFormView]; + _nameEmailForm = nameEmailForm; + + UITextField *nameInput = [[UITextField alloc] init]; + nameInput.text = [ARUserManager sharedManager].trialUserName; + nameInput.textColor = [UIColor blackColor]; + nameInput.tintColor = [self inputTintColor]; + nameInput.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"Your Full Name" attributes:@{ + NSForegroundColorAttributeName : [UIColor artsyHeavyGrey]}]; + nameInput.font = [UIFont serifFontWithSize:15]; + nameInput.clearButtonMode = UITextFieldViewModeNever; + nameInput.keyboardType = UIKeyboardTypeNamePhonePad; + nameInput.autocorrectionType = UITextAutocorrectionTypeNo; + nameInput.autocapitalizationType = UITextAutocapitalizationTypeNone; + nameInput.returnKeyType = UIReturnKeyNext; + nameInput.keyboardAppearance = UIKeyboardAppearanceDark; + nameInput.delegate = self; + [nameEmailForm addSubview:nameInput]; + [nameInput addTarget:self action:@selector(nameInputHasChanged:) forControlEvents:UIControlEventEditingChanged]; + [nameInput alignTopEdgeWithView:nameEmailForm predicate:@"10"]; + [nameInput alignLeading:@"5" trailing:@"-5" toView:nameEmailForm]; + _nameInput = nameInput; + + ARDottedSeparatorView *nameInputSeparator = [[ARDottedSeparatorView alloc] init]; + [nameEmailForm addSubview:nameInputSeparator]; + [nameInputSeparator alignLeading:@"0" trailing:@"0" toView:nameEmailForm]; + [nameInputSeparator constrainTopSpaceToView:nameInput predicate:@"10"]; + + + UITextField *emailInput = [[UITextField alloc] init]; + emailInput.text = [ARUserManager sharedManager].trialUserEmail; + [self setUpEmailValidator]; + // Initial validation of existing trialUseEmail to set initial SEND button state. + [self.emailValidator validate:emailInput.text]; + + emailInput.textColor = [UIColor blackColor]; + emailInput.tintColor = [self inputTintColor]; + emailInput.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"Your Email" attributes:@{ + NSForegroundColorAttributeName : [UIColor artsyHeavyGrey]}]; + emailInput.font = [UIFont serifFontWithSize:15]; + emailInput.clearButtonMode = UITextFieldViewModeNever; + emailInput.keyboardType = UIKeyboardTypeEmailAddress; + emailInput.autocorrectionType = UITextAutocorrectionTypeNo; + emailInput.autocapitalizationType = UITextAutocapitalizationTypeNone; + emailInput.returnKeyType = UIReturnKeyNext; + emailInput.keyboardAppearance = UIKeyboardAppearanceDark; + emailInput.delegate = self; + [emailInput addTarget:self action:@selector(emailInputHasChanged:) forControlEvents:UIControlEventEditingChanged]; + [nameEmailForm addSubview:emailInput]; + [emailInput constrainTopSpaceToView:nameInputSeparator predicate:@"10"]; + [emailInput alignLeading:@"5" trailing:@"-5" toView:nameEmailForm]; + _emailInput = emailInput; + + ARDottedSeparatorView *emailInputSeparator = [[ARDottedSeparatorView alloc] init]; + [nameEmailForm addSubview:emailInputSeparator]; + [emailInputSeparator alignLeading:@"0" trailing:@"0" toView:nameEmailForm]; + [emailInputSeparator constrainTopSpaceToView:emailInput predicate:@"10"]; + + [nameEmailForm alignBottomEdgeWithView:emailInputSeparator predicate:@"0"]; + +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + if (textField == self.nameInput) { + [self.emailInput becomeFirstResponder]; + } else if (textField == self.emailInput) { + [self.textView becomeFirstResponder]; + } + return YES; +} + +- (void)createArtworkSection +{ + UIView *artworkView = [[UIView alloc] init]; + [self.inquiryFormView addSubview:artworkView]; + _artworkView = artworkView; + [artworkView constrainHeight:@"30"]; + [artworkView constrainTopSpaceToView:self.contentSeparator predicate:@"10"]; + [artworkView alignLeading:@"20" trailing:@"-20" toView:self.inquiryFormView]; + + UIImageView *artworkPreview = [[UIImageView alloc] init]; + [artworkView addSubview:artworkPreview]; + [artworkPreview constrainWidth:@"30" height:@"30"]; + artworkPreview.contentMode = UIViewContentModeScaleAspectFit; + [artworkPreview ar_setImageWithURL:self.artwork.defaultImage.urlForSquareImage]; + [artworkPreview alignLeadingEdgeWithView:artworkView predicate:@"0"]; + [artworkPreview alignTopEdgeWithView:artworkView predicate:@"0"]; + + UIView *artworkInfoView = [[UIView alloc] init]; + [artworkView addSubview:artworkInfoView]; + [artworkInfoView constrainLeadingSpaceToView:artworkPreview predicate:@"10"]; + [artworkInfoView alignTrailingEdgeWithView:artworkView predicate:@"0"]; + [artworkInfoView alignTop:@"2" bottom:@"0" toView:artworkView]; + + if (self.state == ARInquireStatePartner) { + UILabel *partnerNameLabel = [[UILabel alloc] init]; + partnerNameLabel.font = [UIFont sansSerifFontWithSize:10]; + partnerNameLabel.text = self.artwork.partner.name.uppercaseString; + partnerNameLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [artworkInfoView addSubview:partnerNameLabel]; + [partnerNameLabel alignTopEdgeWithView:artworkInfoView predicate:@"0"]; + [partnerNameLabel alignLeading:@"0" trailing:@"0" toView:artworkInfoView]; + + NSMutableAttributedString *artworkString = [[NSMutableAttributedString alloc] initWithString:NSStringWithFormat(@"%@", self.artwork.artist.name) attributes:@{NSFontAttributeName: [UIFont serifFontWithSize:13]}]; + if (self.artwork.title.length > 0) { + [artworkString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@", " attributes:@{NSFontAttributeName: [UIFont serifFontWithSize:13]}]]; + [artworkString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:self.artwork.title attributes:@{NSFontAttributeName: [UIFont serifItalicFontWithSize:13]}]]; + } + + UILabel *artworkLabel = [[UILabel alloc] init]; + artworkLabel.attributedText = artworkString.copy; + artworkLabel.textColor = [UIColor artsyHeavyGrey]; + artworkLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [artworkInfoView addSubview:artworkLabel]; + + [artworkLabel alignLeading:@"0" trailing:@"0" toView:artworkInfoView]; + [artworkLabel constrainTopSpaceToView:partnerNameLabel predicate:@"0"]; + [artworkLabel alignBottomEdgeWithView:artworkInfoView predicate:@"0"]; + + } else { + UILabel *artworkArtist = [[UILabel alloc] init]; + artworkArtist.font = [UIFont serifFontWithSize:13]; + artworkArtist.text = self.artwork.artist.name; + artworkArtist.textColor = [UIColor artsyHeavyGrey]; + artworkArtist.lineBreakMode = NSLineBreakByTruncatingTail; + [artworkInfoView addSubview:artworkArtist]; + [artworkArtist alignLeading:@"0" trailing:@"0" toView:artworkInfoView]; + [artworkArtist alignTopEdgeWithView:artworkInfoView predicate:@"0"]; + + UILabel *artworkTitle = [[UILabel alloc] init]; + artworkTitle.font = [UIFont serifItalicFontWithSize:13]; + artworkTitle.text = self.artwork.title; + artworkTitle.textColor = [UIColor artsyHeavyGrey]; + artworkTitle.lineBreakMode = NSLineBreakByTruncatingTail; + [artworkInfoView addSubview:artworkTitle]; + + [artworkTitle alignLeading:@"0" trailing:@"0" toView:artworkInfoView]; + [artworkTitle constrainTopSpaceToView:artworkArtist predicate:@"0"]; + [artworkTitle alignBottomEdgeWithView:artworkInfoView predicate:@"0"]; + } +} + +- (void)createUserSignature +{ + User *currentUser = [User currentUser]; + if (currentUser) { + UILabel *userSignature = [[UILabel alloc] init]; + userSignature.font = self.textView.font; + userSignature.textColor = [UIColor artsyHeavyGrey]; + userSignature.text = currentUser.name ?: currentUser.email; + [self.inquiryFormView addSubview:userSignature]; + [userSignature constrainTopSpaceToView:self.contentView predicate:@"10"]; + [userSignature alignLeadingEdgeWithView:self.contentView predicate:@"5"]; + _userSignature = userSignature; + } +} + +- (void)createContentSeparator +{ + ARSeparatorView *separator = [[ARSeparatorView alloc] init]; + [self.inquiryFormView addSubview:separator]; + [separator alignLeading:@"0" trailing:@"0" toView:self.contentView]; + [separator constrainTopSpaceToView:self.userSignature ?: self.contentView predicate:@"5"]; + _contentSeparator = separator; +} + +- (void)createArtsySpecialistSection +{ + UIView *specialistView = [[UIView alloc] init]; + [self.inquiryFormView addSubview:specialistView]; + _specialistView = specialistView; + [specialistView constrainHeight:@"35"]; + // either at the bottom of the content view or user signature or under the top menu view, depending on which order it has been created with + [specialistView constrainTopSpaceToView:self.contentSeparator ?: self.nameEmailForm ?: self.topMenuView predicate:@"10"]; + [specialistView alignLeading:@"20" trailing:@"-20" toView:self.inquiryFormView]; + + UIImageView *specialistHeadImage = [[UIImageView alloc] init]; + [specialistView addSubview:specialistHeadImage]; + [specialistHeadImage constrainWidth:@"35" height:@"35"]; + specialistHeadImage.contentMode = UIViewContentModeScaleAspectFit; + [specialistHeadImage alignCenterYWithView:specialistView predicate:@"0"]; + [specialistHeadImage alignLeadingEdgeWithView:specialistView predicate:@"0"]; + _specialistHeadImage = specialistHeadImage; + + UIView *specialistInfoView = [[UIView alloc] init]; + [specialistView addSubview:specialistInfoView]; + [specialistInfoView alignAttribute:NSLayoutAttributeLeading toAttribute:NSLayoutAttributeTrailing ofView:specialistHeadImage predicate:@"10"]; + [specialistInfoView alignCenterYWithView:specialistView predicate:@"0"]; + [specialistInfoView alignTrailingEdgeWithView:specialistView predicate:@"0"]; + + UILabel *specialistLabel = [[UILabel alloc] init]; + specialistLabel.font = [UIFont sansSerifFontWithSize:10]; + specialistLabel.text = @"ARTSY SPECIALIST"; + [specialistView addSubview:specialistLabel]; + [specialistLabel alignTopEdgeWithView:specialistInfoView predicate:@"0"]; + [specialistLabel alignLeading:@"0" trailing:@"0" toView:specialistInfoView]; + + UILabel *specialistNameLabel = [[UILabel alloc] init]; + specialistNameLabel.font = [UIFont serifFontWithSize:12]; + specialistNameLabel.text = self.artwork.displayTitle; + specialistNameLabel.textColor = [UIColor artsyHeavyGrey]; + specialistNameLabel.numberOfLines = 0; + specialistNameLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [specialistInfoView addSubview:specialistNameLabel]; + [specialistNameLabel alignAttribute:NSLayoutAttributeTop toAttribute:NSLayoutAttributeBottom ofView:specialistLabel predicate:@"5"]; + [specialistNameLabel alignLeading:@"0" trailing:@"0" toView:specialistInfoView]; + _specialistNameLabel = specialistNameLabel; + + [specialistInfoView alignBottomEdgeWithView:specialistNameLabel predicate:@"0"]; +} + +- (void)createContentScrollView +{ + UIScrollView *contentView = [[UIScrollView alloc] init]; + [self.inquiryFormView addSubview:contentView]; + [contentView constrainTopSpaceToView:self.specialistView ?: self.nameEmailForm ?: self.topMenuView predicate:@"10"]; + [contentView alignLeading:@"20" trailing:@"-20" toView:self.inquiryFormView]; + _contentView = contentView; +} + +- (void)createTextInputArea +{ + UITextView *textView = [[UITextView alloc] init]; + textView.editable = YES; + textView.delegate = self; + textView.clipsToBounds = NO; + textView.textContainerInset = UIEdgeInsetsMake(0,-4, 0, -4); + textView.attributedText = [self attributedStringForTextView]; + textView.keyboardAppearance = UIKeyboardAppearanceDark; + // TODO: if there's a specialist on duty, display their name with Hello [Name] + // TODO: if the artwork has pricing, don't say "confirm the price" + + textView.textColor = [UIColor blackColor]; + textView.tintColor = [self inputTintColor]; + [self.contentView addSubview:textView]; + _textView = textView; + [textView alignLeadingEdgeWithView:self.contentView predicate:@"0"]; + [textView alignTopEdgeWithView:self.contentView predicate:@"0"]; + [textView constrainWidthToView:self.contentView predicate:@"0"]; + [textView constrainHeightToView:self.contentView predicate:@"0"]; + textView.autocorrectionType = UITextAutocorrectionTypeNo; +} + +- (NSAttributedString *)attributedStringForTextView +{ + NSString *message = [NSString stringWithFormat:@"Hello, I'm interested in this work by %@. Please confirm the price and availability of this work.", self.artwork.artist.name]; + NSMutableAttributedString *attributedMessage = [[NSMutableAttributedString alloc] initWithString:message]; + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setLineHeightMultiple:1.2]; + [attributedMessage addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [message length])]; + + [attributedMessage addAttribute:NSFontAttributeName value:[UIFont serifFontWithSize:16] + range:NSMakeRange(0, [message length])]; + + return attributedMessage.copy; +} + + +- (void)showInquiryForm +{ + self.hideInquiryConstraint.priority = 1; + self.inquiryFormView.userInteractionEnabled = YES; + self.textView.editable = YES; + [UIView animateIf:self.shouldAnimate duration:ARAnimationDuration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)hideInquiryForm +{ + self.hideInquiryConstraint.priority = 999; + self.inquiryFormView.userInteractionEnabled = NO; + self.textView.editable = NO; + [UIView animateIf:self.shouldAnimate duration:ARAnimationDuration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + // Seems to be an iOS bug where the text doesn't appear in the text view + // during viewDidLoad - this retriggers rendering and shows the text. ./ + self.textView.textColor = self.textView.textColor; + self.inquiryFormView.backgroundColor = [UIColor whiteColor]; + + // register for keyboard notifications + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillShow:) + name:UIKeyboardWillShowNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; + + [super viewWillAppear:animated]; + +} + +- (void)viewDidAppear:(BOOL)animated +{ + self.inquiryHeightConstraint.constant = -self.topLayoutGuide.length; + if (self.nameInput && !(self.nameInput.text.length > 0)) { + [self.nameInput becomeFirstResponder]; + } else { + [self.textView becomeFirstResponder]; + } + [self fadeBackgroundVisible:YES]; + [self showInquiryForm]; + [super viewDidAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; +} + +- (void)getCurrentAdmin +{ + @weakify(self); + [ArtsyAPI getInquiryContact:^(User *contactStub) { + @strongify(self); + self.specialistNameLabel.text = contactStub.name; + + } withProfile:^(Profile *contactProfile) { + @strongify(self); + // Use a white BG because the square to circle looks ugly + [self.specialistHeadImage ar_setImageWithURL:[NSURL URLWithString:contactProfile.iconURL] placeholderImage:[UIImage imageFromColor:[UIColor whiteColor]]]; + + } failure:^(NSError *error) { + @strongify(self); + ARErrorLog(@"Couldn't get an inquiry contact. %@", error.localizedDescription); + [self performSelector:@selector(getCurrentAdmin) withObject:nil afterDelay:2]; + }]; +} + +- (void)cancelButtonTapped:(UIButton *)sender +{ + [self.inquiryFormView endEditing:YES]; + [self hideInquiryForm]; + [self removeFromHostViewController]; +} + +- (void)sendButtonTapped:(UIButton *)sender +{ + [self.inquiryFormView endEditing:YES]; + [self hideInquiryForm]; + self.cancelButton.enabled = NO; + self.sendButton.enabled = NO; + [self hideFailureButtons]; + [self sendInquiry]; + [self setStatusWithTitle:@"Sending…" body:@""]; + [self presentMessage]; + [self.view endEditing:YES]; +} + +- (void)sendInquiry +{ + void(^success)(id message) = ^(id message) { + [self inquiryCompleted:message]; + }; + + void(^failure)(NSError *error) = ^(NSError *error) { + [self inquiryFailed:error]; + }; + + +#if USE_LIVE_DATA == 0 + [self performSelector:@selector(stubCompletionHandler:) withObject:success afterDelay:2]; +#endif +#if USE_LIVE_DATA == 1 + ARAppDelegate *delegate = [ARAppDelegate sharedInstance]; + + NSDictionary *analyticsDictionary = @{ + ArtsyAPIInquiryAnalyticsInquiryURL: self.inquiryURLRepresentation ?: @"", + ArtsyAPIInquiryAnalyticsReferralURL: delegate.referralURLRepresentation ?: @"", + ArtsyAPIInquiryAnalyticsLandingURL: delegate.landingURLRepresentation ?: @"", + }; + + if (self.state == ARInquireStateRepresentative) { + [ArtsyAPI createRepresentativeArtworkInquiryForArtwork:self.artwork + name:self.nameInput.text + email:self.emailInput.text + message:self.textView.text + analyticsDictionary:analyticsDictionary + success:success + failure:failure + ]; + } else { + [ArtsyAPI createPartnerArtworkInquiryForArtwork:self.artwork + name:self.nameInput.text + email:self.emailInput.text + message:self.textView.text + analyticsDictionary:analyticsDictionary + success:success + failure:failure + ]; + } +#endif +} + +- (void)stubCompletionHandler:(void (^)(id message))block { + block(nil); +} + +- (UIColor *)inputTintColor +{ + return self.shouldAnimate ? [UIColor artsyPurple] : [UIColor whiteColor]; +} + +- (void)inquiryCompleted:(NSString *)message +{ + [self setStatusWithTitle:@"Thank you" body:@"Your message has been sent"]; + [self performSelector:@selector(removeFromHostViewController) withObject:nil afterDelay:2]; +} + +- (void)inquiryFailed:(NSError *)error +{ + ARErrorLog(@"Error sending inquiry for artwork %@. Error: %@", self.artwork.artworkID, error.localizedDescription); + NSString *errorTitle, *errorMessage; + // think we need to return JSON in this error to not do this + errorTitle = @"Error Sending Message"; + errorMessage = @"Please try again or email\nsupport@artsy.net if the issue persists"; + [self setStatusWithTitle:errorTitle body:errorMessage]; + [self presentFailureButtons]; +} + +- (void)hideMessage +{ + self.messageTitleLabel.hidden = YES; + self.messageBodyLabel.hidden = YES; +} + +- (void)presentMessage +{ + self.messageTitleLabel.hidden = NO; + self.messageBodyLabel.hidden = NO; +} + +- (void)hideFailureButtons +{ + self.failureTryAgainButton.hidden = YES; + self.failureDismissButton.hidden = YES; +} + +- (void)presentFailureButtons +{ + self.failureTryAgainButton.hidden = NO; + self.failureDismissButton.hidden = NO; +} + +- (void)nameInputHasChanged:(id)sender +{ + [ARUserManager sharedManager].trialUserName = self.nameInput.text; +} + +- (void)emailInputHasChanged:(id)sender +{ + [self.emailValidator validate:self.emailInput.text]; + [ARUserManager sharedManager].trialUserEmail = self.emailInput.text; + +} + +- (void)setUpEmailValidator +{ + _emailValidator = [ALPValidator validatorWithType:ALPValidatorTypeString]; + [self.emailValidator addValidationToEnsureValidEmailWithInvalidMessage:NSLocalizedString(@"Please enter a valid email", nil)]; + + @weakify(self); + self.emailValidator.validatorStateChangedHandler = ^(ALPValidatorState newState) { + @strongify(self); + self.sendButton.enabled = self.emailValidator.isValid; + // We can also use newState to determine what to do in more complex situations. Validator states include: + // ALPValidatorValidationStateValid, ALPValidatorValidationStateInvalid, ALPValidatorValidationStateWaitingForRemote + }; +} + +- (NSString *)body +{ + return self.textView.text; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARInternalMobileWebViewController.h b/Artsy/Classes/View Controllers/ARInternalMobileWebViewController.h new file mode 100644 index 00000000000..84798052ac1 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARInternalMobileWebViewController.h @@ -0,0 +1,7 @@ +#import "ARExternalWebBrowserViewController.h" + +@interface ARInternalMobileWebViewController : ARExternalWebBrowserViewController + +@property (nonatomic, strong) Fair *fair; + +@end diff --git a/Artsy/Classes/View Controllers/ARInternalMobileWebViewController.m b/Artsy/Classes/View Controllers/ARInternalMobileWebViewController.m new file mode 100644 index 00000000000..be6166c36eb --- /dev/null +++ b/Artsy/Classes/View Controllers/ARInternalMobileWebViewController.m @@ -0,0 +1,129 @@ +#import "ARInternalMobileWebViewController.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARRouter.h" + +@interface TSMiniWebBrowser (Private) +@property(nonatomic, readonly, strong) UIWebView *webView; +- (UIEdgeInsets)webViewContentInset; +- (UIEdgeInsets)webViewScrollIndicatorsInsets; +@end + +@interface ARInternalMobileWebViewController() +@property (nonatomic, readonly, assign) BOOL loaded; +@end + +@implementation ARInternalMobileWebViewController + +- (instancetype)initWithURL:(NSURL *)url +{ + NSString *urlString = url.absoluteString; + NSString *urlHost = url.host; + NSString *urlScheme = url.scheme; + + NSURL *correctBaseUrl = [ARRouter baseWebURL]; + NSString *correctHost = correctBaseUrl.host; + NSString *correctScheme = correctBaseUrl.scheme; + + if ([[ARRouter artsyHosts] containsObject:urlHost]) { + NSMutableString *mutableUrlString = [urlString mutableCopy]; + if (![urlScheme isEqualToString:correctScheme]){ + [mutableUrlString replaceOccurrencesOfString:urlScheme withString:correctScheme options:NSCaseInsensitiveSearch range:NSMakeRange(0, mutableUrlString.length)]; + } + if (![url.host isEqualToString:correctBaseUrl.host]) { + [mutableUrlString replaceOccurrencesOfString:urlHost withString:correctHost options:NSCaseInsensitiveSearch range:NSMakeRange(0, mutableUrlString.length)]; + } + url = [NSURL URLWithString:mutableUrlString]; + } else if (!urlHost) { + url = [NSURL URLWithString:urlString relativeToURL:correctBaseUrl]; + } + + if (![urlString isEqualToString:url.absoluteString]) { + NSLog(@"Rewriting %@ as %@", urlString, url.absoluteString); + } + + self = [super initWithURL:url]; + if (!self) { return nil; } + + self.delegate = self; + self.showNavigationBar = NO; + self.mode = TSMiniWebBrowserModeNavigation; + self.showToolBar = NO; + self.backgroundColor = [UIColor whiteColor]; + self.opaque = NO; + + ARInfoLog(@"Initialized with URL %@", url); + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // As we initially show the loading, we don't want this to appear when you do a back or when a modal covers this view. + if (!self.loaded) { + [self showLoading]; + } +} + +- (void)showLoading +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [UIView animateWithDuration:ARAnimationDuration animations:^{ + self.webView.scrollView.contentInset = [self webViewContentInset]; + self.webView.scrollView.scrollIndicatorInsets = [self webViewScrollIndicatorsInsets]; + }]; + + [super viewDidAppear:animated]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)aWebView +{ + [super webViewDidFinishLoad:aWebView]; + [self hideLoading]; + _loaded = YES; +} + +- (void)hideLoading +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; +} + +// Load a new internal web VC for each link we can do + +- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + ARInfoLog(@"Martsy URL %@", request.URL); + + if (navigationType == UIWebViewNavigationTypeLinkClicked) { + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadURL:request.URL fair:self.fair]; + if (viewController) { + [self.navigationController pushViewController:viewController animated:YES]; + return NO; + } + } else if ([ARRouter isInternalURL:request.URL] && ([request.URL.path isEqual:@"/log_in"] || [request.URL.path isEqual:@"/sign_up"])) { + // hijack AJAX requests + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextNotTrial fromTarget:self selector:@selector(reload)]; + } + return NO; + } + + return YES; +} + +// A full reload, not just a webView.reload, which only refreshes the view without re-requesting data. +- (void)reload +{ + [self.webView loadRequest:[self requestWithURL:self.currentURL]]; +} + +- (NSURLRequest *)requestWithURL:(NSURL *)url +{ + return [ARRouter requestForURL:url]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARKonamiKeyboardView.h b/Artsy/Classes/View Controllers/ARKonamiKeyboardView.h new file mode 100644 index 00000000000..594a8e05a6e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARKonamiKeyboardView.h @@ -0,0 +1,6 @@ +#import + +@interface ARKonamiKeyboardView : UIView +- (id)initWithKonamiGestureRecognizer:(DRKonamiGestureRecognizer *)gestureRecognizer; +@end + diff --git a/Artsy/Classes/View Controllers/ARKonamiKeyboardView.m b/Artsy/Classes/View Controllers/ARKonamiKeyboardView.m new file mode 100644 index 00000000000..9001c7f10a0 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARKonamiKeyboardView.m @@ -0,0 +1,41 @@ +#import "ARKonamiKeyboardView.h" + +@interface ARKonamiKeyboardView () +@property (nonatomic, strong) DRKonamiGestureRecognizer *gestureRecognizer; +@end + +@implementation ARKonamiKeyboardView + +- (id)initWithKonamiGestureRecognizer:(DRKonamiGestureRecognizer *)gestureRecognizer +{ + self = [super init]; + if (self) { + _gestureRecognizer = gestureRecognizer; + } + return self; +} + +- (void)insertText:(NSString *)text { + if ([text.lowercaseString isEqualToString:@"b"]) { + [self.gestureRecognizer BButtonAction]; + } else if ([text.lowercaseString isEqualToString:@"a"]) { + [self.gestureRecognizer AButtonAction]; + [self.gestureRecognizer enterButtonAction]; + } else { + [self.gestureRecognizer enterButtonAction]; + } +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (void)deleteBackward { + +} + +- (BOOL)hasText { + return YES; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARLoginViewController.h b/Artsy/Classes/View Controllers/ARLoginViewController.h new file mode 100644 index 00000000000..e0d3b312f85 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARLoginViewController.h @@ -0,0 +1,16 @@ +#import "AROnboardingViewController.h" + +typedef NS_ENUM(NSInteger, ARLoginViewControllerLoginType) { + ARLoginViewControllerLoginTypeTwitter = 0, + ARLoginViewControllerLoginTypeFacebook, + ARLoginViewControllerLoginTypeEmail +}; + +@interface ARLoginViewController : UIViewController + +- (instancetype)initWithEmail:(NSString *)email; + +@property (nonatomic, weak) AROnboardingViewController *delegate; +@property (nonatomic, assign) BOOL hideDefaultValues; + +@end diff --git a/Artsy/Classes/View Controllers/ARLoginViewController.m b/Artsy/Classes/View Controllers/ARLoginViewController.m new file mode 100644 index 00000000000..4c2f68de303 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARLoginViewController.m @@ -0,0 +1,519 @@ +#import "ARLoginViewController.h" +#import "ARUserManager.h" +#import "AROnboardingNavBarView.h" +#import "ARAuthProviders.h" +#import "UIViewController+FullScreenLoading.h" +#import +#import "ARTextFieldWithPlaceholder.h" +#import "ARSecureTextFieldWithPlaceholder.h" +#import "UIView+HitTestExpansion.h" + +#define SPINNER_TAG 0x555 + +@interface ARLoginViewController () +@property (nonatomic, strong) AROnboardingNavBarView *navView; +@property (nonatomic, strong) UIButton *testBotButton; +@property (nonatomic, strong) ARUppercaseButton *loginButton; +@property (nonatomic, strong) NSString *email; + +@property (nonatomic, strong) ORStackView *containerView; +@property (nonatomic, strong) ARTextFieldWithPlaceholder *emailTextField; +@property (nonatomic, strong) ARSecureTextFieldWithPlaceholder *passwordTextField; +@property (nonatomic, strong) UIButton *forgotPasswordButton; +@property (nonatomic, strong) ARWhiteFlatButton *facebookLoginButton; +@property (nonatomic, strong) ARWhiteFlatButton *twitterLoginButton; + +@property (nonatomic, strong) NSLayoutConstraint *keyboardConstraint; +@end + +@implementation ARLoginViewController + +- (instancetype)initWithEmail:(NSString *)email +{ + self = [super init]; + if (self) { + _email = email; + } + return self; +} + +- (void)viewDidLoad +{ + self.view.backgroundColor = [UIColor clearColor]; + UITapGestureRecognizer *keyboardCancelTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard)]; + [self.view addGestureRecognizer:keyboardCancelTap]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + + self.navView = [self createNav]; + self.loginButton = self.navView.forward; + [self.view addSubview:self.navView]; + + self.containerView = [[ORStackView alloc] init]; + [self.view addSubview:self.containerView]; + [self.containerView alignCenterXWithView:self.view predicate:nil]; + [self.containerView constrainWidth:@"280"]; + + self.emailTextField = [[ARTextFieldWithPlaceholder alloc] initWithFrame:CGRectZero]; + self.emailTextField.placeholder = @"Email"; + self.emailTextField.delegate = self; + self.emailTextField.returnKeyType = UIReturnKeyNext; + self.emailTextField.keyboardType = UIKeyboardTypeEmailAddress; + self.emailTextField.autocorrectionType = UITextAutocorrectionTypeNo; + self.emailTextField.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.emailTextField.keyboardAppearance = UIKeyboardAppearanceDark; + + [self.containerView addSubview:self.emailTextField withTopMargin:@"0" sideMargin:nil]; + + self.passwordTextField = [[ARSecureTextFieldWithPlaceholder alloc] init]; + self.passwordTextField.delegate = self; + self.passwordTextField.returnKeyType = UIReturnKeyDone; + self.passwordTextField.placeholder = @"Password"; + [self.containerView addSubview:self.passwordTextField withTopMargin:@"10" sideMargin:nil]; + + if (self.email) { + self.emailTextField.text = self.email; + [self.passwordTextField becomeFirstResponder]; + } + + for (UITextField *textField in @[self.emailTextField, self.passwordTextField]) { + if ([textField respondsToSelector:@selector(setAutocorrectionType:)]) { textField.autocorrectionType = UITextAutocorrectionTypeNo; } + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + textField.textColor = [UIColor whiteColor]; + textField.keyboardAppearance = UIKeyboardAppearanceDark; + textField.keyboardType = UIKeyboardTypeEmailAddress; + textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; + [textField ar_extendHitTestSizeByWidth:0 andHeight:10]; + } + + self.forgotPasswordButton = [[UIButton alloc] initWithFrame:CGRectZero]; + [self.forgotPasswordButton setTitle:@"FORGOT PASSWORD?" forState:UIControlStateNormal]; + [self.containerView addSubview:self.forgotPasswordButton withTopMargin:@"10"]; + [self.forgotPasswordButton alignTrailingEdgeWithView:self.containerView predicate:@"-15"]; + [self.forgotPasswordButton addTarget:self action:@selector(forgotPassword:) forControlEvents:UIControlEventTouchUpInside]; + self.forgotPasswordButton.titleLabel.font = [UIFont sansSerifFontWithSize:10]; + + self.facebookLoginButton = [[ARWhiteFlatButton alloc] initWithFrame:CGRectZero]; + [self.facebookLoginButton setTitle:@"Connect With Facebook" forState:UIControlStateNormal]; + [self.containerView addSubview:self.facebookLoginButton withTopMargin:@"29" sideMargin:nil]; + [self.facebookLoginButton addTarget:self action:@selector(fb:) forControlEvents:UIControlEventTouchUpInside]; + + + self.twitterLoginButton = [[ARWhiteFlatButton alloc] initWithFrame:CGRectZero]; + [self.twitterLoginButton setTitle:@"Connect With Twitter" forState:UIControlStateNormal]; + [self.containerView addSubview:self.twitterLoginButton withTopMargin:@"10" sideMargin:nil]; + [self.twitterLoginButton addTarget:self action:@selector(twitter:) forControlEvents:UIControlEventTouchUpInside]; + + if ([UIDevice isPad]) { + [self.containerView alignCenterYWithView:self.view predicate:@"0@750"]; + self.keyboardConstraint = [[self.containerView alignBottomEdgeWithView:self.view predicate:@"<=0@1000"] lastObject]; + } else { + [self.containerView alignBottomEdgeWithView:self.view predicate:@"<=-56"]; + self.keyboardConstraint = [[self.forgotPasswordButton alignBottomEdgeWithView:self.view predicate:@"<=0@1000"] lastObject]; + } + + [super viewDidLoad]; +} + +- (void)hideKeyboard +{ + [self.view endEditing:YES]; +} + +- (AROnboardingNavBarView *)createNav +{ + AROnboardingNavBarView *navView = [[AROnboardingNavBarView alloc] init]; + [navView.title setText:@"Welcome Back"]; + + [navView.back setImage:[UIImage imageNamed:@"BackArrow"] forState:UIControlStateNormal]; + [navView.back setImage:[UIImage imageNamed:@"BackArrow_Highlighted"] forState:UIControlStateHighlighted]; + [navView.back addTarget:self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; + + [navView.forward setTitle:@"LOG IN" forState:UIControlStateNormal]; + [navView.forward addTarget:self action:@selector(login:) forControlEvents:UIControlEventTouchUpInside]; + return navView; +} + +- (void)twitter:(id)sender +{ + [self hideKeyboard]; + @weakify(self); + + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + + [ARAuthProviders getReverseAuthTokenForTwitter:^(NSString *token, NSString *secret) { + [[ARUserManager sharedManager] loginWithTwitterToken:token + secret:secret + successWithCredentials:nil + gotUser:^(User *currentUser) { + @strongify(self); + [self loggedInWithType:ARLoginViewControllerLoginTypeTwitter user:currentUser]; + } authenticationFailure:^(NSError *error) { + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self twitterError]; + + } networkFailure:^(NSError *error) { + @strongify(self); + [self failedToLoginToTwitter]; + }]; + + } failure:^(NSError *error) { + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self twitterError]; + }]; +} + +- (void)loggedInWithType:(ARLoginViewControllerLoginType)type user:(User *)currentUser +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self loggedInWithUser:currentUser]; +} + +- (void)failedToLoginToTwitter +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self presentErrorMessage:@"Network Error"]; + self.loginButton.alpha = 1; +} + +- (void)fb:(id)sender +{ + [self hideKeyboard]; + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + + @weakify(self); + [ARAuthProviders getTokenForFacebook:^(NSString *token, NSString *email, NSString *name) { + [[ARUserManager sharedManager] loginWithFacebookToken:token + successWithCredentials:nil gotUser:^(User *currentUser) { + @strongify(self); + [self loggedInWithType:ARLoginViewControllerLoginTypeFacebook user:currentUser]; + } authenticationFailure:^(NSError *error) { + @strongify(self); + + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + + NSString * reason = error.userInfo[@"com.facebook.sdk:ErrorLoginFailedReason"]; + if (![reason isEqualToString:@"com.facebook.sdk:UserLoginCancelled"]) { + [self fbError]; + } else if ([error.userInfo[@"AFNetworkingOperationFailingURLResponseErrorKey"] statusCode] == 401) { + // This case handles a 401 from Artsy's server, which means the Facebook account is not associated with a user. + [self fbNoUser]; + } + + } networkFailure:^(NSError *error) { + @strongify(self); + [self failedToLoginToFacebook]; + }]; + } failure:^(NSError *error) { + @strongify(self); + + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self fbError]; + }]; +} + +- (void)failedToLoginToFacebook +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self presentErrorMessage:@"Network Error"]; + self.loginButton.alpha = 1; +} + +- (void)fbError +{ + [UIAlertView showWithTitle:@"Couldn’t get Facebook credentials" + message:@"Couldn’t get Facebook credentials. If you continue having trouble, please email Artsy support at support@artsy.net" + cancelButtonTitle:@"OK" + otherButtonTitles:nil + tapBlock:nil]; +} + +- (void)fbNoUser +{ + [UIAlertView showWithTitle:@"Account not found" + message:@"We couldn't find an Artsy account associated with your Facebook profile. You can link your Facebook account in your settings on artsy.net. If you continue having trouble, please email Artsy support at support@artsy.net" + cancelButtonTitle:@"OK" + otherButtonTitles:nil + tapBlock:nil]; +} + +- (void)twitterError +{ + [UIAlertView showWithTitle:@"Couldn’t get Twitter credentials" + message:@"Couldn’t get Twitter credentials. Please link a Twitter account in the settings app. If you continue having trouble, please email Artsy support at support@artsy.net" + cancelButtonTitle:@"OK" + otherButtonTitles:nil + tapBlock:nil]; +} + +- (void)keyboardWillShow:(NSNotification *)notification +{ + CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + // In iOS 7 in Landscape orientation, the keyboard's length and width values are as though the orientation was Portrait. + // This is fixed in iOS 8, but we must account for both possibilities. We will therefore assume the actual height to be the smaller of the two dimensions. + // See http://stackoverflow.com/questions/24314222/change-in-metrics-for-the-new-ios-simulator-in-xcode-6 + CGFloat height = MIN(keyboardSize.width, keyboardSize.height); + + self.keyboardConstraint.constant = -height - ([UIDevice isPad] ? 20 : 10); + [UIView animateIf:YES duration:duration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)keyboardWillHide:(NSNotification *)notification +{ + CGFloat duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + self.keyboardConstraint.constant = 0; + [UIView animateIf:YES duration:duration :^{ + [self.view layoutIfNeeded]; + }]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.emailTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; + [self.passwordTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; + + self.testBotButton.titleLabel.font = [ARTheme defaultTheme].fonts[@"ButtonFont"]; + self.loginButton.titleLabel.font = [ARTheme defaultTheme].fonts[@"ButtonFont"]; + + [super viewWillAppear:animated]; +} + +- (void)viewDidAppear:(BOOL)animated +{ +#if (AR_SHOW_ALL_DEBUG) + [self showAutoLoginButtons]; + [self setupDefaultUsernameAndPassword]; + [self textFieldDidChange:nil]; +#endif + + [super viewDidAppear:animated]; +} + +- (void)autoLogIn:(id)sender +{ + // this won't leak passwords into the build unless you've + // somehow got a simulator only build, which is only + // really possible if you grab a dev's laptop + + // ... in which case you've got the source, so who'd bother running strings? + +#if (AR_SHOW_ALL_DEBUG) + NSString *username, *password; + + username = @"energytestbot@artsymail.com"; + password = @"wy-rhu-hoki-tha-whil"; + + self.emailTextField.text = username; + self.passwordTextField.text = password; + + [self login:nil]; +#endif +} + +- (void)showAutoLoginButtons +{ + self.testBotButton.hidden = NO; + self.testBotButton.enabled = YES; +} + +- (void)login:(id)sender +{ + if ([self validates]) { + [self loginWithUsername:_emailTextField.text andPassword:_passwordTextField.text]; + } +} + +- (void)forgotPassword:(id)sender +{ + UIAlertView *alert = [[UIAlertView alloc] + initWithTitle:@"Forgot Password" + message:@"Please enter your email address and we’ll send you a reset link." + delegate:nil + cancelButtonTitle:@"Cancel" + otherButtonTitles:@"Send Link", nil]; + alert.alertViewStyle = UIAlertViewStylePlainTextInput; + [[alert textFieldAtIndex:0] setKeyboardAppearance:UIKeyboardAppearanceDark]; + alert.tapBlock = ^(UIAlertView *alertView, NSInteger buttonIndex) { + if (buttonIndex == alertView.firstOtherButtonIndex) { + NSString *email = [[alertView textFieldAtIndex:0] text]; + if (!email.length || ![email containsString:@"@"]) { + [self passwordResetError:@"Please check your email address"]; + } else { + [self showSpinner]; + [self sendPasswordResetEmail:email]; + } + } + }; + [self hideKeyboard]; + [alert show]; +} + +- (void)sendPasswordResetEmail:(NSString *)email +{ + [[ARUserManager sharedManager] sendPasswordResetForEmail:email success:^{ + [self passwordResetSent]; + ARActionLog(@"Sent password reset request for %@", email); + } failure:^(NSError *error) { + ARErrorLog(@"Password reset failed for %@. Error: %@", email, error.localizedDescription); + [self passwordResetError:@"Couldn’t send reset password link. Please try again, or contact support@artsy.net"]; + }]; +} + +- (BOOL)validates +{ + return self.emailTextField.text.length && self.passwordTextField.text.length; +} + +- (void)loginWithUsername:(NSString *)username andPassword:(NSString *)password +{ + if ([username isEqualToString:@"orta"]) { + username = @"orta.therox@gmail.com"; + } + + self.loginButton.alpha = 0.5; + + @weakify(self); + [[ARUserManager sharedManager] loginWithUsername:username + password:password + successWithCredentials:nil + gotUser:^(User *currentUser) { + @strongify(self); + [self loggedInWithType:ARLoginViewControllerLoginTypeEmail user:currentUser]; + } + + authenticationFailure:^(NSError *error) { + @strongify(self); + [self authenticationFailure]; + } + + networkFailure:^(NSError *error) { + @strongify(self); + [self networkFailure:error]; + }]; +} + +- (void)authenticationFailure +{ + [self resetForm]; + [self presentErrorMessage:@"Please check your email and password"]; +} + +- (void)networkFailure:(NSError *)error +{ + [self presentErrorMessage:@"There is an issue connecting to Artsy"]; + self.loginButton.alpha = 1; +} + +- (void)presentErrorMessage:(NSString *)message +{ + [UIAlertView showWithTitle:@"Couldn’t Log In" + message:message + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil + tapBlock:nil]; +} + +- (void)loggedInWithUser:(User *)user +{ + [self.delegate dismissOnboardingWithVoidAnimation:YES]; +} + +- (void)resetForm +{ + self.passwordTextField.text = @""; + self.loginButton.alpha = 1; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + if ([textField isEqual:self.emailTextField]) { + [self.passwordTextField becomeFirstResponder]; + } else { + [self login:nil]; + } + return YES; +} + +- (void)textFieldDidChange:(UITextView *)textView; +{ + [self.loginButton setEnabled:[self validates] animated:YES]; +} + +- (void)setupDefaultUsernameAndPassword +{ + if (([ARDeveloperOptions options][@"username"] && [ARDeveloperOptions options][@"password"]) && self.hideDefaultValues == NO) { + self.emailTextField.text = [ARDeveloperOptions options][@"username"]; + self.passwordTextField.text = [ARDeveloperOptions options][@"password"]; + [self.passwordTextField sendActionsForControlEvents:UIControlEventEditingDidEnd]; + } +} + +- (void)back:(id)sender +{ + // If self.email is set, we got dropped off here from SSO + // so we wanna go back to the splash instead. + + if (self.email) { + [self.delegate slideshowDone]; + } + + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)showSpinner +{ + UIView *view = [[UIView alloc] initWithFrame:self.view.bounds]; + view.backgroundColor = [UIColor colorWithWhite:0 alpha:.5]; + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + [view addSubview:spinner]; + spinner.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds)); + [spinner startAnimating]; + + UILabel *label = [[UILabel alloc] init]; + label.font = [UIFont serifFontWithSize:16]; + label.textColor = [UIColor whiteColor]; + [label sizeToFit]; + label.center = CGPointMake(spinner.center.x, spinner.center.y + 50); + [view addSubview:label]; + [self.view addSubview:view]; + view.tag = SPINNER_TAG; + + //we dont want the keyboard to shoot up while we're spinning + [self hideKeyboard]; +} + +- (void)hideSpinner +{ + UIView *view = [self.view viewWithTag:SPINNER_TAG]; + [view removeFromSuperview]; +} + +- (void)passwordResetError:(NSString *)message +{ + [self hideSpinner]; + [UIAlertView showWithTitle:@"Couldn’t Reset Password" + message:message + cancelButtonTitle:@"OK" + otherButtonTitles:nil + tapBlock:nil]; +} + +- (void)passwordResetSent +{ + [self hideSpinner]; + [UIAlertView showWithTitle:@"Please Check Your Email" + message:@"We have sent you an email with a link to reset your password" + cancelButtonTitle:@"OK" + otherButtonTitles:nil + tapBlock:nil]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARMenuAwareViewController.h b/Artsy/Classes/View Controllers/ARMenuAwareViewController.h new file mode 100644 index 00000000000..0942bb0d887 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARMenuAwareViewController.h @@ -0,0 +1,12 @@ +#import + +@protocol ARMenuAwareViewController + +@property (readonly, nonatomic, assign) BOOL hidesToolbarMenu; +@property (readonly, nonatomic, assign) BOOL hidesBackButton; + +@optional +@property (readonly, nonatomic, assign) BOOL hidesStatusBarBackground; +@property (readonly, nonatomic, assign) BOOL enableMenuButtons; + +@end diff --git a/Artsy/Classes/View Controllers/ARModelCollectionViewModule.h b/Artsy/Classes/View Controllers/ARModelCollectionViewModule.h new file mode 100644 index 00000000000..419f5628ce7 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARModelCollectionViewModule.h @@ -0,0 +1,32 @@ +#import "ARFeedImageLoader.h" + +/// The ARModelCollectionViewModule exists to act as a tool +/// to deal with different layouts for a collection of items + +// Presentation styles used by artwork-specific subclasses. +typedef NS_ENUM(NSInteger, AREmbeddedArtworkPresentationStyle){ + AREmbeddedArtworkPresentationStyleArtworkOnly, + AREmbeddedArtworkPresentationStyleArtworkMetadata +}; + +@interface ARModelCollectionViewModule : NSObject + +/// A UICollectionView subclass that will deal with the layout of the items +- (UICollectionViewLayout *)moduleLayout; + +/// The module holds the items +@property (nonatomic, copy) NSArray *items; + +/// An intrinsic size that should always redirect to the class method +/// using the current settings for styles +- (CGSize)intrinsicSize; + +/// The cell for displaying the models. Must be set by subclasses. +- (Class)classForCell; + +/// If the module should support some form of paging this function is necessary +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset; + +-(ARFeedItemImageSize)imageSize; + +@end diff --git a/Artsy/Classes/View Controllers/ARModelCollectionViewModule.m b/Artsy/Classes/View Controllers/ARModelCollectionViewModule.m new file mode 100644 index 00000000000..cc568607a5d --- /dev/null +++ b/Artsy/Classes/View Controllers/ARModelCollectionViewModule.m @@ -0,0 +1,45 @@ +#import "ARModelCollectionViewModule.h" + +@implementation ARModelCollectionViewModule + +// As the items & moduleLayout should be treated as +// properties by their subclasses we need to ensure +// that they are correctly set. + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + _items = [NSArray array]; + return self; +} + +- (UICollectionViewLayout *)moduleLayout; +{ + NSAssert(YES, @"moduleLayout not set on module subclass"); + return nil; +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + +} + +- (Class)classForCell +{ + NSAssert(YES, @"class for cell not set on module subclass"); + return nil; +} + +- (CGSize)intrinsicSize +{ + return (CGSize){ 240, 240 }; +} + +- (ARFeedItemImageSize)imageSize +{ + return ARFeedItemImageSizeAuto; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARNavigationButtonsViewController.h b/Artsy/Classes/View Controllers/ARNavigationButtonsViewController.h new file mode 100644 index 00000000000..e6390ad3e18 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARNavigationButtonsViewController.h @@ -0,0 +1,28 @@ +typedef void (^ARNavigationButtonHandler)(UIButton *button); + +// This key tells ARNavigationButtonsViewController which UIButton subclass to +// use for the given button, defaults to ARNavigationButton. +extern NSString * const ARNavigationButtonClassKey; + +extern NSString * const ARNavigationButtonHandlerKey; + +// Associate a dictionary of properties with this key to have +// ARNavigationButtonsViewController set them on the button using KVO. +// +// For convenience, NSNull is treated as nil. +extern NSString * const ARNavigationButtonPropertiesKey; + +// This view controller constructs a stack of UIButton subclasses from an array +// of button descriptions you pass in. +@interface ARNavigationButtonsViewController : UIViewController + +- (id)initWithButtonDescriptions:(NSArray *)descriptions; + +- (NSArray *)navigationButtons; + +@property (readwrite, nonatomic, copy) NSArray *buttonDescriptions; + +- (void)addButtonDescriptions:(NSArray *)buttonDescriptions; +- (void)addButtonDescriptions:(NSArray *)buttonDescriptions unique:(BOOL)unique; + +@end diff --git a/Artsy/Classes/View Controllers/ARNavigationButtonsViewController.m b/Artsy/Classes/View Controllers/ARNavigationButtonsViewController.m new file mode 100644 index 00000000000..cc68259e7f2 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARNavigationButtonsViewController.m @@ -0,0 +1,129 @@ +#import "ARNavigationButtonsViewController.h" +#import "ARNavigationButton.h" + +NSString * const ARNavigationButtonClassKey = @"ARNavigationButtonClassKey"; +NSString * const ARNavigationButtonHandlerKey = @"ARNavigationButtonHandlerKey"; +NSString * const ARNavigationButtonPropertiesKey = @"ARNavigationButtonPropertiesKey"; + +@interface ARNavigationButtonsViewController () + +@property (nonatomic, strong) ORStackView *view; +@property (nonatomic, strong) NSMapTable *handlersByButton; + +@end + +@implementation ARNavigationButtonsViewController + +- (id)init +{ + self = [super init]; + if (!self) { return nil; } + + _handlersByButton = [NSMapTable strongToStrongObjectsMapTable]; + _buttonDescriptions = [[NSArray alloc] init]; + + return self; +} + +- (id)initWithButtonDescriptions:(NSArray *)descriptions +{ + self = [self init]; + if (!self) { return nil; } + + self.buttonDescriptions = descriptions; + + return self; +} + +- (void)loadView +{ + self.view = [[ORStackView alloc] init]; + self.view.bottomMarginHeight = 0; +} + +- (void)tappedItem:(ARNavigationButton *)sender +{ + ARNavigationButtonHandler handler = [self.handlersByButton objectForKey:sender]; + handler(sender); +} + +- (CGSize)preferredContentSize +{ + CGFloat height = 0; + for (UIButton *button in self.navigationButtons) { + height += button.intrinsicContentSize.height; + } + + return (CGSize){ + .width = CGRectGetWidth(self.parentViewController.view.bounds), + .height = height + }; +} + +#pragma mark - Properties + +- (void)setButtonDescriptions:(NSArray *)buttonDescriptions { + // Remove old buttons + for (UIButton *button in self.navigationButtons) { + [self.handlersByButton removeObjectForKey:button]; + [self.view removeSubview:button]; + } + _buttonDescriptions = [NSArray array]; + [self addButtonDescriptions:[buttonDescriptions copy]]; +} + +- (void)addButtonDescriptions:(NSArray *)buttonDescriptions { + [self addButtonDescriptions:buttonDescriptions unique:NO]; +} + +- (void)addButtonDescriptions:(NSArray *)buttonDescriptions unique:(BOOL)unique { + for (NSDictionary *newButtonDescription in buttonDescriptions) { + if (unique && [_buttonDescriptions detect:^BOOL(id buttonDescription) { + return [buttonDescription[ARNavigationButtonPropertiesKey] isEqualToDictionary:newButtonDescription[ARNavigationButtonPropertiesKey]]; + }]) { + continue; + } + + UIButton *button = [self buttonWithDictionary:newButtonDescription]; + + ARNavigationButtonHandler handler = newButtonDescription[ARNavigationButtonHandlerKey]; + if (handler) { + [self.handlersByButton setObject:handler forKey:button]; + [button addTarget:self action:@selector(tappedItem:) forControlEvents:UIControlEventTouchUpInside]; + } + + [self.view addSubview:button withTopMargin:@"-1" sideMargin:@"0"]; + _buttonDescriptions = [_buttonDescriptions arrayByAddingObject:newButtonDescription]; + } +} + +- (NSArray *)navigationButtons +{ + return [self.view.subviews select:^BOOL(UIView *subview) { + return [subview isKindOfClass:[UIButton class]]; + }]; +} + +#pragma mark - ARNavigationButtonsViewController + +- (UIButton *)buttonWithDictionary:(NSDictionary *)dictionary +{ + Class class = dictionary[ARNavigationButtonClassKey] ?: ARNavigationButton.class; + + NSParameterAssert([class isSubclassOfClass:UIButton.class]); + + UIButton *button = [[class alloc] init]; + + NSDictionary *propertiesByKeyPath = dictionary[ARNavigationButtonPropertiesKey]; + [propertiesByKeyPath enumerateKeysAndObjectsUsingBlock:^(NSString *keypath, id value, BOOL *stop) { + if (value == NSNull.null) { + value = nil; + } + + [button setValue:value forKeyPath:keypath]; + }]; + + return button; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARNavigationContainer.h b/Artsy/Classes/View Controllers/ARNavigationContainer.h new file mode 100644 index 00000000000..b1ed2419483 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARNavigationContainer.h @@ -0,0 +1,12 @@ +/// The ARNavigationContainer protocol is a way of abstracting out +/// the navigation stack with the interface, you can experiment +/// with any UI as long as you respond to these methods. + +@protocol ARNavigationContainer + +- (void)loadFeed; + +- (void)startLoading; +- (void)stopLoading; + +@end diff --git a/Artsy/Classes/View Controllers/ARNavigationController.h b/Artsy/Classes/View Controllers/ARNavigationController.h new file mode 100644 index 00000000000..6ffb8b731cf --- /dev/null +++ b/Artsy/Classes/View Controllers/ARNavigationController.h @@ -0,0 +1,26 @@ +/// We want the Artwork View Controller to allow rotation, but +/// in order for this to happen by default *every* other view in the +/// heirarchy has to support this. So in this case we only check the top VC. + +@interface ARNavigationController : UINavigationController + +@property (readonly, nonatomic, strong) UIButton *backButton; + +- (void)showBackButton:(BOOL)visible animated:(BOOL)animated; +- (void)showStatusBar:(BOOL)visible animated:(BOOL)animated; +- (void)showStatusBarBackground:(BOOL)visible animated:(BOOL)animated; + +- (IBAction)back:(id)sender; + +/// Presents a pending operation overlay view controller. +/// +/// @returns A RACCommand representing the work to remove the view controller. +/// Subscribe to the completion of the command's execution to perform work after +/// the pending operation view controller has been removed. + +- (RACCommand *)presentPendingOperationLayover; +- (RACCommand *)presentPendingOperationLayoverWithMessage:(NSString *)message; + +@property (nonatomic, assign) BOOL animatesLayoverChanges; + +@end diff --git a/Artsy/Classes/View Controllers/ARNavigationController.m b/Artsy/Classes/View Controllers/ARNavigationController.m new file mode 100644 index 00000000000..a1a52c3be8e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARNavigationController.m @@ -0,0 +1,410 @@ +#import +#import + +#import "UIView+HitTestExpansion.h" +#import "UIViewController+InnermostTopViewController.h" +#import "UIViewController+SimpleChildren.h" + +#import "ARNavigationTransitionController.h" +#import "ARPendingOperationViewController.h" + +static void * ARNavigationControllerButtonStateContext = &ARNavigationControllerButtonStateContext; +static void * ARNavigationControllerScrollingChiefContext = &ARNavigationControllerScrollingChiefContext; + +@interface ARNavigationController () + +@property (nonatomic, assign) BOOL isAnimatingTransition; +@property (nonatomic, strong) UIView *statusBarView; +@property (nonatomic, strong) NSLayoutConstraint *statusBarVerticalConstraint; + +@property (readwrite, nonatomic, strong) ARPendingOperationViewController *pendingOperationViewController; +@property (readwrite, nonatomic, strong) AIMultiDelegate *multiDelegate; +@property (readwrite, nonatomic, strong) UIViewController *observedViewController; +@property (readwrite, nonatomic, strong) UIPercentDrivenInteractiveTransition *interactiveTransitionHandler; + +- (void)handlePopGuesture:(UIGestureRecognizer *)sender; + +- (BOOL)shouldShowBackButtonForViewController:(UIViewController *)viewController; +- (BOOL)shouldShowStatusBarBackgroundForViewController:(UIViewController *)viewController; + +@end + +@implementation ARNavigationController + +- (instancetype)initWithRootViewController:(UIViewController *)rootViewController +{ + self = [super initWithRootViewController:rootViewController]; + if (!self) { return nil; } + + self.interactivePopGestureRecognizer.delegate = self; + [self.interactivePopGestureRecognizer removeTarget:nil action:nil]; + [self.interactivePopGestureRecognizer addTarget:self action:@selector(handlePopGuesture:)]; + + self.navigationBarHidden = YES; + + _multiDelegate = [[AIMultiDelegate alloc] init]; + [_multiDelegate addDelegate:self]; + [super setDelegate:(id)_multiDelegate]; + + // We observe the allowsMenuButton property and show or hide the buttons + // every time it changes to NO. + // + // We don't check the chief when we do transitions because the buttons + // should be always visible on pop and the scrollsviews should be at the + // top on push anyways. + [ARScrollNavigationChief.chief addObserver:self forKeyPath:@keypath(ARScrollNavigationChief.chief, allowsMenuButtons) options:NSKeyValueObservingOptionNew context:ARNavigationControllerScrollingChiefContext]; + + _animatesLayoverChanges = YES; + + return self; +} + +- (void)dealloc +{ + [ARScrollNavigationChief.chief removeObserver:self forKeyPath:@keypath(ARScrollNavigationChief.chief, allowsMenuButtons) context:ARNavigationControllerScrollingChiefContext]; + [_observedViewController removeObserver:self forKeyPath:@keypath(self.observedViewController, hidesBackButton) context:ARNavigationControllerButtonStateContext]; +} + +- (void)setEnableNavigationButtons:(BOOL)enabled +{ + self.backButton.enabled = enabled; +} + +#pragma mark - Properties + +- (void)setDelegate:(id)delegate +{ + [self.multiDelegate removeAllDelegates]; + [self.multiDelegate addDelegate:delegate]; + [self.multiDelegate addDelegate:self]; +} + +- (void)setObservedViewController:(UIViewController *)observedViewController { + NSParameterAssert(observedViewController == nil || [observedViewController.class conformsToProtocol:@protocol(ARMenuAwareViewController)]); + + NSArray *keyPaths = @[ + @keypath(self.observedViewController, hidesBackButton), + @keypath(self.observedViewController, hidesToolbarMenu), + @keypath(self.observedViewController, enableMenuButtons) + ]; + + [keyPaths each:^(NSString *keyPath) { + if ([self.observedViewController respondsToSelector:NSSelectorFromString(keyPath)]){ + [self.observedViewController removeObserver:self forKeyPath:keyPath context:ARNavigationControllerButtonStateContext]; + } + }]; + + _observedViewController = observedViewController; + + [keyPaths each:^(NSString *keyPath) { + if ([self.observedViewController respondsToSelector:NSSelectorFromString(keyPath)]){ + [self.observedViewController addObserver:self forKeyPath:keyPath options:0 context:ARNavigationControllerButtonStateContext]; + } + }]; +} + +#pragma mark - UIViewControl;er + +- (void)viewDidLoad { + [super viewDidLoad]; + + _statusBarView = [[UIView alloc] init]; + _statusBarView.backgroundColor = UIColor.blackColor; + [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent animated:NO]; + + [self.view addSubview:_statusBarView]; + + _statusBarVerticalConstraint = [_statusBarView constrainHeight:@"20"][0]; + [_statusBarView constrainWidthToView:self.view predicate:nil]; + [_statusBarView alignTopEdgeWithView:self.view predicate:nil]; + [_statusBarView alignLeadingEdgeWithView:self.view predicate:nil]; + + _backButton = [[ARMenuButton alloc] init]; + [_backButton ar_extendHitTestSizeByWidth:10 andHeight:10]; + [_backButton setImage:[UIImage imageNamed:@"BackArrow"] forState:UIControlStateNormal]; + [_backButton addTarget:self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; + _backButton.adjustsImageWhenDisabled = NO; + + [self.view addSubview:_backButton]; + [_backButton constrainTopSpaceToView:_statusBarView predicate:@"12"]; + [_backButton alignLeadingEdgeWithView:self.view predicate:@"12"]; + _backButton.accessibilityIdentifier = @"Back"; + _backButton.alpha = 0; +} + +#pragma mark - Rotation + +-(BOOL)shouldAutorotate +{ + return self.topViewController.shouldAutorotate; +} + +-(NSUInteger)supportedInterfaceOrientations +{ + return self.topViewController.supportedInterfaceOrientations ?: ([UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown); +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation +{ + return self.topViewController.preferredInterfaceOrientationForPresentation ?: UIInterfaceOrientationPortrait; +} + +#pragma mark - UINavigationControllerDelegate + +- (id )navigationController:(UINavigationController *)navigationController + animationControllerForOperation:(UINavigationControllerOperation)operation + fromViewController:(UIViewController *)fromVC + toViewController:(UIViewController *)toVC +{ + ARNavigationTransition *transition = [ARNavigationTransitionController animationControllerForOperation:operation + fromViewController:fromVC + toViewController:toVC]; + + if (self.interactiveTransitionHandler != nil) { + BOOL popToRoot = self.viewControllers.count <= 1; + + transition.backButtonTargetAlpha = popToRoot ? 0 : 1; + transition.menuButtonTargetAlpha = 1; + } + + return transition; +} + +- (id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(ARNavigationTransition *)animationController +{ + NSParameterAssert([animationController isKindOfClass:ARNavigationTransition.class]); + + return animationController.supportsInteractiveTransitioning ? self.interactiveTransitionHandler : nil; +} + +- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + [self setEnableNavigationButtons:NO]; + + // If it is a non-interactive transition, we fade the buttons in or out + // ourselves. Otherwise, we'll leave it to the interactive transition. + if (self.interactiveTransitionHandler == nil) { + [self showBackButton:[self shouldShowBackButtonForViewController:viewController] animated:animated]; + [self showStatusBar:!viewController.prefersStatusBarHidden animated:animated]; + [self showStatusBarBackground:[self shouldShowStatusBarBackgroundForViewController:viewController] animated:animated]; + + BOOL hideToolbar = [self shouldHideToolbarMenuForViewController:viewController]; + [[ARTopMenuViewController sharedController] hideToolbar:hideToolbar animated:animated]; + } +} + +- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + [self setEnableNavigationButtons:YES]; + + if ([viewController conformsToProtocol:@protocol(ARMenuAwareViewController)]) { + self.observedViewController = (UIViewController *)viewController; + } else { + self.observedViewController = nil; + } + + [self showBackButton:[self shouldShowBackButtonForViewController:viewController] animated:NO]; + [self showStatusBarBackground:[self shouldShowStatusBarBackgroundForViewController:viewController] animated:NO]; + + BOOL hideToolbar = [self shouldHideToolbarMenuForViewController:viewController]; + [[ARTopMenuViewController sharedController] hideToolbar:hideToolbar animated:NO]; +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + NSCParameterAssert(gestureRecognizer == self.interactivePopGestureRecognizer); + + BOOL isInnerMost = self.ar_innermostTopViewController.navigationController == self; + BOOL innermostIsAtRoot = self.ar_innermostTopViewController.navigationController.viewControllers.count == 1; + + return isInnerMost || innermostIsAtRoot; +} + +#pragma mark - Handling the pop guesture + +- (void)handlePopGuesture:(UIScreenEdgePanGestureRecognizer *)sender +{ + NSParameterAssert([sender isKindOfClass:UIScreenEdgePanGestureRecognizer.class]); + + CGPoint translation = [sender translationInView:self.view]; + CGFloat fraction = translation.x / CGRectGetWidth(self.view.bounds); + + switch (sender.state) { + case UIGestureRecognizerStatePossible: + break; + + case UIGestureRecognizerStateBegan: + self.interactiveTransitionHandler = [[UIPercentDrivenInteractiveTransition alloc] init]; + self.interactiveTransitionHandler.completionCurve = UIViewAnimationCurveLinear; + [self popViewControllerAnimated:YES]; + break; + + case UIGestureRecognizerStateChanged: + [self.interactiveTransitionHandler updateInteractiveTransition:fraction]; + break; + + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateEnded: + if ([sender velocityInView:self.view].x > 0) { + [self.interactiveTransitionHandler finishInteractiveTransition]; + } else { + [self.interactiveTransitionHandler cancelInteractiveTransition]; + } + + self.interactiveTransitionHandler = nil; + break; + + case UIGestureRecognizerStateFailed: + [self.interactiveTransitionHandler cancelInteractiveTransition]; + self.interactiveTransitionHandler = nil; + break; + } +} + +#pragma mark - Menu buttons + +- (void)showBackButton:(BOOL)visible animated:(BOOL)animated +{ + CGFloat toValue = visible ? 1 : 0; + + self.backButton.layer.opacity = toValue; + + if (animated) { + CABasicAnimation *fade = [CABasicAnimation animation]; + fade.keyPath = @keypath(self.backButton.layer, opacity); + fade.fromValue = @([self.backButton.layer.presentationLayer opacity]); + fade.toValue = @(toValue); + fade.duration = ARAnimationDuration; + + [self.backButton.layer addAnimation:fade forKey:@"fade"]; + } +} + +- (void)showStatusBar:(BOOL)visible animated:(BOOL)animated +{ + if (animated) { + [[UIApplication sharedApplication] setStatusBarHidden:!visible withAnimation:UIStatusBarAnimationSlide]; + } else { + [[UIApplication sharedApplication] setStatusBarHidden:!visible withAnimation:UIStatusBarAnimationNone]; + } + + [UIView animateIf:animated duration:ARAnimationDuration :^{ + self.statusBarVerticalConstraint.constant = visible ? 20 : 0; + + if (animated) [self.view layoutIfNeeded]; + }]; +} + +- (void)showStatusBarBackground:(BOOL)visible animated:(BOOL)animated +{ + [UIView animateIf:animated duration:ARAnimationDuration :^{ + self.statusBarView.alpha = visible ? 1 : 0; + }]; +} + +- (BOOL)shouldShowBackButtonForViewController:(UIViewController *)viewController +{ + if ([viewController conformsToProtocol:@protocol(ARMenuAwareViewController)]) { + return ![(id)viewController hidesBackButton]; + } + + return self.viewControllers.count > 1; +} + +- (BOOL)shouldShowStatusBarBackgroundForViewController:(UIViewController *)viewController +{ + if ([viewController respondsToSelector:@selector(hidesStatusBarBackground)]) { + return ![(id)viewController hidesStatusBarBackground]; + } + + return YES; +} + +- (BOOL)shouldHideToolbarMenuForViewController:(UIViewController *)viewController { + if ([viewController conformsToProtocol:@protocol(ARMenuAwareViewController)]) { + return [(id)viewController hidesToolbarMenu]; + } + + return NO; +} + + +#pragma mark - Public methods + +- (RACCommand *)presentPendingOperationLayover +{ + return [self presentPendingOperationLayoverWithMessage:nil]; +} + +- (RACCommand *)presentPendingOperationLayoverWithMessage:(NSString *)message +{ + self.pendingOperationViewController = [[ARPendingOperationViewController alloc] init]; + if (message) { + self.pendingOperationViewController.message = message; + } + [self ar_addModernChildViewController:self.pendingOperationViewController]; + + @weakify(self); + + return [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { + @strongify(self); + RACSubject *completionSubject = [RACSubject subject]; + + [UIView animateIf:self.animatesLayoverChanges duration:ARAnimationDuration :^{ + self.pendingOperationViewController.view.alpha = 0.0; + } completion:^(BOOL finished) { + [self ar_removeChildViewController:self.pendingOperationViewController]; + self.pendingOperationViewController = nil; + [completionSubject sendCompleted]; + }]; + + return completionSubject; + }]; +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == ARNavigationControllerButtonStateContext) { + UIViewController *vc = object; + + [self showBackButton:!vc.hidesBackButton animated:YES]; + + if ([vc respondsToSelector:@selector(hidesToolbarMenu)]) { + [[ARTopMenuViewController sharedController] hideToolbar:vc.hidesToolbarMenu animated:YES]; + } + + if ([vc respondsToSelector:@selector(enableMenuButtons)]) { + self.backButton.enabled = vc.enableMenuButtons; + } + + } else if (context == ARNavigationControllerScrollingChiefContext) { + // All hail the chief + ARScrollNavigationChief *chief = object; + + [self showBackButton:[self shouldShowBackButtonForViewController:self.topViewController] && chief.allowsMenuButtons animated:YES]; + + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +#pragma mark - Actions + +- (IBAction)back:(id)sender { + if(self.isAnimatingTransition) return; + + UINavigationController *navigationController = self.ar_innermostTopViewController.navigationController; + + if (navigationController.viewControllers.count > 1) { + [navigationController popViewControllerAnimated:YES]; + } else { + [navigationController.navigationController popViewControllerAnimated:YES]; + } +} + +@end + diff --git a/Artsy/Classes/View Controllers/AROnboardingArtistTableController.h b/Artsy/Classes/View Controllers/AROnboardingArtistTableController.h new file mode 100644 index 00000000000..d7029641eef --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingArtistTableController.h @@ -0,0 +1,17 @@ +/// This exists purely because we don't want to have to disambiguate between this tableView +/// and the Gene and Search ones in personalize + +@interface AROnboardingArtistTableController : NSObject + +@property (nonatomic, readonly) NSMutableOrderedSet *artists; + +// It was either this or KVO +@property (nonatomic, copy) void (^postRemoveBlock)(void); + +- (void)addArtist:(Artist *)artist; +- (void)removeArtist:(Artist *)artist; +- (BOOL)hasArtist:(Artist *)artist; +- (void)prepareTableView:(UITableView *)tableView; +- (void)unfollowArtist:(Artist *)artist; + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingArtistTableController.m b/Artsy/Classes/View Controllers/AROnboardingArtistTableController.m new file mode 100644 index 00000000000..a8c41968cb4 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingArtistTableController.m @@ -0,0 +1,91 @@ +#import "AROnboardingArtistTableController.h" +#import "AROnboardingFollowableTableViewCell.h" + +static NSString *CellId = @"OnboardingArtistFollow"; + +@interface AROnboardingArtistTableController () +@property (nonatomic) NSMutableOrderedSet *artists; +@property (nonatomic, assign) BOOL isFollowed; +@end + +@implementation AROnboardingArtistTableController + +- (instancetype)init +{ + self = [super init]; + if (self) { + _artists = [[NSMutableOrderedSet alloc] init]; + } + return self; +} + +- (BOOL)hasArtist:(Artist *)artist +{ + return [self.artists containsObject:artist]; +} + +- (void)addArtist:(Artist *)artist +{ + [self.artists addObject:artist]; +} + +- (void)removeArtist:(Artist *)artist +{ + [self.artists removeObject:artist]; + if (self.postRemoveBlock) { + self.postRemoveBlock(); + } +} + +- (void)unfollowArtist:(Artist *)artist +{ + [self removeArtist:artist]; + + [artist unfollowWithSuccess:^(id response) { + ARActionLog(@"Unfollowed artist %@ from onboarding.", artist.artistID); + + } failure:^(NSError *error) { + ARErrorLog(@"Error unfollowing artist %@ from onboarding. Error: %@", artist.artistID, error.localizedDescription); + [self addArtist:artist]; + }]; +} + +- (void)prepareTableView:(UITableView *)tableView +{ + [tableView registerClass:[AROnboardingFollowableTableViewCell class] forCellReuseIdentifier:CellId]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.artists.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + AROnboardingFollowableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellId]; + Artist *artist = (Artist *)self.artists[indexPath.row]; + cell.textLabel.text = artist.name; + cell.followState = artist.isFollowed; + + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + Artist *artist = [self.artists objectAtIndex:indexPath.row]; + [self.artists removeObject:artist]; + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + [self unfollowArtist:artist]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 54; +} + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingFollowableTableViewCell.h b/Artsy/Classes/View Controllers/AROnboardingFollowableTableViewCell.h new file mode 100644 index 00000000000..4b6e90068b7 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingFollowableTableViewCell.h @@ -0,0 +1,7 @@ +#import +#import "AROnboardingTableViewCell.h" + +@interface AROnboardingFollowableTableViewCell : AROnboardingTableViewCell +- (void)toggleFollowState; +@property (nonatomic) BOOL followState; +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingFollowableTableViewCell.m b/Artsy/Classes/View Controllers/AROnboardingFollowableTableViewCell.m new file mode 100644 index 00000000000..c9311fed318 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingFollowableTableViewCell.m @@ -0,0 +1,52 @@ +#import "AROnboardingFollowableTableViewCell.h" + +@implementation AROnboardingFollowableTableViewCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + self.accessoryType = UITableViewCellAccessoryNone; + _followState = NO; + } + return self; +} + +- (void)prepareForReuse +{ + self.alpha = 1; + self.textLabel.text = @""; + self.imageView.image = nil; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + if (!self.imageView.image) { + self.textLabel.frame = CGRectMake(20, 0, 250, 54); + } + + [self setFollowState:_followState]; +} + +- (void)setFollowState:(BOOL)followState +{ + if (!self.accessoryView) { + UIImage *check = [UIImage imageNamed:@"FollowCheckmark"]; + self.accessoryView = [[UIImageView alloc] initWithImage:check]; + } + self.accessoryView.alpha = followState ? 1.f : .5f; + _followState = followState; +} + +- (void)toggleFollowState +{ + self.followState = !self.followState; +} + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + //to override the flash from AROnboardingTVC +} +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingGeneTableController.h b/Artsy/Classes/View Controllers/AROnboardingGeneTableController.h new file mode 100644 index 00000000000..581f3d67fac --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingGeneTableController.h @@ -0,0 +1,12 @@ +/// This exists purely because we don't want to have to disambiguate between this tableView +/// and the Artist and Search ones in personalize + +@interface AROnboardingGeneTableController : NSObject + +- (instancetype)initWithGenes:(NSArray *)genes; +- (void)prepareTableView:(UITableView *)tableView; + +@property (nonatomic, assign) CGFloat numberOfFollowedGenes; +@property (nonatomic, readonly) NSArray *genes; + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingGeneTableController.m b/Artsy/Classes/View Controllers/AROnboardingGeneTableController.m new file mode 100644 index 00000000000..49392f9074d --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingGeneTableController.m @@ -0,0 +1,91 @@ +#import "AROnboardingGeneTableController.h" +#import "AROnboardingFollowableTableViewCell.h" + +static NSString *CellId = @"OnboardingGeneFollow"; + +@interface AROnboardingGeneTableController () +@property (nonatomic) UITableView *tableView; +@property (nonatomic) NSArray *genes; +@end + +@implementation AROnboardingGeneTableController + +- (instancetype)initWithGenes:(NSArray *)genes +{ + self = [super init]; + if (self) { + _genes = genes; + } + return self; +} + +- (void)prepareTableView:(UITableView *)tableView +{ + [tableView registerClass:[AROnboardingFollowableTableViewCell class] forCellReuseIdentifier:CellId]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.genes.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return 50; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + AROnboardingFollowableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellId]; + Gene *gene = self.genes[indexPath.row]; + cell.textLabel.text = gene.name; + cell.followState = gene.isFollowed; + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 54; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, tableView.bounds.size.width - 20, 50)]; + label.font = [UIFont sansSerifFontWithSize:14]; + label.text = [@"Or Categories" uppercaseString]; + label.textColor = [UIColor whiteColor]; + label.backgroundColor = [UIColor clearColor]; + + UIView *wrapper = [[UIView alloc] init]; + [wrapper addSubview:label]; + return wrapper; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + AROnboardingFollowableTableViewCell *cell = ((AROnboardingFollowableTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]); + [cell toggleFollowState]; + + Gene *gene = [self.genes objectAtIndex:indexPath.row]; + BOOL newState = !gene.isFollowed; + + self.numberOfFollowedGenes += newState ? 1 : -1; + + @weakify(gene); + [gene setFollowState:newState success:^(id response) { + @strongify(gene); + ARActionLog(@"%@ gene %@", newState ? @"Followed" : @"Unfollowed" , gene.geneID); + + } failure:^(NSError *error) { + @strongify(gene); + [cell toggleFollowState]; + ARErrorLog(@"Error %@ gene %@. Error: %@", newState ? @"following" : @"unfollowing", gene, error.localizedDescription); + }]; + +} +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingMoreInfoViewController.h b/Artsy/Classes/View Controllers/AROnboardingMoreInfoViewController.h new file mode 100644 index 00000000000..0a37b511194 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingMoreInfoViewController.h @@ -0,0 +1,15 @@ +@class AROnboardingViewController; + +typedef NS_ENUM(NSInteger, AROnboardingMoreInfoViewControllerLoginType) { + AROnboardingMoreInfoViewControllerLoginTypeFacebook = 0, + AROnboardingMoreInfoViewControllerLoginTypeTwitter +}; + +@interface AROnboardingMoreInfoViewController : UIViewController + +- (instancetype)initForFacebookWithToken:(NSString *)token email:(NSString *)email name:(NSString *)name; +- (instancetype)initForTwitterWithToken:(NSString *)token andSecret:(NSString *)secret; + +@property (nonatomic, weak) AROnboardingViewController *delegate; + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingMoreInfoViewController.m b/Artsy/Classes/View Controllers/AROnboardingMoreInfoViewController.m new file mode 100644 index 00000000000..5fd04fc4f0b --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingMoreInfoViewController.m @@ -0,0 +1,356 @@ +#import "AROnboardingMoreInfoViewController.h" +#import "ARAuthProviders.h" +#import "AROnboardingNavBarView.h" +#import "ARTextFieldWithPlaceholder.h" +#import "ARUserManager.h" +#import "AROnboardingViewController.h" +#import "UIViewController+FullScreenLoading.h" +#import + +//sigh +#define EMAIL_TAG 111 +#define SOCIAL_TAG 222 + +@interface AROnboardingMoreInfoViewController () +@property (nonatomic) NSString *token; +@property (nonatomic) NSString *secret; +@property (nonatomic) NSString *name; +@property (nonatomic) NSString *email; +@property (nonatomic) AROnboardingNavBarView *navBar; +@property (nonatomic) ARTextFieldWithPlaceholder *nameField; +@property (nonatomic) ARTextFieldWithPlaceholder *emailField; + +@property (nonatomic) ARAuthProviderType provider; +@end + +@implementation AROnboardingMoreInfoViewController + +- (instancetype)initForFacebookWithToken:(NSString *)token email:(NSString *)email name:(NSString *)name +{ + self = [super init]; + if (self) { + _provider = ARAuthProviderFacebook; + _token = token; + _name = name; + _email = email; + } + return self; +} + +- (instancetype)initForTwitterWithToken:(NSString *)token andSecret:(NSString *)secret +{ + self = [super init]; + if (self) { + _provider = ARAuthProviderTwitter; + _token = token; + _secret = secret; + } + return self; +} + +- (void)viewDidLoad +{ + [self setupUI]; + [super viewDidLoad]; +} + +- (void)setupUI +{ + self.navBar = [[AROnboardingNavBarView alloc] init]; + self.navBar.title.text = @"Almost Done…"; + [self.navBar.back addTarget:self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; + + [self.navBar.forward setTitle:@"JOIN" forState:UIControlStateNormal]; + [self.view addSubview:self.navBar]; + + NSInteger spacing = 50; + NSInteger heightMinusKeyboard = self.view.bounds.size.height - 280; + + self.nameField = [[ARTextFieldWithPlaceholder alloc] initWithFrame:CGRectMake(20, heightMinusKeyboard - spacing * 2, 280, 30)]; + self.nameField.placeholder = @"Full Name"; + self.nameField.autocapitalizationType = UITextAutocapitalizationTypeWords; + self.nameField.autocorrectionType = UITextAutocorrectionTypeNo; + self.nameField.returnKeyType = UIReturnKeyNext; + self.nameField.keyboardAppearance = UIKeyboardAppearanceDark; + if (self.name) { + self.nameField.text = self.name; + } else { + [self.nameField becomeFirstResponder]; + } + + self.emailField = [[ARTextFieldWithPlaceholder alloc] initWithFrame:CGRectMake(20, heightMinusKeyboard - spacing, 280, 30)]; + self.emailField.placeholder = @"Email"; + self.emailField.keyboardType = UIKeyboardTypeEmailAddress; + self.emailField.autocorrectionType = UITextAutocorrectionTypeNo; + self.emailField.returnKeyType = UIReturnKeyNext; + self.emailField.keyboardAppearance = UIKeyboardAppearanceDark; + + if (self.email) { + self.emailField.text = self.email; + } + + if (self.name) { + [self.emailField becomeFirstResponder]; + if (self.email) { + UITextPosition *start = [self.emailField positionFromPosition:[self.emailField beginningOfDocument] inDirection:UITextLayoutDirectionRight offset:self.email.length]; + self.emailField.selectedTextRange = [self.emailField textRangeFromPosition:start toPosition:start]; + } + } + + [@[self.nameField, self.emailField] each:^(ARTextFieldWithPlaceholder *textField) { + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + textField.delegate = self; + [self.view addSubview:textField]; + }]; + + [self.navBar.forward setEnabled:[self canSubmit] animated:NO]; + [self.navBar.forward addTarget:self action:@selector(submit:) forControlEvents:UIControlEventTouchUpInside]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextFieldTextDidChangeNotification object:nil]; + +} + +- (void)back:(id)sender +{ + [self.delegate popViewControllerAnimated:YES]; +} + +- (BOOL)canSubmit +{ + return self.nameField.text.length && self.emailField.text.length; +} + +- (void)setFormEnabled:(BOOL)enabled +{ + [@[self.nameField, self.emailField] each:^(ARTextFieldWithPlaceholder *textField) { + textField.enabled = enabled; + textField.alpha = enabled? 1 : 0.5; + }]; +} + +- (NSString *)existingAccountSource:(NSDictionary *)JSON +{ + NSArray *providers = JSON[@"providers"]; + if (providers) { + return [providers.first lowercaseString]; + } + NSString *message = JSON[@"text"]; + if (!message) { + return @"email"; + } + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"A user with this email has already signed up with ([A-Za-z]+)." options:0 error:nil]; + NSTextCheckingResult *match = [regex firstMatchInString:message options:0 range:NSMakeRange(0, message.length)]; + if ([match numberOfRanges] != 2) { + return @"email"; + } + NSString *provider = [message substringWithRange:[match rangeAtIndex:1]]; + return [provider lowercaseString]; +} + +- (void)submit:(id)sender +{ + if (![self canSubmit]) { + return; + } + + @weakify(self); + [self setFormEnabled:NO]; + if (self.provider == ARAuthProviderFacebook) { + [[ARUserManager sharedManager] createUserViaFacebookWithToken:self.token + email:self.emailField.text + name:self.nameField.text + success:^(User *user) { + @strongify(self); + [self loginWithFacebookCredential:NO]; + } failure:^(NSError *error, id JSON) { + @strongify(self); + if (JSON && [JSON isKindOfClass:[NSDictionary class]]) { + if ([JSON[@"error"] containsString:@"Another Account Already Linked"]) { + ARActionLog(@"Facebook account already linked"); + [self userAlreadyExistsForLoginType:AROnboardingMoreInfoViewControllerLoginTypeFacebook]; + return; + + // there's already a user with this email + } else if ([JSON[@"error"] isEqualToString:@"User Already Exists"] + || [JSON[@"error"] isEqualToString:@"User Already Invited"]) { + NSString *source = [self existingAccountSource:JSON]; + [self accountExists:source]; + return; + } + } + + ARErrorLog(@"Couldn't link Facebook account. Error: %@. The server said: %@", error.localizedDescription, JSON); + NSString *errorString = [NSString stringWithFormat:@"Server replied saying '%@'.", JSON[@"error"] ?: JSON[@"message"] ?: error.localizedDescription]; + @weakify(self); + [UIAlertView showWithTitle:@"Error Creating\na New Artsy Account" message:errorString cancelButtonTitle:@"Close" otherButtonTitles:nil tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) { + @strongify(self); + [self setFormEnabled:YES]; + }]; + }]; + } else { + [[ARUserManager sharedManager] createUserViaTwitterWithToken:self.token + secret:self.secret + email:self.emailField.text + name:self.nameField.text + success:^(User *user) { + @strongify(self); + [self loginWithTwitterCredential:NO]; + } failure:^(NSError *error, id JSON) { + @strongify(self); + if (JSON && [JSON isKindOfClass:[NSDictionary class]]) { + if ([JSON[@"error"] containsString:@"Another Account Already Linked"]) { + ARActionLog(@"Twitter account already linked"); + [self userAlreadyExistsForLoginType:AROnboardingMoreInfoViewControllerLoginTypeTwitter]; + return; + + // there's already a user with this email + } else if ([JSON[@"error"] isEqualToString:@"User Already Exists"] + || [JSON[@"error"] isEqualToString:@"User Already Invited"]) { + NSString *source = [self existingAccountSource:JSON]; + [self accountExists:source]; + return; + } + } + + ARErrorLog(@"Couldn't link Twitter account. Error: %@. The server said: %@", error.localizedDescription, JSON); + NSString *errorString = [NSString stringWithFormat:@"Server replied saying '%@'.", JSON[@"error"] ?: JSON[@"message"] ?: error.localizedDescription]; + @weakify(self); + [UIAlertView showWithTitle:@"Error Creating\na New Artsy Account" message:errorString cancelButtonTitle:@"Close" otherButtonTitles:nil tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) { + @strongify(self); + [self setFormEnabled:YES]; + }]; + }]; + } +} + +- (void)accountExists:(NSString *)source +{ + NSString *message; + NSInteger tag; + if ([source isEqualToString:@"email"]) { + message= [NSString stringWithFormat:@"An account already exists for the email address \"%@\".", self.emailField.text]; + tag = EMAIL_TAG; + } else { + message= [NSString stringWithFormat:@"An account already exists for the email address \"%@\". Please log in via %@.", + self.emailField.text, + [source capitalizedString]]; + tag = SOCIAL_TAG; + } + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Account Already Exists" message:message delegate:self cancelButtonTitle:@"Log In" otherButtonTitles:nil]; + alert.tag = tag; + [alert show]; +} + +/// skipAhead to pass over the rest of onboarding +- (void)loginWithTwitterCredential:(BOOL)skipAhead +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + @weakify(self); + + [[ARUserManager sharedManager] loginWithTwitterToken:self.token + secret:self.secret + successWithCredentials:nil + gotUser:^(User *currentUser) { + @strongify(self); + [self loginCompletedForLoginType:AROnboardingMoreInfoViewControllerLoginTypeTwitter skipAhead:skipAhead]; + } authenticationFailure:^(NSError *error) { + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + //TODO: handle me + + } networkFailure:^(NSError *error) { + @strongify(self); + [self setFormEnabled:YES]; + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [ARNetworkErrorManager presentActiveErrorModalWithError:error]; + }]; +} + +/// skipAhead to pass over the rest of onboarding +- (void)loginWithFacebookCredential:(BOOL)skipAhead +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + @weakify(self); + + [[ARUserManager sharedManager] loginWithFacebookToken:self.token successWithCredentials:nil + gotUser:^(User *currentUser) { + @strongify(self); + [self loginCompletedForLoginType:AROnboardingMoreInfoViewControllerLoginTypeFacebook skipAhead:skipAhead]; + } authenticationFailure:^(NSError *error) { + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + //TODO: handle me + + } networkFailure:^(NSError *error) { + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self setFormEnabled:YES]; + [ARNetworkErrorManager presentActiveErrorModalWithError:error]; + }]; +} + +- (void)userAlreadyExistsForLoginType:(AROnboardingMoreInfoViewControllerLoginType)loginType +{ + //let's go ahead and log them in + switch (loginType) { + case AROnboardingMoreInfoViewControllerLoginTypeFacebook: + [self loginWithFacebookCredential:YES]; + break; + case AROnboardingMoreInfoViewControllerLoginTypeTwitter: + [self loginWithTwitterCredential:YES]; + break; + } +} + +- (void)loginCompletedForLoginType:(AROnboardingMoreInfoViewControllerLoginType)loginType skipAhead:(BOOL)skipAhead +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + + if (skipAhead) { + [self.delegate dismissOnboardingWithVoidAnimation:YES]; + } else { + [self.delegate signupDone]; + } +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UITextFieldTextDidChangeNotification + object:nil]; +} + +#pragma mark - UITextField notifications + +- (void)textChanged:(NSNotification *)notification +{ + [self.navBar.forward setEnabled:[self canSubmit] animated:YES]; +} + +#pragma mark - UITextField delegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + if (textField == self.nameField && !self.emailField.text.length) { + [self.emailField becomeFirstResponder]; + return YES; + + } else if ([self canSubmit]) { + [self submit:nil]; + return YES; + } + + return NO; +} + +#pragma mark - UIAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + NSString *email = nil; + if (alertView.tag == EMAIL_TAG) { + email = self.emailField.text; + } + [self.delegate logInWithEmail:email]; +} +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingNavBarView.h b/Artsy/Classes/View Controllers/AROnboardingNavBarView.h new file mode 100644 index 00000000000..46232180a29 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingNavBarView.h @@ -0,0 +1,7 @@ +@interface AROnboardingNavBarView : UIView + +@property (nonatomic, readonly) UIButton *back; +@property (nonatomic, readonly) ARUppercaseButton *forward; +@property (nonatomic, readonly) UILabel *title; + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingNavBarView.m b/Artsy/Classes/View Controllers/AROnboardingNavBarView.m new file mode 100644 index 00000000000..1a59253c547 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingNavBarView.m @@ -0,0 +1,60 @@ +#import "AROnboardingNavBarView.h" + +@implementation AROnboardingNavBarView + +- (instancetype)init +{ + self = [super init]; + if (self) { + + // back + _back = [[UIButton alloc] initWithFrame:CGRectZero]; + self.back.backgroundColor = [UIColor clearColor]; + [self.back setImage:[UIImage imageNamed:@"BackArrow"] forState:UIControlStateNormal]; + [self.back setImage:[UIImage imageNamed:@"BackArrow_Highlighted"] forState:UIControlStateHighlighted]; + + self.back.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 12); + + [self addSubview:self.back]; + [self.back alignCenterYWithView:self predicate:nil]; + [self.back alignLeadingEdgeWithView:self predicate:[UIDevice isPad] ? @"22" : @"0"]; + + // title + _title = [[UILabel alloc] initWithFrame:CGRectZero]; + self.title.backgroundColor = [UIColor clearColor]; + self.title.font = [UIFont serifFontWithSize:20]; + self.title.textAlignment = NSTextAlignmentCenter; + self.title.textColor = [UIColor whiteColor]; + + [self addSubview:self.title]; + [self.title alignCenterWithView:self]; + + // forward + _forward = [[ARUppercaseButton alloc] initWithFrame:CGRectZero]; + [self.forward setEnabled:NO animated:NO]; + self.forward.titleLabel.font = [UIFont sansSerifFontWithSize:14]; + self.forward.titleLabel.textAlignment = NSTextAlignmentCenter; + [self.forward setTitleColor:[UIColor artsyHeavyGrey] forState:UIControlStateDisabled]; + [self.forward setTitleColor:[UIColor artsyPurple] forState:UIControlStateHighlighted]; + + self.forward.contentEdgeInsets = UIEdgeInsetsMake(0, 20, 0, 20);; + + [self addSubview:self.forward]; + [self.forward alignCenterYWithView:self predicate:nil]; + [self.forward alignTrailingEdgeWithView:self predicate:[UIDevice isPad] ? @"-22" : @"0"]; + [self.forward constrainLeadingSpaceToView:self.title predicate:@">=0"]; + + [self.back constrainHeight:@"44"]; + [self.forward constrainHeightToView:self.back predicate:nil]; + [self constrainHeightToView:self.back predicate:[UIDevice isPad] ? @"*2" : nil]; + } + + return self; +} + +-(void)didMoveToSuperview +{ + [self alignTop:@"0" leading:@"0" bottom:nil trailing:@"0" toView:self.superview]; +} + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingSearchField.h b/Artsy/Classes/View Controllers/AROnboardingSearchField.h new file mode 100644 index 00000000000..8ef060adf5e --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingSearchField.h @@ -0,0 +1,5 @@ +#import + +@interface AROnboardingSearchField : UITextField + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingSearchField.m b/Artsy/Classes/View Controllers/AROnboardingSearchField.m new file mode 100644 index 00000000000..3cb4859e654 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingSearchField.m @@ -0,0 +1,65 @@ +#import "AROnboardingSearchField.h" +#define CLEAR_BUTTON_TAG 0xbada55 + +@interface AROnboardingSearchField () +@property (nonatomic, assign) BOOL swizzledClear; +@end + +@implementation AROnboardingSearchField + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = [UIColor clearColor]; + self.font = [UIFont serifFontWithSize:20]; + self.textColor = [UIColor artsyLightGrey]; + CALayer *baseline = [CALayer layer]; + baseline.backgroundColor = [UIColor artsyHeavyGrey].CGColor; + baseline.frame = CGRectMake(0, self.frame.size.height - 1, 320, 1); + self.clipsToBounds = NO; + [self.layer addSublayer:baseline]; + + self.clearButtonMode = UITextFieldViewModeWhileEditing; + } + return self; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : [UIColor artsyHeavyGrey]}]; +} + +- (void)addSubview:(UIView *)view +{ + [super addSubview:view]; + + if (!self.swizzledClear && [view class] == [UIButton class]) { + UIView *subview = (UIView *)view.subviews.first; + if ([subview class] == [UIImageView class]) { + [self swizzleClearButton:(UIButton *)view]; + } + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + UIButton *button = (UIButton *)[self viewWithTag:CLEAR_BUTTON_TAG]; + if (button) { + button.center = CGPointMake(button.center.x, button.center.y - 3); + } +} + +- (void)swizzleClearButton:(UIButton *)button +{ + UIImageView *imageView = (UIImageView *)button.subviews.first; + UIImage *image = [imageView image]; + UIImage *templated = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:templated forState:UIControlStateNormal]; + [button setImage:templated forState:UIControlStateHighlighted]; + [button setTintColor:[UIColor whiteColor]]; + self.swizzledClear = YES; + button.tag = CLEAR_BUTTON_TAG; +} +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingTableViewCell.h b/Artsy/Classes/View Controllers/AROnboardingTableViewCell.h new file mode 100644 index 00000000000..78e1882beae --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingTableViewCell.h @@ -0,0 +1,5 @@ +#import + +@interface AROnboardingTableViewCell : UITableViewCell + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingTableViewCell.m b/Artsy/Classes/View Controllers/AROnboardingTableViewCell.m new file mode 100644 index 00000000000..c1884006c23 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingTableViewCell.m @@ -0,0 +1,55 @@ +#import "AROnboardingTableViewCell.h" + +@interface AROnboardingTableViewCell() +@property (nonatomic) BOOL centerFixed; +@end +@implementation AROnboardingTableViewCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + self.textLabel.font = [UIFont serifFontWithSize:24]; + self.textLabel.textColor = [UIColor whiteColor]; + self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + self.contentView.backgroundColor = [UIColor clearColor]; + self.backgroundColor = [UIColor clearColor]; + self.selectionStyle = UITableViewCellSelectionStyleNone; + CGRect frame = self.contentView.frame; + frame.size.height += 10; + self.contentView.frame = frame; + + CALayer *sep = [CALayer layer]; + sep.frame = CGRectMake(15, self.contentView.bounds.size.height - .5, 290, .5); + sep.backgroundColor = [UIColor artsyHeavyGrey].CGColor; + [self.layer addSublayer:sep]; + _centerFixed = NO; + + } + return self; +} + +- (void)prepareForReuse +{ + self.centerFixed = NO; +} +//ick, but this frame is CGRectZero in init, so... +- (void)layoutSubviews +{ + [super layoutSubviews]; + CGRect frame = self.textLabel.frame; + frame.origin.x += 5; + self.textLabel.frame = frame; + self.centerFixed = YES; +} + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + [super setHighlighted:highlighted animated:YES]; + self.backgroundColor = [UIColor artsyLightGrey]; + [UIView animateWithDuration:.5 animations:^{ + self.backgroundColor = [UIColor clearColor]; + }]; +} + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingTransition.h b/Artsy/Classes/View Controllers/AROnboardingTransition.h new file mode 100644 index 00000000000..d57ed18b079 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingTransition.h @@ -0,0 +1,5 @@ +#import "ARNavigationTransition.h" + +@interface AROnboardingTransition : ARNavigationTransition + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingTransition.m b/Artsy/Classes/View Controllers/AROnboardingTransition.m new file mode 100644 index 00000000000..1ca57cf51be --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingTransition.m @@ -0,0 +1,58 @@ +#import "AROnboardingTransition.h" + +@implementation AROnboardingTransition + +- (NSTimeInterval)transitionDuration:(id )transitionContext +{ + return ARAnimationDuration; +} + +- (void)pushTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext { + + CGRect fullFrame = [transitionContext containerView].bounds; + CGRect offScreenRight = fullFrame; + CGRect offScreenLeft = fullFrame; + + offScreenRight.origin.x = fullFrame.size.width; + offScreenLeft.origin.x = -fullFrame.size.width; + + toVC.view.frame = offScreenRight; + + [transitionContext.containerView addSubview:fromVC.view]; + [transitionContext.containerView addSubview:toVC.view]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + toVC.view.frame = fullFrame; + fromVC.view.frame = offScreenLeft; + + } completion:^(BOOL finished) { + [transitionContext completeTransition:YES]; + }]; +} + +- (void)popTransitionFrom:(UIViewController *)fromVC to:(UIViewController *)toVC withContext:(id )transitionContext { + + CGRect fullFrame = [transitionContext containerView].bounds; + CGRect offScreenRight = fullFrame; + CGRect offScreenLeft = fullFrame; + + offScreenRight.origin.x = fullFrame.size.width; + offScreenLeft.origin.x = -fullFrame.size.width; + + fromVC.view.frame = fullFrame; + toVC.view.frame = offScreenLeft; + + [transitionContext.containerView addSubview:toVC.view]; + [transitionContext.containerView addSubview:fromVC.view]; + + [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + fromVC.view.frame = offScreenRight; + toVC.view.frame = fullFrame; + + } completion:^(BOOL finished) { + [transitionContext completeTransition:YES]; + }]; +} + + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingViewController.h b/Artsy/Classes/View Controllers/AROnboardingViewController.h new file mode 100644 index 00000000000..10b144039cf --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingViewController.h @@ -0,0 +1,43 @@ +#import "ARSignUpSplashViewController.h" +#import "ARNavigationController.h" +#import "ARTrialController.h" +#import "ARPersonalizeWebViewController.h" + +typedef NS_ENUM(NSInteger, ARInitialOnboardingState){ + ARInitialOnboardingStateSlideShow, + ARInitialOnboardingStateInApp +}; + +/// A state-machine based VC that implements the app onboarding process + +@interface AROnboardingViewController : UINavigationController + +- (instancetype)initWithState:(enum ARInitialOnboardingState)state; + +- (void)signUpWithFacebook; +- (void)signUpWithTwitter; +- (void)signUpWithEmail; + +- (void)logInWithEmail:(NSString *)email; + +- (void)slideshowDone; + +- (void)splashDone:(ARSignUpSplashViewController *)sender; +- (void)splashDoneWithLogin:(ARSignUpSplashViewController *)sender; + +- (void)signupDone; +- (void)collectorLevelDone:(ARCollectorLevel)level; +- (void)setPriceRangeDone:(NSInteger)range; +- (void)personalizeDone; +- (void)webOnboardingDone; + +- (void)showTermsAndConditions; +- (void)showPrivacyPolicy; + +- (void)dismissOnboardingWithVoidAnimation:(BOOL)createdAccount; + +- (NSString *)onboardingConfigurationString; + +@property (nonatomic, assign) enum ARTrialContext trialContext; + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingViewController.m b/Artsy/Classes/View Controllers/AROnboardingViewController.m new file mode 100644 index 00000000000..91be0c0db38 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingViewController.m @@ -0,0 +1,576 @@ +#import "AROnboardingViewController.h" + +#import "ARAppDelegate.h" +#import "ARUserManager.h" + +#import "AROnboardingTransition.h" +#import "AROnboardingViewControllers.h" +#import "ARNetworkConstants.h" + +#import + +#import "ARPersonalizeViewController.h" +#import "ARAuthProviders.h" +#import "UIViewController+FullScreenLoading.h" +#import "AROnboardingMoreInfoViewController.h" +#import "ARParallaxEffect.h" +#import "NSString+StringCase.h" +#import "ArtsyAPI+Private.h" +#import +#import "ARAnalyticsConstants.h" + +typedef NS_ENUM(NSInteger, AROnboardingStage) { + AROnboardingStageSlideshow, + AROnboardingStageStart, + AROnboardingStageChooseMethod, + AROnboardingStageEmailPassword, + AROnboardingStageCollectorStatus, + AROnboardingStageLocation, + AROnboardingStagePersonalize, + AROnboardingStageFollowNotification, + AROnboardingStagePriceRange, + AROnboardingStageNotes +}; + +@interface AROnboardingViewController () +@property (nonatomic, assign) AROnboardingStage state; +@property (nonatomic, assign) BOOL showBackgroundImage; +@property (nonatomic) UIImageView *backgroundView; +@property (nonatomic) UIScreenEdgePanGestureRecognizer *screenSwipeGesture; +@property (nonatomic) NSArray *genesForPersonalize; +@property (nonatomic, readonly, strong) UIImage *backgroundImage; +@property (nonatomic, strong, readwrite) NSLayoutConstraint *backgroundWidthConstraint; +@property (nonatomic, strong, readwrite) NSLayoutConstraint *backgroundHeightConstraint; +@end + + +@implementation AROnboardingViewController + +- (instancetype)initWithState:(enum ARInitialOnboardingState)state +{ + self = [super init]; + if (!self) { return nil; } + + [[UIApplication sharedApplication] setStatusBarHidden:YES]; + self.navigationBarHidden = YES; + self.delegate = self; + switch (state) { + case ARInitialOnboardingStateSlideShow: + _state = AROnboardingStageSlideshow; + break; + + case ARInitialOnboardingStateInApp: + _state = AROnboardingStageChooseMethod; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor blackColor]; + self.view.tintColor = [UIColor artsyPurple]; + + self.screenSwipeGesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped:)]; + self.screenSwipeGesture.edges = UIRectEdgeLeft; + [self.view addGestureRecognizer:self.screenSwipeGesture]; + + @weakify(self); + + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + @strongify(self); + + @weakify(self); + [ArtsyAPI getPersonalizeGenesWithSuccess:^(NSArray *genes) { + @strongify(self); + self.genesForPersonalize = genes; + } failure:^(NSError *error) { + ARErrorLog(@"Couldn't get personalize genes. Error: %@", error.localizedDescription); + }]; + }]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didBecomeActive) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskPortrait; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; +} + +- (void)didBecomeActive +{ + // If you've cancelled a twitter request we're currently showing the loader + // add a delay so the user gets that they were doing something as they were leaving. + ar_dispatch_after(0.3, ^{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + }); +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self createBackgroundImageView]; + + if (self.state == AROnboardingStageSlideshow) { + [self startSlideshow]; + } + + if (self.state == AROnboardingStageChooseMethod) { + [self getCurrentAppStateBlurredImage]; + [self presentSignInForAlreadyActiveUsers]; + } + + [super viewWillAppear:animated]; +} + +#pragma mark - +#pragma mark Slideshow + +- (void)startSlideshow +{ + NSMutableArray *slides = [NSMutableArray array]; + NSInteger numberOfImages = [UIDevice isPad] ? 5 : 4; + for (int i = 1; i <= numberOfImages ; i++) { + NSString *file = [NSString stringWithFormat:@"splash_%d.jpg", i]; + UIImage *image = [UIImage imageNamed:file]; + [slides addObject:image]; + } + + ARSlideshowViewController *slideshow = [[ARSlideshowViewController alloc] initWithSlides:slides]; + slideshow.delegate = self; + [self pushViewController:slideshow animated:NO]; +} + +- (void)slideshowDone +{ + ARSignUpSplashViewController *splash = [[ARSignUpSplashViewController alloc] init]; + splash.delegate = self; + self.viewControllers = @[splash]; + self.state = AROnboardingStageStart; +} + +- (void)presentSignInForAlreadyActiveUsers +{ + ARSignUpActiveUserViewController *splash = [[ARSignUpActiveUserViewController alloc] init]; + [splash setTrialContext:self.trialContext]; + splash.delegate = self; + [self pushViewController:splash animated:YES]; + + self.state = AROnboardingStageChooseMethod; +} + +#pragma mark - +#pragma mark Signup splash + +- (void)splashDoneWithLogin:(ARSignUpSplashViewController *)sender +{ + [self setBackgroundImage:sender.backgroundImage animated:YES]; + sender.backgroundImage = nil; + + [self logInWithEmail:nil]; +} + +- (void)splashDone:(ARSignUpSplashViewController *)sender +{ + [self setBackgroundImage:sender.backgroundImage animated:YES]; + sender.backgroundImage = nil; + + ARSignupViewController *signup = [[ARSignupViewController alloc] init]; + signup.delegate = self; + [self pushViewController:signup animated:YES]; + self.state = AROnboardingStageChooseMethod; +} + +#pragma mark - +#pragma mark Signup + +- (void)signUpWithEmail +{ + ARCreateAccountViewController *createVC = [[ARCreateAccountViewController alloc] init]; + createVC.delegate = self; + [self pushViewController:createVC animated:YES]; + self.state = AROnboardingStageEmailPassword; +} + +- (void)signupDone +{ + if ([UIDevice isPad]) { + [self presentWebOnboarding]; + } else { + [UIView animateWithDuration:ARAnimationQuickDuration animations:^{ + self.backgroundView.alpha = 0; + }]; + [self presentCollectorLevel]; + } +} + + +#pragma mark - +#pragma mark Web onboarding + +- (void)presentWebOnboarding +{ + NSURL *url = [ARSwitchBoard.sharedInstance resolveRelativeUrl:ARPersonalizePath]; + ARPersonalizeWebViewController *viewController = [[ARPersonalizeWebViewController alloc] initWithURL:url]; + viewController.delegate = self; + [self pushViewController:viewController animated:YES]; +} + +- (void)webOnboardingDone +{ + [self dismissOnboardingWithVoidAnimation:YES]; +} + +#pragma mark - +#pragma mark Collector level + +- (void)presentCollectorLevel +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:AROnboardingSkipCollectorLevelDefault]) { + [self presentPersonalize]; + return; + } + ARCollectorStatusViewController *status = [[ARCollectorStatusViewController alloc] init]; + status.delegate = self; + [self pushViewController:status animated:YES]; + self.state = AROnboardingStageCollectorStatus; +} + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + +- (void)collectorLevelDone:(ARCollectorLevel)level +{ + User *user = [User currentUser]; + user.collectorLevel = level; + + NSString *collectorLevel = [ARCollectorStatusViewController stringFromCollectorLevel:level]; + [ARAnalytics setUserProperty:ARAnalyticsCollectorLevelProperty toValue:collectorLevel]; + + [user setRemoteUpdateCollectorLevel:level success:nil failure:^(NSError *error) { + ARErrorLog(@"Error updating collector level"); + }]; + + [[ARUserManager sharedManager] storeUserData]; + [self presentPersonalize]; +} + +- (void)presentPersonalize +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:AROnboardingSkipPersonalizeDefault]) { + [self personalizeDone]; + return; + } + + ARPersonalizeViewController *personalize = [[ARPersonalizeViewController alloc] initWithGenes:self.genesForPersonalize]; + personalize.delegate = self; + [self pushViewController:personalize animated:YES]; + self.state = AROnboardingStagePersonalize; +} + +- (void)personalizeDone +{ + if ([User currentUser].collectorLevel == ARCollectorLevelNo) { + // They're done + [self dismissOnboardingWithVoidAnimation:YES]; + } else { + [self presentPriceRange]; + } +} + +- (void)presentPriceRange +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:AROnboardingSkipPriceRangeDefault]) { + [self dismissOnboardingWithVoidAnimation:YES]; + return; + } + ARPriceRangeViewController *priceRange = [[ARPriceRangeViewController alloc] init]; + priceRange.delegate = self; + [self pushViewController:priceRange animated:YES]; + self.state = AROnboardingStagePriceRange; +} + +- (void)setPriceRangeDone:(NSInteger)range +{ + NSString *stringRange = [NSString stringWithFormat:@"%@", @(range)]; + [ARAnalytics setUserProperty:ARAnalyticsPriceRangeProperty toValue:stringRange]; + + User *user = [User currentUser]; + user.priceRange = range; + + [user setRemoteUpdatePriceRange:range success:nil failure:^(NSError *error) { + ARErrorLog(@"Error updating price range"); + }]; + + [self dismissOnboardingWithVoidAnimation:YES]; +} + +-(void) resetBackgroundImageView:(BOOL)animated completion:(void (^)(void))completion +{ + self.backgroundWidthConstraint.constant = 0; + self.backgroundHeightConstraint.constant = 0; + @weakify(self); + [UIView animateIf:animated duration:ARAnimationQuickDuration :^{ + @strongify(self); + [self.backgroundView layoutIfNeeded]; + self.backgroundView.alpha = 1; + self.backgroundView.backgroundColor = [UIColor clearColor]; + } completion:^(BOOL finished) { + self.backgroundView.image = self.backgroundImage; + if (completion != nil) { completion(); }; + }]; +} + +- (void)dismissOnboardingWithVoidAnimation:(BOOL)createdAccount +{ + // send them off into the app + + if (createdAccount) { + [[ARAppDelegate sharedInstance] finishOnboardingAnimated:createdAccount]; + } else { + [self resetBackgroundImageView:YES completion:^{ + self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + [[ARAppDelegate sharedInstance] finishOnboardingAnimated:createdAccount]; + }]; + } +} + +- (void)showTermsAndConditions +{ + AROnboardingWebViewController *webViewController = [[AROnboardingWebViewController alloc] initWithMobileArtsyPath:@"terms"]; + [self pushViewController:webViewController animated:YES]; +} + +- (void)showPrivacyPolicy +{ + AROnboardingWebViewController *webViewController = [[AROnboardingWebViewController alloc] initWithMobileArtsyPath:@"privacy"]; + [self pushViewController:webViewController animated:YES]; +} + + +- (void)signUpWithFacebook +{ + @weakify(self); + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + [ARAuthProviders getTokenForFacebook:^(NSString *token, NSString *email, NSString *name) { + @strongify(self); + + AROnboardingMoreInfoViewController *more = [[AROnboardingMoreInfoViewController alloc] initForFacebookWithToken:token email:email name:name]; + more.delegate = self; + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self pushViewController:more animated:YES]; + + } failure:^(NSError *error) { + @strongify(self); + + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + + NSString * reason = error.userInfo[@"com.facebook.sdk:ErrorLoginFailedReason"]; + if (![reason isEqualToString:@"com.facebook.sdk:UserLoginCancelled"]) { + [self fbError]; + } + }]; + +} + +- (void)signUpWithTwitter +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + + @weakify(self); + [ARAuthProviders getReverseAuthTokenForTwitter:^(NSString *token, NSString *secret) { + @strongify(self); + + AROnboardingMoreInfoViewController *more = [[AROnboardingMoreInfoViewController alloc] + initForTwitterWithToken:token andSecret:secret]; + more.delegate = self; + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self pushViewController:more animated:YES]; + + } failure:^(NSError *error) { + @strongify(self); + + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + [self twitterError]; + }]; +} + +- (void)fbError +{ + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn’t get Facebook credentials" + message:@"Couldn’t get Facebook credentials. Please link a Facebook account in the settings app. If you continue having trouble, please email Artsy support at support@artsy.net" + delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; + [alert show]; +} + +- (void)twitterError +{ + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn’t get Twitter credentials" + message:@"Couldn’t get Twitter credentials. Please link a Twitter account in the settings app. If you continue having trouble, please email Artsy support at support@artsy.net" + delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; + [alert show]; +} + +// [store requestAccessToAccountsWithType:fbType options:@{ACFacebookAppIdKey:@"414450748567864", ACFacebookPermissionsKey:@[@"email"]} completion:^(BOOL granted, NSError *error) { +// if (granted) { +// NSArray *accounts = [store accountsWithAccountType:fbType]; +// ACAccount *acc = accounts.first; +// NSString *token = [[acc credential] oauthToken]; +// +// dispatch_async(dispatch_get_main_queue(), ^{ +// [self createFacebookUserWithToken:token email:nil]; +// }); +// +// } else { +// // TODO: definitely copy, hopefully UI? +// +// dispatch_async(dispatch_get_main_queue(), ^{ +// [self ar_removeIndeterminateLoadingIndicatorAnimated:]; +// UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn’t get Facebook credentials" +// message:@"Couldn’t get Facebook credentials. Please link a Facebook account in the settings app. If you continue having trouble, please email Artsy support at support@artsy.net" +// delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; +// [alert show]; +// }); +// ARErrorLog(@"Failed to get Facebook credentials in onboarding. Error: %@", error.localizedDescription); +// } +// }]; + +- (void)logInWithEmail:(NSString *)email +{ + ARLoginViewController *loginViewController = [[ARLoginViewController alloc] initWithEmail:email]; + loginViewController.delegate = self; + + [self pushViewController:loginViewController animated:YES]; + self.state = AROnboardingStageEmailPassword; +} + +- (UIViewController *)popViewControllerAnimated:(BOOL)animated +{ + UIViewController *poppedVC = [super popViewControllerAnimated:animated]; + UIViewController *topVC = self.topViewController; + if (topVC == [self.viewControllers objectAtIndex:0] && [topVC isKindOfClass:ARSignUpSplashViewController.class]){ + [self resetBackgroundImageView:animated completion:nil]; + } + return poppedVC; +} + +- (void)createBackgroundImageView +{ + [self.backgroundView removeFromSuperview]; + self.backgroundView = [[UIImageView alloc] initWithFrame:CGRectZero]; + self.backgroundView.contentMode = UIViewContentModeScaleAspectFill; + [self.view insertSubview:self.backgroundView atIndex:0]; + self.backgroundWidthConstraint = [[self.backgroundView constrainWidthToView:self.view predicate:nil] lastObject]; + self.backgroundHeightConstraint = [[self.backgroundView constrainHeightToView:self.view predicate:nil] lastObject]; + [self.backgroundView alignCenterWithView:self.view]; + [self.backgroundView layoutIfNeeded]; +} + +#pragma mark - +#pragma mark Navigation Delegate + +- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + NSString *viewIdentifier = [NSString humanReadableStringFromClass:[viewController class]]; + if (viewIdentifier) [ARAnalytics pageView:viewIdentifier]; + +} + +- (id )navigationController:(UINavigationController *)navigationController + animationControllerForOperation:(UINavigationControllerOperation)operation + fromViewController:(UIViewController *)fromVC + toViewController:(UIViewController *)toVC +{ + AROnboardingTransition *transition = [[AROnboardingTransition alloc] init]; + transition.operationType = operation; + return transition; +} + +- (BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +#pragma mark - +#pragma mark Background Image + +- (void)getCurrentAppStateBlurredImage +{ + ARAppDelegate *appDelegate = [ARAppDelegate sharedInstance]; + UIView *view = appDelegate.viewController.view; + + UIGraphicsBeginImageContext(view.bounds.size); + [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; + + UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + [self setBackgroundImage:viewImage animated:YES]; +} + +- (void)setBackgroundImage:(UIImage *)backgroundImage animated:(BOOL)animated +{ + _backgroundImage = backgroundImage; + UIImage *blurImage = [backgroundImage blurredImageWithRadius:12 iterations:2 tintColor:[UIColor colorWithWhite:0 alpha:.5]]; + + CGFloat offset = 30; + if (self.backgroundView.motionEffects.count == 0) { + ARParallaxEffect *parallax = [[ARParallaxEffect alloc] initWithOffset:offset]; + [self.backgroundView addMotionEffect:parallax]; + } + + if (animated) { + self.backgroundView.alpha = 1; + } + + self.backgroundWidthConstraint.constant = offset * 2; + self.backgroundHeightConstraint.constant = offset * 2; + @weakify(self); + [UIView animateIf:animated duration:ARAnimationQuickDuration :^{ + @strongify(self); + [self.backgroundView layoutIfNeeded]; + self.backgroundView.image = blurImage; + self.backgroundView.alpha = 0.3; + self.backgroundView.backgroundColor = [UIColor blackColor]; + }]; +} + +- (void)edgeSwiped:(UIScreenEdgePanGestureRecognizer *)gesture +{ + if (gesture.state == UIGestureRecognizerStateCancelled) { + gesture.enabled = YES; + } + + if (gesture.state == UIGestureRecognizerStateBegan) { + // Don't let people get to the slideshow + if (self.viewControllers.count > 1) { + [self popViewControllerAnimated:YES]; + } + gesture.enabled = NO; + } +} + +- (NSString *)onboardingConfigurationString +{ + NSMutableString *configuration = [[NSMutableString alloc] init]; + NSArray *keys = @[ AROnboardingSkipCollectorLevelDefault, + AROnboardingSkipPersonalizeDefault, + AROnboardingSkipPriceRangeDefault ]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + for (NSString *key in keys) { + [configuration appendString:[defaults boolForKey:key] ? @"n" : @"y"]; + } + return configuration; +} + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingViewControllers.h b/Artsy/Classes/View Controllers/AROnboardingViewControllers.h new file mode 100644 index 00000000000..c3f79901c4a --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingViewControllers.h @@ -0,0 +1,9 @@ +#import "ARLoginViewController.h" +#import "ARSignUpSplashViewController.h" +#import "ARSignupViewController.h" +#import "ARSlideshowViewController.h" +#import "ARCreateAccountViewController.h" +#import "ARCollectorStatusViewController.h" +#import "ARPriceRangeViewController.h" +#import "ARSignUpActiveUserViewController.h" +#import "AROnboardingWebViewController.h" diff --git a/Artsy/Classes/View Controllers/AROnboardingWebViewController.h b/Artsy/Classes/View Controllers/AROnboardingWebViewController.h new file mode 100644 index 00000000000..77cd9db4c66 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingWebViewController.h @@ -0,0 +1,5 @@ +@interface AROnboardingWebViewController : UIViewController + +- (instancetype)initWithMobileArtsyPath:(NSString *)path; + +@end diff --git a/Artsy/Classes/View Controllers/AROnboardingWebViewController.m b/Artsy/Classes/View Controllers/AROnboardingWebViewController.m new file mode 100644 index 00000000000..fc84c2385c6 --- /dev/null +++ b/Artsy/Classes/View Controllers/AROnboardingWebViewController.m @@ -0,0 +1,124 @@ +#import "AROnboardingWebViewController.h" +#import "AROnboardingNavBarView.h" +#import "UIViewController+FullScreenLoading.h" + +typedef NS_ENUM(NSInteger, ARScrollState) { + ARScrollStateScrollingUp = -1, + ARScrollStateTop, + ARScrollStateScrollingDown +}; + +@interface AROnboardingWebViewController () +@property (nonatomic, strong) NSString *path; +@property (nonatomic, assign) CGFloat lastY; +@property (nonatomic, assign) CGFloat initialY; +@property (nonatomic, assign) ARScrollState scrollState; +@property (nonatomic, retain) AROnboardingNavBarView *navView; +@end + +@implementation AROnboardingWebViewController + ++ (void)initialize +{ + NSString *build = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero]; + NSString *fullAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; + + NSString *userAgent = [NSString stringWithFormat:@"Artsy-Mobile: %@ | v%@ | %@", version, build, fullAgent]; + + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"UserAgent" : userAgent } ]; +} + +- (instancetype)initWithMobileArtsyPath:(NSString *)path +{ + self = [super init]; + if (!self) { return nil; } + + _path = path; + + return self; +} + +- (void)viewDidLoad +{ + UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds]; + webView.delegate = self; + webView.scalesPageToFit = YES; + webView.backgroundColor = [UIColor whiteColor]; + webView.scrollView.contentInset = UIEdgeInsetsMake(30, 0, 0, 0); + webView.scrollView.delegate = self; + [self.view addSubview:webView]; + + NSURL *url = [ARSwitchBoard.sharedInstance resolveRelativeUrl:self.path]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + [webView loadRequest:request]; + + // Doing this now means the back button is available whilst loading + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + + AROnboardingNavBarView *navView = [[AROnboardingNavBarView alloc] init]; + [self.view addSubview:navView]; + self.navView = navView; + + [navView.title setText:@""]; + [navView.back setImage:[UIImage imageNamed:@"BackArrow_Highlighted"] forState:UIControlStateNormal]; + [navView.back addTarget:self action:@selector(popViewController) forControlEvents:UIControlEventTouchUpInside]; + + [super viewDidLoad]; +} + +- (void)popViewController +{ + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)aWebView +{ + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; +} + +- (void)setBackButtonAlpha:(CGFloat)alpha +{ + UIView *back = self.navView.back; + if (back.alpha != alpha) { + [UIView animateWithDuration:ARAnimationDuration animations:^{ + back.alpha = alpha; + }]; + } +} + +- (ARScrollState)scrollStateForScrollView:(UIScrollView *)scrollView delta:(CGFloat *)delta +{ + CGFloat nextY = scrollView.contentOffset.y; + ARScrollState nextState; + if (nextY >= self.lastY) { + nextState = ARScrollStateScrollingDown; + } else if (nextY <= 0) { + nextState = ARScrollStateTop; + } else { + nextState = ARScrollStateScrollingUp; + } + self.lastY = MAX(nextY, 0); + + *delta = fabsf(nextY - self.initialY); + + if (self.scrollState != nextState) { + self.scrollState = nextState; + self.initialY = nextY; + } + return nextState; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + CGFloat delta; + ARScrollState scrollState = [self scrollStateForScrollView:scrollView delta:&delta]; + if ((scrollState == ARScrollStateTop) || (scrollState == ARScrollStateScrollingUp && delta > 160)) { + [self setBackButtonAlpha:1]; + } else if (scrollState == ARScrollStateScrollingDown) { + [self setBackButtonAlpha:0]; + } +} + +@end diff --git a/Artsy/Classes/View Controllers/ARParallaxHeaderViewController.h b/Artsy/Classes/View Controllers/ARParallaxHeaderViewController.h new file mode 100644 index 00000000000..fecc7577bcb --- /dev/null +++ b/Artsy/Classes/View Controllers/ARParallaxHeaderViewController.h @@ -0,0 +1,12 @@ +#import + +@interface ARParallaxHeaderViewController : UIViewController + +- (instancetype)initWithContainingScrollView:(UIScrollView *)containingScrollView fair:(Fair *)fair profile:(Profile *)profile __attribute((objc_designated_initializer)); +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil __attribute__((unavailable("Designated Initializer initWithContainingScrollView:fair:profile: must be used."))); + +@property (nonatomic, weak, readonly) UIScrollView *containingScrollView; +@property (nonatomic, weak, readonly) Fair *fair; +@property (nonatomic, weak, readonly) Profile *profile; + +@end diff --git a/Artsy/Classes/View Controllers/ARParallaxHeaderViewController.m b/Artsy/Classes/View Controllers/ARParallaxHeaderViewController.m new file mode 100644 index 00000000000..eab275d98d4 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARParallaxHeaderViewController.m @@ -0,0 +1,163 @@ +#import "ARParallaxHeaderViewController.h" +#import + +const CGFloat ARParallaxHeaderViewBannerImageMissingImageHeight = 60.0; +const CGFloat ARParallaxHeaderViewBannerImageHeight = 180.0; +const CGFloat ARParallaxHeaderViewBottomWhitespaceHeight = 53.0; +const CGFloat ARParallaxHeaderViewIconImageViewDimension = 80.0f; + +@interface ARParallaxImageView : UIImageView +@end + +@interface ARParallaxHeaderViewController () + +@property (nonatomic, strong) ARParallaxImageView *bannerImageView; +@property (nonatomic, strong) ARParallaxImageView *iconImageView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *subtitleLabel; + +@property (nonatomic, strong) NSLayoutConstraint *bannerTopLayoutConstraint; + +@end + +@implementation ARParallaxHeaderViewController + +- (instancetype)initWithContainingScrollView:(UIScrollView *)containingScrollView fair:(id)fair profile:(Profile *)profile +{ + self = [super initWithNibName:nil bundle:nil]; + if (self == nil) { return nil; } + + _containingScrollView = containingScrollView; + _fair = fair; + _profile = profile; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self addSubviews]; + [self constrainViews]; + [self downloadImages]; +} + +- (void)addSubviews +{ + self.bannerImageView = [[ARParallaxImageView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:self.bannerImageView]; + + if ([self hasIconImage]) { + self.iconImageView = [[ARParallaxImageView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:self.iconImageView]; + } + + const CGFloat fontSize = 14.0; + + self.titleLabel = [[ARSansSerifHeaderLabel alloc] init]; + self.titleLabel.font = [self.titleLabel.font fontWithSize:fontSize]; + self.titleLabel.backgroundColor = [UIColor clearColor]; + self.titleLabel.textAlignment = NSTextAlignmentLeft; + self.titleLabel.text = self.fair.name; + [self.view addSubview:self.titleLabel]; + + self.subtitleLabel = [[ARSerifLabel alloc] init]; + self.subtitleLabel.font = [self.subtitleLabel.font fontWithSize:fontSize]; + self.subtitleLabel.backgroundColor = [UIColor clearColor]; + self.subtitleLabel.textAlignment = NSTextAlignmentLeft; + NSString *location = [self.fair location]; + if (location.length > 0) { + self.subtitleLabel.text = [NSString stringWithFormat:@"%@ %@", self.fair.ausstellungsdauer, location]; + } else { + self.subtitleLabel.text = self.fair.ausstellungsdauer; + } + [self.view addSubview:self.subtitleLabel]; +} + +- (void)constrainViews +{ + NSArray *constraintsArray = [self.bannerImageView alignTopEdgeWithView:self.view predicate:nil]; + self.bannerTopLayoutConstraint = constraintsArray.firstObject; + NSString *bottomPredicate = [NSString stringWithFormat:@"<=-%@", @(ARParallaxHeaderViewBottomWhitespaceHeight)]; + [self.bannerImageView alignTop:nil leading:@"0" bottom:bottomPredicate trailing:@"0" toView:self.view]; + + CGFloat bannerHeight = [self hasBannerImage] ? ARParallaxHeaderViewBannerImageHeight : ARParallaxHeaderViewBannerImageMissingImageHeight; + NSString *heightPredicate = [NSString stringWithFormat:@"%@", @(bannerHeight + ARParallaxHeaderViewBottomWhitespaceHeight)]; + [self.view constrainHeight:heightPredicate]; + + if ([self hasIconImage]) { + [self.iconImageView alignBottomEdgeWithView:self.view predicate:@"0"]; + [self.iconImageView alignLeadingEdgeWithView:self.view predicate:@"20"]; + [self.iconImageView constrainWidth:@"80"]; + [self.iconImageView constrainHeight:@"80"]; + + [self.titleLabel constrainLeadingSpaceToView:self.iconImageView predicate:@"20"]; + } else { + [self.titleLabel alignLeadingEdgeWithView:self.view predicate:@"20"]; + } + + [self.titleLabel alignTrailingEdgeWithView:self.view predicate:@"-20"]; + [self.titleLabel constrainTopSpaceToView:self.bannerImageView predicate:@"24"]; + + [self.subtitleLabel alignLeadingEdgeWithView:self.titleLabel predicate:nil]; + [self.subtitleLabel alignTrailingEdgeWithView:self.view predicate:@"-20"]; + [self.subtitleLabel constrainTopSpaceToView:self.titleLabel predicate:nil]; + [self.subtitleLabel alignBottomEdgeWithView:self.view predicate:nil]; + + RAC(self.bannerTopLayoutConstraint, constant) = [[RACObserve(self.containingScrollView, contentOffset) map:^id(id value) { + CGPoint contentOffset = [value CGPointValue]; + return @(contentOffset.y); + }] filter:^BOOL(id value) { + return fabsf([value floatValue]) < ARParallaxHeaderViewBannerImageHeight; + }]; +} + +- (void)downloadImages +{ + if ([self hasBannerImage]) { + [self.bannerImageView sd_setImageWithURL:[NSURL URLWithString:[self.fair bannerAddress]]]; + } + + if ([self hasIconImage]) { + @weakify(self); + [self.iconImageView sd_setImageWithURL:[NSURL URLWithString:[self.profile iconURL]] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { + if (image) { + @strongify(self); + // Necessary, since the icons will sometimes be transparent GIFs + self.iconImageView.backgroundColor = [UIColor whiteColor]; + } + }]; + } +} + +- (BOOL)hasBannerImage +{ + return [[self.fair bannerAddress] length] > 0; +} + +- (BOOL)hasIconImage +{ + return [[self.profile iconURL] length] > 0; +} + +@end + + +#pragma mark - Private UIView subclasses + +@implementation ARParallaxImageView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self == nil) { return nil; } + + self.contentMode = UIViewContentModeScaleAspectFill; + self.clipsToBounds = YES; + self.backgroundColor = [UIColor lightGrayColor]; + + return self; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARPendingOperationViewController.h b/Artsy/Classes/View Controllers/ARPendingOperationViewController.h new file mode 100644 index 00000000000..0a101436e1c --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPendingOperationViewController.h @@ -0,0 +1,7 @@ +#import + +@interface ARPendingOperationViewController : UIViewController + +@property (nonatomic, copy) NSString *message; + +@end diff --git a/Artsy/Classes/View Controllers/ARPendingOperationViewController.m b/Artsy/Classes/View Controllers/ARPendingOperationViewController.m new file mode 100644 index 00000000000..5c6bd41fe73 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPendingOperationViewController.m @@ -0,0 +1,47 @@ +#import "ARPendingOperationViewController.h" +#import "ARSpinner.h" + +@interface ARPendingOperationViewController () + +@property (nonatomic, strong) UILabel *label; +@property (nonatomic, strong) ARSpinner *spinner; + +@end + +@implementation ARPendingOperationViewController + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self == nil) { return nil; } + + _message = @"locating..."; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.5f]; + + self.label = [[ARSerifLabel alloc] init]; + self.label.font = [UIFont serifFontWithSize:24]; + RAC(self.label, text) = RACObserve(self, message); + self.label.textColor = [UIColor whiteColor]; + self.label.backgroundColor = [UIColor clearColor]; + [self.view addSubview:self.label]; + [self.label alignCenterXWithView:self.view predicate:nil]; + [self.label alignCenterYWithView:self.view predicate:nil]; + + self.spinner = [[ARSpinner alloc] init]; + self.spinner.spinnerColor = [UIColor whiteColor]; + [self.spinner fadeInAnimated:YES]; + [self.view addSubview:self.spinner]; + [self.spinner alignCenterXWithView:self.view predicate:nil]; + [self.spinner alignTop:@"44" bottom:nil toView:self.label]; + [self.spinner constrainTopSpaceToView:self.label predicate:@"20"]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARPersonalizeViewController.h b/Artsy/Classes/View Controllers/ARPersonalizeViewController.h new file mode 100644 index 00000000000..5c804418f46 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPersonalizeViewController.h @@ -0,0 +1,14 @@ +@class AROnboardingViewController; +@class AROnboardingGeneTableController; +@class AROnboardingArtistTableController; + +@interface ARPersonalizeViewController : UIViewController + +- (instancetype)initWithGenes:(NSArray *)genes; +@property (nonatomic, weak) AROnboardingViewController *delegate; + +@property (nonatomic, assign, readonly) NSInteger followedThisSession; +@property (nonatomic, readonly) AROnboardingGeneTableController *geneController; +@property (nonatomic, readonly) AROnboardingArtistTableController *artistController; + +@end diff --git a/Artsy/Classes/View Controllers/ARPersonalizeViewController.m b/Artsy/Classes/View Controllers/ARPersonalizeViewController.m new file mode 100644 index 00000000000..57b2f5c0ceb --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPersonalizeViewController.m @@ -0,0 +1,416 @@ +#import "ARPersonalizeViewController.h" +#import "AROnboardingGeneTableController.h" +#import "AROnboardingArtistTableController.h" +#import "AROnboardingSearchField.h" +#import "AROnboardingFollowableTableViewCell.h" +#import "AROnboardingViewController.h" + +static NSString *SearchCellId = @"OnboardingSearchCell"; + +@interface ARPersonalizeViewController () + +@property (nonatomic) NSArray *genesToFollow; +@property (nonatomic) NSArray *searchResults; + +@property (nonatomic) AROnboardingGeneTableController *geneController; +@property (nonatomic) AROnboardingArtistTableController *artistController; + + +@property (nonatomic) UIScrollView *scrollView; + +//Search table view is controlled by this VC because it interacts more with the search bar +@property (nonatomic) UITableView *artistTableView, *geneTableView, *searchTableView; +@property (nonatomic) UIView *searchView; +@property (nonatomic) AROnboardingSearchField *searchBar; +@property (nonatomic) UILabel *followedArtistsLabel; +@property (nonatomic) UIButton *cancelButton; +@property (nonatomic, assign) NSInteger followedThisSession; + +@property (nonatomic) ARWhiteFlatButton *continueButton; + +@property (nonatomic) AFJSONRequestOperation *searchRequestOperation; +@end + +@implementation ARPersonalizeViewController + +- (instancetype)initWithGenes:(NSArray *)genes +{ + self = [super init]; + if (self) { + _searchResults = [NSMutableArray array]; + if (!genes || genes.count == 0) { + NSArray *fallabackGenes = @[@"Photography", @"Bauhaus", @"Dada", @"Glitch Aesthetic", @"Computer Art", @"Op Art", @"Minimalism"]; + ARActionLog(@"Using fallback genes in 'Personalize'"); + // Convert names to Gene Objects + _genesToFollow = [fallabackGenes map:^id(NSString *name) { + NSString *geneID = [[name stringByReplacingOccurrencesOfString:@" " withString:@"-"] + lowercaseString]; + Gene *gene = [Gene modelWithJSON:@{ + @"name" : name, + @"id" : geneID + }]; + return gene; + }]; + } else { + _genesToFollow = genes; + } + + _artistController = [[AROnboardingArtistTableController alloc] init]; + _geneController = [[AROnboardingGeneTableController alloc] initWithGenes:_genesToFollow]; + } + return self; +} + +- (CGFloat)bottomOf:(UIView *)view +{ + return CGRectGetMaxY(view.frame); +} + + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.scrollView = [[UIScrollView alloc] init]; + self.scrollView.frame = self.view.frame; + self.scrollView.delaysContentTouches = NO; + self.scrollView.backgroundColor = [UIColor blackColor]; + [self.view addSubview:self.scrollView]; + + CGSize screenSize = self.scrollView.bounds.size; + UILabel *headline = [[UILabel alloc] initWithFrame:CGRectMake(20, 30, 280, 40)]; + headline.font = [UIFont serifFontWithSize:24]; + headline.text = @"What interests you?"; + headline.textColor = [UIColor whiteColor]; + [self.scrollView addSubview:headline]; + + UILabel *followArtists = [[UILabel alloc] initWithFrame:CGRectMake(20, 80, 280, 20)]; + followArtists.font = [UIFont sansSerifFontWithSize:14]; + followArtists.textColor = [UIColor whiteColor]; + followArtists.text = [@"Enter your favorite artists" uppercaseString]; + [self.scrollView addSubview:followArtists]; + + self.searchBar = [[AROnboardingSearchField alloc] initWithFrame:CGRectMake(20, 105, 210, 40)]; + self.searchBar.placeholder = @"Search artists…"; + self.searchBar.delegate = self; + self.searchBar.autocapitalizationType = UITextAutocapitalizationTypeWords; + self.searchBar.autocorrectionType = UITextAutocorrectionTypeNo; + self.searchBar.keyboardAppearance = UIKeyboardAppearanceDark; + [self.searchBar addTarget:self action:@selector(searchBarDown:) forControlEvents:UIControlEventTouchDown]; + + self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.cancelButton.frame = CGRectMake(230, self.searchBar.frame.origin.y, 90, self.searchBar.bounds.size.height -1); + self.cancelButton.titleLabel.font = [UIFont sansSerifFontWithSize:14]; + self.cancelButton.backgroundColor = [UIColor blackColor]; + [self.cancelButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [self.cancelButton setTitle:@"CANCEL" forState:UIControlStateNormal]; + + [self.scrollView addSubview:self.cancelButton]; + [self.cancelButton addTarget:self action:@selector(cancelSearch:) forControlEvents:UIControlEventTouchUpInside]; + self.cancelButton.alpha = 0; + CALayer *cancelButtonSeparator = [CALayer layer]; + cancelButtonSeparator.backgroundColor = [UIColor artsyHeavyGrey].CGColor; + cancelButtonSeparator.frame = CGRectMake(0, 0, .5, self.cancelButton.frame.size.height); + [self.cancelButton.layer addSublayer:cancelButtonSeparator]; + + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startedEditing:) + name:UITextFieldTextDidBeginEditingNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(searchTextChanged:) + name:UITextFieldTextDidChangeNotification object:nil]; + + [self.scrollView addSubview:self.searchBar]; + + self.artistTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, [self bottomOf:self.searchBar], screenSize.width, 10)]; + self.artistTableView.dataSource = self.artistController; + self.artistTableView.delegate = self.artistController; + [self.artistController prepareTableView:self.artistTableView]; + self.artistTableView.backgroundColor = [UIColor blackColor]; + self.artistTableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.artistTableView.scrollEnabled = NO; + + @weakify(self); + [self.artistController setPostRemoveBlock:^{ + @strongify(self); + [self updateArtistTableViewAnimated:YES]; + }]; + + [self.scrollView addSubview:self.artistTableView]; + + self.geneTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, + [self bottomOf:self.artistTableView] + , screenSize.width, + 54 * self.genesToFollow.count + 50)]; + self.geneTableView.dataSource = self.geneController; + self.geneTableView.delegate = self.geneController; + [self.geneController prepareTableView:self.geneTableView]; + self.geneTableView.backgroundColor = [UIColor clearColor]; + self.geneTableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.geneTableView.scrollEnabled = NO; + + [self.scrollView addSubview:self.geneTableView]; + + self.searchView = [[UIView alloc] initWithFrame:CGRectMake(0, + [self bottomOf:self.searchBar], + screenSize.width, 1000)]; + self.searchView.backgroundColor = [UIColor blackColor]; + [self.scrollView addSubview:self.searchView]; + self.searchView.alpha = 0; + + self.followedArtistsLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, screenSize.width - 40, 100)]; + self.followedArtistsLabel.numberOfLines = 0; + self.followedArtistsLabel.font = [UIFont serifFontWithSize:16]; + self.followedArtistsLabel.textColor = [UIColor artsyHeavyGrey]; + [self.searchView addSubview:self.followedArtistsLabel]; + self.followedArtistsLabel.alpha = 0; + + self.searchTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, screenSize.width, 1000)]; + [self.searchView addSubview:self.searchTableView]; + self.searchTableView.dataSource = self; + self.searchTableView.delegate = self; + self.searchTableView.backgroundColor = [UIColor blackColor]; + self.searchTableView.separatorStyle = UITableViewCellSeparatorStyleNone; + [self.searchTableView registerClass:[AROnboardingFollowableTableViewCell class] forCellReuseIdentifier:SearchCellId]; + + self.searchResults = @[]; + + self.scrollView.contentSize = CGSizeMake(320, CGRectGetMaxY(self.geneTableView.frame) + 44); + self.continueButton = [[ARWhiteFlatButton alloc] initWithFrame:CGRectMake(0, screenSize.height - 44, screenSize.width, 44)]; + [self.continueButton addTarget:self action:@selector(continueTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.continueButton setTitle:@"Continue" forState:UIControlStateNormal]; + [self.view addSubview:self.continueButton]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil]; +} + +- (void)continueTapped:(id)sender +{ + [self.delegate personalizeDone]; +} + +- (void)searchToggleFollowStatusForArtist:(Artist *)artist atIndexPath:indexPath +{ + self.searchResults = @[]; + self.searchBar.text = @""; + + AROnboardingFollowableTableViewCell *cell = (AROnboardingFollowableTableViewCell *)[self.searchTableView cellForRowAtIndexPath:indexPath]; + cell.followState = !cell.followState; + + // We need to do this vs. checking if followed because + // the artist instance that was followed is a different object + // to this one + + if ([self.artistController hasArtist:artist]) { + self.followedThisSession--; + artist.followed = NO; + [self.artistController removeArtist:artist]; + + [artist unfollowWithSuccess:nil failure:^(NSError *error) { + [self.artistController addArtist:artist]; + [self updateFollowString]; + }]; + } else { + self.followedThisSession++; + [self.artistController addArtist:artist]; + + [artist followWithSuccess:nil failure:^(NSError *error) { + [self.artistController removeArtist:artist]; + [self updateFollowString]; + }]; + } + + [self updateFollowString]; + + if (self.followedThisSession <= 0) { + [self.cancelButton setTitle:@"CANCEL" forState:UIControlStateNormal]; + + } else if (self.followedThisSession == 1) { //if it went from zero to positive, it's 1 + [self.cancelButton setTitle:@"DONE" forState:UIControlStateNormal]; + } + + NSArray *otherCells = [[self.searchTableView visibleCells] reject:^BOOL(UITableViewCell *aCell) { + return cell == aCell; + }]; + + [UIView animateWithDuration:.2 animations:^{ + for (UIView *view in otherCells) { + view.alpha = 0; + } + }]; + + [UIView animateWithDuration:.2 delay:.2 options:0 animations:^{ + self.searchTableView.alpha = 0; + self.followedArtistsLabel.alpha = 1; + + } completion:^(BOOL finished) { + self.searchResults = @[]; + [self.searchTableView reloadData]; + }]; +} + +- (void)updateFollowString +{ + NSArray *names = [[self.artistController.artists array] map:^id(Artist *artist) { + return artist.name; + }]; + + if (names.count == 0) { + self.followedArtistsLabel.attributedText = [[NSAttributedString alloc] initWithString:@""]; + return; + } + + NSString *text = [@"Following " stringByAppendingString:[names componentsJoinedByString:@", "]]; + NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:text]; + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setLineSpacing:3]; + + [attr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [text length])]; + self.followedArtistsLabel.attributedText = attr; + CGSize suggested = [self.followedArtistsLabel sizeThatFits:self.searchView.bounds.size]; + CGRect frame = self.followedArtistsLabel.frame; + frame.size.height = suggested.height; + self.followedArtistsLabel.frame = frame; +} + +- (void)updateArtistTableViewAnimated:(BOOL)animated +{ + if ([self.searchBar isFirstResponder]) { + return; + } + CGRect aFrame = self.artistTableView.frame; + aFrame.size.height = (54 * self.artistController.artists.count); + + CGRect gFrame = self.geneTableView.frame; + gFrame.origin.y = CGRectGetMaxY(aFrame); + + [UIView animateSpringIf:animated duration:0.3 delay:0 damping:10 velocity:5 :^{ + self.artistTableView.frame = aFrame; + self.geneTableView.frame = gFrame; + self.scrollView.contentSize = CGSizeMake(320, CGRectGetMaxY(self.geneTableView.frame) + 44); + } completion:^(BOOL finished) { + [self.artistTableView reloadData]; + }]; +} + +#pragma mark - +#pragma mark Search bar + +- (void)searchBarDown:(id)sender +{ + if (self.cancelButton.alpha > 0) { + return; + } + + CGPoint currentOffset = self.scrollView.contentOffset; + CGFloat off = -5; + + [UIView animateWithDuration:.07 delay:0 usingSpringWithDamping:.8 initialSpringVelocity:2.5 options:0 animations:^{ + self.scrollView.contentOffset = CGPointMake(currentOffset.x, currentOffset.y + off); + + } completion:^(BOOL finished) { + [UIView animateWithDuration:.3 delay:0 usingSpringWithDamping:.8 initialSpringVelocity:2.5 options:0 animations:^{ + CGPoint currentOffset = self.scrollView.contentOffset; + self.scrollView.contentOffset = CGPointMake(currentOffset.x, currentOffset.y + 105); + self.cancelButton.alpha = 1; + [self.scrollView bringSubviewToFront:self.cancelButton]; + self.scrollView.scrollEnabled = NO; + } completion:nil]; + }]; + +} + +- (void)cancelSearch:(id)sender +{ + [self.searchBar resignFirstResponder]; + self.searchBar.text = @""; + [self updateArtistTableViewAnimated:NO]; + self.scrollView.scrollEnabled = YES; + [UIView animateWithDuration:.35 delay:0 usingSpringWithDamping:.8 initialSpringVelocity:2.5 options:0 animations:^{ + self.scrollView.contentOffset = CGPointZero; + self.cancelButton.alpha = 0; + self.searchView.alpha = 0; + + } completion:nil]; +} + +- (void)startedEditing:(id)sender +{ + [self.cancelButton setTitle:@"CANCEL" forState:UIControlStateNormal]; + self.followedThisSession = 0; + self.followedArtistsLabel.alpha = 0; + self.searchTableView.alpha = 0; + [UIView animateWithDuration:.3 animations:^{ + self.searchView.alpha = 1; + }]; +} + +- (void)searchTextChanged:(NSNotification *)notification +{ + BOOL searchBarIsEmpty = [self.searchBar.text isEqualToString:@""]; + if (self.searchTableView.alpha == 0) { + [UIView animateWithDuration:.1 animations:^{ + self.searchTableView.alpha = 1; + }]; + + } else if (searchBarIsEmpty) { + self.searchTableView.alpha = 0; + } + + if (self.searchRequestOperation) { + [self.searchRequestOperation cancel]; + } + + if (searchBarIsEmpty) { + self.searchResults = @[]; + [self.searchTableView reloadData]; + return; + } + + self.searchRequestOperation = [ArtsyAPI artistSearchWithQuery:self.searchBar.text success:^(NSArray *results) { + self.searchResults = results; + [self.searchTableView reloadData]; + + } failure:^(NSError *error) { + if (error.code != NSURLErrorCancelled) { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + ARErrorLog(@"Personalize search network error %@", error.localizedDescription); + } + }]; +} + +#pragma mark - +#pragma mark Table view delegate + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.searchResults.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 54; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + AROnboardingFollowableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SearchCellId]; + Artist *artist = self.searchResults[indexPath.row]; + cell.textLabel.text = artist.name; + cell.followState = [self.artistController hasArtist:artist]; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [self searchToggleFollowStatusForArtist:self.searchResults[indexPath.row] atIndexPath:indexPath]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARPersonalizeWebViewController.h b/Artsy/Classes/View Controllers/ARPersonalizeWebViewController.h new file mode 100644 index 00000000000..e1a9bdbe206 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPersonalizeWebViewController.h @@ -0,0 +1,10 @@ +#import "ARInternalMobileWebViewController.h" + +@protocol ARPersonalizeWebViewControllerDelegate +- (void)dismissOnboardingWithVoidAnimation:(BOOL)createdAccount; +- (void)webOnboardingDone; +@end + +@interface ARPersonalizeWebViewController : ARInternalMobileWebViewController +@property (nonatomic, weak) id delegate; +@end diff --git a/Artsy/Classes/View Controllers/ARPersonalizeWebViewController.m b/Artsy/Classes/View Controllers/ARPersonalizeWebViewController.m new file mode 100644 index 00000000000..128a8c61210 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPersonalizeWebViewController.m @@ -0,0 +1,66 @@ +#import "ARPersonalizeWebViewController.h" +#import "ARNetworkConstants.h" +#import "ARRouter.h" +#import "ARSpinner.h" + +@interface TSMiniWebBrowser (Private) +@property(nonatomic, readonly, strong) UIWebView *webView; +@end + +@interface ARPersonalizeWebViewController () +@property (nonatomic, strong, readonly) ARSpinner *spinner; +@end + +@implementation ARPersonalizeWebViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + _spinner = [[ARSpinner alloc] init]; + [self.view addSubview:self.spinner]; + [self.spinner alignCenterWithView: self.webView]; + + UITapGestureRecognizer *exitTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(exitOnboarding)]; + [self.view addGestureRecognizer:exitTap]; + [self.webView constrainWidthToView:self.view predicate:@"-200"]; + [self.webView constrainHeightToView:self.view predicate:@"-200"]; + [self.webView alignCenterWithView:self.view]; + self.view.backgroundColor = [UIColor clearColor]; +} + +- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL shouldLoad = [super webView:aWebView shouldStartLoadWithRequest:request navigationType:navigationType]; + NSString *path = [request.URL lastPathComponent]; + + if (shouldLoad && [ARRouter isInternalURL:request.URL] && [path isEqualToString:ARPersonalizePath]) { + return YES; + } else { + + // Force onboarding is all push-state. A new request to load a page indicates that onboarding is complete. + [self.delegate dismissOnboardingWithVoidAnimation:YES]; + return NO; + } +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error +{ + [self exitOnboarding]; +} + +- (void)exitOnboarding +{ + [self.delegate webOnboardingDone]; +} + +- (void)showLoading +{ + [self.spinner fadeInAnimated:YES]; +} + +- (void)hideLoading +{ + [self.spinner fadeOutAnimated:YES]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARPostsViewController.h b/Artsy/Classes/View Controllers/ARPostsViewController.h new file mode 100644 index 00000000000..ecd21964d64 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPostsViewController.h @@ -0,0 +1,16 @@ +@class ARPostsViewController; + +@protocol ARPostsViewControllerDelegate + +-(void)postViewController:(ARPostsViewController *)postViewController shouldShowViewController:(UIViewController *)viewController; + +@end + +@interface ARPostsViewController : UIViewController + +@property (nonatomic, strong) NSArray *posts; + +@property (nonatomic, weak) id delegate; + +@end + diff --git a/Artsy/Classes/View Controllers/ARPostsViewController.m b/Artsy/Classes/View Controllers/ARPostsViewController.m new file mode 100644 index 00000000000..f36dc120944 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPostsViewController.m @@ -0,0 +1,50 @@ +#import "ARPostsViewController.h" +#import "ARPostFeedItemLinkView.h" +#import "ORStackView+ArtsyViews.h" + +@interface ARPostsViewController () +@property (nonatomic, strong) ORStackView *view; +@end + +@implementation ARPostsViewController + +- (void)loadView +{ + self.view = [[ORStackView alloc] init]; + self.view.backgroundColor = [UIColor whiteColor]; + self.view.bottomMarginHeight = 20; +} + +-(void)setPosts:(NSArray *)posts +{ + _posts = posts; + [self reloadData]; +} + +-(void)reloadData +{ + [self addPageTitleWithString:@"Featured Posts"]; + + for(NSInteger i = 0; i < self.posts.count; i++) { + ARPostFeedItem *post = self.posts[i]; + ARPostFeedItemLinkView * postLinkView = [[ARPostFeedItemLinkView alloc] init]; + [postLinkView updateWithPostFeedItem:post]; + [self.view addSubview:postLinkView withTopMargin:nil sideMargin:nil]; + } +} + +-(void)tappedPostFeedItemLinkView:(ARPostFeedItemLinkView *)sender +{ + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadPath:[sender targetPath]]; + if (viewController) { + [self.delegate postViewController:self shouldShowViewController:viewController]; + } +} + +- (UILabel *)addPageTitleWithString:(NSString *)title +{ + return [self.view addPageSubtitleWithString:title]; +} + + +@end diff --git a/Artsy/Classes/View Controllers/ARPriceRangeViewController.h b/Artsy/Classes/View Controllers/ARPriceRangeViewController.h new file mode 100644 index 00000000000..be235e131c9 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPriceRangeViewController.h @@ -0,0 +1,7 @@ +#import +@class AROnboardingViewController; + +@interface ARPriceRangeViewController : UIViewController +@property (nonatomic, weak) AROnboardingViewController *delegate; + +@end diff --git a/Artsy/Classes/View Controllers/ARPriceRangeViewController.m b/Artsy/Classes/View Controllers/ARPriceRangeViewController.m new file mode 100644 index 00000000000..64c25dbf432 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARPriceRangeViewController.m @@ -0,0 +1,97 @@ +#import "ARPriceRangeViewController.h" +#import "AROnboardingViewController.h" +#import "AROnboardingTableViewCell.h" + +@interface ARPriceRangeViewController () +@property (nonatomic) NSArray *ranges; +@end + +@implementation ARPriceRangeViewController +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.backgroundColor = [UIColor blackColor]; + + CGSize screenSize = self.view.bounds.size; + NSInteger tableOrigin; + if (screenSize.height > 480) { + self.ranges = @[ + @{@"range" : @(500), @"display": @"Under $500"}, + @{@"range" : @(2500), @"display": @"Under $2,500"}, + @{@"range" : @(5000), @"display": @"Under $5,000"}, + @{@"range" : @(10000), @"display": @"Under $10,000"}, + @{@"range" : @(25000), @"display": @"Under $25,000"}, + @{@"range" : @(50000), @"display": @"Under $50,000"}, + @{@"range" : @(100000), @"display": @"Under $100,000"}, + @{@"range" : @(1000000), @"display": @"$100,000+"} + ]; + tableOrigin = 120; + + } else { + self.ranges = @[ + @{@"range" : @(1000), @"display": @"Under $1,000"}, + @{@"range" : @(5000), @"display": @"Under $5,000"}, + @{@"range" : @(20000), @"display": @"Under $20,000"}, + @{@"range" : @(50000), @"display": @"Under $50,000"}, + @{@"range" : @(100000), @"display": @"Under $100,000"}, + @{@"range" : @(1000000), @"display": @"$100,000+"} + ]; + tableOrigin = 136; + + } + UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, tableOrigin, screenSize.width, screenSize.height - tableOrigin) + style:UITableViewStylePlain]; + [tableView registerClass:[AROnboardingTableViewCell class] forCellReuseIdentifier:@"StatusCell"]; + tableView.backgroundColor = [UIColor clearColor]; + tableView.dataSource = self; + tableView.delegate = self; + tableView.scrollEnabled = NO; + tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + [self.view addSubview:tableView]; + CALayer *sep = [CALayer layer]; + sep.frame = CGRectMake(15, 0, 290, .5); + sep.backgroundColor = [UIColor artsyHeavyGrey].CGColor; + [tableView.layer addSublayer:sep]; + + UILabel *header = [[UILabel alloc] initWithFrame:CGRectMake(20, 30, 280, 30)]; + header.textColor = [UIColor whiteColor]; + header.font = [UIFont serifFontWithSize:24]; + header.text = @"What’s your price range?"; + [self.view addSubview:header]; +} + +#pragma mark - +#pragma mark Table view + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StatusCell"]; + cell.textLabel.text = self.ranges[indexPath.row][@"display"]; + return cell; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.ranges.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 44 + (5 * 2); +} + + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSNumber *range = self.ranges[indexPath.row][@"range"]; + [self.delegate setPriceRangeDone:[range integerValue]]; +} + + +@end diff --git a/Artsy/Classes/View Controllers/ARProfileViewController.h b/Artsy/Classes/View Controllers/ARProfileViewController.h new file mode 100644 index 00000000000..d40fb543932 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARProfileViewController.h @@ -0,0 +1,9 @@ +#import + +@interface ARProfileViewController : UIViewController + +- (instancetype)initWithProfileID:(NSString *)profileID; + +@property (nonatomic, strong, readonly) NSString *profileID; + +@end diff --git a/Artsy/Classes/View Controllers/ARProfileViewController.m b/Artsy/Classes/View Controllers/ARProfileViewController.m new file mode 100644 index 00000000000..3f18c10f6af --- /dev/null +++ b/Artsy/Classes/View Controllers/ARProfileViewController.m @@ -0,0 +1,101 @@ +// View Controllers +#import "ARProfileViewController.h" +#import "ARFairViewController.h" +#import "ARInternalMobileWebViewController.h" + +// Categories +#import "UIViewController+FullScreenLoading.h" +#import "UIViewController+SimpleChildren.h" + +// Utilities + +@interface ARProfileViewController () + +@property (nonatomic, strong, readwrite) NSString *profileID; + +@property (nonatomic, assign) BOOL hidesBackButton; +@property (nonatomic, assign) BOOL hidesToolbarMenu; + +@end + +@implementation ARProfileViewController + +- (instancetype)initWithProfileID:(NSString *)profileID +{ + self = [super init]; + if (!self) { return nil; } + + self.profileID = profileID; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + @weakify(self) + // On the first viewWillAppear: + [[[self rac_signalForSelector:@selector(viewWillAppear:)] take:1] subscribeNext:^(id _) { + @strongify(self); + [self loadProfile]; + }]; +} + +- (void)loadProfile +{ + [self ar_presentIndeterminateLoadingIndicatorAnimated:YES]; + + [ArtsyAPI getProfileForProfileID:self.profileID success:^(Profile *profile) { + + if ([profile.profileOwner isKindOfClass:[FairOrganizer class]] && ![UIDevice isPad]) { + NSString * defaultFairID = ((FairOrganizer *) profile.profileOwner).defaultFairID; + Fair *fair = [[Fair alloc] initWithFairID:defaultFairID]; + + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:fair andProfile:profile]; + + RAC(self, hidesBackButton) = RACObserve(viewController, hidesBackButton); + + [self showViewController:viewController]; + } else if ([profile.profileOwner isKindOfClass:[Fair class]] && ![UIDevice isPad]) { + NSString * fairID = ((Fair *) profile.profileOwner).fairID; + Fair *fair = [[Fair alloc] initWithFairID:fairID]; + + ARFairViewController *viewController = [[ARFairViewController alloc] initWithFair:fair andProfile:profile]; + + RAC(self, hidesBackButton) = RACObserve(viewController, hidesBackButton); + + [self showViewController:viewController]; + } else { + [self loadMartsyView]; + } + + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + } failure:^(NSError *error) { + ARErrorLog(@"Error getting Profile %@, falling back to Martsy.", self.profileID); + [self loadMartsyView]; + [self ar_removeIndeterminateLoadingIndicatorAnimated:YES]; + }]; +} + +- (void)loadMartsyView +{ + NSURL *profileURL = [ARSwitchBoard.sharedInstance resolveRelativeUrl:NSStringWithFormat(@"%@%@", self.profileID, @"?foo=bar")]; + + ARInternalMobileWebViewController *viewController = [[ARInternalMobileWebViewController alloc] initWithURL:profileURL]; + [self showViewController:viewController]; +} + +- (void)showViewController:(UIViewController *)viewController +{ + [self ar_addModernChildViewController:viewController]; + NSString *top = [NSString stringWithFormat:@"%f", [self.topLayoutGuide length]]; + [viewController.view alignTop:top leading:@"0" bottom:@"0" trailing:@"0" toView:self.view]; +} + +- (BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARQuicksilverViewController.h b/Artsy/Classes/View Controllers/ARQuicksilverViewController.h new file mode 100644 index 00000000000..feeacbb3603 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARQuicksilverViewController.h @@ -0,0 +1,7 @@ +#import "ARQuicksilverSearchBar.h" + +/// Search the Artsy API using the keyboard in this view + +@interface ARQuicksilverViewController : UIViewController + +@end diff --git a/Artsy/Classes/View Controllers/ARQuicksilverViewController.m b/Artsy/Classes/View Controllers/ARQuicksilverViewController.m new file mode 100644 index 00000000000..b2291908c37 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARQuicksilverViewController.m @@ -0,0 +1,180 @@ +#import "ARQuicksilverViewController.h" +#import "ARContentViewControllers.h" +#import "SearchResult.h" +#import "ARSearchTableViewCell.h" + +@interface ARQuicksilverViewController () + +@property (nonatomic, assign) NSInteger selectedIndex; +@property (nonatomic, copy) NSArray *searchResults; +@property (nonatomic, weak) IBOutlet UISearchBar *searchBar; +@property (nonatomic, strong) AFJSONRequestOperation *searchRequest; + +@end + +@implementation ARQuicksilverViewController + +#pragma mark - ARMenuAwareViewController + +- (BOOL)hidesBackButton { + return YES; +} + +- (BOOL)hidesToolbarMenu { + return YES; +} + +#pragma mark - UIViewController + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + [self.searchBar becomeFirstResponder]; +} + +- (void)searchBarDownPressed:(ARQuicksilverSearchBar *)searchBar +{ + [self setHighlight:NO forCellAtIndex:self.selectedIndex]; + + NSInteger nextIndex = self.selectedIndex + 1; + self.selectedIndex = MIN(nextIndex, self.searchResults.count); + + [self setHighlight:YES forCellAtIndex:self.selectedIndex]; +} + +- (void)searchBarUpPressed:(ARQuicksilverSearchBar *)searchBar +{ + [self setHighlight:NO forCellAtIndex:self.selectedIndex]; + + NSInteger nextIndex = self.selectedIndex - 1; + self.selectedIndex = MAX(0, nextIndex); + + [self setHighlight:YES forCellAtIndex:self.selectedIndex]; +} + +- (void)searchBarEscapePressed:(ARQuicksilverSearchBar *)searchBar +{ + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)setHighlight:(BOOL)highlight forCellAtIndex:(NSInteger)index +{ + NSIndexPath *path = [NSIndexPath indexPathForRow:index inSection:0]; + UITableViewCell *cell = [self.searchDisplayController.searchResultsTableView cellForRowAtIndexPath:path]; + + UIColor *background = highlight? [UIColor darkGrayColor] : [UIColor blackColor]; + [cell setBackgroundColor:background]; +} + +- (void)searchBarReturnPressed:(ARQuicksilverSearchBar *)searchBar +{ + [searchBar resignFirstResponder]; + + UITableView *tableView = self.searchDisplayController.searchResultsTableView; + NSIndexPath *path = [NSIndexPath indexPathForRow:self.selectedIndex inSection:0]; + UITableViewCell *cell = [tableView cellForRowAtIndexPath:path]; + [cell setBackgroundColor:[UIColor grayColor]]; + + [self tableView:tableView didSelectRowAtIndexPath:path]; +} + +- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { + return self.searchResults.count; +} + +- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView +{ + [controller.searchResultsTableView registerClass:[ARSearchTableViewCell class] forCellReuseIdentifier:@"SearchCell"]; + tableView.backgroundColor = [UIColor blackColor]; +} + +- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)query +{ + if (self.searchRequest) { + [self.searchRequest cancel]; + } + + if (query.length == 0) { + self.searchResults = nil; + [controller.searchResultsTableView reloadData]; + + } else { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; + + @weakify(self); + self.searchRequest = [ArtsyAPI searchWithQuery:query success:^(NSArray *results) { + @strongify(self); + self.searchResults = [results copy]; + self.selectedIndex = 0; + + [controller.searchResultsTableView reloadData]; + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + [self setHighlight:YES forCellAtIndex:0]; + + } failure:^(NSError *error) { + if (error.code != NSURLErrorCancelled) { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + } + }]; + } + + return NO; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SearchCell"]; + SearchResult *result = self.searchResults[indexPath.row]; + + BOOL published = result.isPublished.boolValue; + if (!published) { + cell.textLabel.text = [result.displayText stringByAppendingString:@" (unpublished)"]; + cell.textLabel.textColor = [UIColor artsyLightGrey]; + } else { + cell.textLabel.text = result.displayText; + cell.textLabel.textColor = [UIColor whiteColor]; + } + cell.backgroundColor = [UIColor blackColor]; + + UIImage *placeholder = [UIImage imageNamed:@"SearchThumb_LightGray"]; + + @weakify(cell); + [cell.imageView setImageWithURLRequest:result.imageRequest placeholderImage:placeholder + + success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { + @strongify(cell); + cell.imageView.image = image; + [cell layoutSubviews]; + + } failure:nil]; + + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + SearchResult *result = self.searchResults[indexPath.row]; + UIViewController *controller = nil; + + if (result.model == [Artwork class]) { + controller = [[ARArtworkSetViewController alloc] initWithArtworkID:result.modelID]; + + } else if (result.model == [Artist class]) { + controller = [[ARArtistViewController alloc] initWithArtistID:result.modelID]; + + } else if (result.model == [Gene class]) { + controller = [[ARGeneViewController alloc] initWithGeneID:result.modelID]; + + } else if (result.model == [Profile class]) { + controller = [ARSwitchBoard.sharedInstance routeProfileWithID:result.modelID]; + + } else if ( result.model == [SiteFeature class]) { + NSString *path = NSStringWithFormat(@"/feature/%@", result.modelID); + controller = [[ARSwitchBoard sharedInstance] loadPath:path]; + } + + [self.navigationController pushViewController:controller animated:YES]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARQuicksilverViewController.xib b/Artsy/Classes/View Controllers/ARQuicksilverViewController.xib new file mode 100644 index 00000000000..464710f2ab7 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARQuicksilverViewController.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Artsy/Classes/View Controllers/ARRelatedArtistsViewController.h b/Artsy/Classes/View Controllers/ARRelatedArtistsViewController.h new file mode 100644 index 00000000000..a8eaab22134 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARRelatedArtistsViewController.h @@ -0,0 +1,9 @@ +@interface ARRelatedArtistsViewController : UIViewController + +- (instancetype)initWithFair:(Fair *)fair; + +@property (nonatomic, strong, readonly) Fair *fair; + +@property (nonatomic, strong) NSArray *relatedArtists; + +@end diff --git a/Artsy/Classes/View Controllers/ARRelatedArtistsViewController.m b/Artsy/Classes/View Controllers/ARRelatedArtistsViewController.m new file mode 100644 index 00000000000..eee1c94c840 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARRelatedArtistsViewController.m @@ -0,0 +1,109 @@ +#import "ARRelatedArtistsViewController.h" +#import "ARFavoriteItemViewCell.h" + +@interface ARRelatedArtistsViewController () + +@property (nonatomic, strong) UICollectionView *view; + +@property (nonatomic, strong) NSLayoutConstraint *heightConstraint; +// Private Access +@property (nonatomic, strong, readwrite) Fair *fair; + +@end + +@implementation ARRelatedArtistsViewController + +- (instancetype)initWithFair:(Fair *)fair { + self = [super init]; + if (!self) { return nil; } + + self.fair = fair; + + return self; +} + +- (void)setRelatedArtists:(NSArray *)relatedArtists +{ + _relatedArtists = relatedArtists; + [self.view reloadData]; + [self updateHeightConstraint]; +} + +- (void)updateHeightConstraint +{ + CGFloat height = self.view.collectionViewLayout.collectionViewContentSize.height; + if (!self.heightConstraint) { + self.heightConstraint = [[self.view constrainHeight:@(height).stringValue] lastObject]; + } else { + self.heightConstraint.constant = height; + } +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self setItemSizeForOrientation:toInterfaceOrientation]; + [self updateHeightConstraint]; + [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self setItemSizeForOrientation:[UIApplication sharedApplication].statusBarOrientation]; + [self updateHeightConstraint]; +} + +- (void) loadView +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + CGFloat sideMargin = [UIDevice isPad] ? 50 : 20; + layout.sectionInset = UIEdgeInsetsMake(20, sideMargin, 20, sideMargin); + UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + + collectionView.delegate = self; + collectionView.dataSource = self; + collectionView.backgroundColor = [UIColor whiteColor]; + [collectionView registerClass:[ARFavoriteItemViewCell class] forCellWithReuseIdentifier:@"RelatedArtistCell"]; + + self.view = collectionView; +} + +- (void)setItemSizeForOrientation:(UIInterfaceOrientation)orientation +{ + UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.view.collectionViewLayout; + layout.itemSize = (CGSize){ [ARFavoriteItemViewCell widthForCellWithOrientation:orientation], [ARFavoriteItemViewCell heightForCellWithOrientation:orientation] }; + [self.view.collectionViewLayout invalidateLayout]; + [self.view layoutIfNeeded]; +} + +#pragma mark - +#pragma mark Related artist delegate/datasource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section; +{ + return self.relatedArtists.count; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; +{ + ARFavoriteItemViewCell *cell = (ARFavoriteItemViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"RelatedArtistCell" forIndexPath:indexPath]; + + Artist *artist = self.relatedArtists[indexPath.row]; + [cell setupWithRepresentedObject:artist]; + + return cell; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + Artist *artist = self.relatedArtists[indexPath.row]; + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadArtistWithID:artist.artistID inFair:self.fair]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (CGSize)preferredContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, self.view.collectionViewLayout.collectionViewContentSize.height); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARSearchFieldButton.h b/Artsy/Classes/View Controllers/ARSearchFieldButton.h new file mode 100644 index 00000000000..5e6224a2039 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchFieldButton.h @@ -0,0 +1,15 @@ +#import + +@class ARSearchFieldButton; + +@protocol ARSearchFieldButtonDelegate + +- (void)searchFieldButtonWasPressed:(ARSearchFieldButton *)sender; + +@end + +@interface ARSearchFieldButton : UIView + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Artsy/Classes/View Controllers/ARSearchFieldButton.m b/Artsy/Classes/View Controllers/ARSearchFieldButton.m new file mode 100644 index 00000000000..220ec0ac392 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchFieldButton.m @@ -0,0 +1,56 @@ +#import "ARSearchFieldButton.h" + +@interface ARSearchFieldButton () + +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) UILabel *label; + +@end + +@implementation ARSearchFieldButton + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self == nil) { return nil; } + + self.backgroundColor = [UIColor colorWithHex:0xf2f2f2]; + + + self.imageView = [[UIImageView alloc] init]; + self.imageView.image = [UIImage imageNamed:@"SearchIcon_HeavyGrey"]; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + [self addSubview:self.imageView]; + [self.imageView alignCenterYWithView:self predicate:nil]; + [self.imageView alignLeadingEdgeWithView:self predicate:@"10"]; + [self.imageView constrainWidth:@"16"]; + [self.imageView constrainHeight:@"16"]; + + self.label = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:6]; + self.label.font = [UIFont serifFontWithSize:16]; + self.label.text = @"Find Exhibitors & Artists"; + self.label.numberOfLines = 0; + self.label.textColor = [UIColor artsyHeavyGrey]; + self.label.backgroundColor = [UIColor clearColor]; + [self addSubview:self.label]; + [self.label alignLeadingEdgeWithView:self.imageView predicate:@"21"]; + [self.label alignTrailingEdgeWithView:self predicate:nil]; + [self.label alignTop:@"2" bottom:@"0" toView:self]; + + UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] init]; + @weakify(self); + [recognizer.rac_gestureSignal subscribeNext:^(id _) { + @strongify(self); + [self.delegate searchFieldButtonWasPressed:self]; + }]; + [self addGestureRecognizer:recognizer]; + + return self; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, 32); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARSearchResultsDataSource.h b/Artsy/Classes/View Controllers/ARSearchResultsDataSource.h new file mode 100644 index 00000000000..f8592703e41 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchResultsDataSource.h @@ -0,0 +1,11 @@ +#import "SearchResult.h" + +@interface ARSearchResultsDataSource : NSObject + +@property(nonatomic) NSOrderedSet *searchResults; +@property(nonatomic, readwrite, strong) UIColor *textColor; +@property(nonatomic, readwrite, strong) UIImage *placeholderImage; + +- (SearchResult *)objectAtIndex:(NSInteger)index; + +@end \ No newline at end of file diff --git a/Artsy/Classes/View Controllers/ARSearchResultsDataSource.m b/Artsy/Classes/View Controllers/ARSearchResultsDataSource.m new file mode 100644 index 00000000000..0d91df3be83 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchResultsDataSource.m @@ -0,0 +1,61 @@ +#import "ARSearchResultsDataSource.h" + +@implementation ARSearchResultsDataSource + +- (id)init +{ + self = [super init]; + if (!self) { return nil; } + + _placeholderImage = [UIImage imageNamed:@"SearchThumb_LightGrey"]; + + return self; +} + +- (id)initWithSearchResults:(NSOrderedSet *)searchResults +{ + self = [self init]; + if (!self) { return nil; } + + _searchResults = searchResults; + + return self; +} + +- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section +{ + return self.searchResults.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SearchCell"]; + SearchResult *result = self.searchResults[indexPath.row]; + + BOOL published = result.isPublished.boolValue; + if (!published) { + cell.textLabel.text = [result.displayText stringByAppendingString:@" (unpublished)"]; + } else { + cell.textLabel.text = result.displayText; + } + + cell.textLabel.textColor = self.textColor ?: [UIColor whiteColor]; + + @weakify(cell); + [cell.imageView setImageWithURLRequest:result.imageRequest placeholderImage:self.placeholderImage + success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { + @strongify(cell); + cell.imageView.image = image; + [cell layoutSubviews]; + + } failure:nil]; + + return cell; +} + +- (SearchResult *)objectAtIndex:(NSInteger)index +{ + return self.searchResults[index]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARSearchViewController+Private.h b/Artsy/Classes/View Controllers/ARSearchViewController+Private.h new file mode 100644 index 00000000000..41353093ee8 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchViewController+Private.h @@ -0,0 +1,7 @@ +@interface ARSearchViewController (Private) + +- (void)closeSearch:(id)sender; +- (void)searchText:(NSString *)text; + +@property(readonly, nonatomic) UIView *contentView; +@end diff --git a/Artsy/Classes/View Controllers/ARSearchViewController.h b/Artsy/Classes/View Controllers/ARSearchViewController.h new file mode 100644 index 00000000000..9c8cae10562 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchViewController.h @@ -0,0 +1,30 @@ +#import "ARSearchResultsDataSource.h" + +typedef NS_ENUM(NSInteger, ARMenuState){ + ARMenuStateCollapsed, + ARMenuStateExpanded +}; + +@interface ARSearchViewController : UIViewController + +@property(readonly, nonatomic) UITextField *textField; +@property(readonly, nonatomic) UILabel *infoLabel; +@property(readonly, nonatomic) UIImageView *searchIcon; +@property(readonly, nonatomic, assign) ARMenuState menuState; +@property(readwrite, nonatomic, copy) NSString *defaultInfoLabelText; +@property(readwrite, nonatomic, copy) NSString *noResultsInfoLabelText; +@property(readwrite, nonatomic, copy) NSString *searchIconImageName; +@property(readwrite, nonatomic, assign) NSInteger fontSize; +@property(readonly, nonatomic) UIView *searchBoxView; +@property(readonly, nonatomic) UIButton *closeButton; +@property(readonly, nonatomic) ARSearchResultsDataSource *searchDataSource; + +- (NSOrderedSet *)searchResults; + +- (void)clearSearch; +- (void)fetchSearchResults:(NSString *)text; +- (void)fetchSearchResults:(NSString *)text replace:(BOOL)replaceResults; +- (void)addResults:(NSArray *)results replace:(BOOL)replaceResults; +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query; + +@end diff --git a/Artsy/Classes/View Controllers/ARSearchViewController.m b/Artsy/Classes/View Controllers/ARSearchViewController.m new file mode 100644 index 00000000000..942fcf6939b --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSearchViewController.m @@ -0,0 +1,403 @@ +#import "ARSearchViewController.h" +#import "ARSearchViewController+Private.h" +#import "ARSearchTableViewCell.h" +#import "UIView+HitTestExpansion.h" + +@interface ARSearchViewController () +@property(readonly, nonatomic) UIActivityIndicatorView *activityIndicator; +@property(readonly, nonatomic) UITableView *resultsView; +@property(readonly, nonatomic) UIView *contentView; +@property(readonly, nonatomic) AFJSONRequestOperation *searchRequest; +@property(readonly, nonatomic, strong) NSLayoutConstraint *contentHeightConstraint; +@property(nonatomic, readwrite, assign) BOOL shouldAnimate; +@end + +@implementation ARSearchViewController + +- (instancetype)init +{ + self = [super init]; + if (self) { + _searchDataSource = [[ARSearchResultsDataSource alloc] init]; + _fontSize = 16; + _noResultsInfoLabelText = @"No results found."; + _shouldAnimate = YES; + } + return self; +} + +- (void)viewDidLoad +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillShow:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; + + self.view.backgroundColor = [UIColor blackColor]; + + UIView *searchBoxView = [[UIView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:searchBoxView]; + [searchBoxView constrainTopSpaceToView:(UIView *)self.topLayoutGuide predicate:@"24"]; + [searchBoxView alignLeading:@"20" trailing:@"-10" toView:self.view]; + [searchBoxView constrainHeight:@(self.fontSize).stringValue]; + _searchBoxView = searchBoxView; + + // search icon + UIImageView *searchIcon = [[UIImageView alloc] init]; + searchIcon.image = [UIImage imageNamed:self.searchIconImageName ?: @"SearchIcon_LightGrey"]; + searchIcon.contentMode = UIViewContentModeScaleAspectFit; + [searchBoxView addSubview:searchIcon]; + _searchIcon = searchIcon; + [searchIcon alignTop:@"0" leading:@"0" bottom:@"0" trailing:nil toView:searchBoxView]; + [searchIcon alignAttribute:NSLayoutAttributeWidth toAttribute:NSLayoutAttributeHeight ofView:searchIcon predicate:nil]; + + // input text field + UITextField * textField = [[UITextField alloc] initWithFrame:CGRectZero]; + textField.textColor = [UIColor whiteColor]; + textField.font = [UIFont serifFontWithSize:self.fontSize]; + textField.tintColor = [UIColor whiteColor]; + textField.keyboardAppearance = UIKeyboardAppearanceDark; + textField.opaque = NO; + textField.autocorrectionType = UITextAutocorrectionTypeNo; + textField.returnKeyType = UIReturnKeySearch; + [searchBoxView addSubview:textField]; + [textField alignTop:@"0" bottom:@"0" toView:self.searchBoxView]; + [textField constrainLeadingSpaceToView:searchIcon predicate:@"4"]; + _textField = textField; + textField.delegate = self; + [textField addTarget:self action:@selector(search:) forControlEvents:UIControlEventEditingChanged]; + [textField ar_extendHitTestSizeByWidth:6 andHeight:16]; + + UIButton *closeButton = [[UIButton alloc] init]; + [closeButton setTitle:@"CLOSE" forState:UIControlStateNormal]; + [closeButton.titleLabel setFont:[UIFont sansSerifFontWithSize:self.fontSize * 0.75]]; + [closeButton setContentHuggingPriority:750 forAxis:UILayoutConstraintAxisHorizontal]; + [closeButton addTarget:self action:@selector(closeSearch:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:closeButton]; + _closeButton = closeButton; + [closeButton alignTop:@"0" leading:nil bottom:@"0" trailing:@"0" toView:searchBoxView]; + [textField alignAttribute:NSLayoutAttributeTrailing toAttribute:NSLayoutAttributeLeading ofView:closeButton predicate:@"-14"]; + + _contentView = [[UIView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:self.contentView]; + [self.contentView constrainTopSpaceToView:self.searchBoxView predicate:@"15"]; + [self.contentView alignLeading:@"20" trailing:@"-20" toView:self.view]; + _contentHeightConstraint = [[self.contentView alignBottomEdgeWithView:self.view predicate:@"-20"] lastObject]; + + // search info label + UILabel *infoLabel = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:6]; + infoLabel.font = [UIFont serifItalicFontWithSize:18]; + infoLabel.numberOfLines = 0; + infoLabel.textAlignment = NSTextAlignmentCenter; + [self.contentView addSubview:infoLabel]; + [infoLabel constrainHeight:@"60"]; + [infoLabel constrainWidthToView:self.contentView predicate:nil]; + [infoLabel alignCenterWithView:self.contentView]; + infoLabel.textColor = [UIColor artsyHeavyGrey]; + infoLabel.backgroundColor = [UIColor clearColor]; + infoLabel.hidden = YES; + _infoLabel = infoLabel; + [self setDefaultInfoLabelText]; + + // search spinner + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; + [self.contentView addSubview:activityIndicator]; + [activityIndicator alignCenterWithView:self.contentView]; + activityIndicator.hidden = YES; + _activityIndicator = activityIndicator; + [super viewDidLoad]; +} + +#pragma mark - +#pragma mark UITextField + +- (void)viewDidAppear:(BOOL)animated +{ + [self.view layoutSubviews]; + [super viewDidAppear:animated]; + [self.textField becomeFirstResponder]; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + return NO; +} + +#pragma mark - Keyboard + +- (void)hideKeyboard +{ + [self.view endEditing:YES]; +} + +- (void)keyboardWillShow:(NSNotification *)notification +{ + CGRect keyboardRect = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + + CGFloat height = CGRectGetHeight(keyboardRect); + self.contentHeightConstraint.constant = -height - 20; + [UIView animateIf:self.shouldAnimate duration:ARAnimationQuickDuration :^{ + [self.view layoutSubviews]; + } completion:^(BOOL finished) { + if (!(self.textField.text.length > 0)) { + [self showInfoLabel:YES animated:self.shouldAnimate]; + } + }]; +} + +- (void)keyboardWillHide:(NSNotification *)notification +{ + [self showInfoLabel:NO animated:self.shouldAnimate completion:^{ + self.contentHeightConstraint.constant = -20; + }]; + [UIView animateIf:self.shouldAnimate duration:ARAnimationQuickDuration :^{ + [self.view layoutSubviews]; + }]; +} + +#pragma mark - Search + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +{ + if([string isEqualToString:@"\n"]) { + [textField resignFirstResponder]; + return NO; + } + return YES; +} + +- (void)stopSearching +{ + [self.activityIndicator stopAnimating]; + self.activityIndicator.hidden = YES; + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; +} + +- (void)startSearching +{ + if (!(self.searchDataSource.searchResults.count > 0)) { + self.activityIndicator.hidden = NO; + [self.activityIndicator startAnimating]; + } + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; +} + +- (void)resetResults +{ + [self addResults:@[] replace:YES]; +} + +- (AFJSONRequestOperation *)searchWithQuery:(NSString *)query success:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +{ + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (void)searchText:(NSString *)text +{ + if (self.searchRequest) { + [self.searchRequest cancel]; + } + + if (text.length == 0) { + [self stopSearching]; + [self resetResults]; + [self setDefaultInfoLabelText]; + if ([self.textField isFirstResponder]) { + [self updateInfoLabel:self.shouldAnimate]; + } else { + [self.textField becomeFirstResponder]; + } + } else { + [self startSearching]; + [self fetchSearchResults:text]; + [self updateInfoLabel:self.shouldAnimate]; + } +} + +- (void)fetchSearchResults:(NSString *)text +{ + [self fetchSearchResults:text replace:YES]; +} + +- (void)fetchSearchResults:(NSString *)text replace:(BOOL)replaceResults +{ + @weakify(self); + _searchRequest = [self searchWithQuery:text success:^(NSArray *results) { + @strongify(self); + [self addResults:results replace:replaceResults]; + [self finishSearching]; + } failure:^(NSError *error) { + if (error.code != NSURLErrorCancelled) { + [self presentNoResults]; + ARErrorLog(@"Search network error %@", error.localizedDescription); + } + }]; +} + +- (void)finishSearching +{ + [self stopSearching]; + + if (self.searchDataSource.searchResults.count == 0) { + [self presentNoResults]; + } else { + [self showInfoLabel:NO animated:self.shouldAnimate]; + } +} + +- (void)addResults:(NSArray *)results replace:(BOOL)replaceResults +{ + if (replaceResults) { + self.searchDataSource.searchResults = [NSOrderedSet orderedSetWithArray:results]; + if (results.count == 0) { + [self removeResultsViewAnimated:self.shouldAnimate]; + } else { + [self presentResultsViewAnimated:self.shouldAnimate]; + } + } else { + NSMutableOrderedSet *searchResults = [NSMutableOrderedSet orderedSetWithOrderedSet:self.searchDataSource.searchResults]; + [searchResults addObjectsFromArray:results]; + self.searchDataSource.searchResults = searchResults; + [self presentResultsViewAnimated:self.shouldAnimate]; + } + + [self.resultsView reloadData]; +} + +- (NSOrderedSet *)searchResults +{ + return self.searchDataSource.searchResults; +} + +- (void)search:(UITextField *)textField +{ + [self searchText:textField.text]; +} + +- (void)closeSearch:(id)sender +{ + [self clearSearch]; +} + +- (void)clearSearch { + self.textField.text = @""; + [self searchText:@""]; +} + +- (void)removeResultsViewAnimated:(BOOL)animated +{ + @weakify(self); + + [UIView animateIf:animated duration:0.15 :^{ + @strongify(self); + self.resultsView.alpha = 0; + } completion:^(BOOL finished) { + @strongify(self); + if (!self) { return; } + + self.resultsView.hidden = YES; + }]; +} + +- (void)presentNoResults +{ + [self resetResults]; + [self setNoResultsInfoLabelText]; + [self showInfoLabel:YES animated:self.shouldAnimate]; + [self stopSearching]; +} + +#pragma mark - Info Label + +- (void)showInfoLabel:(BOOL)show animated:(BOOL)animated +{ + [self showInfoLabel:show animated:animated completion:nil]; +} +- (void)showInfoLabel:(BOOL)show animated:(BOOL)animated completion:(void (^)(void))completion +{ + if (show) self.infoLabel.hidden = NO; + [UIView animateIf:animated duration:ARAnimationQuickDuration :^{ + self.infoLabel.alpha = show ? 1 : 0; + } completion:^(BOOL finished) { + if (!show) self.infoLabel.hidden = YES; + if (completion) { completion(); }; + }]; +} + +// display the info label if there's nothing to search for +- (void)updateInfoLabel:(BOOL)animated +{ + [self showInfoLabel:(self.textField.text.length == 0) animated:animated]; +} + +- (void)setDefaultInfoLabelText +{ + self.infoLabel.text = self.defaultInfoLabelText; +} + +- (void)setNoResultsInfoLabelText +{ + self.infoLabel.text = self.noResultsInfoLabelText; +} + +#pragma mark - Search Results + +- (void)presentResultsViewAnimated:(BOOL)animated +{ + if (!self.resultsView) { + UITableView *tableView = [[UITableView alloc] init]; + tableView.delegate = self; + tableView.dataSource = self.searchDataSource; + tableView.separatorInset = UIEdgeInsetsZero; + tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + [tableView registerClass:[ARSearchTableViewCell class] forCellReuseIdentifier:@"SearchCell"]; + tableView.backgroundColor = [UIColor clearColor]; + tableView.opaque = NO; + tableView.alpha = 0; + + [self stopSearching]; + + [self.view addSubview:tableView]; + [tableView alignToView:self.contentView]; + _resultsView = tableView; + [self.view layoutIfNeeded]; + } + self.resultsView.hidden = NO; + [UIView animateIf:animated duration:ARAnimationQuickDuration :^{ + self.resultsView.alpha = 1; + }]; +} + +#pragma mark UITableViewDataSource and UITableViewDelegate + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView +{ + return 1; +} + +- (void)selectedResult:(SearchResult *)result ofType:(NSString *)type fromQuery:(NSString *)query +{ + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + SearchResult *result = [self.searchDataSource objectAtIndex:indexPath.row]; + NSString *type = [NSStringFromClass([result.model class]) lowercaseString]; + [self selectedResult:result ofType:type fromQuery:self.textField.text]; +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + [self hideKeyboard]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARShowFeedViewController.h b/Artsy/Classes/View Controllers/ARShowFeedViewController.h new file mode 100644 index 00000000000..6ddb2428288 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARShowFeedViewController.h @@ -0,0 +1,18 @@ +#import "ARFeedViewController.h" + +@class ARHeroUnitsNetworkModel; + +/// The initial app view, show Hero Units and lists +/// upcoming shows. + +@interface ARShowFeedViewController : ARFeedViewController + +/// TODO: Cleanup this datasource business + +/// Allows the state restoration to set the hero units +@property (nonatomic, strong) ARHeroUnitsNetworkModel *heroUnitDatasource; + +@property (nonatomic, readonly, getter = isSHowingOfflineView) BOOL showingOfflineView; + +- (void)refreshFeedItems; +@end diff --git a/Artsy/Classes/View Controllers/ARShowFeedViewController.m b/Artsy/Classes/View Controllers/ARShowFeedViewController.m new file mode 100644 index 00000000000..b2aabbf23cf --- /dev/null +++ b/Artsy/Classes/View Controllers/ARShowFeedViewController.m @@ -0,0 +1,345 @@ +#import "ARShowFeedViewController.h" +#import "ARHeroUnitViewController.h" +#import "ARHeroUnitsNetworkModel.h" +#import "ARModernPartnerShowTableViewCell.h" +#import "ARPageSubTitleView.h" +#import "ARFeedLinkUnitViewController.h" +#import "ARFeaturedArtworksViewController.h" +#import "UIViewController+SimpleChildren.h" +#import "ARAppNotificationsDelegate.h" +#import "ArtsyAPI+Private.h" +#import "AROfflineView.h" + +#import +#import "ARKonamiKeyboardView.h" +#import +#import +#import "ARTile+ASCII.h" +#import +#import "ARAnalyticsConstants.h" +#import "ARTopTapThroughTableView.h" + + +static CGFloat ARShowFeedHeaderLabelMarginPad = 20; +static CGFloat ARShowFeedHeaderLabelHeightPad = 55; + +static CGFloat ARFeedLinksNavMarginPhone = 20; +static CGFloat ARFeaturedShowsTitleHeightPhone = 40; + +@interface ARShowFeedViewController() + +@property (nonatomic, strong) ARHeroUnitViewController *heroUnitVC; +@property (nonatomic, strong) ARFeedLinkUnitViewController *feedLinkVC; +@property (nonatomic, strong) UIView *pageTitle; +@property (nonatomic, strong) CALayer *separator; +@property (nonatomic, strong) UIView *featuredArtworksView; +@property (nonatomic, strong) ARFeaturedArtworksViewController *featuredArtworksVC; +@property (nonatomic, readonly) ARKonamiKeyboardView *konamiKeyboardView; +@property (nonatomic, strong, readwrite) UIView *headerView; + +@property (nonatomic, strong) AROfflineView *offlineView; +@property (nonatomic, readwrite, getter = isSHowingOfflineView) BOOL showingOfflineView; +@property (nonatomic, strong) id networkNotificationObserver; + +@end + +@implementation ARShowFeedViewController + +- (instancetype)initWithFeedTimeline:(ARFeedTimeline *)timeline +{ + self = [super initWithFeedTimeline:timeline]; + if (!self) { return nil; } + + _heroUnitVC = [[ARHeroUnitViewController alloc] init]; + + @weakify(self); + NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; + self.networkNotificationObserver = [defaultCenter addObserverForName:ARNetworkUnavailableNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + @strongify(self); + if (self.feedTimeline.numberOfItems == 0) { + // The offline view will be hidden when we load content. + [self showOfflineView]; + } + }]; + self.automaticallyAdjustsScrollViewInsets = NO; + return self; +} + +- (void)dealloc { + NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter removeObserver:self.networkNotificationObserver]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.heroUnitVC.view setNeedsLayout]; + [self.heroUnitVC.view layoutIfNeeded]; + [super viewWillAppear:animated]; + [self addKonami]; +} + +#pragma mark - Overridden Properties + +- (void)setShowingOfflineView:(BOOL)showingOfflineView { + // Force containing VC to re-evaluate the state of the menu button. + [self willChangeValueForKey:@keypath(self, hidesToolbarMenu)]; + _showingOfflineView = showingOfflineView; + [self didChangeValueForKey:@keypath(self, hidesToolbarMenu)]; +} + +#pragma mark - Private Methods + +- (void)showOfflineView { + self.showingOfflineView = YES; + + if (self.offlineView == nil) { + self.offlineView = [[AROfflineView alloc] initWithFrame:self.view.bounds]; + } + + [self.view addSubview:self.offlineView]; + + [self.offlineView alignCenterWithView:self.view]; + [self.offlineView constrainWidthToView:self.view predicate:@""]; + [self.offlineView constrainHeightToView:self.view predicate:@""]; +} + +- (void)hideOfflineView { + self.showingOfflineView = NO; + + [self.offlineView removeFromSuperview]; + + self.offlineView = nil; +} + +#pragma mark - ARMenuAwareViewController + +- (BOOL)hidesBackButton { + return self.navigationController.viewControllers.count <= 1; +} + +- (BOOL)hidesToolbarMenu { + return self.showingOfflineView == YES; +} +- (void)refreshFeedItems +{ + [ARAnalytics startTimingEvent:ARAnalyticsInitialFeedLoadTime]; + [self presentLoadingView]; + + @weakify(self) + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + [self.feedTimeline getNewItems:^{ + @strongify(self); + [self.tableView reloadData]; + [self hideLoadingView]; + [self hideOfflineView]; + [self loadNextFeedPage]; + [ARAnalytics finishTimingEvent:ARAnalyticsInitialFeedLoadTime]; + + } failure:^(NSError *error) { + ARErrorLog(@"There was an error getting newest items for the feed: %@", error.localizedDescription); + [self performSelector:@selector(refreshFeed) withObject:nil afterDelay:3]; + [ARAnalytics finishTimingEvent:ARAnalyticsInitialFeedLoadTime]; + }]; + }]; + // TODO: unify this across iPad/iPhone + if ([UIDevice isPad]) { + [ArtsyAPI getFeaturedWorks:^(NSArray *works) { + @strongify(self); + self.featuredArtworksVC.artworks = works; + } failure:^(NSError *error) { + ARErrorLog(@"Couldn't fetch featured artworks. Error: %@", error.localizedDescription); + }]; + } +} + +- (void)setHeroUnitDatasource:(ARHeroUnitsNetworkModel *)heroUnitDatasource +{ + self.heroUnitVC.heroUnitNetworkModel = heroUnitDatasource; +} + +- (NSDictionary *)tableViewCellIdentifiers +{ + return @{ + @"PartnerShowCellIdentifier" : [ARModernPartnerShowTableViewCell class] + }; +} + +- (Class)classForTableView +{ + return [ARTopTapThroughTableView class]; +} + +- (void)setupTableView +{ + [super setupTableView]; + + self.tableView.backgroundColor = [UIColor clearColor]; + self.tableView.restorationIdentifier = @"ARShowFeedTableViewRID"; + + [self.tableView layoutIfNeeded]; + + CGFloat sideMargin = [UIDevice isPad] ? 50 : 20; + + if ([UIDevice isPad]) { + ARSerifLabel *featuredArtworksLabel = [[ARSerifLabel alloc] init]; + [featuredArtworksLabel constrainHeight:@(ARShowFeedHeaderLabelHeightPad).stringValue]; + featuredArtworksLabel.font = [featuredArtworksLabel.font fontWithSize:24]; + featuredArtworksLabel.text = @"Featured Artworks"; + + self.featuredArtworksVC = [[ARFeaturedArtworksViewController alloc] init]; + + ARSerifLabel *featuredShowsLabel = [[ARSerifLabel alloc] init]; + [featuredShowsLabel constrainHeight:@(ARShowFeedHeaderLabelHeightPad).stringValue]; + featuredShowsLabel.font = [featuredShowsLabel.font fontWithSize:24]; + featuredShowsLabel.text = @"Featured Shows"; + + ARSeparatorView *artworksTitleSeparator = [[ARSeparatorView alloc] init]; + ARSeparatorView *showsTitleSeparator = [[ARSeparatorView alloc] init]; + + self.headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, [self heightForHeader])]; + self.headerView.backgroundColor = [UIColor whiteColor]; + + [self.headerView addSubview:featuredArtworksLabel]; + [featuredArtworksLabel alignTop:@(ARShowFeedHeaderLabelMarginPad).stringValue leading:@(sideMargin).stringValue + bottom:nil trailing:@(-sideMargin).stringValue toView:self.headerView]; + + [self.headerView addSubview:artworksTitleSeparator]; + [artworksTitleSeparator alignBottomEdgeWithView:featuredArtworksLabel predicate:nil]; + [artworksTitleSeparator alignLeading:@(sideMargin).stringValue trailing:@(-sideMargin).stringValue toView:self.headerView]; + + [self ar_addModernChildViewController:self.featuredArtworksVC intoView:self.headerView]; + [self.featuredArtworksVC.view alignLeading:@"0" trailing:@"0" toView:self.headerView]; + [self.featuredArtworksVC.view constrainTopSpaceToView:featuredArtworksLabel predicate:@(ARShowFeedHeaderLabelMarginPad).stringValue]; + + [self.headerView addSubview:featuredShowsLabel]; + [featuredShowsLabel constrainTopSpaceToView:self.featuredArtworksVC.view predicate:@(ARShowFeedHeaderLabelMarginPad).stringValue]; + [featuredShowsLabel alignLeading:@(sideMargin).stringValue trailing:@(-sideMargin).stringValue toView:self.headerView]; + [featuredShowsLabel alignBottomEdgeWithView:self.headerView predicate:@"0"]; + + [self.headerView addSubview:showsTitleSeparator]; + [showsTitleSeparator alignBottomEdgeWithView:featuredShowsLabel predicate:nil]; + [showsTitleSeparator alignLeading:@(sideMargin).stringValue trailing:@(-sideMargin).stringValue toView:self.headerView]; + + } else { + self.feedLinkVC = [[ARFeedLinkUnitViewController alloc] init]; + @weakify(self); + [ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) { + [self.feedLinkVC fetchLinks:^{ + @strongify(self); + if (![UIDevice isPad]) { [self layoutFeedLinks]; } + }]; + }]; + + ARPageSubTitleView *featuredShowsLabel = [[ARPageSubTitleView alloc] initWithTitle:@"Current Shows"]; + [featuredShowsLabel constrainHeight:@(ARFeaturedShowsTitleHeightPhone).stringValue]; + + self.headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, [self heightForHeader])]; + self.headerView.backgroundColor = [UIColor whiteColor]; + + [self ar_addModernChildViewController:self.feedLinkVC intoView:self.headerView]; + [self.feedLinkVC.view alignTop:@(ARFeedLinksNavMarginPhone).stringValue leading:@(sideMargin).stringValue bottom:nil trailing:@(-sideMargin).stringValue toView:self.headerView]; + + [self.headerView addSubview:featuredShowsLabel]; + [featuredShowsLabel constrainTopSpaceToView:self.feedLinkVC.view predicate:@"0"]; + [featuredShowsLabel alignLeading:@"0" trailing:@"0" toView:self.headerView]; + } + + [self.headerView layoutIfNeeded]; + self.tableView.tableHeaderView = self.headerView; +} + +- (void)layoutFeedLinks +{ + CGRect frame = self.headerView.frame; + frame.size.height = [self heightForHeader]; + self.headerView.frame = frame; + self.tableView.tableHeaderView = self.headerView; + [self.feedLinkVC.view setNeedsLayout]; + [self.feedLinkVC.view layoutIfNeeded]; +} + +- (CGFloat)heightForHeader +{ + CGFloat height; + if ([UIDevice isPad] ) { + CGFloat labelHeight = ARShowFeedHeaderLabelMarginPad + ARShowFeedHeaderLabelHeightPad; + CGFloat artworksHeight = ARShowFeedHeaderLabelMarginPad + self.featuredArtworksVC.preferredContentSize.height; + height = (2 * labelHeight) + artworksHeight; + } else { + height = ARFeedLinksNavMarginPhone + self.feedLinkVC.preferredContentSize.height + ARFeaturedShowsTitleHeightPhone; + } + return height; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.backgroundColor = [UIColor whiteColor]; + + [self ar_addModernChildViewController:self.heroUnitVC intoView:self.view belowSubview:self.tableView]; + [self.heroUnitVC.view alignLeading:@"0" trailing:@"0" toView:self.view]; + [self.heroUnitVC.view constrainTopSpaceToView:(UIView *)self.topLayoutGuide predicate:@"0"]; + UIEdgeInsets insets = self.tableView.contentInset; + insets.top = 20 + self.heroUnitVC.preferredContentSize.height; + self.tableView.contentInset = insets; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + ARAppNotificationsDelegate * delegate = (ARAppNotificationsDelegate *) [JSDecoupledAppDelegate sharedAppDelegate].remoteNotificationsDelegate; + [delegate registerForDeviceNotificationsOnce]; +} + +- (BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +- (BOOL)shouldAutomaticallyForwardAppearanceMethods +{ + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return [UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskPortrait; +} + +- (void)konami:(DRKonamiGestureRecognizer *)recognizer +{ + if ([recognizer konamiState] == DRKonamiGestureStateRecognized ) { + UIFont.ascii = ! UIFont.ascii; + UIImageView.ascii = ! UIImageView.ascii; + ARTile.ascii = ! ARTile.ascii; + [self.heroUnitVC fetchHeroUnits]; + [self.tableView reloadData]; + } +} + +- (void)addKonami +{ + DRKonamiGestureRecognizer *konamiGestureRecognizer = [[DRKonamiGestureRecognizer alloc] initWithTarget:self action:@selector(konami:)]; + _konamiKeyboardView = [[ARKonamiKeyboardView alloc] initWithKonamiGestureRecognizer:konamiGestureRecognizer]; + [konamiGestureRecognizer setKonamiDelegate:self]; + [konamiGestureRecognizer setRequiresABEnterToUnlock:YES]; + [self.view addGestureRecognizer:konamiGestureRecognizer]; +} + +#pragma mark - +#pragma mark DRKonamiGestureProtocol + +- (void)DRKonamiGestureRecognizerNeedsABEnterSequence:(DRKonamiGestureRecognizer*)gesture +{ + [self.view addSubview:self.konamiKeyboardView]; + [self.konamiKeyboardView becomeFirstResponder]; +} + +- (void)DRKonamiGestureRecognizer:(DRKonamiGestureRecognizer*)gesture didFinishNeedingABEnterSequenceWithError:(BOOL)error +{ + [self.konamiKeyboardView resignFirstResponder]; + [self.konamiKeyboardView removeFromSuperview]; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.h b/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.h new file mode 100644 index 00000000000..d42566c481e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.h @@ -0,0 +1,19 @@ +#import "AROnboardingViewController.h" +#import "ARTrialController.h" +#import "ARTermsAndConditionsView.h" + +@interface ARSignUpActiveUserViewController : UIViewController + +@property (nonatomic, weak) AROnboardingViewController *delegate; +@property (nonatomic, assign) ARTrialContext trialContext; + +@property (strong, nonatomic) IBOutlet UIView *topView; +@property (strong, nonatomic) IBOutlet UIView *bottomView; +@property (nonatomic, strong) IBOutlet ARSerifLineHeightLabel *bodyCopyLabel; + +@property (nonatomic, strong) IBOutlet NSLayoutConstraint *logoConstraint; +@property (nonatomic, strong) IBOutlet NSLayoutConstraint *textConstraint; +@property (nonatomic, strong) IBOutlet NSLayoutConstraint *buttonsConstraint; + +@property (nonatomic) BOOL shouldAnimate; +@end diff --git a/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.m b/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.m new file mode 100644 index 00000000000..82ee3db7bdd --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.m @@ -0,0 +1,175 @@ +#import "ARSignUpActiveUserViewController.h" +#import "AROnboardingNavBarView.h" + +@interface ARSignUpActiveUserViewController () +@property (nonatomic, strong) AROnboardingNavBarView *navView; +@property (nonatomic, strong) NSString *message; +@end + +@implementation ARSignUpActiveUserViewController + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (void)setTrialContext:(enum ARTrialContext)context +{ + _trialContext = context; + NSString *message = nil; + switch (context) { + case ARTrialContextFavoriteArtist: + message = @"Sign up for a free account\nto save artists and get\npersonal recommendations."; + break; + + case ARTrialContextFavoriteProfile: + message = @"Sign up for a free account\nto save partners and get\npersonal recommendations."; + break; + + case ARTrialContextFavoriteGene: + case ARTrialContextFavoriteArtwork: + message = @"Sign up for a free account\nto save works and get\npersonal recommendations."; + break; + + case ARTrialContextShowingFavorites: + message = @"Sign up for a free account to save works, artists and categories to your favorites."; + break; + + case ARTrialContextPeriodical: + message = @"Sign up for a free account to save works and artists and get personal recommendations."; + break; + + case ARTrialContextRepresentativeInquiry: + message = @"Sign up for a free account and an Artsy specialist will be glad to help you."; + break; + + case ARTrialContextContactGallery: + message = @"Sign up for a free account\nto inquire about works."; + break; + + case ARTrialContextAuctionBid: + message = @"Sign up for a free account\nto bid on works."; + break; + + case ARTrialContextArtworkOrder: + message = @"Sign up for a free account\nto buy works."; + break; + + case ARTrialContextFairGuide: + message = @"Sign up for a free account\nto get personal recommendations."; + break; + + case ARTrialContextNotTrial: + message = @""; + break; + } + self.message = message; +} + +- (void)viewDidLoad +{ + AROnboardingNavBarView *navView = [[AROnboardingNavBarView alloc] init]; + [self.view addSubview:navView]; + self.navView = navView; + + [navView.title setText:@""]; + + [navView.back setImage:[UIImage imageNamed:@"CloseButtonLarge"] forState:UIControlStateNormal]; + [navView.back setImage:[UIImage imageNamed:@"CloseButtonLargeHighlignted"] forState:UIControlStateHighlighted]; + [navView.back addTarget:self action:@selector(goBackToApp:) forControlEvents:UIControlEventTouchUpInside]; + + [navView.forward setTitle:@"LOG IN" forState:UIControlStateNormal]; + [navView.forward addTarget:self action:@selector(goToLogin:) forControlEvents:UIControlEventTouchUpInside]; + [navView.forward setEnabled:YES animated:NO]; + + self.bodyCopyLabel.font = [UIFont serifFontWithSize:22]; + self.bodyCopyLabel.backgroundColor = [UIColor clearColor]; + self.bodyCopyLabel.opaque = NO; + self.bodyCopyLabel.lineHeight = 6; + self.bodyCopyLabel.text = self.message; + + [super viewDidLoad]; +} + +- (BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +- (void)openTerms +{ + [self.delegate showTermsAndConditions]; +} + +- (void)openPrivacy +{ + [self.delegate showPrivacyPolicy]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + self.topView.alpha = 0; + self.bottomView.alpha = 0; + self.logoConstraint.constant = 8; + self.textConstraint.constant = -8; + self.buttonsConstraint.constant = 20; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + self.view.backgroundColor = [UIColor clearColor]; + + [self.topView layoutSubviews]; + [self.bottomView layoutSubviews]; + + [UIView animateIf:self.shouldAnimate duration:0.5 :^{ + self.topView.alpha = 1; + self.logoConstraint.constant = 0; + self.textConstraint.constant = 0; + [self.topView layoutSubviews]; + }]; + + [UIView animateIf:self.shouldAnimate duration:0.3 :^{ + self.bottomView.alpha = 1; + self.buttonsConstraint.constant = 0; + [self.bottomView layoutSubviews]; + }]; +} + +- (IBAction)connectWithFacebook:(id)sender +{ + [self.delegate signUpWithFacebook]; +} + +- (IBAction)connectWithTwitter:(id)sender +{ + [self.delegate signUpWithTwitter]; +} + +- (IBAction)signUpWithEmail:(id)sender +{ + [self.delegate signUpWithEmail]; +} + +- (void)goToLogin:(id)sender +{ + [self.delegate logInWithEmail:nil]; +} + +- (void)goBackToApp:(id)sender +{ + [UIView animateIf:YES duration:ARAnimationQuickDuration :^{ + self.topView.alpha = 0; + self.bottomView.alpha = 0; + }]; + [self.delegate dismissOnboardingWithVoidAnimation:NO]; + +} + +@end diff --git a/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.xib b/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.xib new file mode 100644 index 00000000000..1aa9e33ea15 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignUpActiveUserViewController.xib @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Artsy/Classes/View Controllers/ARSignUpSplashViewController.h b/Artsy/Classes/View Controllers/ARSignUpSplashViewController.h new file mode 100644 index 00000000000..6ab1b409e7e --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignUpSplashViewController.h @@ -0,0 +1,11 @@ +@class AROnboardingViewController; + +@interface ARSignUpSplashViewController : UIViewController + +@property (nonatomic, weak) AROnboardingViewController *delegate; + +@property (nonatomic, strong) UIImage *backgroundImage; + +@property (nonatomic, assign, readonly) NSInteger pageCount; + +@end diff --git a/Artsy/Classes/View Controllers/ARSignUpSplashViewController.m b/Artsy/Classes/View Controllers/ARSignUpSplashViewController.m new file mode 100644 index 00000000000..450abdc831d --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignUpSplashViewController.m @@ -0,0 +1,326 @@ +#import "ARSignUpSplashViewController.h" +#import "ARAppDelegate.h" +#import "ARCrossfadingImageView.h" +#import +#import "UIView+HitTestExpansion.h" + +@interface ARSignUpSplashTextViewController : UIViewController +@property (nonatomic, readwrite) NSInteger index; +@property (nonatomic, strong, readwrite) NSString *text; +- (instancetype)initWithText:(NSString *)text andIndex:(NSInteger)index; +@end + +@interface ARSignUpSplashViewController () + +@property (nonatomic) NSArray *pages; +@property (nonatomic) ARCrossfadingImageView *imageView; +@property (nonatomic) ARWhiteFlatButton *signUpButton; +@property (nonatomic) UIButton *logInButton; +@property (nonatomic) ARClearFlatButton *trialButton; +@property (nonatomic) UIPageViewController *pageViewController; +@property (nonatomic, strong, readwrite) UIPageControl *pageControl; +@end + +@implementation ARSignUpSplashViewController + +- (NSDictionary *)pageWithImageName:(NSString *)imageName bodyCopy:(NSString *)copy +{ + return @{ + @"image": [UIImage imageNamed:imageName], + @"copy" : copy + }; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _pages = @[ + [self pageWithImageName:@"onboard_1.jpg" + bodyCopy:@"Explore and collect\nover 160,000 works from\n2,500 leading galleries,\nart fairs, and museums."], + + [self pageWithImageName:@"onboard_2.jpg" + bodyCopy:@"Favorite artists and works\nto get alerts about\nnew shows and personal recommendations."], + + [self pageWithImageName:@"onboard_3.jpg" + bodyCopy:@"Collect works and\nconnect with galleries.\nNeed help or advice? Artsy's specialists can help."], + ]; + } + + + return self; +} + +- (void)loadView +{ + [super loadView]; + + self.imageView = [[ARCrossfadingImageView alloc] init]; + self.imageView.shouldLoopImages = YES; + [self.view addSubview: self.imageView]; + [self.imageView alignToView:self.view]; + self.imageView.userInteractionEnabled = YES; + + self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; + self.pageViewController.dataSource = self; + self.pageViewController.delegate = self; + [self addChildViewController:self.pageViewController]; + [self.view addSubview:self.pageViewController.view]; + [self.pageViewController didMoveToParentViewController:self]; + ARSignUpSplashTextViewController *initialVC = [self viewControllerForIndex:0]; + if (initialVC) { + self.pageControl.currentPage = 0; + NSArray *initialVCs = @[initialVC]; + [self.pageViewController setViewControllers:initialVCs direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; + } + + UIScrollView *scrollView = [self.pageViewController.view.subviews detect:^BOOL(id object) { + return [(UIView *)object isKindOfClass:[UIScrollView class]]; + }]; + scrollView.delegate = self; +} + +- (void)viewDidLoad +{ + NSString *imageName = NSStringWithFormat(@"full_logo_white_%@", [UIDevice isPad] ? @"large" : @"small"); + UIImageView *logo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imageName]]; + logo.contentMode = UIViewContentModeScaleAspectFit; + [self.view addSubview:logo]; + [logo alignCenterXWithView:self.view predicate:nil]; + [logo alignCenterYWithView:self.view predicate:[UIDevice isPad] ? @"-194" : @"-173"]; + + self.pageControl = [self pageControlForPaging]; + + [self.view addSubview:self.pageControl]; + [self.pageControl constrainTopSpaceToView:logo predicate:[UIDevice isPad] ? @"290" : @"160"]; + [self.pageControl alignCenterXWithView:self.view predicate:nil]; + + self.signUpButton = [[ARWhiteFlatButton alloc] init]; + [self.view addSubview:self.signUpButton]; + [self.signUpButton setTitle:@"SIGN UP" forState:UIControlStateNormal]; + [self.signUpButton addTarget:self action:@selector(signUp:) forControlEvents:UIControlEventTouchUpInside]; + [self.signUpButton constrainTopSpaceToView:self.pageControl predicate:@"29"]; + [self.signUpButton alignCenterXWithView:self.view predicate:nil]; + + self.trialButton = [[ARClearFlatButton alloc] init]; + [self.view addSubview:self.trialButton]; + [self.trialButton setTitle:@"TRY WITHOUT AN ACCOUNT" forState:UIControlStateNormal]; + [self.trialButton addTarget:self action:@selector(startTrial) forControlEvents:UIControlEventTouchUpInside]; + [self.trialButton constrainTopSpaceToView:self.signUpButton predicate:@"12"]; + [self.trialButton alignCenterXWithView:self.view predicate:nil]; + + self.logInButton = [[UIButton alloc] init]; + self.logInButton.titleLabel.font = [UIFont sansSerifFontWithSize:13]; + self.logInButton.titleLabel.textAlignment = NSTextAlignmentRight; + [self.logInButton addTarget:self action:@selector(logIn:) forControlEvents:UIControlEventTouchUpInside]; + [self.logInButton setTitle:@"LOG IN" forState:UIControlStateNormal]; + [self.view addSubview:self.logInButton]; + [self.logInButton ar_extendHitTestSizeByWidth:10 andHeight:10]; + [self.logInButton alignTrailingEdgeWithView:self.view predicate:[UIDevice isPad] ? @"-44" : @"-20"]; + [self.logInButton alignAttribute:NSLayoutAttributeCenterY toAttribute:NSLayoutAttributeBottom ofView:self.view predicate:[UIDevice isPad] ? @"-42" : @"-22"]; + + NSArray *images = [self.pages map:^id(NSDictionary *object) { + return [object objectForKey:@"image"]; + }]; + + self.imageView.images = images; + + [super viewDidLoad]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + self.view.alpha = 0; + [UIView animateWithDuration:ARAnimationDuration animations:^{ + self.view.alpha = 1; + }]; + + [super viewWillAppear:animated]; +} + +#pragma Property overrides + +- (void)setFormEnabled:(BOOL)enabled +{ + self.logInButton.enabled = enabled; + self.signUpButton.enabled = enabled; + self.trialButton.enabled = enabled; +} + +- (void)setBackgroundImage:(UIImage *)backgroundImage +{ + self.imageView.image = nil; +} + +- (UIImage *)backgroundImage +{ + return self.imageView.images[self.imageView.currentIndex]; +} + +#pragma mark - UIPageViewControllerDataSource + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(ARSignUpSplashTextViewController *)viewController +{ + if (self.pageCount <= 1) { return nil; } + + + NSInteger newIndex = viewController.index - 1; + if (newIndex < 0) { + newIndex = self.pageCount - 1; + } + + return [self viewControllerForIndex:newIndex]; +} + +- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(ARSignUpSplashTextViewController *)viewController +{ + if (self.pageCount <= 1) { return nil; } + + NSInteger newIndex = (viewController.index + 1) % self.pageCount; + return [self viewControllerForIndex:newIndex]; +} + +- (ARSignUpSplashTextViewController *)viewControllerForIndex:(NSInteger)index +{ + if (index < 0 || index >= self.pageCount) { return nil; } + + return [[ARSignUpSplashTextViewController alloc] initWithText:self.pages[index][@"copy"] andIndex:index]; +} + +#pragma mark - UIPageViewControllerDelegate + +- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed +{ + if (!completed) { return; } + NSInteger index = [self currentViewController].index; + [self.pageControl setCurrentPage:index]; + self.imageView.currentIndex = index; +} + +- (ARSignUpSplashTextViewController *)currentViewController +{ + return self.pageViewController.viewControllers.count > 0 ? self.pageViewController.viewControllers[0] : nil; +} + +#pragma mark UISCrollViewDelegate methods + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + CGFloat width = scrollView.frame.size.width; + CGFloat offset = scrollView.contentOffset.x; + CGFloat shiftFactor = fabsf(offset - width) / width; + + if (offset < width) { + [self.imageView down:shiftFactor]; + } else { + [self.imageView up:shiftFactor]; + } +} + +#pragma mark Actions + +- (IBAction)signUp:(id)sender +{ + [self.delegate splashDone:self]; +} + +- (IBAction)logIn:(id)sender +{ + [self.delegate splashDoneWithLogin:self]; +} + +- (void)startTrial +{ + [self setFormEnabled:NO animated:YES]; + + [ARTrialController startTrialWithCompletion:^{ + // Load normal app + [self.delegate dismissOnboardingWithVoidAnimation:YES]; + } + failure:^(NSError *error) { + [UIAlertView showWithTitle:@"Couldn’t Reach Artsy" + message:error.localizedDescription + cancelButtonTitle:@"Retry" + otherButtonTitles:nil + tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) { + [self performSelector:@selector(enableForm) withObject:nil]; + }]; + }]; +} + +- (void)enableForm +{ + [self setFormEnabled:YES animated:YES]; +} + +#pragma mark View setup + +- (void)setFormEnabled:(BOOL)enabled animated:(BOOL)animated +{ + [UIView animateIf:animated duration:0.15 :^{ + for (UIView *view in @[self.trialButton, self.logInButton, self.signUpButton]) { + view.userInteractionEnabled = enabled; + view.alpha = enabled ? 1 : 0.3; + } + }]; +} + +- (NSInteger)pageCount +{ + return self.pages.count; +} + +- (UIPageControl *)pageControlForPaging +{ + UIPageControl *control = [[UIPageControl alloc] init]; + control.pageIndicatorTintColor = [UIColor artsyMediumGrey]; + control.numberOfPages = self.pages.count; + return control; +} + +@end + +@implementation ARSignUpSplashTextViewController +- (instancetype)initWithText:(NSString *)text andIndex:(NSInteger)index +{ + self = [super init]; + if (!self) { return nil; } + _text = text; + _index = index; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + UILabel *copyLabel = [self labelForCopy]; + copyLabel.text = self.text; + + [self.view addSubview:copyLabel]; + [copyLabel constrainWidth:@"280" height:@"120"]; + [copyLabel alignCenterXWithView:self.view predicate:nil]; + [copyLabel alignCenterYWithView:self.view predicate:[UIDevice isPad] ? @"40" : @"-60"]; +} + +- (UILabel *)labelForCopy +{ + ARSerifLineHeightLabel *copyLabel = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:6]; + copyLabel.backgroundColor = [UIColor clearColor]; + copyLabel.opaque = NO; + copyLabel.font = [UIFont serifFontWithSize:24]; + copyLabel.textColor = [UIColor whiteColor]; + copyLabel.textAlignment = NSTextAlignmentCenter; + copyLabel.numberOfLines = 0; + copyLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + copyLabel.clipsToBounds = NO; + copyLabel.layer.shadowOpacity = 0.8; + copyLabel.layer.shadowRadius = 2.0; + copyLabel.layer.shadowOffset = CGSizeZero; + copyLabel.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.6].CGColor; + copyLabel.layer.shouldRasterize = YES; + return copyLabel; +} + +@end + diff --git a/Artsy/Classes/View Controllers/ARSignupViewController.h b/Artsy/Classes/View Controllers/ARSignupViewController.h new file mode 100644 index 00000000000..81d0c43edfb --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignupViewController.h @@ -0,0 +1,7 @@ +#import "AROnboardingViewController.h" + +@interface ARSignupViewController : UIViewController + +@property (nonatomic, weak) AROnboardingViewController *delegate; + +@end diff --git a/Artsy/Classes/View Controllers/ARSignupViewController.m b/Artsy/Classes/View Controllers/ARSignupViewController.m new file mode 100644 index 00000000000..94d707efd50 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSignupViewController.m @@ -0,0 +1,86 @@ +#import "ARSignupViewController.h" +#import "AROnboardingNavBarView.h" +#import "ARTermsAndConditionsView.h" + + +@interface ARSignupViewController () +@property (nonatomic) AROnboardingNavBarView *navbar; +@property (nonatomic) ARWhiteFlatButton *email; +@property (nonatomic) ARWhiteFlatButton *twitter; +@property (nonatomic) ARWhiteFlatButton *facebook; +@property (nonatomic) UIImageView *backgroundView; +@end + +@implementation ARSignupViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.navbar = [[AROnboardingNavBarView alloc] init]; + [self.view addSubview:self.navbar]; + [self.navbar.title setText:@"Sign Up"]; + [self.navbar.back addTarget:self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; + + self.facebook = [[ARWhiteFlatButton alloc] init]; + [self.facebook setTitle:@"Connect with Facebook" forState:UIControlStateNormal]; + [self.facebook addTarget:self action:@selector(fb:) forControlEvents:UIControlEventTouchUpInside]; + + self.twitter = [[ARWhiteFlatButton alloc] init]; + [self.twitter setTitle:@"Connect with Twitter" forState:UIControlStateNormal]; + [self.twitter addTarget:self action:@selector(twitter:) forControlEvents:UIControlEventTouchUpInside]; + + self.email = [[ARWhiteFlatButton alloc] init]; + [self.email setTitle:@"Sign up with email" forState:UIControlStateNormal]; + [self.email addTarget:self action:@selector(email:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.email]; + + ARTermsAndConditionsView *label = [[ARTermsAndConditionsView alloc] init]; + [label constrainWidth:@"280"]; + + [@[self.facebook, self.twitter, self.email, label] each:^(UIView *view) { + [self.view addSubview:view]; + [view alignCenterXWithView:self.view predicate:nil]; + }]; + + [self.twitter constrainTopSpaceToView:self.facebook predicate:@"10"]; + [self.email constrainTopSpaceToView:self.twitter predicate:@"10"]; + [label constrainTopSpaceToView:self.email predicate:@"10"]; + + if ([UIDevice isPad]) { + [self.twitter alignCenterYWithView:self.view predicate:@"0"]; + } else { + [label alignBottomEdgeWithView:self.view predicate:@"<=-56"]; + } +} + +- (void)openTerms +{ + [self.delegate showTermsAndConditions]; +} + +- (void)openPrivacy +{ + [self.delegate showPrivacyPolicy]; +} + +- (void)fb:(id)sender +{ + [self.delegate signUpWithFacebook]; +} + +- (void)twitter:(id)sender +{ + [self.delegate signUpWithTwitter]; +} + +- (void)email:(id)sender +{ + [self.delegate signUpWithEmail]; +} + +- (void)back:(id)sender +{ + [self.delegate popViewControllerAnimated:YES]; +} +@end diff --git a/Artsy/Classes/View Controllers/ARSlideshowView.h b/Artsy/Classes/View Controllers/ARSlideshowView.h new file mode 100644 index 00000000000..0b2ea108bde --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSlideshowView.h @@ -0,0 +1,14 @@ +@interface ARSlideshowView : UIView + +/// Create a new slideshow view with an ordered collection of UIImages +- (instancetype)initWithSlides:(NSArray *)slides; + +/// Returns `YES` if there are slides left to show +- (BOOL)hasNext; + +/// Show the next slide, if any +- (void)next; + +@property (nonatomic, assign, readonly) NSInteger index; + +@end diff --git a/Artsy/Classes/View Controllers/ARSlideshowView.m b/Artsy/Classes/View Controllers/ARSlideshowView.m new file mode 100644 index 00000000000..f95cc48d35c --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSlideshowView.m @@ -0,0 +1,50 @@ +#import "ARSlideshowView.h" + +#define TRANSITION_DURATION .15 + +@interface ARSlideshowView () +@property (nonatomic, readonly, copy) NSArray *slides; +@property (nonatomic, assign) NSInteger index; +@property (nonatomic) UIImageView *imageView; +@end + +@implementation ARSlideshowView + +- (instancetype)initWithSlides:(NSArray *)slides +{ + self = [super init]; + if (self) { + _slides = slides; + _index = 0; + _imageView = [[UIImageView alloc] init]; + [self addSubview:_imageView]; + _imageView.image = _slides[0]; + _imageView.contentMode = UIViewContentModeScaleAspectFill; + } + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.imageView.frame = self.bounds; +} + +- (BOOL)hasNext +{ + return (self.index < (self.slides.count - 1)); +} + +- (void)next +{ + if (!self.hasNext) { + return; + } + + self.index++; + + UIImage *nextImage = self.slides[self.index]; + self.imageView.image = nextImage; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARSlideshowViewController.h b/Artsy/Classes/View Controllers/ARSlideshowViewController.h new file mode 100644 index 00000000000..8fe8279b096 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSlideshowViewController.h @@ -0,0 +1,7 @@ +#import +#import "AROnboardingViewController.h" + +@interface ARSlideshowViewController : UIViewController +- (instancetype)initWithSlides:(NSArray *)slides; +@property (nonatomic, weak) AROnboardingViewController *delegate; +@end diff --git a/Artsy/Classes/View Controllers/ARSlideshowViewController.m b/Artsy/Classes/View Controllers/ARSlideshowViewController.m new file mode 100644 index 00000000000..175f5f17ed8 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSlideshowViewController.m @@ -0,0 +1,75 @@ +#import "ARSlideshowViewController.h" +#import "ARSlideshowView.h" + +@interface ARSlideshowViewController () +@property (nonatomic, readonly) NSArray *slides; +@property (nonatomic) ARSlideshowView *view; +@property (nonatomic, assign, readonly) BOOL isDeveloper; +@end + +@implementation ARSlideshowViewController + +- (instancetype)initWithSlides:(NSArray *)slides +{ + self = [super init]; + if (!self) { return nil; } + + _slides = slides; + _isDeveloper = [[ARDeveloperOptions options] isDeveloper]; + + return self; +} + +- (void)loadView +{ + self.view = [[ARSlideshowView alloc] initWithSlides:self.slides]; + +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + NSString *imageName = NSStringWithFormat(@"full_logo_white_%@", [UIDevice isPad] ? @"large" : @"small"); + UIImageView *logo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imageName]]; + logo.contentMode = UIViewContentModeScaleAspectFit; + [self.view addSubview:logo]; + [logo alignCenterWithView:self.view]; + + [self runSlideshow]; +} + +- (void)runSlideshow +{ + // devs get it fast + if (self.isDeveloper) { + [self performSelector:@selector(nextSlide) withObject:nil afterDelay:0.1]; + + } else { + [self performSelector:@selector(nextSlide) withObject:nil afterDelay:0.6]; + } + +} + +- (void)nextSlide +{ + if (![self.view hasNext]) { + [self endSlideshow]; + return; + } + + [self.view next]; + + NSInteger currentSlide = self.view.index + 1; + NSInteger slidesLeft = self.slides.count - currentSlide; + CGFloat delay = 0.6 + (0.1 * slidesLeft); + + delay = self.isDeveloper ? 0.1 : delay; + [self performSelector:@selector(nextSlide) withObject:nil afterDelay:delay]; +} + +- (void)endSlideshow +{ + [self.delegate slideshowDone]; +} +@end diff --git a/Artsy/Classes/View Controllers/ARSwitchCell.xib b/Artsy/Classes/View Controllers/ARSwitchCell.xib new file mode 100644 index 00000000000..6f8902c7f62 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARSwitchCell.xib @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Artsy/Classes/View Controllers/ARTermsAndConditionsView.h b/Artsy/Classes/View Controllers/ARTermsAndConditionsView.h new file mode 100644 index 00000000000..63e71583251 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTermsAndConditionsView.h @@ -0,0 +1,5 @@ +#import "ARTextView.h" + +@interface ARTermsAndConditionsView : ARTextView + +@end diff --git a/Artsy/Classes/View Controllers/ARTermsAndConditionsView.m b/Artsy/Classes/View Controllers/ARTermsAndConditionsView.m new file mode 100644 index 00000000000..3b48cab7422 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTermsAndConditionsView.m @@ -0,0 +1,72 @@ +#import "ARTermsAndConditionsView.h" + +@implementation ARTermsAndConditionsView + + +- (void)awakeFromNib +{ + [super awakeFromNib]; + [self setup]; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + [self setup]; + return self; +} + +- (void)setup +{ + self.tintColor = [UIColor artsyMediumGrey]; + self.editable = NO; + self.scrollEnabled = NO; + NSString *string = @"By creating your Artsy account you agree\nto our Terms of Use and Privacy Policy."; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string + attributes: @{ + NSFontAttributeName: [UIFont serifFontWithSize:14], + NSForegroundColorAttributeName: [UIColor artsyMediumGrey] + }]; + + NSRange termsRange = [attributedString.string rangeOfString:@"Terms of Use"]; + NSRange privacyRange = [attributedString.string rangeOfString:@"Privacy Policy"]; + [attributedString beginEditing]; + [attributedString addAttribute:NSLinkAttributeName + value:[NSURL URLWithString:@"/terms"] + range:termsRange]; + + [attributedString addAttribute:NSUnderlineStyleAttributeName + value:[NSNumber numberWithInt:NSUnderlineStyleSingle] + range:termsRange]; + + [attributedString addAttribute:NSUnderlineStyleAttributeName + value:@(NSUnderlineStyleSingle) + range:privacyRange]; + + [attributedString addAttribute:NSLinkAttributeName + value:[NSURL URLWithString:@"/privacy"] + range:privacyRange]; + [attributedString endEditing]; + + [self setAttributedText:attributedString]; + + self.textAlignment = NSTextAlignmentCenter; + self.delegate = self; + self.backgroundColor = [UIColor clearColor]; +} + +- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange +{ + NSString *path = [[URL.absoluteString componentsSeparatedByString:@"/"] lastObject]; + if ([path isEqualToString:@"terms"]) { + [[UIApplication sharedApplication] sendAction:@selector(openTerms) to:nil from:self forEvent:nil]; + + } else if ([path isEqualToString:@"privacy"]) { + [[UIApplication sharedApplication] sendAction:@selector(openPrivacy) to:nil from:self forEvent:nil]; + } + + return NO; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARTextFieldWithPlaceholder.h b/Artsy/Classes/View Controllers/ARTextFieldWithPlaceholder.h new file mode 100644 index 00000000000..28fcc98e2c2 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTextFieldWithPlaceholder.h @@ -0,0 +1,5 @@ +#import + +@interface ARTextFieldWithPlaceholder : UITextField + +@end diff --git a/Artsy/Classes/View Controllers/ARTextFieldWithPlaceholder.m b/Artsy/Classes/View Controllers/ARTextFieldWithPlaceholder.m new file mode 100644 index 00000000000..a4a758a42f2 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTextFieldWithPlaceholder.m @@ -0,0 +1,83 @@ +#import "ARTextFieldWithPlaceholder.h" + +#define CLEAR_BUTTON_TAG 0xbada55 + +@interface ARTextFieldWithPlaceholder() +@property (nonatomic, assign) BOOL swizzledClear; +@property (nonatomic, strong) CALayer *baseline; +@end + +@implementation ARTextFieldWithPlaceholder + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + [self setup]; + } + return self; +} + +- (void)awakeFromNib +{ + [self setup]; +} + +- (void)setup +{ + self.backgroundColor = [UIColor clearColor]; + self.font = [UIFont serifFontWithSize:20]; + self.textColor = [UIColor whiteColor]; + + self.baseline = [CALayer layer]; + self.baseline.backgroundColor = [UIColor artsyLightGrey].CGColor; + [self.layer addSublayer:self.baseline]; + + self.clearButtonMode = UITextFieldViewModeWhileEditing; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : [UIColor artsyHeavyGrey ]}]; +} + +- (void)addSubview:(UIView *)view +{ + [super addSubview:view]; + + if (!self.swizzledClear && [view class] == [UIButton class]) { + UIView *subview = (UIView *)view.subviews.first; + if ([subview class] == [UIImageView class]) { + [self swizzleClearButton:(UIButton *)view]; + } + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + UIButton *button = (UIButton *)[self viewWithTag:CLEAR_BUTTON_TAG]; + if (button) { + button.center = CGPointMake(button.center.x, button.center.y - 3); + } + self.baseline.frame = CGRectMake(0, self.frame.size.height - 1, self.frame.size.width, 1); +} + +- (void)swizzleClearButton:(UIButton *)button +{ + UIImageView *imageView = (UIImageView *)button.subviews.first; + UIImage *image = [imageView image]; + UIImage *templated = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [button setImage:templated forState:UIControlStateNormal]; + [button setImage:templated forState:UIControlStateHighlighted]; + [button setTintColor:[UIColor whiteColor]]; + self.swizzledClear = YES; + button.tag = CLEAR_BUTTON_TAG; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, 48); +} + +@end diff --git a/Artsy/Classes/View Controllers/ARTextInputCellWithTitle.xib b/Artsy/Classes/View Controllers/ARTextInputCellWithTitle.xib new file mode 100644 index 00000000000..a43b81b0ea8 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTextInputCellWithTitle.xib @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Artsy/Classes/View Controllers/ARTopMenuNavigationDataSource.h b/Artsy/Classes/View Controllers/ARTopMenuNavigationDataSource.h new file mode 100644 index 00000000000..18454d651c3 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTopMenuNavigationDataSource.h @@ -0,0 +1,18 @@ +#import "ARTabContentView.h" + +@class ARShowFeedViewController, ARNavigationController; + +NS_ENUM(NSInteger, ARTopTabControllerIndex){ + ARTopTabControllerIndexSearch, + ARTopTabControllerIndexFeed, + ARTopTabControllerIndexBrowse, + ARTopTabControllerIndexFavorites +}; + +@interface ARTopMenuNavigationDataSource : NSObject + +- (ARNavigationController *)currentNavigationController; + +@property (readwrite, nonatomic, strong) ARShowFeedViewController *showFeedViewController; + +@end diff --git a/Artsy/Classes/View Controllers/ARTopMenuNavigationDataSource.m b/Artsy/Classes/View Controllers/ARTopMenuNavigationDataSource.m new file mode 100644 index 00000000000..7992ebe67e1 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTopMenuNavigationDataSource.m @@ -0,0 +1,97 @@ +#import "ARTopMenuNavigationDataSource.h" +#import "ARShowFeedViewController.h" +#import "ARBrowseViewController.h" +#import "ARFavoritesViewController.h" +#import "ARAppSearchViewController.h" +#import "ARHeroUnitsNetworkModel.h" + +@interface ARTopMenuNavigationDataSource() + +@property (nonatomic, assign, readwrite) NSInteger currentIndex; +@property (nonatomic, strong, readonly) NSArray *navigationControllers; + +@property (readwrite, nonatomic, strong) ARNavigationController *feedNavigationController; +@property (readwrite, nonatomic, strong) ARNavigationController *favoritesNavigationController; +@property (readwrite, nonatomic, strong) ARNavigationController *browseNavigationController; +@property (readwrite, nonatomic, strong) ARNavigationController *searchNavigationController; + +@end + +@implementation ARTopMenuNavigationDataSource + +- (ARNavigationController *)navigationControllerForSearch +{ + if (self.searchNavigationController) { return self.searchNavigationController; } + + ARSearchViewController *searchController = [[ARAppSearchViewController alloc] init]; + _searchNavigationController = [[ARNavigationController alloc] initWithRootViewController: searchController]; + return self.searchNavigationController; +} + +- (ARNavigationController *)navigationControllerForFeed +{ + if (self.feedNavigationController) { return self.feedNavigationController; } + + ARShowFeed *showFeed = [[ARShowFeed alloc] init]; + ARFeedTimeline *showFeedTimeline = [[ARFeedTimeline alloc] initWithFeed:showFeed]; + + _showFeedViewController = [[ARShowFeedViewController alloc] initWithFeedTimeline:showFeedTimeline]; + self.showFeedViewController.heroUnitDatasource = [[ARHeroUnitsNetworkModel alloc] init]; + + _feedNavigationController = [[ARNavigationController alloc] initWithRootViewController: _showFeedViewController]; + return self.feedNavigationController; +} + +- (ARNavigationController *)navigationControllerForBrowse +{ + if (self.browseNavigationController) { return self.browseNavigationController; } + + ARBrowseViewController *browseViewController = [[ARBrowseViewController alloc] init]; + _browseNavigationController = [[ARNavigationController alloc] initWithRootViewController:browseViewController]; + return self.browseNavigationController; +} + +- (ARNavigationController *)navigationControllerForFavorites +{ + ARFavoritesViewController *favoritesViewController = [[ARFavoritesViewController alloc] init]; + _favoritesNavigationController = [[ARNavigationController alloc] initWithRootViewController:favoritesViewController]; + return self.favoritesNavigationController; +} + + +- (ARNavigationController *)currentNavigationController +{ + return (id)[self viewControllerForTabContentView:nil atIndex:self.currentIndex]; +} + +#pragma mark ARTabViewDataSource + +- (UINavigationController *)viewControllerForTabContentView:(ARTabContentView *)tabContentView atIndex:(NSInteger)index +{ + _currentIndex = index; + + switch (index){ + case ARTopTabControllerIndexSearch: + return [self navigationControllerForSearch]; + case ARTopTabControllerIndexBrowse: + return [self navigationControllerForBrowse]; + case ARTopTabControllerIndexFavorites: + return [self navigationControllerForFavorites]; + case ARTopTabControllerIndexFeed: + return [self navigationControllerForFeed]; + } + + return nil; +} + +- (BOOL)tabContentView:(ARTabContentView *)tabContentView canPresentViewControllerAtIndex:(NSInteger)index +{ + return YES; +} + +- (NSInteger)numberOfViewControllersForTabContentView:(ARTabContentView *)tabContentView +{ + return 4; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.h b/Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.h new file mode 100644 index 00000000000..5a95f714658 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.h @@ -0,0 +1,7 @@ +#import "ARTopMenuViewController.h" + +@interface ARTopMenuViewController (DeveloperExtras) + +- (void)runDeveloperExtras; + +@end diff --git a/Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.m b/Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.m new file mode 100644 index 00000000000..494c1ba77ff --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.m @@ -0,0 +1,17 @@ +#import "ARTopMenuViewController+DeveloperExtras.h" + +// You can tell git to ignore changes to this file by running +// +// git update-index --assume-unchanged Artsy/Classes/View\ Controllers/ARTopMenuViewController+DeveloperExtras.m + +@implementation ARTopMenuViewController (DeveloperExtras) + +// Use this function to run code once the app is loaded, useful for pushing a +// specific VC etc. + +- (void)runDeveloperExtras +{ + +} + +@end diff --git a/Artsy/Classes/View Controllers/ARTopMenuViewController.h b/Artsy/Classes/View Controllers/ARTopMenuViewController.h new file mode 100644 index 00000000000..190e18bca6a --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTopMenuViewController.h @@ -0,0 +1,41 @@ +/** Is the App's Root View Controller. + + The Top MenuVC is a member of the ARNavigationContainer protocol, this means it supports + the standard way of pushing new view controllers into a stack using the ARSwitchBoard API. + + It currently handles the status bar API, and the Menu / Back button. +*/ + +#import "ARMenuAwareViewController.h" +#import "ARNavigationContainer.h" +#import "ARNavigationController.h" + +@class ARTabContentView; +@interface ARTopMenuViewController : UIViewController + +/// The main interface of the app ++ (ARTopMenuViewController *)sharedController; + +/// The current navigation controller for the app from inside the tab controller +@property (readonly, nonatomic, strong) ARNavigationController *rootNavigationController; + +/// The content view for the tabbed nav +@property (readonly, nonatomic, weak) ARTabContentView *tabContentView; + +/// Pushes the view controller into the current navigation controller. +/// Using this method makes it easier to change the navigation systems +- (void)pushViewController:(UIViewController *)viewController; + +/// Same as above but with the option to animate +- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; + +/// Hides the toolbar +- (void)hideToolbar:(BOOL)hideToolbar animated:(BOOL)animated; + +/// Removes the Onboarding overlay +- (void)moveToInAppAnimated:(BOOL)animated; + +/// Used in search to exit out of search and back into a previous tab. +- (void)returnToPreviousTab; + +@end diff --git a/Artsy/Classes/View Controllers/ARTopMenuViewController.m b/Artsy/Classes/View Controllers/ARTopMenuViewController.m new file mode 100644 index 00000000000..3eb8feffafe --- /dev/null +++ b/Artsy/Classes/View Controllers/ARTopMenuViewController.m @@ -0,0 +1,276 @@ +#import "ARTopMenuViewController+DeveloperExtras.h" +#import "ARContentViewControllers.h" + +#import "UIViewController+FullScreenLoading.h" +#import "ARTabContentView.h" +#import "ARTopMenuNavigationDataSource.h" +#import "ARSearchViewController.h" + +@interface ARTopMenuViewController () +@property (readwrite, nonatomic, strong) NSArray *constraintsForButtons; + +@property (readwrite, nonatomic, assign) BOOL hidesToolbarMenu; + +@property (readwrite, nonatomic, assign) enum ARTopTabControllerIndex selectedTabIndex; +@property (readwrite, nonatomic, strong) NSLayoutConstraint *tabHeightConstraint; + +@property (readwrite, nonatomic, strong) ARTopMenuNavigationDataSource *navigationDataSource; +@property (readonly, nonatomic, strong) UIView *tabContainer; +@property (readonly, nonatomic, strong) CALayer *divider; +@end + +static const CGFloat ARSearchMenuButtonDimension = 46; + +@implementation ARTopMenuViewController + ++ (ARTopMenuViewController *)sharedController +{ + static ARTopMenuViewController *_sharedManager = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedManager = [[self alloc] init]; + }); + return _sharedManager; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor blackColor]; + _selectedTabIndex = -1; + + _navigationDataSource = _navigationDataSource ?: [[ARTopMenuNavigationDataSource alloc] init]; + + UIView *tabContainer = [[UIView alloc] init]; + _tabContainer = tabContainer; + + ARNavigationTabButton *searchButton = [[ARNavigationTabButton alloc] init]; + ARNavigationTabButton *homeButton = [[ARNavigationTabButton alloc] init]; + ARNavigationTabButton *favoritesButton = [[ARNavigationTabButton alloc] init]; + ARNavigationTabButton *browseButton = [[ARNavigationTabButton alloc] init]; + + [searchButton setImage:[UIImage imageNamed:@"SearchIcon_HeavyGrey"] forState:UIControlStateNormal]; + [searchButton setImage:[UIImage imageNamed:@"SearchIcon_White"] forState:UIControlStateSelected]; + [searchButton.imageView constrainWidth:@"16" height:@"16"]; + searchButton.adjustsImageWhenHighlighted = NO; + + [homeButton setTitle:@"HOME" forState:UIControlStateNormal]; + [browseButton setTitle:@"BROWSE" forState:UIControlStateNormal]; + [favoritesButton setTitle:@"YOU" forState:UIControlStateNormal]; + + NSArray *buttons = @[searchButton, homeButton, browseButton, favoritesButton]; + + ARTabContentView *tabContentView = [[ARTabContentView alloc] initWithFrame:CGRectZero hostViewController:self delegate:self dataSource:self.navigationDataSource]; + tabContentView.supportSwipeGestures = NO; + tabContentView.buttons = buttons; + [tabContentView setCurrentViewIndex:ARTopTabControllerIndexFeed animated:NO]; + _tabContentView = tabContentView; + + // Layout + [self.view addSubview:tabContentView]; + [tabContentView alignTop:@"0" leading:@"0" bottom:nil trailing:@"0" toView:self.view]; + [tabContentView constrainWidthToView:self.view predicate:@"0"]; + + [self.view addSubview: tabContainer]; + [tabContainer constrainHeight:@(ARSearchMenuButtonDimension).stringValue]; + [tabContainer constrainTopSpaceToView:tabContentView predicate:nil]; + [tabContainer alignLeading:@"0" trailing:@"0" toView:self.view]; + + [tabContainer addSubview:searchButton]; + [tabContainer addSubview:homeButton]; + [tabContainer addSubview:browseButton]; + [tabContainer addSubview:favoritesButton]; + + [searchButton constrainWidth:@(ARSearchMenuButtonDimension).stringValue]; + NSMutableArray *constraintsForButtons = [NSMutableArray array]; + [buttons eachWithIndex:^(UIButton *button, NSUInteger index){ + [button alignCenterYWithView:tabContainer predicate:nil]; + [button constrainHeightToView:tabContainer predicate:nil]; + if (index == 0) { + [button alignLeadingEdgeWithView:tabContainer predicate:nil]; + } else { + [constraintsForButtons addObject:[[button constrainLeadingSpaceToView:buttons[index - 1] predicate:nil] lastObject] ]; + } + if (index == buttons.count - 1) { + [constraintsForButtons addObject:[[tabContainer alignTrailingEdgeWithView:button predicate:nil] lastObject]]; + } + }]; + + self.constraintsForButtons = [constraintsForButtons copy]; + + CALayer *divider = [[CALayer alloc] init]; + divider.backgroundColor = [UIColor artsyHeavyGrey].CGColor; + divider.opacity = 0.5; + [tabContainer.layer addSublayer:divider]; + _divider = divider; + + self.tabHeightConstraint = [[tabContainer alignBottomEdgeWithView:self.view predicate:@"0"] firstObject]; +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.view layoutSubviews]; +} + +- (void)viewWillLayoutSubviews +{ + NSArray *buttons = self.tabContentView.buttons; + __block CGFloat buttonsWidth = ARSearchMenuButtonDimension; + [buttons eachWithIndex:^(UIButton *button, NSUInteger index){ + if (index == 0){ return; } + buttonsWidth += button.intrinsicContentSize.width; + }]; + + BOOL isPortrait = UIInterfaceOrientationIsPortrait([UIApplication sharedApplication].statusBarOrientation); + + CGFloat viewWidth = isPortrait ? self.view.frame.size.width : self.view.frame.size.height; + CGFloat extraWidth = viewWidth - buttonsWidth; + CGFloat eachMargin = floorf(extraWidth / (self.tabContentView.buttons.count - 1)); + + [self.constraintsForButtons eachWithIndex:^(NSLayoutConstraint *constraint, NSUInteger index) { + CGFloat margin = eachMargin; + if (index == 0 || index == self.constraintsForButtons.count - 1){ margin /= 2; } + constraint.constant = margin; + }]; +} + +- (ARNavigationController *)rootNavigationController +{ + return self.navigationDataSource.currentNavigationController; +} + +#pragma mark - ARMenuAwareViewController + +- (void)hideToolbar:(BOOL)hideToolbar animated:(BOOL)animated +{ + BOOL isCurrentlyHiding = (self.tabHeightConstraint.constant != 0); + if (isCurrentlyHiding == hideToolbar) { return; } + + [UIView animateIf:animated duration:ARAnimationQuickDuration :^{ + self.tabHeightConstraint.constant = hideToolbar ? self.tabContainer.frame.size.height : 0; + + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + }]; +} + +- (BOOL)hidesBackButton +{ + return YES; +} + +#pragma mark - UIViewController + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + +#ifdef DEBUG + [self runDeveloperExtras]; +#endif +} + +- (void)viewDidLayoutSubviews +{ + CGFloat tabHeight = self.tabContainer.frame.size.height; + self.divider.frame = CGRectMake(tabHeight, tabHeight * .25, 1, tabHeight * .5); +} + +- (void)moveToInAppAnimated:(BOOL)animated +{ + if (self.presentedViewController) { + self.presentedViewController.transitioningDelegate = self; + [self.presentedViewController dismissViewControllerAnimated:animated completion:^{ + if ([User currentUser]) { + [ARTrialController performPostSignupEvent]; + } + }]; + } +} + +#pragma mark - Pushing VCs + +- (void)loadFeed +{ + +} + +- (void)pushViewController:(UIViewController *)viewController +{ + [self pushViewController:viewController animated:YES]; +} + +- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + NSAssert(viewController != nil, @"Attempt to push a nil view controller. "); + [self.rootNavigationController pushViewController:viewController animated:animated]; +} + +#pragma mark - Auto Rotation + +// Let the nav decide what rotations to support + +-(BOOL)shouldAutorotate +{ + return [self.rootNavigationController shouldAutorotate]; +} + +-(NSUInteger)supportedInterfaceOrientations +{ + return self.rootNavigationController.supportedInterfaceOrientations ?: ([UIDevice isPad] ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskPortrait); +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation +{ + return self.rootNavigationController.preferredInterfaceOrientationForPresentation ?: UIInterfaceOrientationPortrait; +} + +#pragma mark Spinners + +- (void)startLoading +{ + ARTopMenuViewController *topMenuViewController = [ARTopMenuViewController sharedController]; + [topMenuViewController ar_presentIndeterminateLoadingIndicatorAnimated:YES]; +} + +- (void)stopLoading +{ + ARTopMenuViewController *topMenuViewController = [ARTopMenuViewController sharedController]; + [topMenuViewController ar_removeIndeterminateLoadingIndicatorAnimated:YES]; +} + +- (void)returnToPreviousTab +{ + [self.tabContentView returnToPreviousViewIndex]; +} + +- (void)tabContentView:(ARTabContentView *)tabContentView didChangeSelectedIndex:(NSInteger)index +{ + _selectedTabIndex = index; + + if (index == ARTopTabControllerIndexSearch) { + ARNavigationController *controller = (id)[tabContentView currentNavigationController]; + + [controller popToRootViewControllerAnimated:NO]; + ARSearchViewController *searchViewController = (id)controller.topViewController; + [searchViewController clearSearch]; + } +} + +- (BOOL)tabContentView:(ARTabContentView *)tabContentView shouldChangeToIndex:(NSInteger)index +{ + if (index == _selectedTabIndex) { + ARNavigationController *controller = (id)[tabContentView currentNavigationController]; + if (index == ARTopTabControllerIndexSearch && controller.viewControllers.count == 1) { + [tabContentView returnToPreviousViewIndex]; + } else { + [controller popToRootViewControllerAnimated:YES]; + } + return NO; + } + + return YES; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARViewInRoomViewController.h b/Artsy/Classes/View Controllers/ARViewInRoomViewController.h new file mode 100644 index 00000000000..57240745c56 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARViewInRoomViewController.h @@ -0,0 +1,28 @@ +/// Has two main states, a decorated version with parallax & the dude, +/// and a simple VIR. The simple VIR has a back button. + +/// Has a corrosponding push animation: ARViewInRoomTransition which handles +/// switching to and from both versions. + +@interface ARViewInRoomViewController : UIViewController + ++ (UIImageView *)imageViewForFramedArtwork; + +/// Init with the artwork for the metadata +- (ARViewInRoomViewController *)initWithArtwork:(Artwork *)artwork; + +@property (nonatomic, strong, readonly) Artwork *artwork; + +@property (nonatomic, assign) BOOL popOnRotation; +@property (nonatomic, weak) UIViewController *rotationDelegate; + +/// The artwork imageview comes *from* the transition +@property (nonatomic, strong) UIImageView *artworkImageView; + +/// Returns the CGRect where the artwork will sit scaled ++ (CGRect)rectForImageViewWithArtwork:(Artwork *)artwork withContainerFrame:(CGRect)containerFrame; + +/// Removes extra walls, benches etc +- (void)hideDecorationViews; + +@end diff --git a/Artsy/Classes/View Controllers/ARViewInRoomViewController.m b/Artsy/Classes/View Controllers/ARViewInRoomViewController.m new file mode 100644 index 00000000000..a9381314ee4 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARViewInRoomViewController.m @@ -0,0 +1,462 @@ +#import "ARViewInRoomViewController.h" +#import "ARFeedImageLoader.h" + +#define DEBUG_VIEW_IN_ROOM 0 + +static const CGFloat InitialWidthOfBenchInches = 96; +static const CGFloat InitialWidthOfBenchPX = 220; +static const CGFloat InitialWidthOfBenchPXLandscape = 150; + +// The minimum distance is the closest point the artwork can come to the ground, +// from there it will scale upwards with the bottom aligned to this line +static const CGFloat ArtworkMinDistanceToBench = 70; + +// How much do we pull the BG down by for Landscope +static const CGFloat LandscapeOrientationBackgroundNegativeBottomMargin = 33; +static const CGFloat LandscapeOrientationArtworkNegativeBottomMargin = 80; + +// The Eyeline level is the point at which we will vertically center the artwork +// unless it's too tall that it touches the minimum distance above +static const CGFloat ArtworkEyelineLevelFromBench = 160; + +static const CGFloat DistanceToTopOfBenchPortrait = 90; + +@interface ARViewInRoomViewController () + +@property (nonatomic, strong) Artwork *artwork; +@property (nonatomic, assign) CGFloat zoomScale; + +@property (nonatomic, assign) NSInteger roomSize; +@property (nonatomic, weak) UIImageView *backgroundImageView; + +// For parallax horizontal VIR +@property (nonatomic, weak) UIImageView *chairImageView; +@property (nonatomic, weak) UIImageView *leftWallImageView; +@property (nonatomic, weak) UIImageView *rightWallImageView; +@property (nonatomic, weak) UIImageView *dudeImageView; + + +// Debug information +@property (nonatomic, weak) UILabel *debugSizeLabel; +@property (nonatomic, weak) UIView *debugMinimumArtworkView; +@property (nonatomic, weak) UIView *debugEyelineView; + +@property (readwrite, nonatomic, assign) BOOL hidesBackButton; + +@end + +@implementation ARViewInRoomViewController + ++ (UIImageView *)imageViewForFramedArtwork +{ + UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + imageView.contentMode = UIViewContentModeScaleAspectFit; + + // Add a shadow to the artwork image + CALayer *layer = [imageView layer]; + layer.shadowOffset = CGSizeMake(0, 4); + layer.shadowOpacity = 0; + layer.shadowColor = [[UIColor blackColor] CGColor]; + + return imageView; +} + +- (ARViewInRoomViewController *)initWithArtwork:(Artwork *)artwork +{ + self = [super init]; + if (!self) { return nil; } + + _artwork = artwork; + self.view.clipsToBounds = YES; + + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + self.hidesBackButton = UIInterfaceOrientationIsLandscape(orientation); + + return self; +} + +#pragma mark - ARMenuAwareViewController + +- (BOOL)hidesToolbarMenu { + return YES; +} + +#pragma mark - UIViewController + +- (void)viewDidAppear:(BOOL)animated +{ + // When not in an ARNavgiationController + if (!self.artworkImageView) { + self.artworkImageView = [ARViewInRoomViewController imageViewForFramedArtwork]; + self.artworkImageView.frame = [ARViewInRoomViewController rectForImageViewWithArtwork:self.artwork withContainerFrame:self.view.bounds]; + + [[ARFeedImageLoader alloc] loadImageAtAddress:[self.artwork baseImageURL] desiredSize:ARFeedItemImageSizeLarge + forImageView:self.artworkImageView customPlaceholder:nil]; + + self.artworkImageView.layer.shadowOpacity = 0.3; + } + + [super viewDidAppear:animated]; +} + +- (void)loadView +{ + UIImageView *galleryBackground = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ViewInRoom_Base"]]; + self.backgroundImageView = galleryBackground; + self.backgroundImageView.contentMode = UIViewContentModeBottom; + self.backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + [super loadView]; + + self.view.backgroundColor = [UIColor colorWithHex:0xfcfbfa]; + + [self.view addSubview:galleryBackground]; + + #if DEBUG_VIEW_IN_ROOM + [self setupDebugTools]; + #endif +} + +- (void)setArtworkImageView:(UIImageView *)artworkImageView { + _artworkImageView = artworkImageView; + self.artworkImageView.contentMode = UIViewContentModeScaleAspectFit; + self.artworkImageView.backgroundColor = [UIColor clearColor]; + + if (self.chairImageView) { + [self.view insertSubview:self.artworkImageView belowSubview:self.chairImageView]; + } else { + [self.view addSubview:self.artworkImageView]; + } + + UITapGestureRecognizer *exitTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self.navigationController action:@selector(popViewControllerAnimated:)]; + self.artworkImageView.userInteractionEnabled = YES; + [self.artworkImageView addGestureRecognizer:exitTapGesture]; + +} + +- (void)tappedArtwork { + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)viewWillLayoutSubviews +{ + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); + + if (isLandscape) { + self.backgroundImageView.image = [UIImage imageNamed:@"ViewInRoom_BaseNoBench"]; + [self setupParallaxVIR]; + + CGRect backgroundFrame = self.view.bounds; + backgroundFrame.origin.y += LandscapeOrientationBackgroundNegativeBottomMargin; + self.backgroundImageView.frame = backgroundFrame; + + } else { + [self hideDecorationViews]; + self.backgroundImageView.image = [UIImage imageNamed:@"ViewInRoom_Base"]; + self.backgroundImageView.frame = self.view.frame; + } + + if (self.artworkImageView) { + self.artworkImageView.frame = [self.class rectForImageViewWithArtwork:self.artwork withContainerFrame:self.view.bounds]; + } + +#if DEBUG_VIEW_IN_ROOM + [self updateDebugViews]; +#endif +} + + +- (void)setupParallaxVIR { + CGFloat wallsWidth = 90; + CGFloat wallsYOffset = -2; + CGFloat wallsStretch = 8; + + CGFloat chairHeight = 50; + CGFloat chairWidth = 160; + CGFloat chairOffset = 70; + + CGFloat dudeCenterXOffset = -130; + CGFloat dudeYOffset = -10; + + // Current aspect ratio = 3.(1/3) + CGFloat dudeHeight = 160; + CGFloat dudeWidth = 48; + + CGFloat chairMotionDelta = 24; + CGFloat dudeMotionDelta = 5; + CGFloat wallMotionDelta = 200; + CGFloat artworkMotionDelta = 50; + + self.artworkImageView.userInteractionEnabled = NO; + + self.hidesBackButton = YES; + + if (self.artworkImageView && self.artworkImageView.motionEffects.count == 0) { + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedArtwork)]; + [self.artworkImageView addGestureRecognizer:tapGesture]; + + UIInterpolatingMotionEffect *artworkMotion = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" + type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + artworkMotion.minimumRelativeValue = @(artworkMotionDelta); + artworkMotion.maximumRelativeValue = @(-artworkMotionDelta); + [self.artworkImageView addMotionEffect:artworkMotion]; + } + + if (!self.chairImageView) { + UIImageView *chairView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ViewInRoom_Bench"]]; + chairView.contentScaleFactor = UIViewContentModeScaleAspectFill; + self.chairImageView = chairView; + + CGRect chairFrame = CGRectMake(CGRectGetWidth(self.view.bounds)/2 - chairWidth/2, CGRectGetHeight(self.view.bounds) - chairOffset, chairWidth, chairHeight); + chairView.frame = chairFrame; + + [self.view addSubview:chairView]; + + UIInterpolatingMotionEffect *chairMotion = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" + type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + chairMotion.minimumRelativeValue = @(chairMotionDelta); + chairMotion.maximumRelativeValue = @(-chairMotionDelta); + [self.chairImageView addMotionEffect:chairMotion]; + } + + if (!self.leftWallImageView) { + UIImageView *leftWallView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ViewInRoom_Wall"]]; + self.leftWallImageView = leftWallView; + leftWallView.contentScaleFactor = UIViewContentModeScaleAspectFit; + + leftWallView.frame = CGRectMake(0, wallsYOffset, wallsWidth, CGRectGetHeight(self.view.bounds) + wallsStretch); + [self.view addSubview:leftWallView]; + + UIInterpolatingMotionEffect *wallMotion = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"bounds" + type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + wallMotion.minimumRelativeValue = [NSValue valueWithCGRect:CGRectMake(0, 0, -wallMotionDelta, 0)]; + wallMotion.maximumRelativeValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 0, 0)]; + [leftWallView addMotionEffect:wallMotion]; + } + + if (!self.rightWallImageView) { + UIImageView *rightWallView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ViewInRoom_Wall_Right"]]; + self.rightWallImageView = rightWallView; + rightWallView.contentScaleFactor = UIViewContentModeScaleAspectFit; + rightWallView.frame = CGRectMake(CGRectGetWidth(self.view.bounds) - wallsWidth, wallsYOffset, wallsWidth, CGRectGetHeight(self.view.bounds) + wallsStretch); + + [self.view addSubview:rightWallView]; + + UIInterpolatingMotionEffect *chairMotion = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" + type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + chairMotion.minimumRelativeValue = @(-7); + chairMotion.maximumRelativeValue = @(7); + [rightWallView addMotionEffect:chairMotion]; + + + UIInterpolatingMotionEffect *wallMotion = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"bounds" + type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + wallMotion.minimumRelativeValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 0, 0)]; + wallMotion.maximumRelativeValue = [NSValue valueWithCGRect:CGRectMake(0, 0, wallMotionDelta, 0)]; + [rightWallView addMotionEffect:wallMotion]; + } + + if (!self.dudeImageView) { + UIImageView *dudeView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ViewInRoom_Man_3"]]; + self.dudeImageView = dudeView; + dudeView.contentScaleFactor = UIViewContentModeScaleAspectFit; + dudeView.frame = CGRectMake(CGRectGetWidth(self.view.bounds)/2 + dudeCenterXOffset, CGRectGetHeight(self.view.bounds) - dudeHeight + dudeYOffset, dudeWidth, dudeHeight); + [self.view addSubview:dudeView]; + + UIInterpolatingMotionEffect *chairMotion = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" + type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + chairMotion.minimumRelativeValue = @(dudeMotionDelta); + chairMotion.maximumRelativeValue = @(-dudeMotionDelta); + [self.dudeImageView addMotionEffect:chairMotion]; + } + +} + +- (void)hideDecorationViews { + if (!self.artworkImageView || !self.leftWallImageView || !self.rightWallImageView) { + return; + } + + self.artworkImageView.userInteractionEnabled = YES; + + self.hidesBackButton = NO; + + NSArray *effects = self.artworkImageView.motionEffects.copy; + for (UIMotionEffect *effect in effects) { + [self.artworkImageView removeMotionEffect:effect]; + } + + NSArray *views = @[self.chairImageView, self.leftWallImageView, self.rightWallImageView]; + for (__strong UIView *decorationView in views) { + [decorationView removeFromSuperview]; + decorationView = nil; + } + + if (self.dudeImageView) { + [self.dudeImageView removeFromSuperview]; + self.dudeImageView = nil; + } +} + ++ (CGRect)rectForImageViewWithArtwork:(Artwork *)artwork withContainerFrame:(CGRect)containerFrame +{ + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); + CGFloat benchWidth = isLandscape? InitialWidthOfBenchPXLandscape : InitialWidthOfBenchPX; + + // Initial Scale in this case is when the image is at 100% zoom + CGFloat initialScale = benchWidth / InitialWidthOfBenchInches; + CGFloat artworkWidth = artwork.widthInches; + CGFloat scale = initialScale; + + if (artworkWidth > InitialWidthOfBenchInches) { + // we have MaximumWidthOfArtworkPX as the horizontal bounds + // CGFloat pixelsPerInch = artworkWidth / MaximumWidthOfArtworkPX; + + // Generate the new background width + // CGFloat newBackgroundImageWidth = (CGRectGetWidth(self.backgroundImageView.frame) / initialScale) * pixelsPerInch; + + } + + CGFloat artworkHeight = artwork.heightInches; + CGFloat artworkDiameter = artwork.diameterInches; + + CGFloat scaledWidth; + CGFloat scaledHeight; + + if (artworkDiameter > 0) { + scaledWidth = scaledHeight = floorf(artworkDiameter * scale); + } else { + scaledWidth = floorf(artworkWidth * scale); + scaledHeight = floorf(artworkHeight * scale); + } + + CGRect frame = { + .origin = { + floorf((CGRectGetWidth(containerFrame) - scaledWidth) * .5f), + floorf(CGRectGetHeight(containerFrame) - [self artworkEyelineLevel] - (scaledHeight * .5f)) + }, + .size = { + scaledWidth, + scaledHeight + }, + }; + + if (frame.origin.y < 0 || [self artworkFrameIsBelowMinimumDistance:frame inContainer:containerFrame]) { + frame.origin.y = CGRectGetHeight(containerFrame) - [self artworkMinimumDistanceToBottom] - scaledHeight; + } + + // HACK for VIR landscape artwork Y adjustments ./ + if (isLandscape) { + frame.origin.y += LandscapeOrientationArtworkNegativeBottomMargin; + } + + return frame; +} + ++ (BOOL)artworkFrameIsBelowMinimumDistance:(CGRect)artworkFrame inContainer:(CGRect) containerFrame +{ + return (CGRectGetMaxY(artworkFrame) > CGRectGetHeight(containerFrame) - [self artworkMinimumDistanceToBottom]); +} + ++ (CGFloat)artworkMinimumDistanceToBottom +{ + return [self distanceToTopOfBench] + ArtworkMinDistanceToBench; +} + ++ (CGFloat)artworkEyelineLevel +{ + return [self distanceToTopOfBench] + ArtworkEyelineLevelFromBench; +} + ++ (CGFloat)distanceToTopOfBench +{ + return DistanceToTopOfBenchPortrait; +} + +#pragma mark - +#pragma mark Visual Debugging tools + +- (void)setupDebugTools +{ + CGRect line = self.view.frame; + line.size.height = 1; + + UIView *eyeline = [[UIView alloc] initWithFrame:line]; + self.debugEyelineView = eyeline; + self.debugEyelineView.backgroundColor = [UIColor redColor]; + self.debugEyelineView.clipsToBounds = NO; + [self.view addSubview: self.debugEyelineView]; + + UILabel *eyelineLabel = [[UILabel alloc] initWithFrame:CGRectMake(18, -20, 200, 24)]; + eyelineLabel.text = @"Eyeline"; + eyelineLabel.font = [UIFont serifItalicFontWithSize:12]; + [self.debugEyelineView addSubview:eyelineLabel]; + + UIView *lowestPointLine = [[UIView alloc] initWithFrame:line]; + self.debugMinimumArtworkView = lowestPointLine; + self.debugMinimumArtworkView.backgroundColor = [UIColor greenColor]; + self.debugMinimumArtworkView.clipsToBounds = NO; + [self.view addSubview: self.debugMinimumArtworkView]; + + UILabel *lowestPointLabel = [[UILabel alloc] initWithFrame:CGRectMake(18, -20, 200, 16)]; + lowestPointLabel.text = @"Lowest Point"; + lowestPointLabel.font = [UIFont serifItalicFontWithSize:12]; + [self.debugMinimumArtworkView addSubview:lowestPointLabel]; + + UILabel *sizesLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 240)]; + sizesLabel.font = [UIFont serifItalicFontWithSize:12]; + sizesLabel.numberOfLines = 0; + sizesLabel.center = CGPointMake(160, 240); + self.debugSizeLabel = sizesLabel; + + [self.view addSubview:sizesLabel]; +} + +- (void)updateDebugViews +{ + CGRect newframe = self.view.frame; + + newframe.origin.y = CGRectGetHeight(self.view.bounds) - [self.class artworkMinimumDistanceToBottom]; + newframe.size.height = 2; + + self.debugMinimumArtworkView.frame = newframe; + + newframe.origin.y = CGRectGetHeight(self.view.bounds) - [self.class artworkEyelineLevel]; + self.debugEyelineView.frame = newframe; + + self.debugSizeLabel.text = [NSString stringWithFormat:@" %@ \n %@ px \n at %f", self.artwork.dimensionsInches, NSStringFromCGSize(self.artworkImageView.bounds.size), self.zoomScale]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.rotationDelegate willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.rotationDelegate willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; + + if (self.popOnRotation) { + if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) { + [self.navigationController popViewControllerAnimated:YES]; + } + } +} + +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation +{ + [self.rotationDelegate didRotateFromInterfaceOrientation:fromInterfaceOrientation]; +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.artwork) { + return @{ @"artwork" : self.artwork.artworkID, @"type" : @"artwork" }; + } + + return nil; +} + +@end diff --git a/Artsy/Classes/View Controllers/ARZoomArtworkImageViewController.h b/Artsy/Classes/View Controllers/ARZoomArtworkImageViewController.h new file mode 100644 index 00000000000..87a5c326e65 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARZoomArtworkImageViewController.h @@ -0,0 +1,15 @@ +#import "ARZoomView.h" + +@interface ARZoomArtworkImageViewController : UIViewController + +- (instancetype)initWithImage:(Image *)image; + +@property (nonatomic, strong, readonly) Image *image; + +// ZoomView is given via the transition +@property (readwrite, nonatomic, strong) ARZoomView *zoomView; +@property (readwrite, nonatomic, assign) BOOL suppressZoomViewCreation; + +- (void)unconstrainZoomView; + +@end diff --git a/Artsy/Classes/View Controllers/ARZoomArtworkImageViewController.m b/Artsy/Classes/View Controllers/ARZoomArtworkImageViewController.m new file mode 100644 index 00000000000..af074bf3a59 --- /dev/null +++ b/Artsy/Classes/View Controllers/ARZoomArtworkImageViewController.m @@ -0,0 +1,116 @@ +#import "ARZoomArtworkImageViewController.h" + +@interface ARZoomArtworkImageViewController () + +@property (nonatomic, assign) BOOL popped; + +@end + +@implementation ARZoomArtworkImageViewController + +- (instancetype)initWithImage:(Image *)image +{ + self = [super init]; + if (!self) { return nil; } + + _image = image; + + return self; +} + +#pragma mark - ARMenuAwareViewController + +- (BOOL)hidesBackButton +{ + return NO; +} + +- (BOOL)hidesToolbarMenu +{ + return YES; +} + +#pragma mark - UIViewController + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + +- (void)loadView +{ + UIView *clearView = [[UIView alloc] init]; + clearView.backgroundColor = [UIColor whiteColor]; + clearView.opaque = NO; + self.view = clearView; +} + +- (void)setZoomView:(ARZoomView *)zoomView +{ + _zoomView = zoomView; + _zoomView.zoomDelegate = self; + + [self.view addSubview:zoomView]; + + RACSignal *unconstrainSignal = [self rac_signalForSelector:@selector(unconstrainZoomView)]; + + // This has immediate effect + RACSignal *viewFrameSignal = [RACObserve(self.view, frame) takeUntil:unconstrainSignal]; + RAC(zoomView, frame) = viewFrameSignal; + + + // throttle: is necessary to push this to the next runloop invocation. + // Well, technically we need to delay it at least 2 invocations, at least on iOS 7. + // Since it's not good to rely on iOS implementation details, this inperceptable delay will do. + @weakify(zoomView); + [[[viewFrameSignal skip:1] throttle:0.01] subscribeNext:^(id x) { + @strongify(zoomView); + + CGRect frame = [x CGRectValue]; + CGSize size = frame.size; + + [zoomView setMaxMinZoomScalesForSize:size]; + CGFloat zoomScale = [zoomView scaleForFullScreenZoomInSize:size]; + CGPoint targetContentOffset = [zoomView centerContentOffsetForZoomScale:zoomScale minimumSize:size]; + + [zoomView performBlockWhileIgnoringContentOffsetChanges:^{ + [zoomView setZoomScale:zoomScale animated:YES]; + }]; + [zoomView setContentOffset:targetContentOffset animated:YES]; + }]; +} + +- (void)unconstrainZoomView +{ + // Empty – just used as a signal for RAC in setZoomView: +} + +- (void)zoomViewFinished:(ARZoomView *)zoomView +{ + if (self.popped || self.navigationController.topViewController != self) { + return; + } + + [self.zoomView finish]; + self.popped = YES; + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + if (!self.suppressZoomViewCreation) { + self.zoomView = [[ARZoomView alloc]initWithImage:self.image frame:self.view.bounds]; + self.zoomView.zoomScale = [self.zoomView scaleForFullScreenZoomInSize:self.view.bounds.size]; + } + + [super viewWillAppear:animated]; +} + +-(BOOL)shouldAutorotate +{ + // YES is buggy. + return [UIDevice isPad]; +} +@end diff --git a/Artsy/Classes/View Controllers/models/ARFairMapZoomManager.h b/Artsy/Classes/View Controllers/models/ARFairMapZoomManager.h new file mode 100644 index 00000000000..2f66b3b4b31 --- /dev/null +++ b/Artsy/Classes/View Controllers/models/ARFairMapZoomManager.h @@ -0,0 +1,14 @@ +@class NAMapView; +@protocol ARTiledImageViewDataSource; + +@interface ARFairMapZoomManager : NSObject + +- (id)initWithMap:(NAMapView *)map dataSource:(NSObject *)dataSource; + +@property (nonatomic, strong, readonly) NAMapView *map; +@property (nonatomic, strong, readonly) NSObject *dataSource; + +- (void)setMaxMinZoomScalesForCurrentBounds; +- (void)zoomToFitAnimated:(BOOL)animate; + +@end diff --git a/Artsy/Classes/View Controllers/models/ARFairMapZoomManager.m b/Artsy/Classes/View Controllers/models/ARFairMapZoomManager.m new file mode 100644 index 00000000000..6705a228de4 --- /dev/null +++ b/Artsy/Classes/View Controllers/models/ARFairMapZoomManager.m @@ -0,0 +1,43 @@ +#import "ARFairMapZoomManager.h" +#import + +@implementation ARFairMapZoomManager + +- (id)initWithMap:(NAMapView *)map dataSource:(NSObject *)dataSource +{ + self = [super init]; + if (!self) { return nil; } + + _map = map; + _dataSource = dataSource; + + return self; +} + +- (void)setMaxMinZoomScalesForCurrentBounds +{ + CGSize boundsSize = self.map.bounds.size; + CGSize imageSize = [self.dataSource imageSizeForImageView:nil]; + + // calculate min/max zoomscale + CGFloat xScale = boundsSize.width / imageSize.width; // the scale needed to perfectly fit the image width-wise + CGFloat yScale = boundsSize.height / imageSize.height; // the scale needed to perfectly fit the image height-wise + CGFloat minScale = MAX(xScale, yScale); // use minimum of these to allow the image to become fully visible + + CGFloat maxScale = 1.0; + + // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) + if (minScale > maxScale) { + minScale = maxScale; + } + + self.map.maximumZoomScale = maxScale * 0.6; + self.map.minimumZoomScale = minScale; +} + +- (void)zoomToFitAnimated:(BOOL)animate +{ + [self.map setZoomScale:self.map.minimumZoomScale animated:animate]; +} + +@end diff --git a/Artsy/Classes/View Controllers/models/ARFairShowMapper.h b/Artsy/Classes/View Controllers/models/ARFairShowMapper.h new file mode 100644 index 00000000000..2aaadd2f8b0 --- /dev/null +++ b/Artsy/Classes/View Controllers/models/ARFairShowMapper.h @@ -0,0 +1,28 @@ +#import +#import "ARFairMapAnnotationView.h" + +@interface ARFairShowMapper : NSObject + +- (id)initWithMapView:(NATiledImageMapView *)mapView map:(Map *)map imageSize:(CGSize)imageSize; + +@property (readonly, nonatomic, strong) NATiledImageMapView *mapView; +@property (readonly, nonatomic, copy) NSSet *shows; +@property (readonly, nonatomic, strong) Map *map; +@property (readwrite, nonatomic, copy) NSSet *favoritedPartnerIDs; +@property (readwrite, nonatomic, assign) BOOL expandAnnotations; + +- (void)setupMapFeatures; +- (void)addShows:(NSSet *)shows; +- (void)mapZoomLevelChanged:(CGFloat)zoomLevel; + +- (void)zoomToPoint:(MapPoint *)point animated:(BOOL)animated; +- (void)zoomToFitPoints:(NSArray *)points animated:(BOOL)animated; +- (void)selectPartnerShow:(PartnerShow *)partnerShow animated:(BOOL)animated; +- (void)selectPartnerShows:(NSArray *)partnerShows animated:(BOOL)animated; +- (void)zoomToFitPartners:(NSArray *)partners animated:(BOOL)animated; +- (void)zoomToFitPartnerShows:(NSArray *)partnerShows animated:(BOOL)animated; +- (void)zoomToFitFavoritePartners:(BOOL)animated; + +- (NSArray *)mapFeatureViewsForShows:(NSArray *)shows; + +@end diff --git a/Artsy/Classes/View Controllers/models/ARFairShowMapper.m b/Artsy/Classes/View Controllers/models/ARFairShowMapper.m new file mode 100644 index 00000000000..90f0bc65107 --- /dev/null +++ b/Artsy/Classes/View Controllers/models/ARFairShowMapper.m @@ -0,0 +1,411 @@ +#import "ARFairShowMapper.h" + +@interface ARFairShowMapper() +@property (readwrite, nonatomic, copy) NSSet *highlightedShows; +@property (readwrite, nonatomic, copy) NSSet *shows; +@property (readonly, nonatomic, assign) CGFloat overlappingZoomLevel; +@property (nonatomic, strong) NSMapTable *annotationsToAnnotationViews; +@property (nonatomic, strong) NSMapTable *objectsToAnnotations; +@property (nonatomic, readonly, assign) CGSize imageSize; +@property (nonatomic, readonly, strong) NSMapTable *partnerToShowsMap; +@property (nonatomic, readonly, assign) CGFloat annotationZoomScaleThreshold; +@end + +@implementation ARFairShowMapper + +- (id)initWithMapView:(NATiledImageMapView *)mapView map:(Map *)map imageSize:(CGSize)imageSize +{ + self = [super init]; + if (!self) { return nil; } + + _mapView = mapView; + _map = map; + _overlappingZoomLevel = 0; + _annotationsToAnnotationViews = [NSMapTable strongToStrongObjectsMapTable]; + _objectsToAnnotations = [NSMapTable strongToStrongObjectsMapTable]; + _imageSize = imageSize; + _annotationZoomScaleThreshold = self.mapView.minimumZoomScale + (self.mapView.maximumZoomScale - self.mapView.minimumZoomScale)/2; + _expandAnnotations = YES; + + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(Fair *)fair change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@keypath(Fair.new, shows)]) { + [self addShows:fair.shows]; + } +} + +- (void)addShows:(NSSet *)shows +{ + // removed shows? + if (self.shows) { + NSMutableSet *showsToRemove = [NSMutableSet setWithSet:self.shows]; + [showsToRemove minusSet:shows]; + [self removeShowAnnotations:showsToRemove]; + } + + // added shows? + self.shows = shows.copy; + [self addShowAnnotations:self.shows]; + [self mapZoomLevelChanged:self.overlappingZoomLevel]; + [self rebuildPartnerToShowsMap]; +} + +- (void)setupMapFeatures +{ + [self.map.features each:^(MapFeature *mapFeature) { + CGPoint pointOnMap = [mapFeature coordinateOnImage:self.map.image]; + NAAnnotation *annotation = [[ARFairMapAnnotation alloc] initWithPoint:pointOnMap representedObject:mapFeature]; + [self.mapView addAnnotation:annotation animated:NO]; + [self.annotationsToAnnotationViews setObject:annotation.view forKey:annotation]; + [self.objectsToAnnotations setObject:annotation forKey:mapFeature]; + }]; +} + +- (void)mapZoomLevelChanged:(CGFloat)zoomLevel +{ + if (self.mapView.zoomScale >= self.annotationZoomScaleThreshold && self.expandAnnotations) { + [self expandAllAnnotations:zoomLevel]; + } else { + [self reduceAllAnnotations:zoomLevel]; + } +} + +- (void)expandOrReduceAllAnnotations +{ + if (self.mapView.zoomScale >= self.annotationZoomScaleThreshold) { + [self expandAllAnnotations]; + } else { + [self reduceAllAnnotations]; + } +} + +- (void)removeShowAnnotations:(NSSet *)shows +{ + [shows each:^(PartnerShow *show) { + for(MapPoint *mapPoint in show.fairLocation.mapPoints) { + CGPoint pointOnMap = [mapPoint coordinateOnImage:self.map.image]; + NAAnnotation *existingAnnotation = [self annotationForRepresentedObject:show]; + if (existingAnnotation) { + if (CGPointEqualToPoint(existingAnnotation.point, pointOnMap)) { + [self.mapView removeAnnotation:existingAnnotation]; + } + [self.annotationsToAnnotationViews removeObjectForKey:existingAnnotation]; + [self.objectsToAnnotations removeObjectForKey:show]; + } + } + }]; +} + +- (void)addShowAnnotations:(NSSet *)shows +{ + [shows each:^(PartnerShow *show) { + for(MapPoint *mapPoint in show.fairLocation.mapPoints) { + CGPoint pointOnMap = [mapPoint coordinateOnImage:self.map.image]; + + // cache will cause old points to be plotted + NAAnnotation *existingAnnotation = [self annotationForRepresentedObject:show]; + if (existingAnnotation) { + if (CGPointEqualToPoint(existingAnnotation.point, pointOnMap)) { + continue; + } else { + [self.mapView removeAnnotation:existingAnnotation]; + [self.annotationsToAnnotationViews removeObjectForKey:existingAnnotation]; + [self.objectsToAnnotations removeObjectForKey:show]; + } + } + + NAAnnotation *annotation = [[ARFairMapAnnotation alloc] initWithPoint:pointOnMap representedObject:show]; + [self.mapView addAnnotation:annotation animated:NO]; + [self.annotationsToAnnotationViews setObject:annotation.view forKey:annotation]; + [self.objectsToAnnotations setObject:annotation forKey:show]; + } + }]; +} + +- (void)reduceAllAnnotations:(CGFloat)zoomLevel +{ + if (zoomLevel >= self.overlappingZoomLevel) { + return; + } + + [self reduceAllAnnotations]; + _overlappingZoomLevel = zoomLevel; +} + +- (void)reduceAllAnnotations +{ + for (ARFairMapAnnotationView *annotationView in self.annotationViews) { + if (annotationView.mapFeatureType != ARMapFeatureTypeDefault) { + continue; + } + annotationView.userInteractionEnabled = NO; + [annotationView reduceToPoint]; + } +} + +- (void)expandAllAnnotations:(CGFloat)zoomLevel +{ + if (zoomLevel <= self.overlappingZoomLevel) { + return; + } + + [self expandAllAnnotations]; + _overlappingZoomLevel = zoomLevel; +} + +- (void)expandAllAnnotations +{ + for (ARFairMapAnnotationView *annotationView in self.annotationViews) { + annotationView.userInteractionEnabled = YES; + [annotationView expandToFull]; + } +} + +- (BOOL)showIsSaved:(PartnerShow *)show +{ + return [self.favoritedPartnerIDs containsObject:show.partner.partnerID]; +} + +- (void)zoomToPoint:(MapPoint *)point animated:(BOOL)animated +{ + CGPoint pointOnMap = [point coordinateOnImage:self.map.image]; + [self.mapView setZoomScale:self.mapView.maximumZoomScale animated:animated]; + [self.mapView centerOnPoint:pointOnMap animated:animated]; +} + +- (void)zoomToFitPoints:(NSArray *)points animated:(BOOL)animated +{ + if (points.count == 0) { + return; + + } else if (points.count == 1) { + [self zoomToPoint:points.firstObject animated:animated]; + + } else { + + CGPoint topLeft = CGPointMake(FLT_MAX, FLT_MAX); + CGPoint bottomRight = CGPointMake(-FLT_MAX, -FLT_MAX); + + for (MapPoint *point in points) { + CGPoint coordinates = [point coordinateOnImage:self.map.image]; + + if (topLeft.x > coordinates.x) { topLeft.x = coordinates.x; } + if (topLeft.y > coordinates.y) { topLeft.y = coordinates.y; } + + if (bottomRight.x < coordinates.x) { bottomRight.x = coordinates.x; } + if (bottomRight.y < coordinates.y) { bottomRight.y = coordinates.y; } + } + + CGPoint centerPoint = (CGPoint){ + .x = topLeft.x + ((bottomRight.x - topLeft.x)/2), + .y = topLeft.y + ((bottomRight.y - topLeft.y)/2) + }; + + if(animated){ + [UIView animateWithDuration:0.5 animations:^{ + [self.mapView centerOnPoint:centerPoint animated:NO]; + [self.mapView setZoomScale:self.mapView.minimumZoomScale animated:NO]; + }]; + + } else { + [self.mapView setZoomScale:self.mapView.minimumZoomScale animated:NO]; + [self.mapView centerOnPoint:centerPoint animated:NO]; + } + } +} + +- (void)zoomToFitFavoritePartners:(BOOL)animated +{ + NSArray *favorites = [self.shows select:^BOOL(PartnerShow *show) { + return [self showIsSaved:show]; + }]; + + NSArray *partners = [favorites valueForKeyPath:@keypath(PartnerShow.new, partner)]; + [self zoomToFitPartners:partners animated:animated]; +} + +- (void)selectPartnerShow:(PartnerShow *)partnerShow animated:(BOOL)animated +{ + [self selectPartnerShows:[NSArray arrayWithObject:partnerShow] animated:animated]; +} + +- (void)selectPartnerShows:(NSArray *)partnerShows animated:(BOOL)animated +{ + [self zoomToFitPartnerShows:partnerShows animated:animated]; +} + +- (void)rebuildPartnerToShowsMap +{ + @weakify(self); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + @strongify(self); + + NSMapTable *result = [NSMapTable strongToStrongObjectsMapTable]; + for(PartnerShow *show in self.shows) { + if (show.partner) { + NSMutableArray *shows = [result objectForKey:show.partner]; + if (!shows) { + shows = [NSMutableArray arrayWithObject:show]; + [result setObject:shows forKey:show.partner]; + } else { + [shows addObject:show]; + } + } + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + if (!self) { return; } + self->_partnerToShowsMap = result; + }); + }); +} + +- (void)zoomToFitPartners:(NSArray *)partners animated:(BOOL)animated +{ + if (partners.count == 0) { + return; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + NSMutableSet *highlighted = [NSMutableSet set]; + + NSMapTable *partnerToShowsMap = self.partnerToShowsMap; + for (Partner *partner in partners){ + NSArray *partnerShows = [partnerToShowsMap objectForKey:partner]; + if (partnerShows) { + [highlighted addObjectsFromArray:partnerShows]; + } + } + + [self zoomToFitPartnerShows:highlighted.allObjects animated:animated]; + }); +} + +- (void)zoomToFitPartnerShows:(NSArray *)partnerShows animated:(BOOL)animated +{ + if (partnerShows.count == 0) { + return; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + NSArray *mapPoints = [[partnerShows reject:^BOOL(PartnerShow *show) { + return show.fairLocation.mapPoints.count == 0; + }] map:^id(PartnerShow *show) { + return show.fairLocation.mapPoints.firstObject; + }]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self setHighlightedShows:[NSSet setWithArray:partnerShows]]; + NSArray *mapFeatureViews = [self mapFeatureViewsForShows:self.highlightedShows.allObjects]; + [self expandToFullForFeatureViews:mapFeatureViews]; + [self zoomToFitPoints:mapPoints animated:animated]; + }); + }); +} + +- (void)setHighlightedShows:(NSSet *)highlightedShows +{ + NSArray *oldMapFeatureViews = [self mapFeatureViewsForShows:self.highlightedShows.allObjects]; + + _highlightedShows = highlightedShows.copy; + + [oldMapFeatureViews each:^(ARFairMapAnnotationView *view) { + view.annotation.highlighted = NO; + view.mapFeatureType = view.annotation.featureType; + }]; + + NSArray *newMapFeatureViews = [self mapFeatureViewsForShows:self.highlightedShows.allObjects]; + [newMapFeatureViews each:^(ARFairMapAnnotationView *view) { + view.annotation.highlighted = YES; + view.hidden = NO; + [view expandToFull]; + }]; + + [self expandOrReduceAllAnnotations]; +} + +- (void)setFavoritedPartnerIDs:(NSSet *)favoritedPartnerIDs +{ + _favoritedPartnerIDs = favoritedPartnerIDs.copy; + + // update existing shows + NSArray *favoriteShows = [self.shows.allObjects select:^BOOL(PartnerShow *show) { + return [self showIsSaved:show]; + }]; + + NSArray *mapFeatureViews = [self mapFeatureViewsForShows:favoriteShows]; + [self setFeatureType:ARMapFeatureTypeSaved forFeatureViews:mapFeatureViews]; + [mapFeatureViews each:^(ARFairMapAnnotationView *view) { + view.annotation.saved = YES; + view.hidden = NO; + [view expandToFull]; + }]; + + [self expandOrReduceAllAnnotations]; +} + +- (void)setFeatureType:(enum ARMapFeatureType)featureType forFeatureViews:(NSArray *)featureViews +{ + [featureViews each:^(ARFairMapAnnotationView *featureView) { + featureView.mapFeatureType = featureType; + }]; +} + +- (void)setFeatureTypeAndExpandToFull:(enum ARMapFeatureType)featureType forFeatureViews:(NSArray *)featureViews +{ + [featureViews each:^(ARFairMapAnnotationView *featureView) { + [featureView expandToFull]; + featureView.mapFeatureType = featureType; + }]; +} + +- (void)expandToFullForFeatureViews:(NSArray *)featureViews +{ + [featureViews each:^(ARFairMapAnnotationView *featureView) { + [featureView expandToFull]; + }]; +} + +- (NSArray *)mapFeatureViewsForShows:(NSArray *)shows +{ + NSMutableArray *featureViews = [NSMutableArray array]; + [shows each:^(PartnerShow *show) { + for(MapPoint *mapPoint in show.fairLocation.mapPoints) { + CGPoint pointOnMap = [mapPoint coordinateOnImage:self.map.image]; + ARFairMapAnnotationView *featureView = (ARFairMapAnnotationView *) [self viewForPoint:pointOnMap andRepresentedObject:show]; + if (featureView) { + [featureViews addObject:featureView]; + } + } + }]; + return featureViews; +} + +- (ARFairMapAnnotation *)annotationForRepresentedObject:(id)representedObject +{ + return [self.objectsToAnnotations objectForKey:representedObject]; +} + +- (ARFairMapAnnotationView *)viewForPoint:(CGPoint)point andRepresentedObject:(id)representedObject +{ + ARFairMapAnnotation *annotation = [self annotationForRepresentedObject:representedObject]; + if (annotation && CGPointEqualToPoint(point, annotation.point)) { + return (ARFairMapAnnotationView *)annotation.view; + } + return nil; +} + +-(NSEnumerator *)annotationViews +{ + return [self.annotationsToAnnotationViews objectEnumerator]; +} + +-(NSEnumerator *)annotations +{ + return [self.annotationsToAnnotationViews keyEnumerator]; +} + +@end diff --git a/Artsy/Classes/View Controllers/models/ARTiledImageDataSourceWithImage.h b/Artsy/Classes/View Controllers/models/ARTiledImageDataSourceWithImage.h new file mode 100644 index 00000000000..0b3a345f9e6 --- /dev/null +++ b/Artsy/Classes/View Controllers/models/ARTiledImageDataSourceWithImage.h @@ -0,0 +1,9 @@ +#import +#import + +@interface ARTiledImageDataSourceWithImage : ARWebTiledImageDataSource + +- (id)initWithImage:(Image *)image; +@property (nonatomic, strong, readonly) Image *image; + +@end diff --git a/Artsy/Classes/View Controllers/models/ARTiledImageDataSourceWithImage.m b/Artsy/Classes/View Controllers/models/ARTiledImageDataSourceWithImage.m new file mode 100644 index 00000000000..a08689398c1 --- /dev/null +++ b/Artsy/Classes/View Controllers/models/ARTiledImageDataSourceWithImage.m @@ -0,0 +1,28 @@ +#import "ARTiledImageDataSourceWithImage.h" + +@implementation ARTiledImageDataSourceWithImage + +- (id)initWithImage:(Image *)image +{ + self = [super init]; + if (!self) { return nil; } + + _image = image; + + self.tileFormat = @"jpg"; + self.tileBaseURL = [NSURL URLWithString:self.image.tileBaseUrl]; + self.tileSize = image.tileSize; + self.maxTiledHeight = self.image.maxTiledHeight; + self.maxTiledWidth = self.image.maxTiledWidth; + self.maxTileLevel = self.image.maxTileLevel; + self.minTileLevel = 11; + + return self; +} + +- (NSURL *)tiledImageView:(ARTiledImageView *)imageView urlForImageTileAtLevel:(NSInteger)level x:(NSInteger)x y:(NSInteger)y +{ + return [self.image urlTileForLevel:level atX:x andY:y]; +} + +@end diff --git a/Artsy/Classes/View Controllers/quicksilver/ARQuicksilverSearchBar.h b/Artsy/Classes/View Controllers/quicksilver/ARQuicksilverSearchBar.h new file mode 100644 index 00000000000..8a958ac3b96 --- /dev/null +++ b/Artsy/Classes/View Controllers/quicksilver/ARQuicksilverSearchBar.h @@ -0,0 +1,16 @@ +@class ARQuicksilverSearchBar; + +@protocol ARQuicksilverSearchBarDelegate + +- (void)searchBarUpPressed:(ARQuicksilverSearchBar *)searchBar; +- (void)searchBarDownPressed:(ARQuicksilverSearchBar *)searchBar; +- (void)searchBarReturnPressed:(ARQuicksilverSearchBar *)searchBar; +- (void)searchBarEscapePressed:(ARQuicksilverSearchBar *)searchBar; + +@end + +@interface ARQuicksilverSearchBar : UISearchBar + +@property (nonatomic, weak) IBOutlet id upDownDelegate; + +@end diff --git a/Artsy/Classes/View Controllers/quicksilver/ARQuicksilverSearchBar.m b/Artsy/Classes/View Controllers/quicksilver/ARQuicksilverSearchBar.m new file mode 100644 index 00000000000..d7de4c1ac82 --- /dev/null +++ b/Artsy/Classes/View Controllers/quicksilver/ARQuicksilverSearchBar.m @@ -0,0 +1,35 @@ +#import "ARQuicksilverSearchBar.h" + +@implementation ARQuicksilverSearchBar + +- (NSArray *) keyCommands +{ + UIKeyCommand *upArrow = [UIKeyCommand keyCommandWithInput: UIKeyInputUpArrow modifierFlags: 0 action: @selector(upArrow:)]; + UIKeyCommand *downArrow = [UIKeyCommand keyCommandWithInput: UIKeyInputDownArrow modifierFlags: 0 action: @selector(downArrow:)]; + UIKeyCommand *returnCommand = [UIKeyCommand keyCommandWithInput: @"\r" modifierFlags: 0 action: @selector(returnCommand:)]; + UIKeyCommand *escapeCommand = [UIKeyCommand keyCommandWithInput: UIKeyInputEscape modifierFlags: 0 action: @selector(escapeCommand:)]; + + return @[ upArrow, downArrow, returnCommand, escapeCommand]; +} + +- (void)escapeCommand:(UIKeyCommand *)keyCommand +{ + [self.upDownDelegate searchBarEscapePressed:self]; +} + +- (void)returnCommand:(UIKeyCommand *)keyCommand +{ + [self.upDownDelegate searchBarReturnPressed:self]; +} + +- (void)upArrow:(UIKeyCommand *)keyCommand +{ + [self.upDownDelegate searchBarUpPressed:self]; +} + +- (void)downArrow:(UIKeyCommand *) keyCommand +{ + [self.upDownDelegate searchBarDownPressed:self]; +} + +@end diff --git a/Artsy/Classes/Views/ARActionButtonsView.h b/Artsy/Classes/Views/ARActionButtonsView.h new file mode 100644 index 00000000000..fa45eed6fbf --- /dev/null +++ b/Artsy/Classes/Views/ARActionButtonsView.h @@ -0,0 +1,11 @@ +typedef void (^ARActionButtonHandler)(UIButton *button); + +extern NSString * const ARActionButtonImageKey; +extern NSString * const ARActionButtonHandlerKey; + +@interface ARActionButtonsView : UIView + +/// Takes a collection of dictionaries with the above keys +@property (readwrite, nonatomic, copy) NSArray *actionButtonDescriptions; + +@end diff --git a/Artsy/Classes/Views/ARActionButtonsView.m b/Artsy/Classes/Views/ARActionButtonsView.m new file mode 100644 index 00000000000..ed27a2ed32f --- /dev/null +++ b/Artsy/Classes/Views/ARActionButtonsView.m @@ -0,0 +1,56 @@ +#import "ARActionButtonsView.h" + +NSString * const ARActionButtonImageKey = @"ARActionButtonImageKey"; +NSString * const ARActionButtonHandlerKey = @"ARActionButtonHandlerKey"; + +@interface ARActionButtonsView () +@property (nonatomic, strong) NSMapTable *handlersByButton; +@end + +@implementation ARActionButtonsView + +- (void)setActionButtonDescriptions:(NSArray *)actionButtonDescriptions +{ + _handlersByButton = [NSMapTable strongToStrongObjectsMapTable]; + + [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + + _actionButtonDescriptions = [actionButtonDescriptions copy]; + + for(NSDictionary *description in actionButtonDescriptions) { + ARActionButtonHandler handler = description[ARActionButtonHandlerKey]; + NSString *imageName = description[ARActionButtonImageKey]; + + ARCircularActionButton *actionButton = [[ARCircularActionButton alloc] initWithImageName:imageName]; + [self.handlersByButton setObject:handler forKey:actionButton]; + + [actionButton addTarget:self action:@selector(tappedItem:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:actionButton]; + + NSInteger index = [actionButtonDescriptions indexOfObject:description]; + if(index == 0){ + [actionButton alignTrailingEdgeWithView:self predicate:nil]; + + } else { + UIView *closestSibling = self.subviews[index-1]; + [actionButton alignAttribute:NSLayoutAttributeTrailing toAttribute:NSLayoutAttributeLeading ofView:closestSibling predicate:@"-10"]; + } + [actionButton alignTopEdgeWithView:self predicate:nil]; + } +} + +- (void)tappedItem:(ARCircularActionButton *)sender +{ + ARActionButtonHandler handler = [self.handlersByButton objectForKey:sender]; + handler(sender); +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize){ + .height = [ARCircularActionButton buttonSize], + .width = UIViewNoIntrinsicMetric + }; +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkActionsView.h b/Artsy/Classes/Views/ARArtworkActionsView.h new file mode 100644 index 00000000000..d222bd69f84 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkActionsView.h @@ -0,0 +1,16 @@ +// Shows contact buttons, or buy buttons, or edition prices, etc + +#import +@class ARArtworkActionsView; + +@protocol ARArtworkActionsViewDelegate +- (void)didUpdateArtworkActionsView:(ARArtworkActionsView *)actionsView; +@end + +@interface ARArtworkActionsView : ORStackView +- (instancetype)initWithArtwork:(Artwork *)artwork; +@property (nonatomic, assign) BOOL enabled; +@property (nonatomic, weak) id delegate; +@end + + diff --git a/Artsy/Classes/Views/ARArtworkActionsView.m b/Artsy/Classes/Views/ARArtworkActionsView.m new file mode 100644 index 00000000000..ebe6b0d1193 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkActionsView.m @@ -0,0 +1,300 @@ +#import "ARArtworkActionsView.h" +#import "ARArtworkPriceView.h" +#import "ARArtworkAuctionPriceView.h" +#import "ARCountdownView.h" +#import "ARNavigationButtonsViewController.h" +#import "ARNavigationButton.h" +#import "ARAuctionBidderStateLabel.h" +#import "ARBidButton.h" + +@interface ARArtworkActionsView() + +@property (nonatomic, strong) ARCountdownView *countdownView; +@property (nonatomic, strong) ARBlackFlatButton *contactGalleryButton; +@property (nonatomic, strong) ARInquireButton *inquireWithArtsyButton; +@property (nonatomic, strong) ARArtworkPriceView *priceView; +@property (nonatomic, strong) ARArtworkAuctionPriceView *auctionPriceView; +@property (nonatomic, strong) ARAuctionBidderStateLabel *bidderStatusLabel; +@property (nonatomic, strong) ARBidButton *bidButton; +@property (nonatomic, strong) Artwork *artwork; +@property (nonatomic, strong) SaleArtwork *saleArtwork; +@property (nonatomic, strong) ARNavigationButtonsViewController *navigationButtonsVC; + +@end + +@implementation ARArtworkActionsView + +- (instancetype)initWithArtwork:(Artwork *)artwork +{ + self = [super init]; + if (!self) { return nil; } + + _artwork = artwork; + self.bottomMarginHeight = 0; + + return self; +} + +- (void)setDelegate:(id)delegate +{ + _delegate = delegate; + @weakify(self); + + KSPromise *artworkPromise = [self.artwork onArtworkUpdate:nil failure:nil]; + KSPromise *saleArtworkPromise = [self.artwork onSaleArtworkUpdate:^(SaleArtwork *saleArtwork) { + @strongify(self); + self.saleArtwork = saleArtwork; + } failure:nil]; + + [[KSPromise when:@[artworkPromise, saleArtworkPromise]] then:^id(id value) { + @strongify(self); + id returnable = nil; + [self updateUI]; + return returnable; + } error:nil]; +} + +- (void)updateUI +{ + for (UIView *subview in self.subviews) { + [self removeSubview:subview]; + } + + BOOL shownPrice = NO; + if ([self showAuctionControls]) { + + ARAuctionState state = self.saleArtwork.auctionState; + if (state & (ARAuctionStateUserIsHighBidder | ARAuctionStateUserIsBidder)) { + self.bidderStatusLabel = [[ARAuctionBidderStateLabel alloc] init]; + [self.bidderStatusLabel updateWithSaleArtwork:self.saleArtwork]; + [self addSubview:self.bidderStatusLabel withTopMargin:@"0" sideMargin:@"0"]; + } + + self.auctionPriceView = [[ARArtworkAuctionPriceView alloc] init]; + [self.auctionPriceView updateWithSaleArtwork:self.saleArtwork]; + [self addSubview:self.auctionPriceView withTopMargin:@"12" sideMargin:@"0"]; + shownPrice = YES; + } else if ([self showPriceLabel]) { + self.priceView = [[ARArtworkPriceView alloc] initWithFrame:CGRectZero]; + [self.priceView updateWithArtwork:self.artwork]; + [self addSubview:self.priceView withTopMargin:@"4" sideMargin:@"0"]; + shownPrice = YES; + } + + NSString *buttonMargin = shownPrice? @"30" : @"0"; + + if ([self showAuctionControls]) { + ARBidButton *bidButton = [[ARBidButton alloc] init]; + bidButton.auctionState = self.saleArtwork.auctionState; + [self addSubview:bidButton withTopMargin:shownPrice? @"30" : @"0" sideMargin:@"0"]; + [bidButton addTarget:nil action:@selector(tappedBidButton:) forControlEvents:UIControlEventTouchUpInside]; + self.bidButton = bidButton; + buttonMargin = @"8"; + + if ([self showBuyButton]) { + self.priceView = [[ARArtworkPriceView alloc] initWithFrame:CGRectZero]; + [self.priceView updateWithArtwork:self.artwork andSaleArtwork:self.saleArtwork]; + [self addSubview:self.priceView withTopMargin:@"4" sideMargin:@"0"]; + + ARBlackFlatButton *buy = [[ARBlackFlatButton alloc] init]; + [buy setTitle:@"Buy Now" forState:UIControlStateNormal]; + [buy addTarget:nil action:@selector(tappedBuyButton:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:buy withTopMargin:buttonMargin sideMargin:nil]; + } + + [self setupCountdownView]; + + } else if ([self showBuyButton]) { + ARBlackFlatButton *buy = [[ARBlackFlatButton alloc] init]; + [buy setTitle:@"Buy" forState:UIControlStateNormal]; + [buy addTarget:nil action:@selector(tappedBuyButton:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:buy withTopMargin:buttonMargin sideMargin:nil]; + } + + if ([self showContactButton]) { + + NSString *title = nil; + if (self.artwork.partner.type == ARPartnerTypeGallery) { + title = NSLocalizedString(@"Contact Gallery", @"Contact Gallery"); + } else { + title = NSLocalizedString(@"Contact Seller", @"Contact Seller"); + } + + ARBlackFlatButton *contact = [[ARBlackFlatButton alloc] init]; + [contact setTitle:title forState:UIControlStateNormal]; + + [contact addTarget:nil action:@selector(tappedContactGallery:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:contact withTopMargin:buttonMargin sideMargin:nil]; + self.contactGalleryButton = contact; + } + + if ([self showInquireButton]) { + ARInquireButton *specialist = [[ARInquireButton alloc] init]; + [specialist setTitle:@"Ask an Artsy Specialist" forState:UIControlStateNormal]; + [specialist addTarget:nil action:@selector(tappedContactRepresentative:) forControlEvents:UIControlEventTouchUpInside]; + + BOOL hasAnotherButton = ([self showContactButton] || [self showBuyButton]); + NSString *topMargin = (hasAnotherButton) ? @"8" : buttonMargin; + [self addSubview:specialist withTopMargin:topMargin sideMargin:nil]; + self.inquireWithArtsyButton = specialist; + } + + if ([self showAuctionControls]) { + ARInquireButton *auctionsInfo = [[ARInquireButton alloc] init]; + [auctionsInfo setTitle:@"How Auctions Work?" forState:UIControlStateNormal]; + [auctionsInfo addTarget:nil action:@selector(tappedAuctionInfo:) forControlEvents:UIControlEventTouchUpInside]; + BOOL hasAnotherButton = ([self showContactButton] || [self showBuyButton] || self.inquireWithArtsyButton); + NSString *topMargin = (hasAnotherButton) ? @"8" : buttonMargin; + [self addSubview:auctionsInfo withTopMargin:topMargin sideMargin:nil]; + } + + NSArray *navigationButtons = [self navigationButtons]; + if (navigationButtons.count > 0) { + self.navigationButtonsVC = [[ARNavigationButtonsViewController alloc] init]; + [self.navigationButtonsVC addButtonDescriptions:[self navigationButtons] unique:YES]; + [self addSubview:self.navigationButtonsVC.view withTopMargin:@"20" sideMargin:@"0"]; + } + + [self.delegate didUpdateArtworkActionsView:self]; +} + +- (NSArray *)navigationButtons +{ + NSMutableArray *navigationButtons = [[NSMutableArray alloc] init]; + + if ([self showAuctionResultsButton]) { + [navigationButtons addObject:@{ + ARNavigationButtonClassKey: ARNavigationButton.class, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARNavigationButton.new, title): @"Auction Results" + }, + ARNavigationButtonHandlerKey: ^(UIButton *sender) { + // This will pass the message up the responder chain + [[UIApplication sharedApplication] sendAction:@selector(tappedAuctionResults:) + to:nil from:self forEvent:nil]; + } + }]; + } + if ([self showMoreInfoButton]) { + + [navigationButtons addObject:@{ + ARNavigationButtonClassKey: ARNavigationButton.class, + ARNavigationButtonPropertiesKey: @{ + @keypath(ARNavigationButton.new, title): @"More Info" + }, + ARNavigationButtonHandlerKey: ^(UIButton *sender) { + // This will pass the message up the responder chain + [[UIApplication sharedApplication] sendAction:@selector(tappedMoreInfo:) + to:nil from:self forEvent:nil]; + } + }]; + } + return [navigationButtons copy]; +} + +- (void)setEnabled:(BOOL)enabled { + [self.contactGalleryButton setEnabled:enabled animated:YES]; + [self.inquireWithArtsyButton setEnabled:enabled animated:YES]; +} + +#pragma mark - Info Logic + +- (BOOL)showNotForSaleLabel +{ + return self.artwork.inquireable.boolValue + && !self.artwork.sold.boolValue + && !self.artwork.forSale.boolValue; +} + +- (BOOL)showPriceLabel +{ + return self.artwork.price.length + && !self.artwork.hasMultipleEditions + && (self.artwork.inquireable.boolValue || self.artwork.sold.boolValue); +} + +- (BOOL)showContactButton +{ + return self.artwork.forSale.boolValue + && !self.artwork.acquireable.boolValue + && ![self showAuctionControls]; +} + +- (BOOL)showBuyButton +{ + return self.artwork.acquireable.boolValue; +} + +- (BOOL)showInquireButton +{ + return self.artwork.inquireable.boolValue; +} + +- (BOOL)showAuctionControls +{ + return (self.saleArtwork != nil) && !self.artwork.sold.boolValue; +} + +- (BOOL)showAuctionResultsButton +{ + return self.artwork.shouldShowAuctionResults; +} + +- (BOOL)showMoreInfoButton +{ + return self.artwork.hasMoreInfo; +} + +#pragma mark ARContactViewDelegate + +- (void)setupCountdownView +{ + ARCountdownView *countdownView = [[ARCountdownView alloc] init]; + countdownView.delegate = self; + [self addSubview:countdownView withTopMargin:@"8" sideMargin:@"120"]; + self.countdownView = countdownView; + [self updateCountdownView]; +} + +- (void)updateCountdownView +{ + NSDate *now = [ARSystemTime date]; + + NSDate *startDate = self.saleArtwork.auction.startDate; + NSDate *endDate = self.saleArtwork.auction.endDate; + + self.bidButton.auctionState = self.saleArtwork.auctionState; + + if (!self.saleArtwork.auction) { + [self removeSubview:self.bidButton]; + [self removeSubview:self.countdownView]; + [self removeSubview:self.bidderStatusLabel]; + [self removeSubview:self.auctionPriceView]; + + } else if ([now compare:startDate] == NSOrderedAscending) { + self.countdownView.heading = @"Auction Opens"; + self.countdownView.targetDate = startDate; + [self.countdownView startTimer]; + + } else if ([now compare:endDate] == NSOrderedAscending) { + self.countdownView.heading = @"Auction Ends"; + self.countdownView.targetDate = endDate; + [self.countdownView startTimer]; + + } else { + [self removeSubview:self.countdownView]; + [self.delegate didUpdateArtworkActionsView:self]; + } +} + +- (void)countdownViewDidFinish:(ARCountdownView *)countdownView +{ + [self updateCountdownView]; +} + +-(CGSize) intrinsicContentSize +{ + return CGSizeMake(280, UIViewNoIntrinsicMetric); +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkAuctionPriceView.h b/Artsy/Classes/Views/ARArtworkAuctionPriceView.h new file mode 100644 index 00000000000..e82a6ae6e4b --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkAuctionPriceView.h @@ -0,0 +1,5 @@ +#import + +@interface ARArtworkAuctionPriceView : ORStackView +- (void)updateWithSaleArtwork:(SaleArtwork *)saleArtwork; +@end diff --git a/Artsy/Classes/Views/ARArtworkAuctionPriceView.m b/Artsy/Classes/Views/ARArtworkAuctionPriceView.m new file mode 100644 index 00000000000..b34e8a26809 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkAuctionPriceView.m @@ -0,0 +1,52 @@ +#import "ARArtworkAuctionPriceView.h" +#import "ARArtworkPriceRowView.h" +#import "NSNumberFormatter+ARCurrency.h" +#import "UIView+ARDrawing.h" + +@implementation ARArtworkAuctionPriceView + +- (void)updateWithSaleArtwork:(SaleArtwork *)saleArtwork +{ + ARArtworkPriceRowView *row = [[ARArtworkPriceRowView alloc] initWithFrame:CGRectZero]; + BOOL hasBids = saleArtwork.auctionState & ARAuctionStateArtworkHasBids; + row.messageLabel.text = hasBids ? @"Current Bid:" : @"Starting Bid:"; + row.messageLabel.font = [UIFont serifSemiBoldFontWithSize:16]; + + NSNumber *cents = hasBids ? saleArtwork.saleHighestBid.cents : saleArtwork.openingBidCents; + row.priceLabel.text = [NSNumberFormatter currencyStringForCents:cents]; + row.priceLabel.font = [UIFont sansSerifFontWithSize:24]; + + row.margin = 16; + [self addSubview:row withTopMargin:@"0" sideMargin:@"0"]; + [row alignLeadingEdgeWithView:self predicate:nil]; + + row.bidStatusText = [self statusMessageForSaleArtwork:saleArtwork]; +} + +- (NSString *)statusMessageForSaleArtwork:(SaleArtwork *)saleArtwork +{ + ARReserveStatus reserveStatus = saleArtwork.reserveStatus; + + NSInteger bidCount = saleArtwork.artworkNumPositions.integerValue; + NSString *bids = bidCount > 1 ? @"Bids" : @"Bid"; + BOOL startedAuction = saleArtwork.auctionState & ARAuctionStateStarted; + + if (bidCount && reserveStatus == ARReserveStatusReserveNotMet) { + return [NSString stringWithFormat:@"(%@ %@, Reserve not met)", @(bidCount), bids]; + } else if (bidCount && reserveStatus == ARReserveStatusNoReserve) { + return [NSString stringWithFormat:@"(%@ %@)", @(bidCount), bids]; + } else if (bidCount && reserveStatus == ARReserveStatusReserveMet) { + return [NSString stringWithFormat:@"(%@ %@, Reserve met)", @(bidCount), bids]; + } else if (reserveStatus == ARReserveStatusReserveNotMet) { + return startedAuction ? @"This work has a reserve" : @"(This work has a reserve)"; + } else { + return nil; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self drawDottedBorders]; +} +@end diff --git a/Artsy/Classes/Views/ARArtworkBlurbView.h b/Artsy/Classes/Views/ARArtworkBlurbView.h new file mode 100644 index 00000000000..412dc79457b --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkBlurbView.h @@ -0,0 +1,17 @@ +#import + +@class ARArtworkBlurbView; + +@protocol ARArtworkBlurbViewDelegate + +-(void)artworkBlurView:(ARArtworkBlurbView *)blurbView shouldPresentViewController:(UIViewController *)viewController; + +@end + +@interface ARArtworkBlurbView : ORStackView + +- (instancetype)initWithArtwork:(Artwork *)artwork; + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Artsy/Classes/Views/ARArtworkBlurbView.m b/Artsy/Classes/Views/ARArtworkBlurbView.m new file mode 100644 index 00000000000..2b32cf11628 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkBlurbView.m @@ -0,0 +1,65 @@ +#import "ARArtworkBlurbView.h" +#import "ARTextView.h" +#import "ORStackView+ArtsyViews.h" + +@interface ARArtworkBlurbView () +@property (nonatomic, strong) UILabel *aboutHeading; +@property (nonatomic, strong) ARTextView *blurbTextView; + +@end + +@implementation ARArtworkBlurbView + +- (instancetype)initWithArtwork:(Artwork *)artwork +{ + self = [super init]; + if (!self) { return nil; } + + @weakify(self); + + [artwork onArtworkUpdate:^{ + @strongify(self); + [self updateWithArtwork:artwork]; + } failure:nil]; + + return self; +} + +- (void)updateWithArtwork:(Artwork *)artwork +{ + BOOL showBio = (artwork.blurb.length > 0); + self.bottomMarginHeight = 0; + + if (showBio){ + + if (!self.aboutHeading) { + self.aboutHeading = [self addPageSubtitleWithString:[NSLocalizedString(@"About this Artwork", @"About this artwork header") uppercaseString]]; + } + + if (!self.blurbTextView) { + self.blurbTextView = [[ARTextView alloc] init]; + self.blurbTextView.viewControllerDelegate = self; + } + + [self.blurbTextView setMarkdownString:artwork.blurb]; + + NSString *sideMargin = [UIDevice isPad]? @"280" : @"0"; + [self addSubview:self.blurbTextView withTopMargin:@"16" sideMargin:sideMargin]; + [self invalidateIntrinsicContentSize]; + } +} + +- (CGSize)intrinsicContentSize +{ + CGFloat height = self.blurbTextView.text.length > 0 ? UIViewNoIntrinsicMetric : 0; + return CGSizeMake(UIViewNoIntrinsicMetric, height); +} + +#pragma mark - ARTextViewDelegate + +-(void)textView:(ARTextView *)textView shouldOpenViewController:(UIViewController *)viewController +{ + [self.delegate artworkBlurView:self shouldPresentViewController:viewController]; +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkMetadataView.h b/Artsy/Classes/Views/ARArtworkMetadataView.h new file mode 100644 index 00000000000..c25d5f87212 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkMetadataView.h @@ -0,0 +1,33 @@ +// Running out of names. + +/// Contains a device agnostic view of the Artwork +/// preview images, action buttons, details & for sale messaages + +#import +#import "ARArtworkDetailView.h" +#import "ARArtworkActionsView.h" + +@class ARArtworkMetadataView; + +@protocol ARArtworkMetadataViewDelegate + +- (void)artworkMetadataView:(ARArtworkMetadataView *)metadataView shouldPresentViewController:(UIViewController *)viewController; +- (void)artworkMetadataView:(ARArtworkMetadataView *)metadataView didUpdateArtworkDetailView:(ARArtworkDetailView *)detailView; +- (void)artworkMetadataView:(ARArtworkMetadataView *)metadataView didUpdateArtworkActionsView:(ARArtworkActionsView *)actionsView; + +@end + +@interface ARArtworkMetadataView : ORStackView + +- (instancetype)initWithArtwork:(Artwork *)artwork andFair:(Fair *)fair; +- (void)updateWithFair:(Fair *)fair; + +- (UIImageView *)imageView; + +/// TODO: Make this a view controller so that we can negate doing this. +/// Let subviews know that we're in a fair context +@property (readwrite, nonatomic) Fair *fair; + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Artsy/Classes/Views/ARArtworkMetadataView.m b/Artsy/Classes/Views/ARArtworkMetadataView.m new file mode 100644 index 00000000000..60401aa928f --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkMetadataView.m @@ -0,0 +1,128 @@ +#import "ARArtworkMetadataView.h" +#import "ARArtworkPreviewImageView.h" +#import "ARArtworkPreviewActionsView.h" +#import "ARSplitStackView.h" +#import "ARWhitespaceGobbler.h" + +static const CGFloat ARPadRightColumnWidth = 280; + +@interface ARArtworkMetadataView() +@property (nonatomic, strong) ARArtworkPreviewActionsView *artworkPreviewActions; +@property (nonatomic, strong) ARArtworkPreviewImageView *artworkPreview; +@property (nonatomic, strong) ARArtworkActionsView *actionsView; +@property (nonatomic, strong) ARArtworkDetailView *artworkDetailView; +@end + +@implementation ARArtworkMetadataView + +- (instancetype)initWithArtwork:(Artwork *)artwork andFair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + [self setTranslatesAutoresizingMaskIntoConstraints:NO]; + _fair = fair; + + ARArtworkPreviewImageView *artworkPreview = [[ARArtworkPreviewImageView alloc] init]; + ARArtworkPreviewActionsView *previewActionsView = [[ARArtworkPreviewActionsView alloc] initWithArtwork:artwork andFair:fair]; + ARArtworkDetailView *artworkDetailView = [[ARArtworkDetailView alloc] initWithArtwork:artwork andFair:fair]; + ARArtworkActionsView *artworkActionsView = [[ARArtworkActionsView alloc] initWithArtwork:artwork]; + artworkActionsView.alpha = 0; + self.artworkPreview = artworkPreview; + self.actionsView = artworkActionsView; + self.artworkPreviewActions = previewActionsView; + self.artworkDetailView = artworkDetailView; + + if ([UIDevice isPad]) { + NSString *rightWidthString = [NSString stringWithFormat:@"%.0f", ARPadRightColumnWidth]; + ARSplitStackView *splitView = [[ARSplitStackView alloc] initWithLeftPredicate:nil rightPredicate:rightWidthString]; + [self addSubview:splitView withTopMargin:nil sideMargin:@"100"]; + [splitView.rightStack constrainLeadingSpaceToView:splitView.leftStack predicate:@"40"]; + [splitView.leftStack addSubview:artworkPreview withTopMargin:@"40" sideMargin:@"0@750"]; + [artworkPreview constrainWidthToView:splitView.leftStack predicate:@"0@1000"]; + [splitView.leftStack addSubview:previewActionsView withTopMargin:@"28" sideMargin:@"0"]; + [splitView.rightStack addSubview:artworkDetailView withTopMargin:@"30" sideMargin:@"0"]; + [splitView.rightStack addSubview:artworkActionsView withTopMargin:@"8" sideMargin:@"0"]; + ARWhitespaceGobbler *whitespaceGobbler = [[ARWhitespaceGobbler alloc] init]; + [splitView.leftStack addSubview:whitespaceGobbler withTopMargin:@"0" sideMargin:nil]; + + whitespaceGobbler = [[ARWhitespaceGobbler alloc] init]; + [splitView.rightStack addSubview:whitespaceGobbler withTopMargin:@"0" sideMargin:nil]; + } else { + [self constrainWidth:@"320"]; + [self addSubview:artworkPreview withTopMargin:@"0" sideMargin:@"0"]; + [self addSubview:previewActionsView withTopMargin:@"28" sideMargin:@"40"]; + [self addSubview:artworkDetailView withTopMargin:@"0" sideMargin:@"40"]; + [self addSubview:artworkActionsView withTopMargin:@"8" sideMargin:@"40"]; + } + self.bottomMarginHeight = 0; + [self registerForNetworkNotifications]; + // Create and add the artworkPreview as a subview before setting its artwork + // so that the necessary constraints already exist when setting its image. + artworkPreview.artwork = artwork; + return self; +} + +- (void)setDelegate:(id)delegate +{ + _delegate = delegate; + self.artworkDetailView.delegate = self; + self.actionsView.delegate = self; +} +- (void)registerForNetworkNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkAvailable) name:ARNetworkAvailableNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkUnavailable) name:ARNetworkUnavailableNotification object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:ARNetworkAvailableNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:ARNetworkUnavailableNotification object:nil]; +} + +- (void)updateWithFair:(Fair *)fair +{ + [self.artworkDetailView updateWithFair:fair]; +} + +- (void)networkAvailable +{ + self.actionsView.enabled = YES; +} + +- (void)networkUnavailable +{ + self.actionsView.enabled = NO; +} + +- (UIImageView *)imageView +{ + return self.artworkPreview; +} + +- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled +{ + [super setUserInteractionEnabled:userInteractionEnabled]; + [self.artworkPreview setUserInteractionEnabled:userInteractionEnabled]; +} + +#pragma mark - ARArtworkActionsViewDelegate + +-(void)didUpdateArtworkActionsView:(ARArtworkActionsView *)actionsView +{ + [self.delegate artworkMetadataView:self didUpdateArtworkActionsView:actionsView]; +} + +#pragma mark - ARArtworkDetailViewDelegate + +-(void)artworkDetailView:(ARArtworkDetailView *)detailView shouldPresentViewController:(UIViewController *)viewController +{ + [self.delegate artworkMetadataView:self shouldPresentViewController:viewController]; +} + +-(void)didUpdateArtworkDetailView:(ARArtworkDetailView *)detailView +{ + [self.delegate artworkMetadataView:self didUpdateArtworkDetailView:detailView]; +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkPriceRowView.h b/Artsy/Classes/Views/ARArtworkPriceRowView.h new file mode 100644 index 00000000000..22cf07d1642 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkPriceRowView.h @@ -0,0 +1,6 @@ +@interface ARArtworkPriceRowView : UIView +@property (nonatomic, strong) UILabel *messageLabel; +@property (nonatomic) CGFloat margin; +@property (nonatomic, assign) NSString *bidStatusText; +@property (nonatomic, strong) UILabel *priceLabel; +@end diff --git a/Artsy/Classes/Views/ARArtworkPriceRowView.m b/Artsy/Classes/Views/ARArtworkPriceRowView.m new file mode 100644 index 00000000000..4119d81acd3 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkPriceRowView.m @@ -0,0 +1,68 @@ +#import "ARArtworkPriceRowView.h" + +@interface ARArtworkPriceRowView () +@property (nonatomic, strong, readonly) ARSerifLabel *bidStatusLabel; +@end + +@implementation ARArtworkPriceRowView + +- (id)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + UILabel *messageLabel = [[ARSerifLabel alloc] init]; + [self addSubview:messageLabel]; + self.messageLabel = messageLabel; + + UILabel *priceLabel = [[ARSerifLabel alloc] init]; + priceLabel.textAlignment = NSTextAlignmentRight; + [self addSubview:priceLabel]; + self.priceLabel = priceLabel; + + _bidStatusLabel = [[ARSerifLabel alloc] init]; + self.bidStatusLabel.font = [UIFont serifFontWithSize:16]; + self.bidStatusLabel.textColor = [UIColor artsyHeavyGrey]; + self.bidStatusLabel.hidden = YES; + [self addSubview:self.bidStatusLabel]; + + // As the prefix & price share a horizontal constraint, so we use the hugging priority + // to let them decide who gets more of the bed. In this case, the price. + + [messageLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; + [priceLabel setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; + } + return self; +} + +- (void)setBidStatusText:(NSString *)bidStatusText +{ + self.bidStatusLabel.text = bidStatusText; + [self updateConstraints]; +} + +- (void)updateConstraints +{ + [super updateConstraints]; + [self.messageLabel alignLeadingEdgeWithView:self predicate:nil]; + [self.messageLabel alignTopEdgeWithView:self predicate:@(self.margin).stringValue]; + [self.priceLabel alignTrailingEdgeWithView:self predicate:nil]; + [self.priceLabel constrainLeadingSpaceToView:self.messageLabel predicate:@"4"]; + [self.priceLabel alignTopEdgeWithView:self predicate:@(self.margin).stringValue]; + [self.messageLabel alignCenterYWithView:self.priceLabel predicate:@"0"]; + + [self alignBottomEdgeWithView:self.messageLabel predicate:NSStringWithFormat(@">=%@@1000", @(self.margin).stringValue)]; + [self alignBottomEdgeWithView:self.priceLabel predicate:NSStringWithFormat(@">=%@@1000", @(self.margin).stringValue)]; + + + [self alignBottomEdgeWithView:self.messageLabel predicate:NSStringWithFormat(@"%@@250", @(self.margin))]; + [self alignBottomEdgeWithView:self.priceLabel predicate:NSStringWithFormat(@"%@@250", @(self.margin))]; + + if (self.bidStatusLabel.text.length > 0) { + self.bidStatusLabel.hidden = NO; + [self.bidStatusLabel constrainTopSpaceToView:self.messageLabel predicate:@"0"]; + [self.bidStatusLabel alignLeadingEdgeWithView:self.messageLabel predicate:@"0"]; + [self alignBottomEdgeWithView:self.bidStatusLabel predicate:NSStringWithFormat(@">=%@@1000", @(self.margin - 4).stringValue)]; + [self alignBottomEdgeWithView:self.bidStatusLabel predicate:NSStringWithFormat(@"%@@250", @(self.margin - 4).stringValue)]; + } +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkPriceView.h b/Artsy/Classes/Views/ARArtworkPriceView.h new file mode 100644 index 00000000000..244985492df --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkPriceView.h @@ -0,0 +1,8 @@ +#import + +@interface ARArtworkPriceView : ORStackView + +- (void)updateWithArtwork:(Artwork *)artwork; +- (void)updateWithArtwork:(Artwork *)artwork andSaleArtwork:(SaleArtwork *)saleArtwork; + +@end diff --git a/Artsy/Classes/Views/ARArtworkPriceView.m b/Artsy/Classes/Views/ARArtworkPriceView.m new file mode 100644 index 00000000000..d6310956ab5 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkPriceView.m @@ -0,0 +1,57 @@ +#import "ARArtworkPriceView.h" +#import "ARArtworkPriceRowView.h" +#import "UIView+ARDrawing.h" + +@interface ARArtworkPriceView() +@end + +@implementation ARArtworkPriceView + +- (id)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + self.bottomMarginHeight = 0; + } + return self; +} + +- (void)updateWithArtwork:(Artwork *)artwork +{ + [self updateWithArtwork:artwork andSaleArtwork:nil]; +} + +- (void)updateWithArtwork:(Artwork *)artwork andSaleArtwork:(SaleArtwork *)saleArtwork +{ + ARArtworkPriceRowView *row = [[ARArtworkPriceRowView alloc] initWithFrame:CGRectZero]; + + if (artwork.sold.boolValue) { + row.messageLabel.textColor = [UIColor artsyRed]; + row.messageLabel.font = [UIFont sansSerifFontWithSize:row.messageLabel.font.pointSize]; + row.messageLabel.text = @"SOLD"; + } else if (saleArtwork != nil) { + row.messageLabel.text = @"Buy Now Price:"; + } else { + row.messageLabel.text = @"Price:"; + } + + if (artwork.availability == ARArtworkAvailabilityForSale && artwork.isPriceHidden.boolValue) { + row.priceLabel.font = [UIFont serifItalicFontWithSize:row.priceLabel.font.pointSize]; + row.priceLabel.text = @"Contact for Price"; + } else { + row.priceLabel.font = [UIFont sansSerifFontWithSize:24]; + row.priceLabel.text = artwork.price; + } + + row.margin = artwork.sold.boolValue ? 10 : 16; + [self addSubview:row withTopMargin:@"0" sideMargin:@"0"]; + [row alignLeadingEdgeWithView:self predicate:nil]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self drawDottedBorders]; +} + +@end + diff --git a/Artsy/Classes/Views/ARArtworkRelatedArtworksView.h b/Artsy/Classes/Views/ARArtworkRelatedArtworksView.h new file mode 100644 index 00000000000..ee053afacf7 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkRelatedArtworksView.h @@ -0,0 +1,22 @@ +#import "ARArtworkMasonryModule.h" +#import "ARArtworkViewController.h" +@class ARArtworkRelatedArtworksView; + +@protocol ARArtworkRelatedArtworksViewParentViewController + +@required +- (void)relatedArtworksView:(ARArtworkRelatedArtworksView *)view shouldShowViewController:(UIViewController *)viewController; +- (void)didUpdateRelatedArtworksView:(ARArtworkRelatedArtworksView *)relatedArtworksView; +@optional + +- (Fair *)fair; +@end + +@interface ARArtworkRelatedArtworksView : ORStackView + +@property (nonatomic, weak) ARArtworkViewController *parentViewController; + +- (void)updateWithArtwork:(Artwork *)artwork; +- (void)cancel; + +@end diff --git a/Artsy/Classes/Views/ARArtworkRelatedArtworksView.m b/Artsy/Classes/Views/ARArtworkRelatedArtworksView.m new file mode 100644 index 00000000000..16ae0a26dfc --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkRelatedArtworksView.m @@ -0,0 +1,200 @@ +#import "ARArtworkRelatedArtworksView.h" +#import "AREmbeddedModelsViewController.h" +#import "ORStackView+ArtsyViews.h" +#import "ARArtworkSetViewController.h" + +@interface ARArtworkRelatedArtworksView() +@property (nonatomic, assign) BOOL hasRequested; +@property (nonatomic, strong) AFJSONRequestOperation *relatedArtworksRequest; +@property (nonatomic, strong) Artwork *artwork; +@property (nonatomic) SaleArtwork *saleArtwork; +@property (nonatomic) BOOL hasArtworks; +@property (nonatomic, strong) AREmbeddedModelsViewController *artworksVC; +@end + +@implementation ARArtworkRelatedArtworksView + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _hasArtworks = NO; + return self; +} +- (void)updateWithArtwork:(Artwork *)artwork +{ + [self updateWithArtwork:artwork withCompletion:nil]; +} +- (void)updateWithArtwork:(Artwork *)artwork withCompletion:(void(^)())completion +{ + if (self.hasRequested) { return; } + + self.artwork = artwork; + self.hasRequested = YES; + + @weakify(self); + + // TODO: refactor these callbacks to return so we can use + // results from the values array in a `when` + KSPromise *salePromise = [artwork onSaleArtworkUpdate:^(SaleArtwork *saleArtwork) { + self.saleArtwork = saleArtwork; + } failure:nil]; + + KSPromise *fairPromise = [artwork onFairUpdate:nil failure:nil]; + + [[KSPromise when:@[salePromise, fairPromise]] then:^id(id value) { + @strongify(self); + id returnable = nil; + + Fair *fairContext; + + if ([self.parentViewController respondsToSelector:@selector(fair)]) { + fairContext = [self.parentViewController fair]; + } + + if (!fairContext) { + fairContext = self.artwork.fair; + } + + if (self.saleArtwork.auction) { + returnable = self.saleArtwork.auction; + [self fetchSaleArtworks]; + + } else if (fairContext) { + returnable = fairContext; + [self fetchFairArtworksForFair:fairContext]; + + } else { + [self fetchRelatedArtworks]; + } + return returnable; + + } error:^id(NSError *error) { + @strongify(self); + ARErrorLog(@"Error fetching sale/fair for %@. Error: %@", self.artwork.artworkID, error.localizedDescription); + [self fetchRelatedArtworks]; + return error; + }]; +} + +- (void)fetchRelatedArtworks +{ + @weakify(self); + self.relatedArtworksRequest = [self.artwork getRelatedArtworks:^(NSArray *artworks) { + @strongify(self); + [self renderWithArtworks:artworks heading:@"Suggested Artworks"]; + }]; +} + +- (void)fetchSaleArtworks +{ + @weakify(self); + Sale *sale = self.saleArtwork.auction; + self.relatedArtworksRequest = [ArtsyAPI getArtworksForSale:sale.saleID success:^(NSArray *artworks) { + @strongify(self); + NSString *heading = [NSString stringWithFormat:@"Works from %@", sale.name]; + [self renderWithArtworks:artworks heading:heading]; + } failure:^(NSError *error) { + ARErrorLog(@"Couldn't fetch artworks for sale %@. Error: %@", sale.saleID, error.localizedDescription); + }]; + +} + +- (void)fetchFairArtworksForFair:(Fair *)fair +{ + @weakify(self); + self.relatedArtworksRequest = [self.artwork getRelatedFairArtworks:fair success:^(NSArray *artworks) { + @strongify(self); + NSString *heading = [NSString stringWithFormat:@"Suggested artworks from %@", fair.name]; + [self renderWithArtworks:artworks heading:heading]; + }]; +} + +- (void)fetchRelatedArtworksForSale:(Sale *)sale +{ + @weakify(self); + if (sale) { + self.relatedArtworksRequest = [ArtsyAPI getArtworksForSale:sale.saleID success:^(NSArray *artworks) { + @strongify(self); + NSString *heading = [NSString stringWithFormat:@"Works from %@", sale.name]; + [self renderWithArtworks:artworks heading:heading]; + } failure:^(NSError *error) { + ARErrorLog(@"Couldn't fetch artworks for sale %@. Error: %@", sale.saleID, error.localizedDescription); + }]; + } else { + } +} + +- (void)renderWithArtworks:(NSArray *)artworks heading:(NSString *)heading +{ + if (self.relatedArtworksRequest.isCancelled) { return; } + + artworks = [artworks reject:^BOOL(Artwork *artwork) { + return [artwork isEqual:self.artwork]; + }]; + + BOOL hasArtworks = artworks && artworks.count > 0; + self.hasArtworks = hasArtworks; + if (!hasArtworks) { + ARActionLog(@"No similar artworks for %@", self.artwork.artworkID); + } else { + [self addPageSubtitleWithString:heading.uppercaseString]; + + ARArtworkMasonryLayout layout = [UIDevice isPad] ? [self masonryLayoutForPadWithOrientation:[[UIApplication sharedApplication] statusBarOrientation]] : ARArtworkMasonryLayout2Column; + ARArtworkMasonryModule *module = [ARArtworkMasonryModule masonryModuleWithLayout:layout andStyle:AREmbeddedArtworkPresentationStyleArtworkMetadata]; + module.layoutProvider = self; + + self.artworksVC = [[AREmbeddedModelsViewController alloc] init]; + self.artworksVC.shouldAnimate = self.parentViewController.shouldAnimate; + self.artworksVC.delegate = self; + self.artworksVC.activeModule = module; + self.artworksVC.constrainHeightAutomatically = YES; + [self.artworksVC appendItems:artworks]; + + [self addViewController:self.artworksVC toParent:self.parentViewController withTopMargin:@"0" sideMargin:@"0"]; + self.bottomMarginHeight = 20; + [self invalidateIntrinsicContentSize]; + + [self layoutIfNeeded]; + [self.parentViewController didUpdateRelatedArtworksView:self]; + } +} + +- (ARArtworkMasonryLayout)masonryLayoutForPadWithOrientation:(UIInterfaceOrientation)orientation +{ + return UIInterfaceOrientationIsLandscape(orientation) ? ARArtworkMasonryLayout4Column : ARArtworkMasonryLayout3Column; +} + +- (void)cancel +{ + [self.relatedArtworksRequest cancel]; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, self.hasArtworks ? UIViewNoIntrinsicMetric : 0); +} + +#pragma mark - AREmbeddedModelsDelegate + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController +{ + [self.parentViewController relatedArtworksView:self shouldShowViewController:viewController]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:self.artworksVC.items inFair:self.fair atIndex:index]; + [self.parentViewController relatedArtworksView:self shouldShowViewController:viewController]; +} + +- (Fair *)fair +{ + if ([self.parentViewController respondsToSelector:@selector(fair)]) { + return [self.parentViewController fair]; + } else { + return nil; + } +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkThumbnailMetadataView.h b/Artsy/Classes/Views/ARArtworkThumbnailMetadataView.h new file mode 100644 index 00000000000..639a125eb9e --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkThumbnailMetadataView.h @@ -0,0 +1,9 @@ +#import "ARPostAttachment.h" + +@interface ARArtworkThumbnailMetadataView : UIView + ++ (CGFloat)heightForView; +- (void)configureWithArtwork:(Artwork *)artwork; +- (void)resetLabels; + +@end diff --git a/Artsy/Classes/Views/ARArtworkThumbnailMetadataView.m b/Artsy/Classes/Views/ARArtworkThumbnailMetadataView.m new file mode 100644 index 00000000000..aa8c0894478 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkThumbnailMetadataView.m @@ -0,0 +1,70 @@ +#import "ARArtworkThumbnailMetadataView.h" + +static CGFloat ARMetadataFontSize; + +@interface ARArtworkThumbnailMetadataView () + +@property (nonatomic, strong, readonly) ARSerifLabel *primaryLabel; +@property (nonatomic, strong, readonly) ARArtworkTitleLabel *secondaryLabel; + +@end + +@implementation ARArtworkThumbnailMetadataView + ++ (void)initialize{ + [super initialize]; + ARMetadataFontSize = [UIDevice isPad] ? 15 : 12; +} + ++ (CGFloat)heightForMargin { + return 8; +} + ++ (CGFloat)heightForView { + return [UIDevice isPad] ? 42 : 34; +} + +- (instancetype)init { + self = [super init]; + if (!self) { return nil; } + + _primaryLabel = [[ARSerifLabel alloc] init]; + _secondaryLabel = [[ARArtworkTitleLabel alloc] init]; + + [@[self.primaryLabel, self.secondaryLabel] each:^(UILabel *label) { + label.font = [label.font fontWithSize:ARMetadataFontSize]; + label.textColor = [UIColor artsyHeavyGrey]; + [self addSubview:label]; + }]; + + return self; +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize){ UIViewNoIntrinsicMetric, [self.class heightForView] }; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGRect labelFrame = self.bounds; + labelFrame.size.height /= 2; + + self.primaryLabel.frame = labelFrame; + + labelFrame.origin.y = labelFrame.size.height; + self.secondaryLabel.frame = labelFrame; +} + +- (void)configureWithArtwork:(Artwork *)artwork { + self.primaryLabel.text = artwork.artist.name; + [self.secondaryLabel setTitle:artwork.title date:artwork.date]; +} + +- (void)resetLabels{ + self.primaryLabel.text = nil; + [self.secondaryLabel setTitle:nil date:nil]; +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkView.h b/Artsy/Classes/Views/ARArtworkView.h new file mode 100644 index 00000000000..8bcfce5c972 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkView.h @@ -0,0 +1,25 @@ +#import +#import "ARArtworkMetadataView.h" +#import "ARArtworkRelatedArtworksView.h" +#import "ARArtworkBlurbView.h" +#import "ARArtworkViewController.h" + +NS_ENUM(NSInteger, ARArtworkViewIndex){ + ARArtworkBanner = 1, + ARArtworkPreview, + ARArtworkBlurb, + ARArtworkSpinner, + ARArtworkAuctionButton, + ARArtworkRelatedArtworks, + ARArtworkRelatedPosts, + ARArtworkGobbler +}; + +@interface ARArtworkView : ORStackScrollView +@property (nonatomic, strong) ARArtworkMetadataView *metadataView; +@property (nonatomic, strong) ARArtworkRelatedArtworksView *relatedArtworksView; +@property (nonatomic, strong) ORStackView *postsView; +@property (nonatomic, strong) ARArtworkBlurbView *artworkBlurbView; +@property (nonatomic, weak) ARArtworkViewController *parentViewController; +- (instancetype)initWithArtwork:(Artwork *)artwork fair:(Fair *)fair andParentViewController:(ARArtworkViewController *)parentViewController; +@end diff --git a/Artsy/Classes/Views/ARArtworkView.m b/Artsy/Classes/Views/ARArtworkView.m new file mode 100644 index 00000000000..b8f4391ddb8 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkView.m @@ -0,0 +1,137 @@ +#import "ARArtworkView.h" +#import "ARSpinner.h" +#import "ARAuctionBannerView.h" +#import "ARWhitespaceGobbler.h" +#import + +@interface ARArtworkView() +@property (nonatomic, strong) Artwork *artwork; +@property (nonatomic, strong) Fair *fair; +@property (nonatomic, strong) ARSpinner *spinner; +@property (nonatomic, strong) ARAuctionBannerView *banner; +@property (nonatomic, strong) ARWhitespaceGobbler *gobbler; +@end + +@implementation ARArtworkView + +static const CGFloat ARArtworkImageHeightAdjustmentForPad = -100; +static const CGFloat ARArtworkImageHeightAdjustmentForPhone = -56; + +- (id)initWithArtwork:(Artwork *)artwork fair:(Fair *)fair andParentViewController:(ARArtworkViewController *)parentViewController +{ + self = [super initWithStackViewClass:[ORTagBasedAutoStackView class]]; + + if (!self) { return nil; } + _artwork = artwork; + _fair = fair; + _parentViewController = parentViewController; + self.scrollsToTop = NO; + self.scrollEnabled = YES; + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + if ([self.parentViewController.navigationController isKindOfClass:ARNavigationController.class]) { + self.contentInset = UIEdgeInsetsMake(20, 0, 0, 0); + self.scrollIndicatorInsets = UIEdgeInsetsMake(20, 0, 0, 0); + } + + self.backgroundColor = [UIColor blackColor]; + self.stackView.backgroundColor = [UIColor whiteColor]; + self.stackView.bottomMarginHeight = 20; + + ARArtworkMetadataView *metadataView = [[ARArtworkMetadataView alloc] initWithArtwork:self.artwork andFair:self.fair]; + metadataView.tag = ARArtworkPreview; + self.metadataView = metadataView; + + ARArtworkBlurbView *artworkBlurbView = [[ARArtworkBlurbView alloc] initWithArtwork:self.artwork]; + artworkBlurbView.tag = ARArtworkBlurb; + self.artworkBlurbView = artworkBlurbView; + + ARSpinner *spinner = [[ARSpinner alloc] initWithFrame:CGRectMake(0, 0, 44, 44)]; + [spinner fadeInAnimated:self.parentViewController.shouldAnimate]; + spinner.tag = ARArtworkSpinner; + self.spinner = spinner; + [spinner constrainHeight:@"100"]; + + ARArtworkRelatedArtworksView *relatedArtworks = [[ARArtworkRelatedArtworksView alloc] init]; + relatedArtworks.alpha = 0; + relatedArtworks.tag = ARArtworkRelatedArtworks; + self.relatedArtworksView = relatedArtworks; + + ARAuctionBannerView *banner = [[ARAuctionBannerView alloc] init]; + banner.tag = ARArtworkBanner; + self.banner = banner; + + ARWhitespaceGobbler *gobbler = [[ARWhitespaceGobbler alloc] init]; + gobbler.tag = ARArtworkGobbler; + self.gobbler = gobbler; + + [self setUpSubviews]; + return self; +} + +- (void)setUpSubviews +{ + [self.stackView addSubview:self.metadataView withTopMargin:@"0" sideMargin:nil]; + [self.stackView addSubview:self.artworkBlurbView withTopMargin:@"0" sideMargin:[UIDevice isPad] ? @"100" : @"40"]; + [self.stackView addSubview:self.spinner withTopMargin:@"0" sideMargin:@"0"]; + [self.stackView addSubview:self.relatedArtworksView withTopMargin:@"0" sideMargin:@"0"]; + [self.stackView addSubview:self.banner withTopMargin:@"0" sideMargin:nil]; + [self.stackView addSubview:self.gobbler withTopMargin:@"0"]; + [self setNeedsLayout]; + [self layoutIfNeeded]; +} + +- (void)setUpCallbacks +{ + + @weakify(self); + + void (^completion)(void) = ^{ + @strongify(self); + [self.spinner fadeOutAnimated:self.parentViewController.shouldAnimate]; + [self.stackView removeSubview:self.spinner]; + }; + + [self.artwork onArtworkUpdate:^{ + completion(); + } failure:^(NSError *error) { + completion(); + }]; + + [self.artwork onFairUpdate:^(Fair *fair) { + @strongify(self); + [self.metadataView updateWithFair:fair]; + [self.stackView layoutIfNeeded]; + } failure:nil]; + + [self.artwork onSaleArtworkUpdate:^(SaleArtwork *saleArtwork) { + @strongify(self); + if (saleArtwork.auctionState & ARAuctionStateUserIsBidder) { + [ARAnalytics setUserProperty:@"has_placed_bid" toValue:@"true"]; + self.banner.auctionState = saleArtwork.auctionState; + [UIView animateIf:self.parentViewController.shouldAnimate duration:ARAnimationDuration :^{ + [self.banner updateHeightConstraint]; + [self.stackView layoutIfNeeded]; + }]; + } + } failure:nil]; +} + +- (void)createHeightConstraints +{ + NSLayoutConstraint *minimumHeight = [[self.stackView constrainHeightToView:self.superview predicate:@">=0"] lastObject]; + minimumHeight.constant -=self.contentInset.top; + NSLayoutConstraint *imageMaxHeight = [[self.metadataView.imageView constrainHeightToView:self.superview predicate:@"<=0"] lastObject]; + // Make the image height somewhat smaller than the superview height so that Artwork favorite and share buttons are visible. + imageMaxHeight.constant = [UIDevice isPad] ? ARArtworkImageHeightAdjustmentForPad : ARArtworkImageHeightAdjustmentForPhone; +} + +- (void)didMoveToSuperview +{ + if (self.superview) { + [self setUpCallbacks]; + [self createHeightConstraints]; + } +} + +@end diff --git a/Artsy/Classes/Views/ARArtworkWithMetadataThumbnailCell.h b/Artsy/Classes/Views/ARArtworkWithMetadataThumbnailCell.h new file mode 100644 index 00000000000..555aa723619 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkWithMetadataThumbnailCell.h @@ -0,0 +1,14 @@ +/// Shows an image above a standard metadata view + +#import "ARItemThumbnailViewCell.h" +#import "ARFeedImageLoader.h" + +@interface ARArtworkWithMetadataThumbnailCell : ARItemThumbnailViewCell + ++ (CGFloat)heightForMetaData; + +- (void)setupWithRepresentedObject:(Artwork *)artwork; + +@property (nonatomic, assign) enum ARFeedItemImageSize imageSize; + +@end diff --git a/Artsy/Classes/Views/ARArtworkWithMetadataThumbnailCell.m b/Artsy/Classes/Views/ARArtworkWithMetadataThumbnailCell.m new file mode 100644 index 00000000000..de7b73d71f9 --- /dev/null +++ b/Artsy/Classes/Views/ARArtworkWithMetadataThumbnailCell.m @@ -0,0 +1,77 @@ +#import "ARArtworkWithMetadataThumbnailCell.h" +#import "ARArtworkThumbnailMetadataView.h" + +static const CGFloat ARArtworkCellMetadataMargin = 8; + +@interface ARArtworkWithMetadataThumbnailCell() +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) ARArtworkThumbnailMetadataView *metadataView; +@end + +@implementation ARArtworkWithMetadataThumbnailCell + ++ (CGFloat)heightForMetaData +{ + return [ARArtworkThumbnailMetadataView heightForView] + ARArtworkCellMetadataMargin; +} + +- (void)prepareForReuse +{ + self.imageView.image = [ARFeedImageLoader defaultPlaceholder]; + [self.metadataView resetLabels]; +} + +- (void)setupWithRepresentedObject:(Artwork *)artwork +{ + if (!self.imageView) { + UIImageView *imageView = [[UIImageView alloc] init]; + imageView.contentMode = UIViewContentModeScaleAspectFill; + imageView.clipsToBounds = YES; + + [self.contentView addSubview:imageView]; + + [imageView alignTopEdgeWithView:self.contentView predicate:nil]; + [imageView alignCenterXWithView:self.contentView predicate:nil]; + [imageView constrainWidthToView:self.contentView predicate:nil]; + + self.imageView = imageView; + } + + if (!self.metadataView) { + ARArtworkThumbnailMetadataView *metaData = [[ARArtworkThumbnailMetadataView alloc] init]; + [self.contentView addSubview:metaData]; + + NSString *marginFormat = [NSString stringWithFormat:@"%0.f", ARArtworkCellMetadataMargin]; + NSString *heightFormat = [NSString stringWithFormat:@"%0.f", [ARArtworkThumbnailMetadataView heightForView]]; + + [metaData constrainTopSpaceToView:self.imageView predicate:marginFormat]; + [metaData alignBottomEdgeWithView:self.contentView predicate:nil]; + [metaData alignCenterXWithView:self.contentView predicate:nil]; + [metaData constrainWidthToView:self.contentView predicate:nil]; + [metaData constrainHeight:heightFormat]; + + self.metadataView = metaData; + } + + [self layoutIfNeeded]; + + NSString *baseUrl = [artwork baseImageURL]; + + ARFeedItemImageSize size = self.imageSize; + if (self.imageSize == ARFeedItemImageSizeAuto) { + CGSize imageSize = self.imageView.bounds.size; + CGFloat longestDimension = (imageSize.height > imageSize.width)? imageSize.height : imageSize.width; + size = (longestDimension > 200) ? ARFeedItemImageSizeLarge : ARFeedItemImageSizeSmall; + } + + [[ARFeedImageLoader alloc] loadImageAtAddress:baseUrl desiredSize:size + forImageView:self.imageView customPlaceholder:nil]; + + [self.metadataView configureWithArtwork:artwork]; + + self.accessibilityLabel = [artwork title]; + self.isAccessibilityElement = YES; + self.accessibilityTraits = UIAccessibilityTraitButton; +} + +@end diff --git a/Artsy/Classes/Views/ARAspectRatioImageView.h b/Artsy/Classes/Views/ARAspectRatioImageView.h new file mode 100644 index 00000000000..39c65e37acb --- /dev/null +++ b/Artsy/Classes/Views/ARAspectRatioImageView.h @@ -0,0 +1,5 @@ +#import + +@interface ARAspectRatioImageView : UIImageView + +@end diff --git a/Artsy/Classes/Views/ARAspectRatioImageView.m b/Artsy/Classes/Views/ARAspectRatioImageView.m new file mode 100644 index 00000000000..9bf299e80ba --- /dev/null +++ b/Artsy/Classes/Views/ARAspectRatioImageView.m @@ -0,0 +1,15 @@ +#import "ARAspectRatioImageView.h" + +@implementation ARAspectRatioImageView + +- (CGSize)intrinsicContentSize +{ + if (self.image.size.width == 0) { + return (CGSize) { UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric }; + } else { + CGFloat imageRatio = self.image.size.height / self.image.size.width; + return CGSizeMake(CGRectGetWidth(self.bounds), CGRectGetWidth(self.bounds) * imageRatio); + } +} + +@end diff --git a/Artsy/Classes/Views/ARAuctionBannerView.h b/Artsy/Classes/Views/ARAuctionBannerView.h new file mode 100644 index 00000000000..84662e272d3 --- /dev/null +++ b/Artsy/Classes/Views/ARAuctionBannerView.h @@ -0,0 +1,6 @@ +#import + +@interface ARAuctionBannerView : UIView +@property (nonatomic, assign) ARAuctionState auctionState; +- (void)updateHeightConstraint; +@end diff --git a/Artsy/Classes/Views/ARAuctionBannerView.m b/Artsy/Classes/Views/ARAuctionBannerView.m new file mode 100644 index 00000000000..4c077e24b27 --- /dev/null +++ b/Artsy/Classes/Views/ARAuctionBannerView.m @@ -0,0 +1,52 @@ +#import "ARAuctionBannerView.h" + +@interface ARAuctionBannerView () +@property (nonatomic) UILabel *label; +@property (nonatomic, strong) NSLayoutConstraint *heightConstraint; +@end + +@implementation ARAuctionBannerView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + UILabel *label = [[UILabel alloc] init]; + label.numberOfLines = 2; + label.font = [UIFont serifFontWithSize:14]; + label.textAlignment = NSTextAlignmentCenter; + [self addSubview:label]; + self.heightConstraint = [[self constrainHeight:@"0@750"] lastObject]; + [label setContentCompressionResistancePriority:300 forAxis:UILayoutConstraintAxisVertical]; + [label constrainWidthToView:self predicate:@"-40"]; + [label constrainHeightToView:self predicate:@"-40@500"]; + [label alignCenterWithView:self]; + label.text = nil; + _label = label; + } + return self; +} + +- (void)setAuctionState:(ARAuctionState)auctionState +{ + if (auctionState == _auctionState) { + return; + } + _auctionState = auctionState; + + if (auctionState & ARAuctionStateUserIsHighBidder) { + self.backgroundColor = [UIColor artsyPurple]; + self.label.text = @"You are currently the high\nbidder for this lot."; + self.label.textColor = [UIColor whiteColor]; + } else if (auctionState & ARAuctionStateUserIsBidder){ + self.backgroundColor = [UIColor artsyAttention]; + self.label.textColor = [UIColor blackColor]; + self.label.text = @"You’ve been outbid.\nPlease place another bid."; + } +} + +- (void)updateHeightConstraint +{ + self.heightConstraint.constant = self.label.text.length > 0 ? 69 :0; +} +@end diff --git a/Artsy/Classes/Views/ARAuctionBidderStateLabel.h b/Artsy/Classes/Views/ARAuctionBidderStateLabel.h new file mode 100644 index 00000000000..8fb0d6a83d6 --- /dev/null +++ b/Artsy/Classes/Views/ARAuctionBidderStateLabel.h @@ -0,0 +1,5 @@ +@interface ARAuctionBidderStateLabel : ARBorderLabel + +- (void)updateWithSaleArtwork:(SaleArtwork *)saleArtwork; + +@end diff --git a/Artsy/Classes/Views/ARAuctionBidderStateLabel.m b/Artsy/Classes/Views/ARAuctionBidderStateLabel.m new file mode 100644 index 00000000000..f0e0368b4bb --- /dev/null +++ b/Artsy/Classes/Views/ARAuctionBidderStateLabel.m @@ -0,0 +1,20 @@ +#import "ARAuctionBidderStateLabel.h" +#import + +@implementation ARAuctionBidderStateLabel + +-(void)updateWithSaleArtwork:(SaleArtwork *)saleArtwork +{ + ARAuctionState state = saleArtwork.auctionState; + if (state & ARAuctionStateUserIsHighBidder) { + NSString *bidString = [NSNumberFormatter currencyStringForCents:saleArtwork.saleHighestBid.cents]; + self.text = [NSString stringWithFormat:@"You are currently the high bidder for this lot with a bid at %@.", bidString]; + self.textColor = [UIColor artsyPurple]; + } else if (state & ARAuctionStateUserIsBidder) { + NSString *maxBidString = [NSNumberFormatter currencyStringForCents:saleArtwork.userMaxBidderPosition.maxBidAmountCents]; + self.text = [NSString stringWithFormat:@"Your max bid of %@ has been outbid.", maxBidString]; + self.textColor = [UIColor artsyRed]; + } +} + +@end diff --git a/Artsy/Classes/Views/ARBidButton.h b/Artsy/Classes/Views/ARBidButton.h new file mode 100644 index 00000000000..029f95fb1ed --- /dev/null +++ b/Artsy/Classes/Views/ARBidButton.h @@ -0,0 +1,8 @@ +extern NSString * const ARBidButtonRegisterStateTitle; +extern NSString * const ARBidButtonRegisteredStateTitle; +extern NSString * const ARBidButtonBiddingOpenStateTitle; +extern NSString * const ARBidButtonBiddingClosedStateTitle; + +@interface ARBidButton : ARFlatButton +- (void)setAuctionState:(ARAuctionState)state; +@end diff --git a/Artsy/Classes/Views/ARBidButton.m b/Artsy/Classes/Views/ARBidButton.m new file mode 100644 index 00000000000..06f9947caf5 --- /dev/null +++ b/Artsy/Classes/Views/ARBidButton.m @@ -0,0 +1,54 @@ +#import "ARBidButton.h" + +NSString * const ARBidButtonRegisterStateTitle = @"REGISTER TO BID"; +NSString * const ARBidButtonRegisteredStateTitle = @"YOU ARE REGISTERED TO BID"; +NSString * const ARBidButtonBiddingOpenStateTitle = @"BID"; +NSString * const ARBidButtonBiddingClosedStateTitle = @"BIDDING CLOSED"; + +@implementation ARBidButton + +- (void)setup +{ + [super setup]; + + self.titleLabel.font = [UIFont sansSerifFontWithSize:15]; + [self setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; + [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize){ UIViewNoIntrinsicMetric, 46 }; +} + +- (void)setAuctionState:(ARAuctionState)state +{ + NSString *title = nil; + UIColor *backgroundColor = [UIColor blackColor]; + BOOL enabled = YES; + BOOL interactionEnabled = YES; + + if (state & ARAuctionStateEnded) { + title = ARBidButtonBiddingClosedStateTitle; + enabled = NO; + } else if (state & ARAuctionStateStarted) { + title = ARBidButtonBiddingOpenStateTitle; + } else if (state & ARAuctionStateUserIsRegistered) { + title = ARBidButtonRegisteredStateTitle; + // TODO: replace with a standard artsy color + backgroundColor = [UIColor colorWithHex:0x529900]; + // don't want the 'disabled' flavor of the green color + enabled = YES; + interactionEnabled = NO; + } else { + title = ARBidButtonRegisterStateTitle; + } + + [self setTitle:title forState:UIControlStateNormal]; + [self setBackgroundColor:backgroundColor forState:UIControlStateNormal]; + [self setBorderColor:backgroundColor forState:UIControlStateNormal]; + [self setEnabled:enabled animated:YES]; + [self setUserInteractionEnabled:interactionEnabled]; +} + +@end diff --git a/Artsy/Classes/Views/ARBrowseFeaturedLinkInsetCell.h b/Artsy/Classes/Views/ARBrowseFeaturedLinkInsetCell.h new file mode 100644 index 00000000000..a19169cbe2e --- /dev/null +++ b/Artsy/Classes/Views/ARBrowseFeaturedLinkInsetCell.h @@ -0,0 +1,6 @@ + +#import "ARBrowseFeaturedLinksCollectionViewCell.h" + +@interface ARBrowseFeaturedLinkInsetCell : ARBrowseFeaturedLinksCollectionViewCell + +@end diff --git a/Artsy/Classes/Views/ARBrowseFeaturedLinkInsetCell.m b/Artsy/Classes/Views/ARBrowseFeaturedLinkInsetCell.m new file mode 100644 index 00000000000..d4c43198c74 --- /dev/null +++ b/Artsy/Classes/Views/ARBrowseFeaturedLinkInsetCell.m @@ -0,0 +1,45 @@ +#import "ARBrowseFeaturedLinkInsetCell.h" + +@interface ARBrowseFeaturedLinkInsetCell () +@property (nonatomic, strong) UIImageView *overlayImageView; +@end + +@implementation ARBrowseFeaturedLinkInsetCell + +- (void)updateWithTitle:(NSString *)title imageURL:(NSURL *)imageURL +{ + [super updateWithTitle:title imageURL:imageURL]; + + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.font = [self.titleLabel.font fontWithSize:12]; + + UIColor *shadowColor = [UIColor artsyHeavyGrey]; + self.titleLabel.clipsToBounds = NO; + self.titleLabel.layer.shadowOpacity = 0.8; + self.titleLabel.layer.shadowRadius = 2.0; + self.titleLabel.layer.shadowOffset = CGSizeZero; + self.titleLabel.layer.shadowColor = shadowColor.CGColor; + self.titleLabel.layer.shouldRasterize = YES; +} + +- (void)setImageWithURL:(NSURL *)imageURL +{ + @weakify(self); + + [self.imageView ar_setImageWithURL:imageURL + completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { + @strongify(self); + + if (!image || error || self.overlayImageView) { + return; + } + + self.overlayImageView = [[UIImageView alloc] init]; + [self.contentView insertSubview:self.overlayImageView belowSubview:self.titleLabel]; + self.overlayImageView.image = [UIImage imageNamed:@"Image_Shadow_Overlay.png"]; + self.overlayImageView.frame = self.contentView.frame; + } + ]; +} + +@end diff --git a/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionView.h b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionView.h new file mode 100644 index 00000000000..97afb69af49 --- /dev/null +++ b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionView.h @@ -0,0 +1,21 @@ +typedef NS_ENUM(NSInteger, ARFeaturedLinkStyle){ + ARFeaturedLinkLayoutSingleRow, + ARFeaturedLinkLayoutDoubleRow, + ARFeaturedLinkLayoutSinglePaging, +}; + +@class ARBrowseFeaturedLinksCollectionView; + +@protocol ARBrowseFeaturedLinksCollectionViewDelegate +@required +- (void)didSelectFeaturedLink:(FeaturedLink *)featuredLink; +@end + +@interface ARBrowseFeaturedLinksCollectionView : UICollectionView + +- (instancetype)initWithStyle:(enum ARFeaturedLinkStyle)style; + +@property (nonatomic, copy, readwrite) NSArray *featuredLinks; +@property (nonatomic, assign, readonly) ARFeaturedLinkStyle style; +@property (nonatomic, strong, readwrite) id selectionDelegate; +@end diff --git a/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionView.m b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionView.m new file mode 100644 index 00000000000..1cb02afbd0b --- /dev/null +++ b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionView.m @@ -0,0 +1,220 @@ +#import "ARBrowseFeaturedLinksCollectionView.h" +#import "ARBrowseFeaturedLinksCollectionViewCell.h" +#import "ARBrowseFeaturedLinkInsetCell.h" + +static CGFloat const ARPagingStyleDimension = 195; +static CGFloat const ARPagingStyleSpacing = 13; +static CGFloat const ARDoubleRowStyleSpacing = 11; + +@interface ARBrowseFeaturedLinksCollectionView() +@property (nonatomic, strong) UIScrollView *secondaryScroll; +@end + +@implementation ARBrowseFeaturedLinksCollectionView + +- (instancetype)initWithStyle:(enum ARFeaturedLinkStyle)style +{ + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + CGFloat width = [UIScreen mainScreen].bounds.size.width; + CGFloat height = [self heightForCollectionViewWithStyle:style]; + self = [super initWithFrame:CGRectMake(0, 0, width, height) collectionViewLayout:flowLayout]; + if (!self) { return nil; } + + _style = style; + flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + self.backgroundColor = [UIColor whiteColor]; + self.dataSource = self; + self.delegate = self; + self.showsHorizontalScrollIndicator = NO; + + if (style == ARFeaturedLinkLayoutSinglePaging) { + flowLayout.minimumLineSpacing = ARPagingStyleSpacing; + [self setupPaging]; + [self registerClass:ARBrowseFeaturedLinkInsetCell.class forCellWithReuseIdentifier:self.reuseIdentifier]; + + } else { + [self registerClass:ARBrowseFeaturedLinksCollectionViewCell.class forCellWithReuseIdentifier:self.reuseIdentifier]; + } + + return self; +} + +- (void)setupPaging +{ + // Create a second scroll view which pages and is the size of a collection + // view cell. Then move the scrollviews pan gesture to the collectionview. + + // Works, bit cray. + // http://khanlou.com/2013/04/paging-a-overflowing-collection-view/ + + CGFloat totalPageWidth = ARPagingStyleDimension + ARPagingStyleSpacing; + CGFloat margin = (CGRectGetWidth(self.frame) - totalPageWidth)/ 2; + self.contentInset = UIEdgeInsetsMake(0, margin, 0, 0); + + _secondaryScroll = [[UIScrollView alloc] initWithFrame:self.frame]; + _secondaryScroll.bounds = CGRectMake(0, 0, totalPageWidth, ARPagingStyleDimension); + _secondaryScroll.clipsToBounds = NO; + _secondaryScroll.delegate = self; + _secondaryScroll.hidden = YES; + _secondaryScroll.pagingEnabled = YES; + + [self addSubview:_secondaryScroll]; + + [self addGestureRecognizer:_secondaryScroll.panGestureRecognizer]; + self.panGestureRecognizer.enabled = NO; + self.scrollEnabled = NO; +} + + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + if (scrollView == self.secondaryScroll) { + // Ignore collection view scrolling callbacks + CGPoint contentOffset = scrollView.contentOffset; + contentOffset.x = contentOffset.x - self.contentInset.left; + self.contentOffset = contentOffset; + } +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, [self heightForCollectionViewWithStyle:self.style]); +} + +- (CGFloat)heightForCollectionViewWithStyle:(ARFeaturedLinkStyle)style +{ + CGFloat cellHeight = [self heightForCellWithStyle:style]; + if (style == ARFeaturedLinkLayoutDoubleRow) { + return cellHeight * 2 + ARDoubleRowStyleSpacing; + } else { + return cellHeight; + } +} +- (CGFloat)heightForCellWithStyle:(ARFeaturedLinkStyle)style +{ + + switch (style) { + case ARFeaturedLinkLayoutSingleRow: + if ([UIDevice isPhone]) { + return 200; + } else if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { + return 310; + } else { + return 226; + } + case ARFeaturedLinkLayoutSinglePaging: + return ARPagingStyleDimension; + + case ARFeaturedLinkLayoutDoubleRow: + if ([UIDevice isPhone]) { + return 90; + } else if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { + return 120; + } else { + return 85; + } + } +} + +- (CGFloat)widthForCellWithStyle:(ARFeaturedLinkStyle)style{ + + switch (self.style) { + case ARFeaturedLinkLayoutSingleRow: + if ([UIDevice isPhone]) { + return 272; + } else if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { + return 456; + } else { + return 328; + } + case ARFeaturedLinkLayoutSinglePaging: + return ARPagingStyleDimension; + + case ARFeaturedLinkLayoutDoubleRow: + if ([UIDevice isPhone]) { + return 132; + } else if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { + return 176; + } else { + return 125; + } + } +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat width = [self widthForCellWithStyle:self.style]; + CGFloat height = [self heightForCellWithStyle:self.style]; + + if (!(width && height)) { return CGSizeZero; } + + return CGSizeMake(width, height); +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section +{ + CGFloat inset; + if (self.style == ARFeaturedLinkLayoutSinglePaging){ + inset = ARPagingStyleSpacing / 2; + } else if ([UIDevice isPhone]) { + inset = 20; + } else { + inset = 50; + } + return UIEdgeInsetsMake(0, inset, 0, inset); +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section; +{ + return self.featuredLinks.count; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; +{ + ARBrowseFeaturedLinksCollectionViewCell *cell = (id)[self dequeueReusableCellWithReuseIdentifier:self.reuseIdentifier forIndexPath:indexPath]; + + FeaturedLink *link = self.featuredLinks[indexPath.row]; + NSURL *imageURL; + switch (self.style){ + case ARFeaturedLinkLayoutSingleRow: + imageURL = link.largeImageURL; + break; + case ARFeaturedLinkLayoutSinglePaging: + imageURL = link.largeImageURL; + break; + case ARFeaturedLinkLayoutDoubleRow: + imageURL = link.smallImageURL; + break; + } + + [cell updateWithTitle:link.title imageURL:imageURL]; + + return cell; +} + +- (void)setFeaturedLinks:(NSArray *)featuredLinks +{ + _featuredLinks = featuredLinks.copy; + [self reloadData]; + if (self.secondaryScroll){ + self.secondaryScroll.contentSize = [self.collectionViewLayout collectionViewContentSize]; + self.secondaryScroll.contentOffset = CGPointMake(ARPagingStyleDimension + ARPagingStyleSpacing, 0); + }; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + FeaturedLink *link = self.featuredLinks[indexPath.row]; + [self.selectionDelegate didSelectFeaturedLink:link]; +} + +- (NSString *)reuseIdentifier +{ + if (self.style == ARFeaturedLinkLayoutSinglePaging) { + return [ARBrowseFeaturedLinkInsetCell reuseID]; + } + + return [ARBrowseFeaturedLinksCollectionViewCell reuseID]; +} + +@end diff --git a/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionViewCell.h b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionViewCell.h new file mode 100644 index 00000000000..aba4ec4cf2c --- /dev/null +++ b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionViewCell.h @@ -0,0 +1,12 @@ +#import "FeaturedLink.h" + +@interface ARBrowseFeaturedLinksCollectionViewCell : UICollectionViewCell + +- (void)updateWithTitle:(NSString *)title imageURL:(NSURL *)imageURL; + ++ (NSString *)reuseID; + +@property (nonatomic, strong, readonly) UIImageView *imageView; +@property (nonatomic, strong, readonly) UILabel *titleLabel; + +@end diff --git a/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionViewCell.m b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionViewCell.m new file mode 100644 index 00000000000..c8eeb817426 --- /dev/null +++ b/Artsy/Classes/Views/ARBrowseFeaturedLinksCollectionViewCell.m @@ -0,0 +1,49 @@ +#import "ARBrowseFeaturedLinksCollectionViewCell.h" + +@interface ARBrowseFeaturedLinksCollectionViewCell() +@end + +@implementation ARBrowseFeaturedLinksCollectionViewCell + +- (void)updateWithTitle:(NSString *)title imageURL:(NSURL *)imageURL; +{ + if (!self.imageView) { + _imageView = [[UIImageView alloc] init]; + self.imageView.contentMode = UIViewContentModeScaleAspectFill; + self.imageView.clipsToBounds = YES; + + [self.contentView addSubview:self.imageView]; + [self.imageView alignToView:self.contentView]; + } + + if (!self.titleLabel) { + _titleLabel = [[UILabel alloc] init]; + self.titleLabel.textColor = [UIColor whiteColor]; + self.titleLabel.font = [UIFont sansSerifFontWithSize:12]; + + [self.contentView addSubview:self.titleLabel]; + + [self.titleLabel constrainWidthToView:self.contentView predicate:@"-20"]; + [self.titleLabel alignCenterXWithView:self.contentView predicate:nil]; + [self.titleLabel alignBottomEdgeWithView:self.contentView predicate:@"-12"]; + } + + [self setImageWithURL:imageURL]; + self.titleLabel.text = title.uppercaseString; + + self.isAccessibilityElement = YES; + self.accessibilityLabel = title; + self.accessibilityTraits = UIAccessibilityTraitButton; +} + ++ (NSString *)reuseID +{ + return NSStringFromClass(self.class); +} + +- (void)setImageWithURL:(NSURL *)imageURL +{ + [self.imageView ar_setImageWithURL:imageURL]; +} + +@end diff --git a/Artsy/Classes/Views/ARButtonWithCircularImage.h b/Artsy/Classes/Views/ARButtonWithCircularImage.h new file mode 100644 index 00000000000..4e0f5b48567 --- /dev/null +++ b/Artsy/Classes/Views/ARButtonWithCircularImage.h @@ -0,0 +1,5 @@ +#import "ARButtonWithImage.h" + +@interface ARButtonWithCircularImage : ARButtonWithImage + +@end diff --git a/Artsy/Classes/Views/ARButtonWithCircularImage.m b/Artsy/Classes/Views/ARButtonWithCircularImage.m new file mode 100644 index 00000000000..d9c3cc9c759 --- /dev/null +++ b/Artsy/Classes/Views/ARButtonWithCircularImage.m @@ -0,0 +1,15 @@ +#import "ARButtonWithCircularImage.h" + + +@implementation ARButtonWithCircularImage + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { return nil; } + self.buttonImageView.layer.cornerRadius = 40; + self.buttonImageView.clipsToBounds = YES; + + return self; +} + +@end diff --git a/Artsy/Classes/Views/ARCollapsableTextView.h b/Artsy/Classes/Views/ARCollapsableTextView.h new file mode 100644 index 00000000000..fdf36052911 --- /dev/null +++ b/Artsy/Classes/Views/ARCollapsableTextView.h @@ -0,0 +1,11 @@ +#import "ARTextView.h" +@class ARCollapsableTextView; + +typedef void (^ARCollapsableTextBlock)(ARCollapsableTextView *textView); + +/// Defaults to collapsed. +@interface ARCollapsableTextView : ARTextView + +@property (nonatomic, copy) ARCollapsableTextBlock expansionBlock; + +@end diff --git a/Artsy/Classes/Views/ARCollapsableTextView.m b/Artsy/Classes/Views/ARCollapsableTextView.m new file mode 100644 index 00000000000..48a6489b25b --- /dev/null +++ b/Artsy/Classes/Views/ARCollapsableTextView.m @@ -0,0 +1,94 @@ +#import "ARCollapsableTextView.h" + +static const CGFloat ARCollapsableTextViewHeight = 80; + +@interface ARCollapsableTextView() +@property (nonatomic, assign) CGFloat collapsedHeight; +@property (nonatomic, strong) UITapGestureRecognizer *tapGesture; +@property (nonatomic, strong) UISwipeGestureRecognizer *downSwipeGesture; +@property (nonatomic, strong) NSLayoutConstraint *heightCollapsingConstraint; +@property (nonatomic, strong) UIView *collapsedOverlapView; +@end + +@implementation ARCollapsableTextView + +- (instancetype)init +{ + self = [super init]; + if (self) { + _collapsedHeight = ARCollapsableTextViewHeight; + + _heightCollapsingConstraint = [[self constrainHeight:@(_collapsedHeight).stringValue] lastObject]; + } + return self; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + [super setAttributedText:attributedText]; + + if (attributedText && !self.tapGesture) { + + // Only show the more indicator if the height of the text exceeds the height of the constraint. + if (self.intrinsicContentSize.height > self.heightCollapsingConstraint.constant) { + + self.collapsedOverlapView = [[UIView alloc] init]; + self.collapsedOverlapView.backgroundColor = [UIColor whiteColor]; + [self addSubview:self.collapsedOverlapView]; + + NSString *accuratePositionString = [NSString stringWithFormat:@"%0.f", self.collapsedHeight - 12]; + + [self.collapsedOverlapView constrainWidthToView:self predicate:nil]; + [self.collapsedOverlapView alignBottomEdgeWithView:self predicate:accuratePositionString]; + [self.collapsedOverlapView alignCenterXWithView:self predicate:nil]; + [self.collapsedOverlapView constrainHeight:@"8"]; + + UIView *border = [[UIView alloc] init]; + border.backgroundColor = [UIColor artsyMediumGrey]; + [self.collapsedOverlapView addSubview:border]; + [border constrainWidthToView:self predicate:nil]; + [border alignTopEdgeWithView:_collapsedOverlapView predicate:nil]; + [border alignCenterXWithView:self predicate:nil]; + [border constrainHeight:@"1"]; + + UIImageView *hintImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"SmallMoreVerticalArrow"]]; + [self.collapsedOverlapView addSubview:hintImage]; + [hintImage constrainTopSpaceToView:border predicate:@"8"]; + [hintImage alignCenterXWithView:self.collapsedOverlapView predicate:nil]; + + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openToFullHeight)]; + [self addGestureRecognizer:tapGesture]; + self.tapGesture = tapGesture; + + [self setNeedsLayout]; + [self.superview setNeedsLayout]; + [self layoutIfNeeded]; + [self.superview layoutIfNeeded]; + } else { + self.heightCollapsingConstraint.constant = self.intrinsicContentSize.height; + } + } +} + +- (void)openToFullHeight +{ + self.tapGesture.enabled = NO; + self.downSwipeGesture.enabled = NO; + + [self layoutIfNeeded]; + [UIView animateWithDuration:0.3 animations:^{ + self.heightCollapsingConstraint.constant = self.intrinsicContentSize.height; + self.collapsedOverlapView.alpha = 0; + + [self setNeedsLayout]; + [self.superview setNeedsLayout]; + [self layoutIfNeeded]; + [self.superview layoutIfNeeded]; + }]; + + if (self.expansionBlock) { + self.expansionBlock(self); + } +} + +@end diff --git a/Artsy/Classes/Views/ARCountdownView.h b/Artsy/Classes/Views/ARCountdownView.h new file mode 100644 index 00000000000..8962b599b7c --- /dev/null +++ b/Artsy/Classes/Views/ARCountdownView.h @@ -0,0 +1,17 @@ +#import + +@class ARCountdownView; + +@protocol ARCountdownViewDelegate +- (void)countdownViewDidFinish:(ARCountdownView *)countdownView; +@end + +@interface ARCountdownView : UIView +- (void)startTimer; +- (void)stopTimer; + +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) NSDate *targetDate; +@property (nonatomic, copy) NSString *heading; + +@end diff --git a/Artsy/Classes/Views/ARCountdownView.m b/Artsy/Classes/Views/ARCountdownView.m new file mode 100644 index 00000000000..5b880120d19 --- /dev/null +++ b/Artsy/Classes/Views/ARCountdownView.m @@ -0,0 +1,100 @@ +#import "ARCountdownView.h" + +#define kARCountdownViewWidth 160 +#define kARCountdownViewHeight 50 + +@interface ARCountdownView () +@property (nonatomic, strong) NSTimer *timer; +@property (nonatomic, strong) UILabel *countdown; +@property (nonatomic, strong) UILabel *headingLabel; +@end + +@implementation ARCountdownView + +- (instancetype)init +{ + self = [super init]; + if (self) { + [self setupSubviews]; + } + return self; +} + +- (void)setupSubviews +{ + self.bounds = (CGRect) { CGPointZero, [self intrinsicContentSize] }; + UILabel *headingLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, kARCountdownViewWidth, 14)]; + headingLabel.textAlignment = NSTextAlignmentCenter; + headingLabel.font = [UIFont smallCapsSerifFontWithSize:14]; + headingLabel.textColor = [UIColor blackColor]; + [self addSubview:headingLabel]; + self.headingLabel = headingLabel; + + self.countdown = [[UILabel alloc] initWithFrame:CGRectMake(0, 10, kARCountdownViewWidth, 30)]; + self.countdown.font = [UIFont sansSerifFontWithSize:20]; + self.countdown.textColor = [UIColor blackColor]; + self.countdown.text = [self countdownString]; + self.countdown.textAlignment = NSTextAlignmentCenter; + [self addSubview:self.countdown]; + + [@[@"Days", @"Hrs", @"Min", @"Sec"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5 + idx * 38, 38, 38, 10)]; + label.text = [obj uppercaseString]; + label.textColor = [UIColor artsyHeavyGrey]; + label.font = [UIFont sansSerifFontWithSize:8]; + label.textAlignment = NSTextAlignmentCenter; + [self addSubview:label]; + }]; +} + +- (void)startTimer +{ + if (self.timer) { + return; + } + self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(tick:) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; + [self tick:self.timer]; +} + +- (void)stopTimer +{ + [self.timer invalidate]; + self.timer = nil; +} + +- (void)setHeading:(NSString *)heading +{ + _heading = [heading copy]; + self.headingLabel.text = [heading uppercaseString]; +} + +- (void)tick:(NSTimer *)timer +{ + self.countdown.text = [self countdownString]; +} + +- (NSString *)countdownString +{ + NSDate *now = [ARSystemTime date]; + //TODO: better "failure" state here? + if ([now compare:self.targetDate] != NSOrderedAscending) { + [self stopTimer]; + [self.delegate countdownViewDidFinish:self]; + return @"00 : 00 : 00 : 00"; + } + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSCalendarUnit dhms = NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; + NSDateComponents *components = [calendar components:dhms + fromDate:now + toDate:self.targetDate + options:0]; + return [NSString stringWithFormat:@"%02d : %02d : %02d : %02d", (unsigned int)components.day, (unsigned int)components.hour, (unsigned int)components.minute, (unsigned int)components.second]; +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize){ kARCountdownViewWidth, kARCountdownViewHeight }; +} + +@end diff --git a/Artsy/Classes/Views/ARCustomEigenLabels.h b/Artsy/Classes/Views/ARCustomEigenLabels.h new file mode 100644 index 00000000000..8a8043363eb --- /dev/null +++ b/Artsy/Classes/Views/ARCustomEigenLabels.h @@ -0,0 +1,8 @@ +@interface ARArtworkTitleLabel: ARItalicsSerifLabel +- (void)setTitle:(NSString *)artworkTitle date:(NSString *)date; +@end + +// Title label for use at the top of certain views + +@interface ARSansSerifHeaderLabel : ARLabel +@end \ No newline at end of file diff --git a/Artsy/Classes/Views/ARCustomEigenLabels.m b/Artsy/Classes/Views/ARCustomEigenLabels.m new file mode 100644 index 00000000000..7338b207284 --- /dev/null +++ b/Artsy/Classes/Views/ARCustomEigenLabels.m @@ -0,0 +1,51 @@ +@interface ARLabel (Private) +- (void)setup; +@end + +@implementation ARArtworkTitleLabel + +- (void)setTitle:(NSString *)artworkTitle date:(NSString *)date; +{ + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setLineSpacing:3]; + + NSMutableAttributedString *titleAndDate = [[NSMutableAttributedString alloc] initWithString:artworkTitle ?: @"" attributes:@{ + NSParagraphStyleAttributeName: paragraphStyle + }]; + + if (date.length > 0) { + NSString *formattedTitleDate = [@", " stringByAppendingString:date]; + NSAttributedString *andDate = [[NSAttributedString alloc] initWithString:formattedTitleDate attributes:@{ + NSFontAttributeName : [UIFont serifFontWithSize:self.font.pointSize] + }]; + [titleAndDate appendAttributedString:andDate]; + } + + self.font = [UIFont serifItalicFontWithSize:self.font.pointSize]; + self.numberOfLines = 0; + self.attributedText = titleAndDate; +} + +@end + +@implementation ARSansSerifHeaderLabel + +- (void)setup +{ + self.backgroundColor = [UIColor whiteColor]; + self.opaque = YES; + CGFloat fontSize = [UIDevice isPad] ? 25 : 18; + self.font = [UIFont sansSerifFontWithSize:fontSize]; + self.numberOfLines = 0; + self.lineBreakMode = NSLineBreakByWordWrapping; + + self.textAlignment = NSTextAlignmentCenter; + self.preferredMaxLayoutWidth = [UIDevice isPad] ? 640 : 200; +} + +- (void)setText:(NSString *)text +{ + [self setText:text.uppercaseString withLetterSpacing:[UIDevice isPad] ? 1.9 : 0.5]; +} + +@end \ No newline at end of file diff --git a/Artsy/Classes/Views/ARFairMapAnnotationCallOutView.h b/Artsy/Classes/Views/ARFairMapAnnotationCallOutView.h new file mode 100644 index 00000000000..4b1598b2fa1 --- /dev/null +++ b/Artsy/Classes/Views/ARFairMapAnnotationCallOutView.h @@ -0,0 +1,11 @@ +#import "ARFairMapAnnotation.h" + +@interface ARFairMapAnnotationCallOutView : UIView + +- (id)initOnMapView:(NAMapView *)mapView fair:(Fair *)fair; + +@property (nonatomic, strong, readonly) Fair *fair; + +@property(readwrite, nonatomic, strong) ARFairMapAnnotation *annotation; + +@end \ No newline at end of file diff --git a/Artsy/Classes/Views/ARFairMapAnnotationCallOutView.m b/Artsy/Classes/Views/ARFairMapAnnotationCallOutView.m new file mode 100644 index 00000000000..187a6799e90 --- /dev/null +++ b/Artsy/Classes/Views/ARFairMapAnnotationCallOutView.m @@ -0,0 +1,158 @@ +#import "ARFairMapAnnotationCallOutView.h" +#import "ARFairShowViewController.h" + +@interface ARFairMapAnnotationCallOutView () + +@property (nonatomic, strong, readonly) UILabel *partnerLocation; +@property (nonatomic, strong, readonly) UILabel *partnerName; +@property (nonatomic, strong, readonly) UIImageView *partnerImage; +@property (nonatomic, strong, readonly) UIImage *defaultPartnerImage; +@property (nonatomic, strong, readonly) UIImageView *anchorImage; +@property (nonatomic, strong, readonly) UIImageView *arrowImage; +@property (nonatomic, strong, readonly) UIView *verticalSeparator; + +@property (nonatomic, assign) CGPoint position; + +@property (nonatomic, weak) NAMapView *mapView; + +@end + +@implementation ARFairMapAnnotationCallOutView + +- (id)initOnMapView:(NAMapView *)mapView fair:(Fair *)fair +{ + self = [super init]; + if (!self) { return nil; } + + [self constrainHeight:@"80"]; + + UIView *container = [[UIView alloc] init]; + container.backgroundColor = [UIColor blackColor]; + [self addSubview:container]; + [container alignLeadingEdgeWithView:self predicate:@"0"]; + [container constrainHeight:@"60"]; + [container constrainWidth:[NSString stringWithFormat:@"%@", @(CGRectGetWidth(mapView.bounds) - 40)]]; + + UIImageView *partnerImage = [[UIImageView alloc] init]; + [container addSubview:partnerImage]; + _partnerImage = partnerImage; + _defaultPartnerImage = [UIImage imageNamed:@"MapAnnotationCallout_Partner"]; + [partnerImage constrainHeightToView:container predicate:@"-20"]; + [partnerImage alignAttribute:NSLayoutAttributeWidth toAttribute:NSLayoutAttributeHeight ofView:partnerImage predicate:@"0"]; + [partnerImage alignCenterYWithView:container predicate:@"0"]; + [partnerImage alignLeadingEdgeWithView:container predicate:@"10"]; + + UIView *labelView = [[UIView alloc] init]; + [container addSubview:labelView]; + [labelView alignCenterYWithView:container predicate:@"0"]; + [labelView alignAttribute:NSLayoutAttributeLeading toAttribute:NSLayoutAttributeTrailing ofView:partnerImage predicate:@"10"]; + + UILabel *partnerName = [ARThemedFactory labelForLinkItemTitles]; + partnerName.textColor = [UIColor whiteColor]; + partnerName.backgroundColor = [UIColor clearColor]; + partnerName.numberOfLines = 1; + partnerName.lineBreakMode = NSLineBreakByTruncatingTail; + [labelView addSubview:partnerName]; + _partnerName = partnerName; + [partnerName alignTopEdgeWithView:labelView predicate:@"0"]; + [partnerName alignLeadingEdgeWithView:labelView predicate:@"0"]; + [partnerName alignTrailingEdgeWithView:labelView predicate:@"0"]; + + UILabel *partnerLocation = [ARThemedFactory labelForLinkItemTitles]; + partnerLocation.textColor = [UIColor artsyMediumGrey]; + partnerLocation.backgroundColor = [UIColor clearColor]; + partnerLocation.numberOfLines = 1; + partnerLocation.lineBreakMode = NSLineBreakByTruncatingHead; + [labelView addSubview:partnerLocation]; + _partnerLocation = partnerLocation; + [partnerLocation constrainTopSpaceToView:partnerName predicate:@"0"]; + [partnerLocation alignLeadingEdgeWithView:self.partnerName predicate:@"0"]; + [partnerLocation alignTrailingEdgeWithView:labelView predicate:@"0"]; + + [labelView alignBottomEdgeWithView:partnerLocation predicate:@"0"]; + + UIView *separator = [[UIView alloc] init]; + separator.backgroundColor = [UIColor artsyLightGrey]; + [container addSubview:separator]; + _verticalSeparator = separator; + [separator alignAttribute:NSLayoutAttributeLeft toAttribute:NSLayoutAttributeRight ofView:labelView predicate:@"10"]; + [separator constrainWidth:@"1"]; + [separator alignTopEdgeWithView:container predicate:@"10"]; + [separator alignBottomEdgeWithView:container predicate:@"-10"]; + + UIImageView *arrowImage = [[UIImageView alloc] init]; + arrowImage.contentMode = UIViewContentModeScaleAspectFit; + [arrowImage setImage:[UIImage imageNamed:@"MapAnnotationCallout_Arrow"]]; + [container addSubview:arrowImage]; + _arrowImage = arrowImage; + [arrowImage alignAttribute:NSLayoutAttributeLeft toAttribute:NSLayoutAttributeRight ofView:separator predicate:@"20"]; + [arrowImage alignCenterYWithView:container predicate:@"0"]; + [arrowImage alignTrailingEdgeWithView:container predicate:@"-20"]; + [arrowImage setContentHuggingPriority:500 forAxis:UILayoutConstraintAxisHorizontal]; + + UIImageView *anchorImage = [[UIImageView alloc] init]; + anchorImage.contentMode = UIViewContentModeScaleAspectFit; + [anchorImage setImage:[UIImage imageNamed:@"MapAnnotationCallout_Anchor"]]; + [self addSubview:anchorImage]; + [anchorImage constrainTopSpaceToView:container predicate:@"0"]; + [anchorImage alignLeadingEdgeWithView:self predicate:@"0"]; + [anchorImage alignTrailingEdgeWithView:self predicate:@"0"]; + [anchorImage constrainHeight:@"10"]; + + [self alignBottomEdgeWithView:anchorImage predicate:@"10"]; + [self alignTrailingEdgeWithView:container predicate:@"0"]; + + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + [container addGestureRecognizer:tap]; + + _fair = fair; + _mapView = mapView; + return self; +} + +- (void)setAnnotation:(ARFairMapAnnotation *)annotation +{ + _annotation = annotation; + self.partnerName.text = annotation.title; + self.partnerLocation.text = annotation.subTitle; + self.position = annotation.point; + + id representedObject = self.annotation.representedObject; + if([representedObject isKindOfClass:PartnerShow.class]) { + PartnerShow *partnerShow = (PartnerShow *) representedObject; + self.partnerImage.hidden = NO; + [self.partnerImage setImage:self.defaultPartnerImage]; + [self.partnerImage ar_setImageWithURL:[partnerShow imageURLWithFormatName:@"square"]]; + self.verticalSeparator.hidden = NO; + self.arrowImage.hidden = NO; + } else { + self.partnerImage.hidden = YES; + self.verticalSeparator.hidden = self.arrowImage.hidden = (annotation.href ? NO : YES); + } + + [self updatePosition]; +} + +#pragma - Private helpers + +- (void)updatePosition +{ + CGPoint point = [self.mapView zoomRelativePoint:self.position]; + CGFloat xPos = point.x - (self.frame.size.width / 2.0f); + CGFloat yPos = point.y - (self.frame.size.height); + self.frame = CGRectMake(floor(xPos), yPos, self.frame.size.width, self.frame.size.height); +} + +- (void)tapped:(id)sender +{ + id representedObject = self.annotation.representedObject; + if([representedObject isKindOfClass:PartnerShow.class]) { + PartnerShow *partnerShow = (PartnerShow *) representedObject; + ARFairShowViewController *viewController = [[ARSwitchBoard sharedInstance] loadShow:partnerShow fair:self.fair]; + [[ARTopMenuViewController sharedController] pushViewController:viewController]; + } else if (self.annotation.href) { + [[ARSwitchBoard sharedInstance] loadPath:self.annotation.href]; + } +} + +@end diff --git a/Artsy/Classes/Views/ARFavoriteItemViewCell.h b/Artsy/Classes/Views/ARFavoriteItemViewCell.h new file mode 100644 index 00000000000..9df8fc49830 --- /dev/null +++ b/Artsy/Classes/Views/ARFavoriteItemViewCell.h @@ -0,0 +1,7 @@ +@interface ARFavoriteItemViewCell : UICollectionViewCell + ++ (CGFloat)heightForCellWithOrientation:(UIInterfaceOrientation)orientation; ++ (CGFloat)widthForCellWithOrientation:(UIInterfaceOrientation)orientation; +- (void)setupWithRepresentedObject:(id)object; + +@end diff --git a/Artsy/Classes/Views/ARFavoriteItemViewCell.m b/Artsy/Classes/Views/ARFavoriteItemViewCell.m new file mode 100644 index 00000000000..f001dbf7f28 --- /dev/null +++ b/Artsy/Classes/Views/ARFavoriteItemViewCell.m @@ -0,0 +1,91 @@ +#import "ARFavoriteItemViewCell.h" +#import "ARFeedImageLoader.h" + +static const CGFloat ARFavoriteCellMetadataMargin = 8; +static const CGFloat ARFavoriteCellLabelHeight = 34; + +@interface ARFavoriteItemViewCell() +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) NSLayoutConstraint *imageHeightConstraint; +@end + +@implementation ARFavoriteItemViewCell + +- (void)prepareForReuse +{ + self.titleLabel.text = @""; + self.imageView.image = [ARFeedImageLoader defaultPlaceholder]; +} + ++ (CGFloat)heightForCellWithOrientation:(UIInterfaceOrientation)orientation +{ + return ARFavoriteCellLabelHeight + ARFavoriteCellMetadataMargin + [self heightForImageWithOrientation:orientation]; +} + ++ (CGFloat)heightForImageWithOrientation:(UIInterfaceOrientation)orientation +{ + if ([UIDevice isPad]) { + if (UIInterfaceOrientationIsLandscape(orientation)) { + return 184; + } else { + return 134; + } + } else { + return 90; + } +} + ++ (CGFloat)widthForCellWithOrientation:(UIInterfaceOrientation)orientation +{ + if ([UIDevice isPad]) { + if (UIInterfaceOrientationIsLandscape(orientation)) { + return 276; + } else { + return 201; + } + } else { + return 130; + } +} + +- (void)setupWithRepresentedObject:(id)object +{ + if (!self.imageView) { + UIImageView *imageView = [[UIImageView alloc] init]; + imageView.contentMode = UIViewContentModeScaleAspectFill; + imageView.clipsToBounds = YES; + [self.contentView addSubview:imageView]; + [imageView alignTop:@"0" leading:@"0" bottom:nil trailing:@"0" toView:self.contentView]; + [imageView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical]; + self.imageView = imageView; + } + + if ([object respondsToSelector:@selector(largeImageURL)]) { + NSURL *url = [object largeImageURL]; + [self.imageView ar_setImageWithURL:url]; + } + + if (!self.titleLabel) { + UILabel *label = [[UILabel alloc] init]; + label.font = [UIFont sansSerifFontWithSize:12]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.backgroundColor = [UIColor whiteColor]; + label.opaque = YES; + [self.contentView addSubview:label]; + [label constrainTopSpaceToView:self.imageView predicate:@(ARFavoriteCellMetadataMargin).stringValue]; + [label alignBottomEdgeWithView:self.contentView predicate:@"0"]; + [label constrainWidthToView:self.contentView predicate:nil]; + [label alignCenterXWithView:self.contentView predicate:nil]; + [label constrainHeight:@(ARFavoriteCellLabelHeight).stringValue]; + self.titleLabel = label; + } + + if ([object respondsToSelector:@selector(name)]) { + self.titleLabel.text = [object name].uppercaseString; + } + [self layoutIfNeeded]; +} + +@end diff --git a/Artsy/Classes/Views/ARFollowableButton.h b/Artsy/Classes/Views/ARFollowableButton.h new file mode 100644 index 00000000000..e0efce6a1dc --- /dev/null +++ b/Artsy/Classes/Views/ARFollowableButton.h @@ -0,0 +1,17 @@ +/// Like a normal button but can be automated to deal with a ARFollowableNetworkModel's KVO + +@class ARFollowableNetworkModel; + +@interface ARFollowableButton : ARFlatButton + + +- (void)setupKVOOnNetworkModel:(ARFollowableNetworkModel *)model; +- (void)setFollowingStatus:(BOOL)following; + +// Defaults to "Follow" +@property (readwrite, nonatomic, copy) NSString *toFollowTitle; + +// Defaults to "Following" +@property (readwrite, nonatomic, copy) NSString *toUnfollowTitle; + +@end diff --git a/Artsy/Classes/Views/ARFollowableButton.m b/Artsy/Classes/Views/ARFollowableButton.m new file mode 100644 index 00000000000..458d8b28f3e --- /dev/null +++ b/Artsy/Classes/Views/ARFollowableButton.m @@ -0,0 +1,76 @@ +#import "ARFollowableButton.h" +#import "ARFollowableNetworkModel.h" + +@interface ARFollowableButton () +@property (readwrite, nonatomic, weak) ARFollowableNetworkModel *model; +@property (readonly, nonatomic, assign) BOOL followingStatus; +@end + +@implementation ARFollowableButton + +- (void)setup + +{ + [super setup]; + + [self setBackgroundColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [self setBorderColor:[UIColor artsyMediumGrey] forState:UIControlStateNormal];; + self.toFollowTitle = @"Follow"; + self.toUnfollowTitle = @"Following"; + + [self setFollowingStatus:NO]; +} + +- (void)setupKVOOnNetworkModel:(ARFollowableNetworkModel *)model +{ + [model addObserver:self forKeyPath:@keypath(ARFollowableNetworkModel.new, following) options:NSKeyValueObservingOptionNew context:nil]; + self.model = model; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(ARFollowableNetworkModel *)followableNetworkModel change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@keypath(ARFollowableNetworkModel.new, following)]) { + [self setFollowingStatus: followableNetworkModel.isFollowing]; + } +} + +- (void)setFollowingStatus:(BOOL)following +{ + NSString *title = (following) ? self.toUnfollowTitle : self.toFollowTitle; + UIColor *titleColor = (following) ? [UIColor artsyPurple] : [UIColor blackColor]; + UIColor *tapBackgroundColor = (following) ? [UIColor artsyPurple] : [UIColor blackColor]; + + [self setTitle:title forState:UIControlStateNormal]; + [self setTitleColor:titleColor forState:UIControlStateNormal]; + [self setBackgroundColor:tapBackgroundColor forState:UIControlStateHighlighted]; + + _followingStatus = following; +} + +- (void) setToFollowTitle:(NSString *)title +{ + _toFollowTitle = title; + if (!self.followingStatus) { + [self setTitle:title forState:UIControlStateNormal]; + } +} + +- (void) setToUnfollowTitle:(NSString *)title +{ + _toUnfollowTitle = title; + if (self.followingStatus) { + [self setTitle:title forState:UIControlStateNormal]; + } +} + +- (void)dealloc +{ + [self.model removeObserver:self forKeyPath:@keypath(ARFollowableNetworkModel .new, following)]; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(UIViewNoIntrinsicMetric, 46); +} + +@end diff --git a/Artsy/Classes/Views/ARGeneViewController.h b/Artsy/Classes/Views/ARGeneViewController.h new file mode 100644 index 00000000000..486e56df7f8 --- /dev/null +++ b/Artsy/Classes/Views/ARGeneViewController.h @@ -0,0 +1,9 @@ +@interface ARGeneViewController : UIViewController + +- (instancetype)initWithGeneID:(NSString *)geneID; +- (instancetype)initWithGene:(Gene *)gene; + +@property (nonatomic, strong, readonly) Gene *gene; +@property (nonatomic) BOOL shouldAnimate; + +@end diff --git a/Artsy/Classes/Views/ARGeneViewController.m b/Artsy/Classes/Views/ARGeneViewController.m new file mode 100644 index 00000000000..8f449d2915b --- /dev/null +++ b/Artsy/Classes/Views/ARGeneViewController.m @@ -0,0 +1,286 @@ +#import "ARGeneViewController.h" +#import "ARHeartButton.h" +#import "ARTextView.h" +#import "AREmbeddedModelsViewController.h" +#import "UIViewController+FullScreenLoading.h" +#import "ARSharingController.h" +#import "ARCollapsableTextView.h" +#import "UIViewController+SimpleChildren.h" +#import "ARGeneArtworksNetworkModel.h" +#import "ARArtworkSetViewController.h" +#import "ORStackView+ArtsyViews.h" + +@interface ARGeneViewController () + +@property (nonatomic, strong) ARGeneArtworksNetworkModel *artworkCollection; + +@property (nonatomic, strong) ORStackView *headerContainerView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UIView *titleSeparator; +@property (nonatomic, strong) ARTextView *descriptionTextView; +@property (nonatomic, strong) AREmbeddedModelsViewController *artworksViewController; +@end + +@implementation ARGeneViewController + +- (instancetype)initWithGeneID:(NSString *)geneID +{ + Gene *gene = [[Gene alloc] initWithGeneID:geneID]; + return [self initWithGene:gene]; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithGene:(Gene *)gene +{ + self = [self init]; + + _gene = gene; + _artworkCollection = [[ARGeneArtworksNetworkModel alloc] initWithGene:gene]; + return self; +} + + +- (void)dealloc +{ + [self.artworksViewController.collectionView setDelegate:nil]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [self ar_presentIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + + [self createGeneArtworksViewController]; + + ORStackView *headerContainerView = [[ORStackView alloc] init]; + headerContainerView.bottomMarginHeight = 20; + + [self loadGene]; + + self.titleLabel = [headerContainerView addPageTitleWithString:self.gene.name]; + self.titleSeparator = [headerContainerView addWhiteSpaceWithHeight:@"20"]; + + ARTextView *textView; + if ([UIDevice isPad]) { + textView = [[ARTextView alloc] init]; + } else { + textView = [[ARCollapsableTextView alloc] init]; + [(ARCollapsableTextView *)textView setExpansionBlock: ^(ARCollapsableTextView *textView){ + [self viewDidLayoutSubviews]; + }]; + } + textView.viewControllerDelegate = self; + self.descriptionTextView = textView; + [headerContainerView addSubview:textView withTopMargin:@"20" sideMargin:@"40"]; + + + + UIView *actionsWrapper = [self createGeneActionsView]; + [headerContainerView addSubview:actionsWrapper withTopMargin:@"0" sideMargin:@"0"]; + + self.headerContainerView = headerContainerView; + self.artworksViewController.headerView = headerContainerView; + + [self updateBody]; +} + + +- (void)loadGene +{ + @weakify(self); + [self.gene updateGene:^{ + @strongify(self); + [self ar_removeIndeterminateLoadingIndicatorAnimated:self.shouldAnimate]; + [self updateBody]; + + if (self.gene.geneDescription.length == 0) { + [self.headerContainerView removeSubview:self.descriptionTextView]; + } else { + [self.headerContainerView removeSubview:self.titleSeparator]; + } + + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + }]; +} + +- (UIView *)createGeneActionsView +{ + UIView *actionsWrapper = [[UIView alloc] init]; + UIButton *shareButton = [[ARCircularActionButton alloc] initWithImageName:@"Artwork_Icon_Share"]; + [shareButton addTarget:self action:@selector(shareGene:) forControlEvents:UIControlEventTouchUpInside]; + [actionsWrapper addSubview:shareButton]; + + ARHeartButton *favoriteButton = [[ARHeartButton alloc] init]; + [favoriteButton addTarget:self action:@selector(toggleFollowingGene:) forControlEvents:UIControlEventTouchUpInside]; + [actionsWrapper addSubview:favoriteButton]; + + [self.gene getFollowState:^(ARHeartStatus status) { + [favoriteButton setStatus:status animated:self.shouldAnimate]; + } failure:^(NSError *error) { + [favoriteButton setStatus:ARHeartStatusNo]; + }]; + + [actionsWrapper addSubview:favoriteButton]; + [favoriteButton alignCenterXWithView:actionsWrapper predicate:@"-30"]; + [shareButton alignCenterXWithView:actionsWrapper predicate:@"30"]; + [UIView alignTopAndBottomEdgesOfViews:@[actionsWrapper, favoriteButton, shareButton]]; + return actionsWrapper; +} + +- (void)createGeneArtworksViewController +{ + ARArtworkMasonryLayout layout = [UIDevice isPad] ? [self masonryLayoutForPadWithOrientation:[UIApplication sharedApplication].statusBarOrientation] : ARArtworkMasonryLayout2Column; + + ARArtworkMasonryModule *module = [ARArtworkMasonryModule masonryModuleWithLayout:layout andStyle:AREmbeddedArtworkPresentationStyleArtworkMetadata]; + + module.layoutProvider = self; + self.artworksViewController = [[AREmbeddedModelsViewController alloc] init]; + self.artworksViewController.shouldAnimate = self.shouldAnimate; + self.artworksViewController.activeModule = module; + self.artworksViewController.delegate = self; + self.artworksViewController.showTrailingLoadingIndicator = YES; + + [self ar_addModernChildViewController:self.artworksViewController]; + + [self.artworksViewController.view constrainTopSpaceToView:(UIView *)self.topLayoutGuide predicate:nil]; + [self.artworksViewController.view alignTop:nil leading:@"0" bottom:@"0" trailing:@"0" toView:self.view]; + + self.artworksViewController.collectionView.showsVerticalScrollIndicator = YES; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + self.artworksViewController.collectionView.scrollsToTop = YES; + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; +} + +- (void)getNextGeneArtworks +{ + [self.artworkCollection getNextArtworkPage:^(NSArray *artworks) { + if (artworks.count > 0) { + [self.artworksViewController appendItems:artworks]; + } else { + self.artworksViewController.showTrailingLoadingIndicator = NO; + } + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + }]; +} + +- (void)toggleFollowingGene:(ARHeartButton *)sender +{ + if ([User isTrialUser]) { + [ARTrialController presentTrialWithContext:ARTrialContextFavoriteGene fromTarget:self selector:_cmd]; + return; + } + + BOOL hearted = !sender.hearted; + [sender setHearted:hearted animated:self.shouldAnimate]; + + [ArtsyAPI setFavoriteStatus:sender.isHearted forGene:self.gene success:^(id response) {} + failure:^(NSError *error) { + [ARNetworkErrorManager presentActiveErrorModalWithError:error]; + [sender setHearted:!hearted animated:self.shouldAnimate]; + }]; +} + +- (void)updateBody +{ + [self.titleLabel setText:self.gene.name.uppercaseString withLetterSpacing:0.5]; + + // For now we're doing the simplest model possible + + if (self.gene.geneDescription.length ) { + [self.descriptionTextView setMarkdownString:self.gene.geneDescription]; + } + self.artworksViewController.collectionView.scrollsToTop = YES; + [self.headerContainerView setNeedsLayout]; + [self.headerContainerView layoutIfNeeded]; + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + [self getNextGeneArtworks]; +} + +- (void)shareGene:(id)sender +{ + [ARSharingController shareObject:self.gene withThumbnailImageURL:self.gene.smallImageURL]; +} + +-(BOOL)shouldAutorotate +{ + return [UIDevice isPad]; +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.view setNeedsLayout]; +} + +- (void)viewDidLayoutSubviews +{ + CGFloat height = CGRectGetHeight(self.headerContainerView.bounds); + self.artworksViewController.headerHeight = height; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + if ( scrollView.contentSize.height > scrollView.bounds.size.height) { + [[ARScrollNavigationChief chief] scrollViewDidScroll:scrollView]; + } +} + +- (NSDictionary *)dictionaryForAnalytics +{ + if (self.gene) { + return @{ @"gene" : self.gene.geneID, @"type" : @"gene" }; + } + + return nil; +} + +#pragma mark - ARArtworkMasonryLayoutProvider + +-(ARArtworkMasonryLayout)masonryLayoutForPadWithOrientation:(UIInterfaceOrientation)orientation +{ + if (UIInterfaceOrientationIsLandscape(orientation)) { + return ARArtworkMasonryLayout4Column; + } else { + return ARArtworkMasonryLayout3Column; + } +} +#pragma mark - AREmbeddedModelsDelegate + +-(void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:self.artworksViewController.items inFair:nil atIndex:index]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)embeddedModelsViewControllerDidScrollPastEdge:(AREmbeddedModelsViewController *)controller +{ + [self getNextGeneArtworks]; +} + +#pragma mark - ARTextViewDelegate + +-(void)textView:(ARTextView *)textView shouldOpenViewController:(UIViewController *)viewController +{ + [self.navigationController pushViewController:viewController animated:YES]; +} + +@end diff --git a/Artsy/Classes/Views/ARHeartButton.h b/Artsy/Classes/Views/ARHeartButton.h new file mode 100644 index 00000000000..bc8b4942cb1 --- /dev/null +++ b/Artsy/Classes/Views/ARHeartButton.h @@ -0,0 +1,11 @@ +@interface ARHeartButton : ARCircularActionButton + +@property (nonatomic, assign) ARHeartStatus status; +@property (nonatomic, readonly, getter = isHearted) BOOL hearted; + +- (void)setStatus:(ARHeartStatus)hearted; +- (void)setStatus:(ARHeartStatus)hearted animated:(BOOL)animated; +- (void)setHearted:(BOOL)hearted; +- (void)setHearted:(BOOL)hearted animated:(BOOL)animated; + +@end diff --git a/Artsy/Classes/Views/ARHeartButton.m b/Artsy/Classes/Views/ARHeartButton.m new file mode 100644 index 00000000000..11028314171 --- /dev/null +++ b/Artsy/Classes/Views/ARHeartButton.m @@ -0,0 +1,112 @@ +#import "ARHeartButton.h" + +@interface ARHeartButton () +// Front = Active, back = inactive +@property (nonatomic, strong) UIImageView *frontView; +@property (nonatomic, strong) UIImageView *backView; +@end + +@implementation ARHeartButton + +- (instancetype)init +{ + self = [super initWithImageName:nil]; + if (!self) { return nil; } + + [self setImage:nil forState:UIControlStateNormal]; + + CGFloat dimension = [self.class buttonSize]; + + UIImageView *backView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Heart_Black"]]; + backView.frame = CGRectMake(0, 0, dimension, dimension); + backView.contentMode = UIViewContentModeCenter; + _backView = backView; + + CALayer *whiteLayer = _backView.layer; + whiteLayer.borderColor = [UIColor artsyLightGrey].CGColor; + whiteLayer.borderWidth = 1; + whiteLayer.cornerRadius = dimension * .5f; + whiteLayer.backgroundColor = [UIColor whiteColor].CGColor; + + UIImageView *frontView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Heart_White"]]; + frontView.frame = CGRectMake(0, 0, dimension, dimension); + frontView.contentMode = UIViewContentModeCenter; + _frontView = frontView; + + CALayer *purpleLayer = _frontView.layer; + purpleLayer.backgroundColor = [UIColor artsyPurple].CGColor; + purpleLayer.borderColor = [UIColor whiteColor].CGColor; + purpleLayer.cornerRadius = dimension * .5f; + + _status = ARHeartStatusNotFetched; + + self.enabled = NO; + + [self addSubview:self.backView]; + self.layer.borderWidth = 1; + + return self; +} + +- (BOOL)isHearted +{ + return (self.status == ARHeartStatusYes); +} + +- (void)setHearted:(BOOL)hearted +{ + [self setHearted:hearted animated:NO]; +} + +- (void)setHearted:(BOOL)hearted animated:(BOOL)animated +{ + [self setStatus:(hearted ? ARHeartStatusYes : ARHeartStatusNo) animated:animated]; +} + +- (void)setStatus:(ARHeartStatus)status +{ + [self setStatus:status animated:NO]; +} + +- (void)setStatus:(ARHeartStatus)status animated:(BOOL)animated +{ + if (_status == status) { + return; + } + + self.enabled = (status != ARHeartStatusNotFetched); + + // only animate when changing from unset/no -> yes or yes -> unset/no + if (_status != ARHeartStatusYes && status != ARHeartStatusYes) { + _status = status; + return; + } + + _status = status; + + @weakify(self); + void (^animation)() = ^() { + @strongify(self); + if (status == ARHeartStatusYes) { + [self.backView removeFromSuperview]; + [self addSubview:self.frontView]; + self.layer.borderWidth = 0; + } else { + [self.frontView removeFromSuperview]; + [self addSubview:self.backView]; + self.layer.borderWidth = 1; + } + }; + + if (animated) { + [UIView transitionWithView:self + duration:ARAnimationDuration + options:UIViewAnimationOptionTransitionFlipFromBottom + animations:animation + completion:NULL]; + } else { + animation(); + } +} + +@end diff --git a/Artsy/Classes/Views/ARItemThumbnailViewCell.h b/Artsy/Classes/Views/ARItemThumbnailViewCell.h new file mode 100644 index 00000000000..b1ecefc2427 --- /dev/null +++ b/Artsy/Classes/Views/ARItemThumbnailViewCell.h @@ -0,0 +1,15 @@ +#import "ARFeedImageLoader.h" + +/// The ARItemThumbnail is an object aware re-usable +/// view that loads a thumbnail + +@interface ARItemThumbnailViewCell : UICollectionViewCell + +/// Deals with setting up it's own imageview and setting the +/// thumbnail for that object + +- (void)setupWithRepresentedObject:(id)object; + +@property (nonatomic, assign) enum ARFeedItemImageSize imageSize; + +@end diff --git a/Artsy/Classes/Views/ARItemThumbnailViewCell.m b/Artsy/Classes/Views/ARItemThumbnailViewCell.m new file mode 100644 index 00000000000..6556fcd48c9 --- /dev/null +++ b/Artsy/Classes/Views/ARItemThumbnailViewCell.m @@ -0,0 +1,59 @@ +#import "ARItemThumbnailViewCell.h" + +@interface ARItemThumbnailViewCell() +@property (nonatomic, strong) UIImageView *imageView; +@end + +@implementation ARItemThumbnailViewCell + +- (void)prepareForReuse +{ + self.imageView.image = [ARFeedImageLoader defaultPlaceholder]; +} + +- (void)setupWithRepresentedObject:(id)object +{ + if (!self.imageView) { + UIImageView *imageView = [[UIImageView alloc] init]; + imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + imageView.contentMode = UIViewContentModeScaleAspectFill; + imageView.clipsToBounds = YES; + + [self.contentView addSubview:imageView]; + [imageView alignToView:self.contentView]; + + self.imageView = imageView; + } + + if ([object conformsToProtocol:@protocol(ARHasImageBaseURL)]) { + NSString *baseUrl = [object baseImageURL]; + CGSize imageSize = self.bounds.size; + ARFeedItemImageSize size = self.imageSize; + + if (self.imageSize == ARFeedItemImageSizeAuto) { + CGFloat longestDimension = (imageSize.height > imageSize.height)? imageSize.height : imageSize.width; + size = (longestDimension > 100) ? ARFeedItemImageSizeLarge : ARFeedItemImageSizeSmall; + } + + [[ARFeedImageLoader alloc] loadImageAtAddress:baseUrl desiredSize:size + forImageView:self.imageView customPlaceholder:nil]; + } + + //TODO - deprecate this + else if ([object respondsToSelector:@selector(urlForThumbnail)]) { + NSURL *url = [object urlForThumbnail]; + [self.imageView ar_setImageWithURL:url]; + + } else { + // HACK: this needs a better implementation? + ARErrorLog(@"Could not make thumbnail for %@", object); + } + + if ([object respondsToSelector:@selector(title)]) { + self.accessibilityLabel = [object title]; + self.isAccessibilityElement = YES; + self.accessibilityTraits = UIAccessibilityTraitButton; + } +} + +@end diff --git a/Artsy/Classes/Views/ARNavigationButton.h b/Artsy/Classes/Views/ARNavigationButton.h new file mode 100644 index 00000000000..97c64e44116 --- /dev/null +++ b/Artsy/Classes/Views/ARNavigationButton.h @@ -0,0 +1,15 @@ +@interface ARNavigationButton : UIButton + +- (id)initWithTitle:(NSString *)title; +- (id)initWithTitle:(NSString *)title andSubtitle:(NSString *)subtitle; +- (id)initWithTitle:(NSString *)title andSubtitle:(NSString *)subtitle withBorder:(CGFloat)borderWidth; +- (id)initWithFrame:(CGRect)frame withBorder:(CGFloat)borderWidth; +- (id)initWithFrame:(CGRect)frame andTitle:(NSString *)title andSubtitle:(NSString *)subtitle withBorder:(CGFloat)borderWidth; + +@property (readwrite, nonatomic, copy) NSString *title; +@property (readwrite, nonatomic, copy) NSString *subtitle; +@property (readwrite, nonatomic, copy) void (^onTap)(UIButton *button); +@end + +@interface ARSerifNavigationButton : ARNavigationButton +@end diff --git a/Artsy/Classes/Views/ARNavigationButton.m b/Artsy/Classes/Views/ARNavigationButton.m new file mode 100644 index 00000000000..86347f8c6d5 --- /dev/null +++ b/Artsy/Classes/Views/ARNavigationButton.m @@ -0,0 +1,164 @@ +#import "ARNavigationButton.h" + +@interface ARNavigationButton () + +@property (nonatomic, strong, readonly) UILabel *primaryTitleLabel; +@property (nonatomic, strong, readonly) UILabel *subtitleLabel; +@property (nonatomic, strong, readonly) UIView *topBorder; +@property (nonatomic, strong, readonly) UIView *bottomBorder; +@property (nonatomic, strong, readonly) UIImageView *arrowView; +@property (nonatomic, assign, readonly) CGFloat borderWidth; + +@end + +@implementation ARNavigationButton + +- (id)initWithFrame:(CGRect)frame +{ + return [self initWithFrame:frame withBorder:1]; +} + +- (id)initWithFrame:(CGRect)frame withBorder:(CGFloat)borderWidth +{ + self = [super initWithFrame:frame]; + if (!self) { return nil; } + + _borderWidth = borderWidth; + _topBorder = [[UIView alloc] init]; + [self.topBorder constrainHeight:NSStringWithFormat(@"%f", borderWidth)]; + self.topBorder.backgroundColor = [UIColor artsyLightGrey]; + [self addSubview:self.topBorder]; + [self.topBorder alignCenterXWithView:self predicate:nil]; + [self.topBorder constrainWidthToView:self predicate:nil]; + [self alignTopEdgeWithView:self.topBorder predicate:nil]; + + _primaryTitleLabel = [[UILabel alloc] init]; + self.primaryTitleLabel.backgroundColor = [UIColor clearColor]; + self.primaryTitleLabel.font = [UIFont sansSerifFontWithSize:14]; + [self addSubview:self.primaryTitleLabel]; + [self.primaryTitleLabel constrainTopSpaceToView:self.topBorder predicate:@"10"]; + [self.primaryTitleLabel alignLeadingEdgeWithView:self predicate:nil]; + + _subtitleLabel = [[UILabel alloc] init]; + self.subtitleLabel.backgroundColor = [UIColor clearColor]; + self.subtitleLabel.font = [UIFont serifFontWithSize:14]; + self.subtitleLabel.textColor = [UIColor blackColor]; + [self addSubview:self.subtitleLabel]; + [self.subtitleLabel constrainTopSpaceToView:self.primaryTitleLabel predicate:nil]; + [self.subtitleLabel alignLeadingEdgeWithView:self predicate:nil]; + + _arrowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"MoreArrow"]]; + self.arrowView.backgroundColor = [UIColor clearColor]; + self.arrowView.contentMode = UIViewContentModeCenter; + [self addSubview:self.arrowView]; + [self.arrowView alignTrailingEdgeWithView:self predicate:nil]; + [self.arrowView alignCenterYWithView:self predicate:nil]; + + _bottomBorder = [[UIView alloc] init]; + [self.bottomBorder constrainHeight:NSStringWithFormat(@"%f", borderWidth)]; + self.bottomBorder.backgroundColor = [UIColor artsyLightGrey]; + [self addSubview:self.bottomBorder]; + [self.bottomBorder constrainTopSpaceToView:self.subtitleLabel predicate:@"10"]; + [self.bottomBorder alignCenterXWithView:self predicate:nil]; + [self.bottomBorder constrainWidthToView:self predicate:nil]; + [self alignBottomEdgeWithView:self.bottomBorder predicate:nil]; + + return self; +} + +- (id)initWithTitle:(NSString *)title +{ + return [self initWithTitle:title andSubtitle:nil]; +} + +- (id)initWithTitle:(NSString *)title andSubtitle:(NSString *)subtitle +{ + return [self initWithTitle:title andSubtitle:subtitle withBorder:1]; +} + +- (id)initWithTitle:(NSString *)title andSubtitle:(NSString *)subtitle withBorder:(CGFloat)borderWidth +{ + return [self initWithFrame:CGRectZero andTitle:title andSubtitle:subtitle withBorder:borderWidth]; +} + +- (id)initWithFrame:(CGRect)frame andTitle:(NSString *)title andSubtitle:(NSString *)subtitle withBorder:(CGFloat)borderWidth +{ + self = [self initWithFrame:frame withBorder:borderWidth]; + if (!self) { return nil; } + + self.title = title; + self.subtitle = subtitle; + + return self; +} + +- (void)setTitle:(NSString *)title +{ + _title = [title copy]; + + self.primaryTitleLabel.text = title.uppercaseString; +} + +- (void)setSubtitle:(NSString *)subtitle +{ + _subtitle = [subtitle copy]; + + [self.subtitleLabel setText:subtitle]; +} + +#pragma mark - UIView + +- (void)tappedButton +{ + [self sendActionsForControlEvents:UIControlEventTouchUpInside]; +} + +- (void)setEnabled:(BOOL)enabled +{ + [super setEnabled:enabled]; + + self.primaryTitleLabel.alpha = enabled ? 1 : 0.6; + self.arrowView.alpha = enabled ? 1 : 0.6; +} + +- (void)setOnTap:(void (^)(UIButton *))onTap +{ + _onTap = [onTap copy]; + [self addTarget:self action:@selector(tappedForBlockAPI:) forControlEvents:UIControlEventTouchUpInside]; +} + + +- (void)tappedForBlockAPI:(id)sender +{ + self.onTap(self); +} + +- (CGSize)intrinsicContentSize +{ + CGFloat labelMarginsHeight = 20; + CGFloat height = labelMarginsHeight + self.borderWidth + self.primaryTitleLabel.intrinsicContentSize.height + self.subtitleLabel.intrinsicContentSize.height; + height = MAX(height, self.arrowView.intrinsicContentSize.height); + return CGSizeMake(280, height); +} +@end + +@implementation ARSerifNavigationButton + +- (id)initWithFrame:(CGRect)frame withBorder:(CGFloat)borderWidth +{ + self = [super initWithFrame:frame withBorder:borderWidth]; + if (self) { + self.primaryTitleLabel.font = [UIFont serifFontWithSize:18]; + self.subtitleLabel.font = [UIFont serifFontWithSize:16]; + } + + return self; +} + +- (void)setTitle:(NSString *)title { + [super setTitle:title]; + + self.primaryTitleLabel.text = title; +} + +@end diff --git a/Artsy/Classes/Views/ARNetworkErrorView.h b/Artsy/Classes/Views/ARNetworkErrorView.h new file mode 100644 index 00000000000..606ccdea722 --- /dev/null +++ b/Artsy/Classes/Views/ARNetworkErrorView.h @@ -0,0 +1,3 @@ +@interface ARNetworkErrorView : UICollectionReusableView + +@end diff --git a/Artsy/Classes/Views/ARNetworkErrorView.m b/Artsy/Classes/Views/ARNetworkErrorView.m new file mode 100644 index 00000000000..d8f1b6780a1 --- /dev/null +++ b/Artsy/Classes/Views/ARNetworkErrorView.m @@ -0,0 +1,34 @@ +#import "ARNetworkErrorView.h" + +@interface ARNetworkErrorView () +@property (nonatomic, strong) UILabel *errorText; +@end + +@implementation ARNetworkErrorView + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (!self) { return nil; } + [self setup]; + return self; +} + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { return nil; } + [self setup]; + return self; +} + +- (void)setup { + self.backgroundColor = [UIColor whiteColor]; + self.errorText = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:2]; + self.errorText.font = [UIFont serifFontWithSize:12]; + self.errorText.text = @"Couldn't reach Artsy. Please check your internet connection or try again later."; + self.errorText.textAlignment = NSTextAlignmentCenter; + [self addSubview:self.errorText]; + [self.errorText alignLeading:@"20" trailing:@"-20" toView:self]; + [self.errorText alignCenterYWithView:self predicate:@"0"]; +} + +@end diff --git a/Artsy/Classes/Views/ARNotificationView.h b/Artsy/Classes/Views/ARNotificationView.h new file mode 100644 index 00000000000..6694c7344f6 --- /dev/null +++ b/Artsy/Classes/Views/ARNotificationView.h @@ -0,0 +1,8 @@ +// An Artsy styled version of AJNotificationView +// https://github.com/ajerez/AJNotificationView + +@interface ARNotificationView : UIView ++ (ARNotificationView *)showNoticeInView:(UIView *)view title:(NSString *)title hideAfter:(NSTimeInterval)hideInterval response:(void (^)(void))response; ++ (void)hideCurrentNotificationView; +- (void)hide; +@end diff --git a/Artsy/Classes/Views/ARNotificationView.m b/Artsy/Classes/Views/ARNotificationView.m new file mode 100644 index 00000000000..759bc65c4c6 --- /dev/null +++ b/Artsy/Classes/Views/ARNotificationView.m @@ -0,0 +1,146 @@ +// An Artsy styled version of AJNotificationView +// https://github.com/ajerez/AJNotificationView + +#import "ARNotificationView.h" + +@interface ARNotificationView () +@property (nonatomic,strong) UILabel *titleLabel; +@property (nonatomic, copy) void (^responseBlock)(void); +@property (nonatomic, strong) UIView *parentView; +@property (nonatomic, assign) NSTimeInterval hideInterval; +- (void) show; +@end + +const CGFloat panelHeight = 80; +const CGFloat panelMargin = 20; + +static NSMutableArray *notificationQueue = nil; // Global notification queue + +@implementation ARNotificationView + +#pragma mark - View LifeCycle + +- (id)initWithFrame:(CGRect)frame +{ + return [self initWithFrame:frame andResponseBlock:nil]; +} + +- (id)initWithFrame:(CGRect)frame andResponseBlock:(void (^)(void))response +{ + self = [super initWithFrame:frame]; + if (self) { + self.hidden = YES; + + _responseBlock = response; + + UIView *backgroundView = [[UIView alloc] init]; + backgroundView.backgroundColor = [UIColor blackColor]; + [self addSubview:backgroundView]; + [backgroundView alignToView:self]; + + _titleLabel = [[UILabel alloc] init]; + self.titleLabel.textColor = [UIColor whiteColor]; + self.titleLabel.font = [UIFont serifFontWithSize:16]; + self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.titleLabel.numberOfLines = 0; + self.titleLabel.backgroundColor = [UIColor clearColor]; + [self addSubview:self.titleLabel]; + [self.titleLabel alignTop:@"30" leading:@"20" bottom:@"-10" trailing:@"-20" toView:self]; + + ARSeparatorView *separator = [[ARSeparatorView alloc] init]; + [self addSubview:separator]; + [separator constrainWidthToView:self predicate:nil]; + [separator alignLeadingEdgeWithView:self predicate:nil]; + [separator alignAttribute:NSLayoutAttributeTop toAttribute:NSLayoutAttributeBottom ofView:self predicate:@"-1"]; + } + return self; +} + +#pragma mark - Show + ++ (ARNotificationView *)showNoticeInView:(UIView *)view title:(NSString *)title hideAfter:(NSTimeInterval)hideInterval response:(void (^)(void))response +{ + ARNotificationView *noticeView = [[self alloc] initWithFrame:CGRectMake(0, -panelHeight, view.bounds.size.width, 0) andResponseBlock:response]; + noticeView.titleLabel.text = title; + noticeView.parentView = view; + noticeView.hideInterval = hideInterval; + + if(notificationQueue == nil) { + notificationQueue = [[NSMutableArray alloc] init]; + } + + [notificationQueue addObject:noticeView]; + + if([notificationQueue count] == 1) { + // since this notification is the only one in the queue, it can be shown and its delay interval can be honored + [noticeView show]; + } + + return noticeView; +} + +- (void) show +{ + [self.parentView addSubview:self]; + [self setNeedsDisplay]; + + [UIView animateWithDuration:1 + delay:0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^{ + self.hidden = NO; + self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), panelHeight); + } + completion:^(BOOL finished) { + if (finished){ + if (self.hideInterval > 0) { + [self performSelector:@selector(hide) withObject:self.parentView afterDelay:self.hideInterval]; + } + } + }]; +} + +#pragma mark - Hide + +- (void)hide{ + [UIView animateWithDuration:0.4f + delay:0.0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^{ + self.frame = CGRectMake(0, -panelHeight, self.frame.size.width, 1); + } + completion:^(BOOL finished) { + if (finished){ + [self performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1f]; + + [notificationQueue removeObjectIdenticalTo:self]; + + // show the next notification in the queue + if([notificationQueue count] > 0) { + + ARNotificationView *nextNotification = [notificationQueue objectAtIndex:0]; + [nextNotification show]; + } + } + }]; +} + ++ (void)hideCurrentNotificationView +{ + if([notificationQueue count] > 0) + { + ARNotificationView *currentNotification = [notificationQueue objectAtIndex:0]; + [currentNotification hide]; + } +} + +#pragma mark - Touch events + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + [self hide]; + if(self.responseBlock != nil) { + self.responseBlock(); + } +} + +@end diff --git a/Artsy/Classes/Views/AROfflineView.h b/Artsy/Classes/Views/AROfflineView.h new file mode 100644 index 00000000000..5d4dd0f1da4 --- /dev/null +++ b/Artsy/Classes/Views/AROfflineView.h @@ -0,0 +1,6 @@ +#import +#import "ORStackView.h" + +@interface AROfflineView : ORStackView + +@end diff --git a/Artsy/Classes/Views/AROfflineView.m b/Artsy/Classes/Views/AROfflineView.m new file mode 100644 index 00000000000..b9c525335f0 --- /dev/null +++ b/Artsy/Classes/Views/AROfflineView.m @@ -0,0 +1,23 @@ +#import "AROfflineView.h" +#import "ORStackView+ArtsyViews.h" + +@interface AROfflineView () + +@end + +@implementation AROfflineView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (!self) { return nil; } + + self.backgroundColor = [UIColor whiteColor]; + [self addWhiteSpaceWithHeight:@"200"]; + [self addSerifPageTitle:NSLocalizedString(@"Connection Error", @"Offline mode view title") + subtitle:NSLocalizedString(@"The internet connection\nappears to be offline.", @"Offline mode view subtitle")]; + + return self; +} + +@end diff --git a/Artsy/Classes/Views/ARPageSubTitleView.h b/Artsy/Classes/Views/ARPageSubTitleView.h new file mode 100644 index 00000000000..d5ea25187b9 --- /dev/null +++ b/Artsy/Classes/Views/ARPageSubTitleView.h @@ -0,0 +1,14 @@ +/// A simple title view for content + +@interface ARPageSubTitleView : UIView + +/// Designated init, sets the title, uses autolayout +- (instancetype)initWithTitle:(NSString *)title; + +/// Alternative init function that allows passing in the frame +- (instancetype)initWithTitle:(NSString *)title andFrame:(CGRect)frame; + +/// Change the text on the subtitle +@property (nonatomic, copy) NSString *title; + +@end diff --git a/Artsy/Classes/Views/ARPageSubTitleView.m b/Artsy/Classes/Views/ARPageSubTitleView.m new file mode 100644 index 00000000000..51258f0794b --- /dev/null +++ b/Artsy/Classes/Views/ARPageSubTitleView.m @@ -0,0 +1,65 @@ +#import "ARPageSubTitleView.h" + +@interface ARPageSubTitleView() +@property (nonatomic, assign) CGFloat margin; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UIView *separator; +@end + +@implementation ARPageSubTitleView + +- (CGSize)intrinsicContentSize +{ + CGSize labelSize = self.titleLabel.intrinsicContentSize; + CGFloat width = (CGRectGetWidth(self.bounds) == 0) ? CGRectGetWidth(self.bounds) : CGRectGetWidth(self.superview.bounds); + return (CGSize){ width, labelSize.height + (self.margin * 2) + CGRectGetHeight(self.separator.bounds)}; +} + +- (instancetype)initWithTitle:(NSString *)title +{ + return [self initWithTitle:title andFrame:CGRectZero]; +} + +- (instancetype)initWithTitle:(NSString *)title andFrame:(CGRect)frame +{ + self = [super init]; + if(!self) return nil; + + ARThemeLayoutVendor *layout = [ARTheme defaultTheme].layout; + + _titleLabel = [ARThemedFactory labelForFeedSectionHeaders]; + _margin = [layout[@"FeedSectionTitleVerticalMargin"] floatValue]; + _separator = [ARThemedFactory viewForFeedItemSeperatorAttachedToView:self]; + + self.backgroundColor = [UIColor whiteColor]; + [self setTitle:title]; + + [self addSubview:_titleLabel]; + if (CGRectEqualToRect(CGRectZero, frame)) { + [_titleLabel alignCenterXWithView:self predicate:@""]; + [_titleLabel alignCenterYWithView:self predicate:layout[@"FeedSectionTitleVerticalOffset"]]; + } else { + self.frame = frame; + CGFloat offset = [[ARTheme defaultTheme] floatForKey:@"FeedSectionTitleVerticalOffset"]; + _titleLabel.center = (CGPoint){ self.center.x, self.center.y - offset }; + } + + return self; +} + +- (NSString *)title +{ + return self.titleLabel.text; +} + +- (void)setTitle:(NSString *)title +{ + NSMutableAttributedString *attributedTitle = nil; + attributedTitle = [[NSMutableAttributedString alloc] initWithString:[title uppercaseString]]; + [attributedTitle addAttribute:NSKernAttributeName value:@0.5 range:NSMakeRange(0, title.length)]; + + self.titleLabel.attributedText = attributedTitle; + +} + +@end diff --git a/Artsy/Classes/Views/ARPostFeedItemLinkView.h b/Artsy/Classes/Views/ARPostFeedItemLinkView.h new file mode 100644 index 00000000000..dc5cfe13201 --- /dev/null +++ b/Artsy/Classes/Views/ARPostFeedItemLinkView.h @@ -0,0 +1,10 @@ +@class ARPostFeedItem; + +@interface ARPostFeedItemLinkView : UIButton + +- (void)updateWithPostFeedItem:(ARPostFeedItem *)postFeedItem; +- (void)updateWithPostFeedItem:(ARPostFeedItem *)postFeedItem withSeparator:(BOOL)withSeparator; + +@property(nonatomic, strong, readonly) NSString *targetPath; + +@end diff --git a/Artsy/Classes/Views/ARPostFeedItemLinkView.m b/Artsy/Classes/Views/ARPostFeedItemLinkView.m new file mode 100644 index 00000000000..c51ad847119 --- /dev/null +++ b/Artsy/Classes/Views/ARPostFeedItemLinkView.m @@ -0,0 +1,92 @@ +#import "ARPostFeedItemLinkView.h" +#import "ARPostFeedItem.h" +#import "ARAspectRatioImageView.h" + +@implementation ARPostFeedItemLinkView + +- (id)init +{ + self = [super init]; + if (self) { + [self addTarget:nil action:@selector(tappedPostFeedItemLinkView:) forControlEvents:UIControlEventTouchUpInside]; + } + return self; +} + +- (void)updateWithPostFeedItem:(ARPostFeedItem *)postFeedItem +{ + [self updateWithPostFeedItem:postFeedItem withSeparator:YES]; +} + +- (void)updateWithPostFeedItem:(ARPostFeedItem *)postFeedItem withSeparator:(BOOL)withSeparator +{ + ARSeparatorView *separatorView = [[ARSeparatorView alloc] init]; + [self addSubview:separatorView]; + + ARAspectRatioImageView *imageView = [[ARAspectRatioImageView alloc] init]; + [self addSubview:imageView]; + + UIView *imageFiller = [[UIView alloc] init]; + imageFiller.userInteractionEnabled = NO; + [self addSubview:imageFiller]; + + UIView *labelContainer = [[UIView alloc] init]; + labelContainer.userInteractionEnabled = NO; + [self addSubview:labelContainer]; + + UIView *labelFiller = [[UIView alloc] init]; + labelFiller.userInteractionEnabled = NO; + [self addSubview:labelFiller]; + + UILabel *postTitleLabel = [ARThemedFactory labelForFeedItemHeaders]; + postTitleLabel.font = [postTitleLabel.font fontWithSize:20]; + [postTitleLabel setText:[postFeedItem title] withLineHeight:1.5]; + [labelContainer addSubview:postTitleLabel]; + + // Auto Layout + + [separatorView alignTop:nil leading:@"10" bottom:@"0" trailing:@"-10" toView:self]; + + [imageView constrainWidth:@"120"]; + + [imageView alignLeadingEdgeWithView:self predicate:@"10"]; + [imageView alignTopEdgeWithView:self predicate:@"20"]; + [imageView setContentMode:UIViewContentModeScaleAspectFit]; + + [imageFiller constrainTopSpaceToView:imageView predicate:@"0"]; + + [labelContainer constrainLeadingSpaceToView:imageView predicate:@"20"]; + + [labelContainer alignTopEdgeWithView:imageView predicate:@"0"]; + [labelContainer alignTrailingEdgeWithView:self predicate:@"-10"]; + [postTitleLabel alignTopEdgeWithView:labelContainer predicate:@"0"];\ + + if (postFeedItem.profile.profileName) { + UILabel *postAuthorLabel = [ARThemedFactory labelForLinkItemSubtitles]; + postAuthorLabel.text = [postFeedItem.profile.profileName uppercaseString]; + [labelContainer addSubview:postAuthorLabel]; + [postAuthorLabel constrainTopSpaceToView:postTitleLabel predicate:@"5"]; + [labelContainer alignBottomEdgeWithView:postAuthorLabel predicate:@"0"]; + [UIView alignLeadingAndTrailingEdgesOfViews:@[postTitleLabel, postAuthorLabel, labelContainer]]; + } else { + [UIView alignLeadingAndTrailingEdgesOfViews:@[postTitleLabel, labelContainer]]; + [labelContainer alignBottomEdgeWithView:postTitleLabel predicate:@"0"]; + }; + [UIView alignLeadingAndTrailingEdgesOfViews:@[labelFiller, labelContainer]]; + [labelFiller constrainTopSpaceToView:labelContainer predicate:@"0"]; + + [self alignBottomEdgeWithView:labelFiller predicate:@"21"]; + [self alignBottomEdgeWithView:imageFiller predicate:@"21"]; + + _targetPath = NSStringWithFormat(@"/post/%@", postFeedItem.postID); + + // Layout the view to calculate bounds and frame for the image view before adding the image. + [self layoutIfNeeded]; + NSURL *imageUrl = [NSURL URLWithString:postFeedItem.imageURL]; + [imageView ar_setImageWithURL:imageUrl completed:(SDWebImageCompletionBlock)^{ + [self setNeedsLayout]; + [self layoutIfNeeded]; + }]; +} + +@end diff --git a/Artsy/Classes/Views/ARReusableLoadingView.h b/Artsy/Classes/Views/ARReusableLoadingView.h new file mode 100644 index 00000000000..0f488f52f5a --- /dev/null +++ b/Artsy/Classes/Views/ARReusableLoadingView.h @@ -0,0 +1,9 @@ +@interface ARReusableLoadingView : UICollectionReusableView + +- (void)startIndeterminateAnimated:(BOOL)animated; +- (void)stopIndeterminateAnimated:(BOOL)animated; + +- (void)startIndeterminateAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; +- (void)stopIndeterminateAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; + +@end diff --git a/Artsy/Classes/Views/ARReusableLoadingView.m b/Artsy/Classes/Views/ARReusableLoadingView.m new file mode 100644 index 00000000000..27cd9adec68 --- /dev/null +++ b/Artsy/Classes/Views/ARReusableLoadingView.m @@ -0,0 +1,66 @@ +#import "ARReusableLoadingView.h" +#import "ARSpinner.h" + +@interface ARReusableLoadingView() +@property (nonatomic, strong) ARSpinner *spinner; +@end + +@implementation ARReusableLoadingView + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (!self) { return nil; } + [self setup]; + return self; +} + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { return nil; } + [self setup]; + return self; +} + +- (void)setup { + self.backgroundColor = [UIColor whiteColor]; + self.spinner = [[ARSpinner alloc] initWithFrame:CGRectMake(0, 0, 44, 44)]; + [self addSubview:self.spinner]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.spinner.center = CGPointMake( CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2); +} + +- (void)prepareForReuse +{ + [self.spinner removeFromSuperview]; +} + +- (void)startIndeterminateAnimated:(BOOL)animated +{ + [self startIndeterminateAnimated:animated completion:nil]; +} + +- (void)stopIndeterminateAnimated:(BOOL)animated +{ + [self stopIndeterminateAnimated:animated completion:nil]; +} + +- (void)startIndeterminateAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion +{ + [self.spinner fadeInAnimated:animated completion:completion]; +} + +- (void)stopIndeterminateAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion +{ + [self.spinner fadeOutAnimated:animated completion:completion]; +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize){ 44, 44 }; +} + +@end diff --git a/Artsy/Classes/Views/ARSeparatorViews.h b/Artsy/Classes/Views/ARSeparatorViews.h new file mode 100644 index 00000000000..9343cd3aa95 --- /dev/null +++ b/Artsy/Classes/Views/ARSeparatorViews.h @@ -0,0 +1,9 @@ +#import + +@interface ARSeparatorView : UIView + +@end + +@interface ARDottedSeparatorView : ARSeparatorView + +@end diff --git a/Artsy/Classes/Views/ARSeparatorViews.m b/Artsy/Classes/Views/ARSeparatorViews.m new file mode 100644 index 00000000000..679a21e57d1 --- /dev/null +++ b/Artsy/Classes/Views/ARSeparatorViews.m @@ -0,0 +1,32 @@ +#import "UIView+ARDrawing.h" + +@implementation ARSeparatorView + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + self.backgroundColor = [UIColor artsyLightGrey]; + [self constrainHeight:@"1"]; + return self; +} + +@end + +@implementation ARDottedSeparatorView + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + self.backgroundColor = [UIColor clearColor]; + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self drawTopDottedBorder]; +} + +@end diff --git a/Artsy/Classes/Views/ARShadowView.h b/Artsy/Classes/Views/ARShadowView.h new file mode 100644 index 00000000000..9350ccbeb21 --- /dev/null +++ b/Artsy/Classes/Views/ARShadowView.h @@ -0,0 +1,7 @@ +#import + +@interface ARShadowView : UIView + +- (void)createShadow; + +@end diff --git a/Artsy/Classes/Views/ARShadowView.m b/Artsy/Classes/Views/ARShadowView.m new file mode 100644 index 00000000000..f2c24e643e3 --- /dev/null +++ b/Artsy/Classes/Views/ARShadowView.m @@ -0,0 +1,27 @@ +#import "ARShadowView.h" + +@interface ARShadowView() +@end + +@implementation ARShadowView + +- (void)createShadow +{ + CAGradientLayer *shadow = [CAGradientLayer layer]; + CGRect shadowFrame = self.bounds; + shadow.frame = shadowFrame; + + shadow.colors = @[ + (id)[UIColor colorWithWhite:0 alpha:0].CGColor, + (id)[UIColor colorWithWhite:0 alpha:0.9].CGColor, + (id)[UIColor colorWithWhite:0 alpha:0.9].CGColor + ]; + + shadow.locations = @[ @0, @0.85, @1 ]; + shadow.startPoint = CGPointMake(0, 1); + shadow.endPoint = CGPointMake(0, 0); + + [self.layer insertSublayer:shadow atIndex:0]; +} + +@end diff --git a/Artsy/Classes/Views/ARSiteHeroUnitView.h b/Artsy/Classes/Views/ARSiteHeroUnitView.h new file mode 100644 index 00000000000..2b6852557cf --- /dev/null +++ b/Artsy/Classes/Views/ARSiteHeroUnitView.h @@ -0,0 +1,10 @@ +#import "SiteHeroUnit.h" + +@interface ARSiteHeroUnitView : UIView + +- (id)initWithFrame:(CGRect)frame unit:(SiteHeroUnit *)unit; + +@property (nonatomic, readonly, assign) enum ARHeroUnitImageColor style; +@property (nonatomic, copy, readonly) NSString *linkAddress; + +@end diff --git a/Artsy/Classes/Views/ARSiteHeroUnitView.m b/Artsy/Classes/Views/ARSiteHeroUnitView.m new file mode 100644 index 00000000000..c937106c594 --- /dev/null +++ b/Artsy/Classes/Views/ARSiteHeroUnitView.m @@ -0,0 +1,215 @@ +#import "ARSiteHeroUnitView.h" +#import "ARParallaxEffect.h" + +#define AR_HERO_TITLE_FONT 26 + +static ARParallaxEffect *backgroundParallax; +static const NSInteger ARHeroUnitParallaxDistance = 30; +static const CGFloat ARHeroUnitBottomMargin = 34; +static const CGFloat ARHeroUnitDescriptionButtonMargin = 25; +static const CGFloat ARHeroUnitButtonCreditMargin = 21; +static const CGFloat ARHeroUnitTopMargin = 97; +static CGFloat ARHeroUnitSideMargin; +static CGFloat ARHeroUnitHeadingTitleMargin; +static CGFloat ARHeroUnitTitleDescriptionMargin; +static CGFloat ARHeroUnitDescriptionFont; + +@interface ARSiteHeroUnitView() +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, readonly) ARHeroUnitAlignment alignment; +@end + +@implementation ARSiteHeroUnitView + ++ (void)initialize +{ + backgroundParallax = [[ARParallaxEffect alloc] initWithOffset:ARHeroUnitParallaxDistance]; + if ([UIDevice isPad]) { + ARHeroUnitSideMargin = 44; + ARHeroUnitHeadingTitleMargin = 26; + ARHeroUnitTitleDescriptionMargin = 31; + ARHeroUnitDescriptionFont = 22; + } else { + ARHeroUnitSideMargin = 20; + ARHeroUnitHeadingTitleMargin = 25; + ARHeroUnitTitleDescriptionMargin = 22; + ARHeroUnitDescriptionFont = 16; + } +} + +- (id)initWithFrame:(CGRect)frame unit:(SiteHeroUnit *)unit +{ + self = [super initWithFrame:frame]; + if (!self) { return nil; } + + self.clipsToBounds = YES; + + _style = unit.backgroundStyle; + _alignment = [UIDevice isPad] ? unit.alignment : ARHeroUnitAlignmentLeft; + UIImageView *imageView = [[UIImageView alloc] init]; + [self addSubview:imageView]; + [imageView constrainWidthToView:self predicate:@(ARHeroUnitParallaxDistance * 2).stringValue]; + [imageView constrainHeightToView:self predicate:@(ARHeroUnitParallaxDistance * 2).stringValue]; + [imageView alignCenterWithView:self]; + [imageView ar_setImageWithURL:unit.preferredImageURL]; + imageView.contentMode = UIViewContentModeScaleAspectFill; + imageView.clipsToBounds = YES; + imageView.backgroundColor = (self.style == ARHeroUnitImageColorBlack) ? [UIColor blackColor] : [UIColor whiteColor]; + _imageView = imageView; + + + ORStackView *textViewsContainer = [[ORStackView alloc] init]; + textViewsContainer.bottomMarginHeight = 0; + [self addSubview:textViewsContainer]; + [textViewsContainer alignLeadingEdgeWithView:self predicate:NSStringWithFormat(@"%f", ARHeroUnitSideMargin)]; + [textViewsContainer alignTrailingEdgeWithView:self predicate:NSStringWithFormat(@"-%f", ARHeroUnitSideMargin)]; + + UILabel *headingLabel = [self createHeadingLabelWithText:unit.heading]; + UIView *titleView; + if ([UIDevice isPad] && unit.titleImageURL) { + titleView = [self createTitleImageWithImageURL:unit.titleImageURL]; + } else { + titleView = [self createTitleLabelWithText:unit.title]; + } + UILabel *descriptionLabel = [self createDescriptionLabelWithText:unit.body]; + + [textViewsContainer addSubview:headingLabel withTopMargin:nil sideMargin:nil]; + + [textViewsContainer addSubview:titleView withTopMargin:NSStringWithFormat(@"%f", ARHeroUnitHeadingTitleMargin) + sideMargin:nil]; + + [textViewsContainer addSubview:descriptionLabel withTopMargin:NSStringWithFormat(@"%f", ARHeroUnitTitleDescriptionMargin) + sideMargin:nil]; + + if ([UIDevice isPad]) { + ARHeroUnitButton *button = [self createButtonWithColor:unit.buttonColor inverseColor:unit.inverseButtonColor andText:unit.linkText]; + [textViewsContainer addSubview:button withTopMargin:NSStringWithFormat(@"%f", ARHeroUnitDescriptionButtonMargin)]; + if (self.alignment == ARHeroUnitAlignmentRight) { + [button alignTrailingEdgeWithView:textViewsContainer predicate:nil]; + } else { + [button alignLeadingEdgeWithView:textViewsContainer predicate:nil]; + } + UILabel *credit = [self createCreditLabelWithText:unit.creditLine]; + [textViewsContainer addSubview:credit withTopMargin:NSStringWithFormat(@"%f", ARHeroUnitButtonCreditMargin) sideMargin:nil]; + [textViewsContainer alignTopEdgeWithView:self predicate:NSStringWithFormat(@"%f", ARHeroUnitTopMargin)]; + } else { + [textViewsContainer alignBottomEdgeWithView:self predicate:NSStringWithFormat(@"-%f", ARHeroUnitBottomMargin)]; + } + + self.userInteractionEnabled = YES; + [self.imageView addMotionEffect:backgroundParallax]; + + return self; +} + +- (UILabel *)createHeadingLabelWithText:(NSString *)text +{ + UILabel *headingLabel = [[UILabel alloc] init]; + headingLabel.font = [UIDevice isPad] ? [UIFont serifFontWithSize:17] : [UIFont sansSerifFontWithSize:14]; + + NSMutableAttributedString *attributedHeader = nil; + attributedHeader = [[NSMutableAttributedString alloc] initWithString:[text uppercaseString]]; + [attributedHeader addAttribute:NSKernAttributeName value:@0.9 range:NSMakeRange(0, text.length)]; + + headingLabel.attributedText = attributedHeader; + [self styleLabel:headingLabel withAlignment:self.alignment andStyle:self.style]; + return headingLabel; +} + +- (UILabel *)createTitleLabelWithText:(NSString *)text +{ + UILabel *titleLabel = [[UILabel alloc] init]; + titleLabel.font = [UIFont sansSerifFontWithSize:AR_HERO_TITLE_FONT]; + + NSMutableAttributedString *attributedTitle = nil; + NSString *title = [text stringByReplacingOccurrencesOfString:@"\n\n" withString:@"\n"]; + attributedTitle = [[NSMutableAttributedString alloc] initWithString:[title uppercaseString]]; + [attributedTitle addAttribute:NSKernAttributeName value:@1.5 range:NSMakeRange(0, title.length)]; + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + [style setMaximumLineHeight:27]; + [attributedTitle addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, title.length)]; + + titleLabel.attributedText = attributedTitle; + [self styleLabel:titleLabel withAlignment:self.alignment andStyle:self.style]; + return titleLabel; +} + +- (UIImageView *)createTitleImageWithImageURL:(NSURL *)titleImageURL +{ + UIImageView *titleImageView = [[UIImageView alloc] init]; + @weakify(titleImageView); + [titleImageView ar_setImageWithURL:titleImageURL completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { + @strongify(titleImageView); + titleImageView.image = [UIImage imageWithCGImage:image.CGImage scale:2.48 orientation:image.imageOrientation]; + }]; + titleImageView.image = [UIImage imageWithCGImage:[titleImageView.image CGImage] + scale:(titleImageView.image.scale * 1.25) + orientation:(titleImageView.image.imageOrientation)]; + if (self.alignment == ARHeroUnitAlignmentRight) { + titleImageView.contentMode = UIViewContentModeRight; + } else { + titleImageView.contentMode = UIViewContentModeLeft; + } + return titleImageView; +} + +- (UILabel *)createDescriptionLabelWithText:(NSString *)text +{ + UILabel *descriptionLabel = [[ARSerifLineHeightLabel alloc] initWithLineSpacing:6]; + descriptionLabel.font = [UIFont serifFontWithSize:ARHeroUnitDescriptionFont]; + + // On the site they use double \n which looks ugly when small + text = [text stringByReplacingOccurrencesOfString:@"\n\n" withString:@"\n"]; + + // They also have a tendency to only check line-break alignment on desktop + descriptionLabel.text = [text stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; + + [self styleLabel:descriptionLabel withAlignment:self.alignment andStyle:self.style]; + return descriptionLabel; +} +- (UILabel *)createCreditLabelWithText:(NSString *)text +{ + ARItalicsSerifLabel *creditLabel = [[ARItalicsSerifLabel alloc] init]; + creditLabel.font = [[creditLabel font] fontWithSize:9]; + creditLabel.text = text; + [self styleLabel:creditLabel withAlignment:self.alignment andStyle:self.style]; + return creditLabel; +} +- (void)styleLabel:(UILabel *)label withAlignment:(ARHeroUnitAlignment)alignment andStyle:(ARHeroUnitImageColor)style +{ + label.backgroundColor = [UIColor clearColor]; + label.numberOfLines = 0; + if (alignment == ARHeroUnitAlignmentRight) { + [label setTextAlignment:NSTextAlignmentRight]; + } + if (style == ARHeroUnitImageColorBlack) { + [self applyShadowToLabel:label]; + } + label.textColor = [self textColorForStyle:style]; +} + +- (UIColor *)textColorForStyle:(ARHeroUnitImageColor)style +{ + return style == ARHeroUnitImageColorBlack ? [UIColor whiteColor] : [UIColor blackColor]; +} + +- (void)applyShadowToLabel:(UILabel *)label +{ + label.clipsToBounds = NO; + label.layer.shadowOpacity = 0.8; + label.layer.shadowRadius = 2.0; + label.layer.shadowOffset = CGSizeZero; + label.layer.shadowColor = [UIColor artsyHeavyGrey].CGColor; + label.layer.shouldRasterize = YES; +} + +- (ARHeroUnitButton *)createButtonWithColor:(UIColor *)color inverseColor:(UIColor *)inverseColor andText:(NSString *)text +{ + ARHeroUnitButton *button = [[ARHeroUnitButton alloc] init]; + [button setColor:color]; + [button setInverseColor:inverseColor]; + [button setTitle:text forState:UIControlStateNormal]; + return button; +} + +@end diff --git a/Artsy/Classes/Views/ARSpinner.h b/Artsy/Classes/Views/ARSpinner.h new file mode 100644 index 00000000000..1287e1e1653 --- /dev/null +++ b/Artsy/Classes/Views/ARSpinner.h @@ -0,0 +1,14 @@ +@interface ARSpinner : UIView + +- (void)fadeInAnimated:(BOOL)animated; +- (void)fadeOutAnimated:(BOOL)animated; + +- (void)fadeInAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; +- (void)fadeOutAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; + +- (void)startAnimating; +- (void)stopAnimating; + +@property (nonatomic, strong) UIColor *spinnerColor; + +@end diff --git a/Artsy/Classes/Views/ARSpinner.m b/Artsy/Classes/Views/ARSpinner.m new file mode 100644 index 00000000000..9118d97896b --- /dev/null +++ b/Artsy/Classes/Views/ARSpinner.m @@ -0,0 +1,97 @@ +#import "ARSpinner.h" + +@interface ARSpinner() +@property (nonatomic, strong) UIView *spinnerView; +@property (nonatomic, strong) CABasicAnimation *rotationAnimation; +@end + + +@implementation ARSpinner + +CGFloat RotationDuration = 0.9; + +- (id)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self setupBar]; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self setupBar]; + } + return self; +} + +- (void)setupBar { + _spinnerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 20, 5)]; + _spinnerView.backgroundColor = [UIColor blackColor]; + [self layoutSubviews]; + [self addSubview:_spinnerView]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + _spinnerView.center = CGPointMake( CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2); +} + +- (void)fadeInAnimated:(BOOL)animated +{ + [self fadeInAnimated:animated completion:nil]; +} + +- (void)fadeOutAnimated:(BOOL)animated +{ + [self fadeOutAnimated:animated completion:nil]; +} + +- (void)fadeInAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion { + self.alpha = 0; + [self startAnimating]; + + [UIView animateIf:animated duration:0.3 :^{ + self.alpha = 1; + } completion:completion]; +} + +- (void)fadeOutAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion { + self.alpha = 1; + [self stopAnimating]; + + [UIView animateIf:animated duration:0.3 :^{ + self.alpha = 0; + } completion:completion]; +} + +- (void)startAnimating { + [self animate:HUGE_VAL]; +} + +- (void)animate:(NSInteger)times { + CATransform3D rotationTransform = CATransform3DMakeRotation(-1.01f * M_PI, 0, 0, 1.0); + _rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform"]; + + _rotationAnimation.toValue = [NSValue valueWithCATransform3D:rotationTransform]; + _rotationAnimation.duration = RotationDuration; + _rotationAnimation.cumulative = YES; + _rotationAnimation.repeatCount = times; + [self.layer addAnimation:_rotationAnimation forKey:@"transform"]; +} + +- (void)stopAnimating { + [self.layer removeAllAnimations]; + [self animate:1]; +} + +- (UIColor *)spinnerColor +{ + return self.spinnerView.backgroundColor; +} + +- (void)setSpinnerColor:(UIColor *)spinnerColor +{ + self.spinnerView.backgroundColor = spinnerColor; +} + +@end diff --git a/Artsy/Classes/Views/ARSwitchView+Artist.h b/Artsy/Classes/Views/ARSwitchView+Artist.h new file mode 100644 index 00000000000..dd64329fb38 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView+Artist.h @@ -0,0 +1,12 @@ +#import "ARSwitchView.h" + +extern NSInteger ARSwitchViewArtistButtonIndex; +extern NSInteger ARSwitchViewForSaleButtonIndex; + +@interface ARSwitchView (Artist) + +- (void)disableForSale; + ++ (NSArray *)artistButtonTitlesArray; + +@end diff --git a/Artsy/Classes/Views/ARSwitchView+Artist.m b/Artsy/Classes/Views/ARSwitchView+Artist.m new file mode 100644 index 00000000000..e57136e69d6 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView+Artist.m @@ -0,0 +1,25 @@ +#import "ARSwitchView+Artist.h" + +NSInteger ARSwitchViewArtistButtonIndex = 0; +NSInteger ARSwitchViewForSaleButtonIndex = 1; + +@interface ARSwitchView() +@property (nonatomic, strong, readwrite) NSArray *buttons; +@property (nonatomic, strong, readonly) UIView *selectionIndicator; +@end + +@implementation ARSwitchView (Artist) + +- (void)disableForSale +{ + UIButton *forSaleButton = self.buttons[ARSwitchViewForSaleButtonIndex]; + forSaleButton.enabled = NO; + [forSaleButton setTitleColor:[UIColor artsyHeavyGrey] forState:UIControlStateDisabled]; +} + ++ (NSArray *)artistButtonTitlesArray +{ + return @[@"ARTWORKS", @"FOR SALE"]; +} + +@end diff --git a/Artsy/Classes/Views/ARSwitchView+FairGuide.h b/Artsy/Classes/Views/ARSwitchView+FairGuide.h new file mode 100644 index 00000000000..cb79aa52f43 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView+FairGuide.h @@ -0,0 +1,11 @@ +#import "ARSwitchView.h" + +extern NSInteger const ARSwitchViewWorkButtonIndex; +extern NSInteger const ARSwitchViewExhibitorsButtonIndex; +extern NSInteger const ARSwitchViewArtistsButtonIndex; + +@interface ARSwitchView (FairGuide) + ++ (NSArray *)fairGuideButtonTitleArray; + +@end diff --git a/Artsy/Classes/Views/ARSwitchView+FairGuide.m b/Artsy/Classes/Views/ARSwitchView+FairGuide.m new file mode 100644 index 00000000000..e5f4b130802 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView+FairGuide.m @@ -0,0 +1,18 @@ +#import "ARSwitchView+FairGuide.h" + +NSInteger const ARSwitchViewWorkButtonIndex = 0; +NSInteger const ARSwitchViewExhibitorsButtonIndex = 1; +NSInteger const ARSwitchViewArtistsButtonIndex = 2; + +@implementation ARSwitchView (FairGuide) + ++ (NSArray *)fairGuideButtonTitleArray +{ + return @[ + @"WORK", + @"EXHIBITORS", + @"ARTISTS" + ]; +} + +@end diff --git a/Artsy/Classes/Views/ARSwitchView+Favorites.h b/Artsy/Classes/Views/ARSwitchView+Favorites.h new file mode 100644 index 00000000000..713ffce3cc3 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView+Favorites.h @@ -0,0 +1,11 @@ +#import "ARSwitchView.h" + +extern NSInteger const ARSwitchViewFavoriteArtworksIndex; +extern NSInteger const ARSwitchViewFavoriteArtistsIndex; +extern NSInteger const ARSwitchViewFavoriteCategoriesIndex; + +@interface ARSwitchView (Favorites) + ++ (NSArray *)favoritesButtonsTitlesArray; + +@end diff --git a/Artsy/Classes/Views/ARSwitchView+Favorites.m b/Artsy/Classes/Views/ARSwitchView+Favorites.m new file mode 100644 index 00000000000..8595d367cf2 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView+Favorites.m @@ -0,0 +1,13 @@ +NSInteger const ARSwitchViewFavoriteArtworksIndex = 0; +NSInteger const ARSwitchViewFavoriteArtistsIndex = 1; +NSInteger const ARSwitchViewFavoriteCategoriesIndex = 2; + +#import "ARSwitchView+Favorites.h" + +@implementation ARSwitchView (Favorites) + ++ (NSArray *)favoritesButtonsTitlesArray +{ + return @[@"ARTWORKS", @"ARTISTS", @"CATEGORIES"]; +} +@end diff --git a/Artsy/Classes/Views/ARSwitchView.h b/Artsy/Classes/Views/ARSwitchView.h new file mode 100644 index 00000000000..89542f2ab58 --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView.h @@ -0,0 +1,26 @@ +@class ARSwitchView; + +@protocol ARSwitchViewDelegate +- (void)switchView:(ARSwitchView *)switchView didPressButtonAtIndex:(NSInteger)buttonIndex animated:(BOOL)animated; +@end + +@interface ARSwitchView : UIView + +- (instancetype)initWithButtonTitles:(NSArray *)buttonTitlesArray; +- (void)setTitle:(NSString *)title forButtonAtIndex:(NSInteger)index; + +@property (nonatomic, weak, readwrite) id delegate; +@property (nonatomic, strong, readonly) NSArray *buttons; + +/// Use highlighting instead of disabling the button +@property (nonatomic, assign, readwrite) BOOL preferHighlighting; + +@property (nonatomic, strong) NSArray *enabledStates; +- (void)setEnabledStates:(NSArray *)enabledStates animated:(BOOL)animated; + +@property (nonatomic, assign, readwrite) NSInteger selectedIndex; +- (void)setSelectedIndex:(NSInteger)index animated:(BOOL)animated; + +@property (nonatomic, assign, readwrite) BOOL shouldAnimate; + +@end diff --git a/Artsy/Classes/Views/ARSwitchView.m b/Artsy/Classes/Views/ARSwitchView.m new file mode 100644 index 00000000000..8958b314eca --- /dev/null +++ b/Artsy/Classes/Views/ARSwitchView.m @@ -0,0 +1,204 @@ +#import "ARSwitchView.h" + +@interface ARSwitchView() +@property (nonatomic, strong, readwrite) NSLayoutConstraint *selectionConstraint; +@property (nonatomic, strong, readwrite) NSArray *buttons; +@property (nonatomic, strong, readonly) UIView *selectionIndicator; +@property (nonatomic, strong, readonly) UIView *topSelectionIndicator; +@property (nonatomic, strong, readonly) UIView *bottomSelectionIndicator; +@end + +@implementation ARSwitchView + +// Light grey = background, visible by the buttons being a bit smaller than full size +// black bit that moves = uiviews + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + _shouldAnimate = YES; + return self; +} + +- (instancetype)initWithButtonTitles:(NSArray *)buttonTitlesArray +{ + self = [self init]; + if (!self) { return nil; } + + __block NSInteger index = 0; + _buttons = [buttonTitlesArray map:^id(id object) { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [self setupButton:button]; + [button setTitle:object forState:UIControlStateNormal]; + [button setTitle:object forState:UIControlStateDisabled]; + [button addTarget:self action:@selector(selectedButton:) forControlEvents:UIControlEventTouchUpInside]; + + if (index == 0) { + [button setEnabled:NO]; + } + + index++; + return button; + }]; + + NSString *widthPredicateWithMultiplier = NSStringWithFormat(@"*%f", self.widthMultiplier); + + [self.buttons eachWithIndex:^(UIButton *button, NSUInteger index) { + [self addSubview:button]; + [button constrainWidthToView:self predicate:widthPredicateWithMultiplier]; + if (index == 0) { + [button alignLeadingEdgeWithView:self predicate:nil]; + } else { + [button constrainLeadingSpaceToView:self.buttons[index - 1] predicate:nil]; + } + [button alignTop:@"2" bottom:@"-2" toView:self]; + }]; + + _selectionIndicator = [[UIView alloc] init]; + _topSelectionIndicator = [[UIView alloc] init]; + _bottomSelectionIndicator = [[UIView alloc] init]; + + self.topSelectionIndicator.backgroundColor = [UIColor blackColor]; + self.bottomSelectionIndicator.backgroundColor = [UIColor blackColor]; + self.backgroundColor = [UIColor artsyMediumGrey]; + + [self.selectionIndicator addSubview:self.topSelectionIndicator]; + [self.selectionIndicator addSubview:self.bottomSelectionIndicator]; + + [self.topSelectionIndicator alignTop:@"0" leading:@"0" bottom:nil trailing:@"0" toView:self.selectionIndicator]; + [self.bottomSelectionIndicator alignTop:nil leading:@"0" bottom:@"0" trailing:@"0" toView:self.selectionIndicator]; + + [self.topSelectionIndicator constrainHeight:@"2"]; + [self.bottomSelectionIndicator constrainHeight:@"2"]; + + [self insertSubview:self.selectionIndicator atIndex:0]; + [self.selectionIndicator constrainWidthToView:self predicate:widthPredicateWithMultiplier]; + [self.selectionIndicator alignTop:@"0" bottom:@"0" toView:self]; + + _selectionConstraint = [[self.selectionIndicator alignLeadingEdgeWithView:self predicate:nil] lastObject]; + + return self; +} + +- (CGFloat)widthMultiplier +{ + return 1.0 / self.buttons.count; +} + +- (void)selectedButton:(UIButton *)sender +{ + NSInteger buttonIndex = [self.buttons indexOfObject:sender]; + [self setSelectedIndex:buttonIndex animated:self.shouldAnimate]; +} + +- (void)setupButton:(UIButton *)button +{ + button.titleLabel.font = [UIFont sansSerifFontWithSize:14]; + button.titleLabel.backgroundColor = [UIColor whiteColor]; + button.titleLabel.opaque = YES; + button.backgroundColor = [UIColor whiteColor]; + + [button setTitleColor:[UIColor blackColor] forState:UIControlStateDisabled]; + [button setTitleColor:[UIColor blackColor] forState:UIControlStateSelected]; + [button setTitleColor:[UIColor artsyHeavyGrey] forState:UIControlStateNormal]; +} + +- (CGSize)intrinsicContentSize +{ + return (CGSize){ UIViewNoIntrinsicMetric, 46 }; +} + +- (void)setTitle:(NSString *)title forButtonAtIndex:(NSInteger)index +{ + NSAssert(index >= 0, @"Index must be >= zero. "); + NSAssert(index < self.buttons.count, @"Index exceeds buttons count. "); + + [self.buttons[index] setTitle:title forState:UIControlStateNormal]; + [self.buttons[index] setTitle:title forState:UIControlStateDisabled]; +} + +- (void)setSelectedIndex:(NSInteger)index +{ + [self setSelectedIndex:index animated:NO]; +} + +- (void)setSelectedIndex:(NSInteger)index animated:(BOOL)animated +{ + [UIView animateIf:self.shouldAnimate && animated duration:ARAnimationQuickDuration options:UIViewAnimationOptionCurveEaseOut :^{ + UIButton *button = self.buttons[index]; + + [self.buttons each:^(UIButton *button) { + [self highlightButton:button highlighted:NO]; + }]; + + [self highlightButton:button highlighted:YES]; + + // Set the x-position of the selection indicator as a fraction of the total width of the switch view according to which button was pressed. + double buttonIndex = [[NSNumber numberWithInteger:[self.buttons indexOfObject:button]] doubleValue]; + double buttonCount = [[NSNumber numberWithInteger:self.buttons.count] doubleValue]; + double multiplier = buttonIndex/buttonCount; + [self removeConstraint:self.selectionConstraint]; + + // Must be a constraint with a multiple rather than a numerical value so that position is updated if switchview width changes. + self.selectionConstraint = [NSLayoutConstraint constraintWithItem:self.selectionIndicator + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeRight + multiplier:multiplier + constant:0]; + + [self addConstraint:self.selectionConstraint]; + [self layoutIfNeeded]; + }]; + + [self.delegate switchView:self didPressButtonAtIndex:index animated:self.shouldAnimate && animated]; +} + +- (void)highlightButton:(UIButton *)button highlighted:(BOOL)highlighted +{ + if (self.preferHighlighting) { + button.selected = highlighted; + } else { + button.enabled = !highlighted; + } +} + +- (void)setEnabledStates:(NSArray *)enabledStates +{ + [self setEnabledStates:enabledStates animated:NO]; +} + +- (void)setEnabledStates:(NSArray *)enabledStates animated:(BOOL)animated +{ + NSAssert(enabledStates.count == self.buttons.count, @"Need to have a consistent number of enabled states for buttons"); + + [UIView animateIf:self.shouldAnimate && animated duration:ARAnimationQuickDuration :^{ + for (NSInteger i = 0; i < self.enabledStates.count; i++) { + UIButton *button = self.buttons[i]; + BOOL enabled = [self.enabledStates[i] boolValue]; + + if (!enabled) { + button.enabled = NO; + button.alpha = 0.3; + } else { + button.alpha = 1; + } + } + }]; +} + +- (void)setPreferHighlighting:(BOOL)preferHighlighting +{ + _preferHighlighting = preferHighlighting; + + [self.buttons each:^(UIButton *button) { + if (!button.isEnabled || button.isHighlighted) { + button.enabled = preferHighlighting; + button.selected = !preferHighlighting; + } + }]; +} + +@end diff --git a/Artsy/Classes/Views/ARTabContentView.h b/Artsy/Classes/Views/ARTabContentView.h new file mode 100644 index 00000000000..a43cf703bef --- /dev/null +++ b/Artsy/Classes/Views/ARTabContentView.h @@ -0,0 +1,50 @@ +@class ARTabContentView; +@protocol ARTabViewDataSource +@required +- (UINavigationController *)viewControllerForTabContentView:(ARTabContentView *)tabContentView atIndex:(NSInteger)index; +- (BOOL)tabContentView:(ARTabContentView *)tabContentView canPresentViewControllerAtIndex:(NSInteger)index; +- (NSInteger)numberOfViewControllersForTabContentView:(ARTabContentView *)tabContentView; +@end + +@protocol ARTabViewDelegate +@optional +- (BOOL)tabContentView:(ARTabContentView *)tabContentView shouldChangeToIndex:(NSInteger)index; +- (void)tabContentView:(ARTabContentView *)tabContentView didChangeSelectedIndex:(NSInteger)index; +@end + +/** + + The ARTabContentView is a Child View Controller compliant tab view, it is only the viewport itself, + and is a view for a view controller like ARTabbedViewController, or ARTopViewController. + + ViewControllers are added as children to the hostVC, and it has built-in support for hooking + up with the ARSwitchView subclasses for the usual tab & switch. + +**/ + +@interface ARTabContentView : UIView + +- (id)initWithFrame:(CGRect)frame hostViewController:(UIViewController *)controller delegate:(id )delegate dataSource:(id )dataSource; + +@property (nonatomic, strong, readwrite) NSArray *buttons; +@property (nonatomic, weak, readonly) UIViewController *hostViewController; + +@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) id dataSource; + + +@property (nonatomic, strong, readonly) UINavigationController *currentNavigationController; +@property (nonatomic, assign) BOOL supportSwipeGestures; + +@property (nonatomic, strong, readonly) UISwipeGestureRecognizer *leftSwipeGesture; +@property (nonatomic, strong, readonly) UISwipeGestureRecognizer *rightSwipeGesture; + +@property (nonatomic, assign, readonly) NSInteger currentViewIndex; +@property (nonatomic, assign, readonly) NSInteger previousViewIndex; +- (void)setCurrentViewIndex:(NSInteger)currentViewIndex animated:(BOOL)animated; +- (void)returnToPreviousViewIndex; + +// Move to the next or previous tab, if you're at the end it does nothing +- (void)showNextTabAnimated:(BOOL)animated; +- (void)showPreviousTabAnimated:(BOOL)animated; +@end \ No newline at end of file diff --git a/Artsy/Classes/Views/ARTabContentView.m b/Artsy/Classes/Views/ARTabContentView.m new file mode 100644 index 00000000000..77d53ad148d --- /dev/null +++ b/Artsy/Classes/Views/ARTabContentView.m @@ -0,0 +1,212 @@ +#import "ARTabContentView.h" + +static BOOL ARTabViewDirectionLeft = NO; +static BOOL ARTabViewDirectionRight = YES; + +@interface ARTabContentView () +@end + +@implementation ARTabContentView + +- (id)initWithFrame:(CGRect)frame hostViewController:(UIViewController *)controller delegate:(id )delegate dataSource:(id )dataSource +{ + self =[super initWithFrame:frame]; + if(!self) return nil; + + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.opaque = YES; + self.clipsToBounds = YES; + + _hostViewController = controller; + _delegate = delegate; + _dataSource = dataSource; + + _leftSwipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipedViewLeft:)]; + _leftSwipeGesture.direction = UISwipeGestureRecognizerDirectionLeft; + [self addGestureRecognizer:_leftSwipeGesture]; + + _rightSwipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipedViewRight:)]; + _rightSwipeGesture.direction = UISwipeGestureRecognizerDirectionRight; + [self addGestureRecognizer:_rightSwipeGesture]; + + return self; +} + +- (void)removeFromSuperview +{ + [_currentNavigationController willMoveToParentViewController:nil]; + [_currentNavigationController removeFromParentViewController]; + + [super removeFromSuperview]; +} + +#pragma mark - +#pragma mark Custom Properties + +- (void)setSupportSwipeGestures:(BOOL)supportSwipeGestures +{ + self.leftSwipeGesture.enabled = supportSwipeGestures; + self.rightSwipeGesture.enabled = supportSwipeGestures; +} + +- (void)setButtons:(NSArray *)buttons +{ + _buttons = buttons; + + [self.buttons eachWithIndex:^(UIButton *button, NSUInteger index) { + button.enabled = [self.dataSource tabContentView:self canPresentViewControllerAtIndex:index]; + [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + }]; +} + +#pragma mark - +#pragma mark Gestures + +- (void)swipedViewRight:(UISwipeGestureRecognizer *)gesture +{ + [self showPreviousTabAnimated:YES]; +} + +- (void)showPreviousTabAnimated:(BOOL)animated +{ + NSInteger nextIndex = [self nextEnabledIndexInDirection:ARTabViewDirectionLeft]; + + if (self.currentViewIndex != nextIndex) { + self.currentViewIndex = nextIndex; + [self setCurrentViewIndex:nextIndex animated:animated]; + } +} + +- (void)swipedViewLeft:(UISwipeGestureRecognizer *)gesture +{ + [self showNextTabAnimated:YES]; +} + +- (void)showNextTabAnimated:(BOOL)animated +{ + NSInteger nextIndex = [self nextEnabledIndexInDirection:ARTabViewDirectionRight]; + if (self.currentViewIndex != nextIndex) { + self.currentViewIndex = nextIndex; + [self setCurrentViewIndex:nextIndex animated:animated]; + } +} + +- (NSInteger)nextEnabledIndexInDirection:(BOOL)direction +{ + // can't go any further + if (self.currentViewIndex == 0 && direction == ARTabViewDirectionLeft) return self.currentViewIndex; + if (self.currentViewIndex == [self numberOfViewControllers] - 1 && direction == ARTabViewDirectionRight) return self.currentViewIndex; + + NSInteger nextViewIndex = direction? self.currentViewIndex + 1 : self.currentViewIndex - 1; + // loop until we hit an enabled view + while (![self.dataSource tabContentView:self canPresentViewControllerAtIndex:nextViewIndex]) { + if (direction) { + nextViewIndex++; + } else { + nextViewIndex--; + } + + // if we're going to go too far, stop and return the current index + if (nextViewIndex == -1) return self.currentViewIndex; + if (nextViewIndex == [self numberOfViewControllers]) return self.currentViewIndex; + } + + return nextViewIndex; +} + +- (NSInteger)numberOfViewControllers +{ + return [self.dataSource numberOfViewControllersForTabContentView:self]; +} + +#pragma mark - +#pragma mark Setting the Current View Index + +- (void)setCurrentViewIndex:(NSInteger)currentViewIndex +{ + [self setCurrentViewIndex:currentViewIndex animated:YES]; +} + +- (void)setCurrentViewIndex:(NSInteger)index animated:(BOOL)animated +{ + if ([self.delegate respondsToSelector:@selector(tabContentView:shouldChangeToIndex:)]) { + if ([self.delegate tabContentView:self shouldChangeToIndex:index] == NO) return; + } + + [self.buttons each:^(UIButton *button) { + button.selected = NO; + }]; + + if (index < self.buttons.count) { [(UIButton *)self.buttons[index] setSelected:YES]; } + + // Setup positions of views + NSInteger direction = (_currentViewIndex > index) ? -1 : 1; + CGRect nextViewInitialFrame = self.bounds; + CGRect oldViewEndFrame = self.bounds; + nextViewInitialFrame.origin.x = direction * CGRectGetWidth(self.superview.bounds); + oldViewEndFrame.origin.x = -direction * CGRectGetWidth(self.superview.bounds); + + __block UIViewController *oldViewController = self.currentNavigationController; + _previousViewIndex = self.currentViewIndex; + _currentViewIndex = index; + + // Get the next View Controller, add to self + _currentNavigationController = [self navigationControllerForIndex:index]; + self.currentNavigationController.view.frame = nextViewInitialFrame; + + // Add the new ViewController our view's host + [self.currentNavigationController willMoveToParentViewController:self.hostViewController]; + [self.hostViewController addChildViewController:self.currentNavigationController]; + [self.currentNavigationController didMoveToParentViewController:_hostViewController]; + + void (^animationBlock)(); + animationBlock = ^{ + self.currentNavigationController.view.frame = self.bounds; + oldViewController.view.frame = oldViewEndFrame; + }; + + void (^completionBlock)(BOOL finished); + completionBlock = ^(BOOL finished) { + // Remove the old one + [oldViewController willMoveToParentViewController:nil]; + + [oldViewController removeFromParentViewController]; + [oldViewController.view removeFromSuperview]; + + if ([self.delegate respondsToSelector:@selector(tabContentView:didChangeSelectedIndex:)]) { + [self.delegate tabContentView:self didChangeSelectedIndex:index]; + } + }; + + ar_dispatch_main_queue( ^{ + + if (animated && oldViewController && oldViewController.parentViewController) { + [self.hostViewController transitionFromViewController:oldViewController toViewController:self.currentNavigationController duration:0.3 options:0 animations:animationBlock completion:completionBlock]; + } else { + [self.currentNavigationController beginAppearanceTransition:YES animated:NO]; + [self addSubview:self.currentNavigationController.view]; + [self.currentNavigationController endAppearanceTransition]; + + animationBlock(); + completionBlock(YES); + } + }); +} + +- (void)returnToPreviousViewIndex +{ + [self setCurrentViewIndex:self.previousViewIndex animated:NO]; +} + +- (UINavigationController *)navigationControllerForIndex:(NSInteger)index +{ + return [self.dataSource viewControllerForTabContentView:self atIndex:index]; +} + +- (void)buttonTapped:(id)sender +{ + NSInteger index = [self.buttons indexOfObject:sender]; + [self setCurrentViewIndex:index animated:NO]; +} + +@end diff --git a/Artsy/Classes/Views/ARTextView.h b/Artsy/Classes/Views/ARTextView.h new file mode 100644 index 00000000000..1020117197e --- /dev/null +++ b/Artsy/Classes/Views/ARTextView.h @@ -0,0 +1,22 @@ +@class ARTextView; + +@protocol ARTextViewDelegate + +-(void)textView:(ARTextView *)textView shouldOpenViewController:(UIViewController *)viewController; + +@end + +@interface ARTextView : UITextView + +- (void)setMarkdownString:(NSString *)string; +- (void)setHTMLString:(NSString *)string; + +/// Make sure there are no trailing paragraph endings in intrinsicContentSize +@property (nonatomic, assign) BOOL expectsSingleLine; + +/// Don't underline links +@property (nonatomic, assign) BOOL plainLinks; + +@property(nonatomic,assign) id viewControllerDelegate; + +@end diff --git a/Artsy/Classes/Views/ARTextView.m b/Artsy/Classes/Views/ARTextView.m new file mode 100644 index 00000000000..422a73ce746 --- /dev/null +++ b/Artsy/Classes/Views/ARTextView.m @@ -0,0 +1,145 @@ +#import "ARTextView.h" +#import + +@implementation ARTextView + +- (instancetype)init +{ + self = [super init]; + if (!self) { return nil; } + + self.font = [ARTheme defaultTheme].fonts[@"BodyText"]; + self.scrollEnabled = NO; + self.editable = NO; + self.selectable = YES; + self.bounces = NO; + self.dataDetectorTypes = UIDataDetectorTypeNone; + self.tintColor = [UIColor blackColor]; + self.delegate = self; + self.opaque = YES; + + return self; +} + +- (void)setMarkdownString:(NSString *)string +{ + if (string.length == 0) { + DDLogWarn(@"You shouldn't be using markdown with an empty string. Noop-ing."); + return; + } + + + NSError *error = nil; + NSString *HTML = [MMMarkdown HTMLStringWithMarkdown:string error:&error]; + if (error) { + ARErrorLog(@"Error Parsing markdown! %@", string); + self.text = @"Error Parsing markdown"; + return; + } + + [self setHTMLString:HTML]; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + [super setBackgroundColor:backgroundColor]; + + // If we want the layer to not blend + // we need to get the _UITextContainerView off UIColor clearColor + for (UIView *subview in self.subviews) { + subview.backgroundColor = backgroundColor; + } +} + +- (void)setHTMLString:(NSString *)HTMLstring +{ + NSAttributedString *string = [self.class artsyBodyTextAttributedStringFromHTML:HTMLstring withFont:self.font]; + + // SCREW IT MEGAHACK to get paragraph spacing right. + NSMutableAttributedString *mutableCopy = string.mutableCopy; + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + + if (self.expectsSingleLine) { + [style setParagraphSpacing:-1 * self.font.pointSize]; + } else { + [style setParagraphSpacing:0]; + } + + [style setLineSpacing:5]; + + [mutableCopy addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, mutableCopy.length)]; + + if (self.plainLinks) { + [mutableCopy addAttribute:NSUnderlineStyleAttributeName value:@0 range:NSMakeRange(0, mutableCopy.length)]; + } + + self.attributedText = mutableCopy; + + [self.superview setNeedsUpdateConstraints]; +} + ++ (NSAttributedString *)artsyBodyTextAttributedStringFromHTML:(NSString *)HTML withFont:(UIFont *)font +{ + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + style.lineHeightMultiple = 1.2; + + style.paragraphSpacing = font.pointSize * .5; + + NSDictionary *textParams = @{ + NSFontAttributeName : font, + NSParagraphStyleAttributeName : style, + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle) + }; + + return [self _attributedStringWithTextParams:textParams andHTML:HTML]; +} + + ++ (NSString *)_cssStringFromAttributedStringAttributes:(NSDictionary *)dictionary +{ + NSMutableString *cssString = [NSMutableString stringWithString:@""]; + return cssString; +} + ++ (NSAttributedString *)_attributedStringWithTextParams:(NSDictionary *)textParams andHTML:(NSString *)HTML +{ + NSDictionary *importParams = @{ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType }; + + NSError *error = nil; + NSString *formatString = [[self _cssStringFromAttributedStringAttributes:textParams] stringByAppendingFormat:@"%@", HTML]; + NSData *stringData = [formatString dataUsingEncoding:NSUnicodeStringEncoding] ; + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:stringData options:importParams documentAttributes:NULL error:&error]; + if (error) { + ARErrorLog(@"Error creating NSAttributedString from HTML %@", error.localizedDescription); + return nil; + } + + return attributedString; +} + +- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange +{ + UIViewController *viewController = [ARSwitchBoard.sharedInstance loadURL:URL]; + if (viewController) { + [self.viewControllerDelegate textView:self shouldOpenViewController:viewController]; + } + + return NO; +} + +@end diff --git a/Artsy/Classes/Views/ARTextViewSubclasses.m b/Artsy/Classes/Views/ARTextViewSubclasses.m new file mode 100644 index 00000000000..b6d8ab2fbc0 --- /dev/null +++ b/Artsy/Classes/Views/ARTextViewSubclasses.m @@ -0,0 +1,40 @@ +#import "ARTextViewSubclasses.h" + +// UITextView has a bit of padding by default +#define ARTextViewContentInset UIEdgeInsetsMake(-8, -8, -8, -8) + +@implementation ARArtworkTitleTextView + +- (id)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + self.textContainer.lineFragmentPadding = 0; + } + return self; +} + +- (void)setTitleFromArtwork:(Artwork *)artwork +{ + NSAssert(artwork.title, @"Artwork With no title given to an ARArtworkTitleTextView"); + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setLineSpacing:3]; + + NSMutableAttributedString *titleAndDate = [[NSMutableAttributedString alloc] initWithString:artwork.title attributes:@{ NSParagraphStyleAttributeName: paragraphStyle }]; + + [titleAndDate addAttribute:NSFontAttributeName value:[UIFont serifItalicFontWithSize:16] range:NSMakeRange(0, [artwork.title length])]; + + if (artwork.date.length) { + NSString *formattedTitleDate = [NSString stringWithFormat:@", %@", artwork.date]; + NSAttributedString *andDate = [[NSAttributedString alloc] initWithString:formattedTitleDate attributes:@{NSFontAttributeName : [UIFont serifFontWithSize:16]}]; + [titleAndDate appendAttributedString:andDate]; + } + + self.attributedText = titleAndDate; +} + +@end + + + + diff --git a/Artsy/Classes/Views/ARTile+ASCII.h b/Artsy/Classes/Views/ARTile+ASCII.h new file mode 100644 index 00000000000..03a7c5ddd4d --- /dev/null +++ b/Artsy/Classes/Views/ARTile+ASCII.h @@ -0,0 +1,9 @@ +#import +#import + +@interface ARTile (ASCII) + ++ (BOOL)ascii; ++ (void)setAscii:(BOOL)value; + +@end \ No newline at end of file diff --git a/Artsy/Classes/Views/ARTile+ASCII.m b/Artsy/Classes/Views/ARTile+ASCII.m new file mode 100644 index 00000000000..ea41108e684 --- /dev/null +++ b/Artsy/Classes/Views/ARTile+ASCII.m @@ -0,0 +1,45 @@ +#import "ARTile+ASCII.h" +#import + +@implementation ARTile (ASCII) + +static BOOL _ascii = NO; + ++ (BOOL)ascii +{ + @synchronized(self) { + return _ascii; + } +} + ++ (void)setAscii:(BOOL)value +{ + @synchronized(self) { + if (_ascii != value) { + _ascii = value; + if (value) { + [ARTile swizzle:@selector(drawInRect:blendMode:alpha:) forMethod:@selector(drawInRectASCII:blendMode:alpha:)]; + } else { + [ARTile swizzle:@selector(drawInRectASCII:blendMode:alpha:) forMethod:@selector(drawInRect:blendMode:alpha:)]; + } + } + } +} + ++ (void)swizzle:(SEL)originalSelector forMethod:(SEL)overrideSelector +{ + Method originalMethod = class_getInstanceMethod(self, originalSelector); + Method overrideMethod = class_getInstanceMethod(self, overrideSelector); + if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) { + class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, overrideMethod); + } +} + +- (void)drawInRectASCII:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha +{ + // do nothing, don't paint tiles +} + +@end diff --git a/Artsy/Classes/Views/ARTopTapThroughTableView.h b/Artsy/Classes/Views/ARTopTapThroughTableView.h new file mode 100644 index 00000000000..af2ae2a3b2c --- /dev/null +++ b/Artsy/Classes/Views/ARTopTapThroughTableView.h @@ -0,0 +1,5 @@ +#import + +@interface ARTopTapThroughTableView : UITableView + +@end diff --git a/Artsy/Classes/Views/ARTopTapThroughTableView.m b/Artsy/Classes/Views/ARTopTapThroughTableView.m new file mode 100644 index 00000000000..f471887b1a6 --- /dev/null +++ b/Artsy/Classes/Views/ARTopTapThroughTableView.m @@ -0,0 +1,11 @@ +#import "ARTopTapThroughTableView.h" + +@implementation ARTopTapThroughTableView + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + if (point.y < 0) { return NO; } + return [super pointInside:point withEvent:event]; +} + +@end \ No newline at end of file diff --git a/Artsy/Classes/Views/ARWhitespaceGobbler.h b/Artsy/Classes/Views/ARWhitespaceGobbler.h new file mode 100644 index 00000000000..d9e4afcc391 --- /dev/null +++ b/Artsy/Classes/Views/ARWhitespaceGobbler.h @@ -0,0 +1,5 @@ +#import + +@interface ARWhitespaceGobbler : UIView + +@end diff --git a/Artsy/Classes/Views/ARWhitespaceGobbler.m b/Artsy/Classes/Views/ARWhitespaceGobbler.m new file mode 100644 index 00000000000..d8eea801f32 --- /dev/null +++ b/Artsy/Classes/Views/ARWhitespaceGobbler.m @@ -0,0 +1,20 @@ +#import "ARWhitespaceGobbler.h" + +@implementation ARWhitespaceGobbler + +- (instancetype)init +{ + self = [super initWithFrame:CGRectNull]; + if (!self) { return nil; } + self.backgroundColor = [UIColor clearColor]; + [self setContentHuggingPriority:50 forAxis:UILayoutConstraintAxisVertical]; + [self setContentHuggingPriority:50 forAxis:UILayoutConstraintAxisHorizontal]; + + return self; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(0, 0); +} +@end diff --git a/Artsy/Classes/Views/ARZoomView.h b/Artsy/Classes/Views/ARZoomView.h new file mode 100644 index 00000000000..a6d2662801a --- /dev/null +++ b/Artsy/Classes/Views/ARZoomView.h @@ -0,0 +1,25 @@ +@class ARZoomView; + +@protocol ARZoomViewDelegate +- (void)zoomViewFinished:(ARZoomView *)zoomView; +@end + +@interface ARZoomView : UIScrollView + +@property (nonatomic, strong, readonly) Image *image; +@property (nonatomic, strong) UIImage *backgroundImage; +@property (nonatomic, weak) id zoomDelegate; + +- (instancetype)initWithImage:(Image *)image frame:(CGRect)frame; + +- (void)performBlockWhileIgnoringContentOffsetChanges:(dispatch_block_t)block; + +- (CGPoint)centerContentOffsetForZoomScale:(CGFloat)zoomScale minimumSize:(CGSize)minimumSize; +- (CGPoint)centerContentOffsetForZoomScale:(CGFloat)zoomScale; +- (void)setMaxMinZoomScalesForCurrentFrame; +- (void)setMaxMinZoomScalesForSize:(CGSize)size; +- (CGFloat)scaleForFullScreenZoomInSize:(CGSize)size; +- (void)removeZoomViewForTransition; +- (void)finish; + +@end diff --git a/Artsy/Classes/Views/ARZoomView.m b/Artsy/Classes/Views/ARZoomView.m new file mode 100644 index 00000000000..a2e5bac316c --- /dev/null +++ b/Artsy/Classes/Views/ARZoomView.m @@ -0,0 +1,250 @@ +#import "ARZoomView.h" +#import "ARFeedImageLoader.h" +#import "ARTiledImageDataSourceWithImage.h" + +#import + +static const CGFloat ARZoomMultiplierForDoubleTap = 1.5; + +@interface ARZoomView () + +@property (nonatomic, assign) BOOL overrideContentOffsetChanges; +@property (nonatomic, strong) UIButton *closeButton; +@property (nonatomic, strong) ARTiledImageView *zoomableView; +@property (nonatomic, strong) UIImageView *backgroundView; +@property (nonatomic, strong) ARTiledImageDataSourceWithImage *tileDataSource; + +@end + +@implementation ARZoomView + +- (instancetype)initWithImage:(Image *)image frame:(CGRect)frame +{ + NSAssert([image needsTiles], @"Don't instantiate zoom views for images that don't need tiles in Eigen"); + + self = [super initWithFrame:frame]; + if (!self) { return nil; } + + _image = image; + self.scrollsToTop = NO; + self.delegate = self; + self.decelerationRate = UIScrollViewDecelerationRateFast; + + [self setupWithEventualFrame:frame]; + + // Needs to be reset after setting anchorPoint + self.frame = frame; + + return self; +} + +- (void)setupWithEventualFrame:(CGRect)eventualFrame +{ + self.showsHorizontalScrollIndicator = NO; + self.showsVerticalScrollIndicator = NO; + self.backgroundColor = [UIColor clearColor]; + + UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; + doubleTapGesture.numberOfTapsRequired = 2; + [self addGestureRecognizer:doubleTapGesture]; + + UITapGestureRecognizer *twoFingerTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(twoFingerTap:)]; + twoFingerTapGesture.numberOfTapsRequired = 1; + twoFingerTapGesture.numberOfTouchesRequired = 2; + [self addGestureRecognizer:twoFingerTapGesture]; + + UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)]; + [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture]; + [singleTapGesture requireGestureRecognizerToFail:twoFingerTapGesture]; + [self addGestureRecognizer:singleTapGesture]; + + _tileDataSource = [[ARTiledImageDataSourceWithImage alloc] initWithImage:_image]; + _zoomableView = [[ARTiledImageView alloc] initWithDataSource:_tileDataSource minimumSize:eventualFrame.size]; + _backgroundView = [[UIImageView alloc] initWithFrame:_zoomableView.frame]; + + [[ARFeedImageLoader alloc] loadImageAtAddress:[_image baseImageURL] desiredSize:ARFeedItemImageSizeLarge forImageView:_backgroundView customPlaceholder:nil]; + + [self addSubview:_backgroundView]; + [self addSubview:_zoomableView]; + [self setMaxMinZoomScalesForCurrentFrame]; + + if ([UIDevice isPhone]) { + self.layer.anchorPoint = CGPointMake(.5, 0); + } + self.zoomScale = self.minimumZoomScale; +} + +- (void)removeZoomViewForTransition +{ + [_zoomableView removeFromSuperview]; + _zoomableView = nil; +} + +- (void)finish +{ + _zoomableView.alpha = 0; +} + +- (void)setBackgroundImage:(UIImage *)backgroundImage +{ + if (UIImageView.ascii) { + return; + } + + _backgroundView.image = backgroundImage; + _backgroundImage = backgroundImage; +} + +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView +{ + return (_zoomableView) ? _zoomableView : _backgroundView; +} + +- (void)performBlockWhileIgnoringContentOffsetChanges:(dispatch_block_t)block +{ + NSParameterAssert(block); + self.overrideContentOffsetChanges = YES; + block(); + self.overrideContentOffsetChanges = NO; +} + +- (void)scrollViewDidZoom:(UIScrollView *)scrollView +{ + //so the background view doesnt show over the edges + _backgroundView.frame = _zoomableView.frame; + + //close if the zoom goes below minimumZoomScale + CGFloat fullScreenScale = [self scaleForFullScreenZoomInSize:self.frame.size]; + if (self.zoomScale < (fullScreenScale * .95)) { + [self.zoomDelegate zoomViewFinished:self]; + } +} + +- (CGFloat)scaleForFullScreenZoomInSize:(CGSize)size +{ + CGSize imageSize = CGSizeMake(_image.maxTiledWidth, _image.maxTiledHeight); + + CGFloat xScale = size.width / imageSize.width; + CGFloat yScale = size.height / imageSize.height; + return MIN(1.f, MAX(xScale, yScale)); +} + +- (void)setContentOffset:(CGPoint)contentOffset +{ + // Changing the zoomScale in an animation block makes the contentOffset "jump" at the beginning of the animation. + if (self.overrideContentOffsetChanges) { + ARInfoLog(@"Ignoring contentOffset change: %@", NSStringFromCGPoint(contentOffset)); + } else { + [super setContentOffset:contentOffset]; + } +} + +- (CGPoint)centerContentOffsetForZoomScale:(CGFloat)zoomScale minimumSize:(CGSize)minimumSize +{ + CGSize imageSize = [self.tileDataSource imageSizeForImageView:self.zoomableView]; + imageSize.height *= zoomScale; + imageSize.width *= zoomScale; + + CGPoint point = CGPointMake(-(MIN(minimumSize.width - imageSize.width, 0) / 2.0), -(MIN(minimumSize.height - imageSize.height, 0)) / 2.0); + return point; +} + +- (CGPoint)centerContentOffsetForZoomScale:(CGFloat)zoomScale +{ + CGSize minimumSize = self.frame.size; + return [self centerContentOffsetForZoomScale:zoomScale minimumSize:minimumSize]; +} + +- (void)setMaxMinZoomScalesForCurrentFrame +{ + [self setMaxMinZoomScalesForSize:self.frame.size]; +} + +- (void)setMaxMinZoomScalesForSize:(CGSize)size +{ + CGSize imageSize = CGSizeMake(_image.maxTiledWidth, _image.maxTiledHeight); + + // calculate min/max zoomscale + CGFloat xScale = size.width / imageSize.width; // the scale needed to perfectly fit the image width-wise + CGFloat yScale = size.height / imageSize.height; // the scale needed to perfectly fit the image height-wise + CGFloat minScale = MIN(xScale, yScale); // use minimum of these to allow the image to become fully visible + + CGFloat maxScale = 1.0; + + // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) + if (minScale > maxScale) { + minScale = maxScale; + } + + self.maximumZoomScale = maxScale; + self.minimumZoomScale = minScale; +} + +#pragma mark - +#pragma mark Responding to gestures + +- (void)doubleTap:(UITapGestureRecognizer *)tapGesture +{ + if (self.zoomScale == self.maximumZoomScale) { + [self.zoomDelegate zoomViewFinished:self]; + + } else{ + CGPoint tapCenter = [tapGesture locationInView:_zoomableView]; + CGFloat newScale = MIN(self.zoomScale * ARZoomMultiplierForDoubleTap, self.maximumZoomScale); + CGRect maxZoomRect = [self rectAroundPoint:tapCenter atZoomScale:newScale]; + [self zoomToRect:maxZoomRect animated:YES]; + } +} + +- (void)twoFingerTap:(UITapGestureRecognizer *)tapGesture +{ + if (self.zoomScale == self.minimumZoomScale) { + [self setZoomScale:self.maximumZoomScale animated:YES]; + + } else{ + CGPoint tapCenter = [tapGesture locationInView:_zoomableView]; + CGFloat newScale = MAX(self.zoomScale / ARZoomMultiplierForDoubleTap, self.minimumZoomScale); + CGRect minZoomRect = [self rectAroundPoint:tapCenter atZoomScale:newScale]; + [self zoomToRect:minZoomRect animated:YES]; + } +} + +- (void)singleTap:(UITapGestureRecognizer *)gesture +{ + [self.zoomDelegate zoomViewFinished:self]; +} + + +#pragma mark helpers + +- (CGRect)rectAroundPoint:(CGPoint)point atZoomScale:(CGFloat)zoomScale { + + // Define the shape of the zoom rect. + CGSize boundsSize = self.bounds.size; + + // Modify the size according to the requested zoom level. + // For example, if we're zooming in to 0.5 zoom, then this will increase the bounds size + // by a factor of two. + CGSize scaledBoundsSize = CGSizeMake(boundsSize.width / zoomScale, + boundsSize.height / zoomScale); + + CGRect rect = CGRectMake(point.x - scaledBoundsSize.width / 2, + point.y - scaledBoundsSize.height / 2, + scaledBoundsSize.width, + scaledBoundsSize.height); + + return rect; +} + +- (UIImage *)artworkImage +{ + return self.backgroundView.image; +} + +- (void)dealloc +{ + self.zoomableView = nil; + self.backgroundView = nil; +} + +@end diff --git a/Artsy/Classes/Views/StyledSubclasses.h b/Artsy/Classes/Views/StyledSubclasses.h new file mode 100644 index 00000000000..3944a400462 --- /dev/null +++ b/Artsy/Classes/Views/StyledSubclasses.h @@ -0,0 +1,4 @@ +#import +#import +#import "ARSeparatorViews.h" +#import "ARCustomEigenLabels.h" \ No newline at end of file diff --git a/Artsy/Classes/Views/Table View Cells/ARAdminTableViewCell.h b/Artsy/Classes/Views/Table View Cells/ARAdminTableViewCell.h new file mode 100644 index 00000000000..0e2bd797f03 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARAdminTableViewCell.h @@ -0,0 +1,9 @@ +#import + +extern CGFloat ARTableViewCellSettingsHeight; + +@interface ARAdminTableViewCell : UITableViewCell + +@property (nonatomic, assign) BOOL useSerifFont; + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARAdminTableViewCell.m b/Artsy/Classes/Views/Table View Cells/ARAdminTableViewCell.m new file mode 100644 index 00000000000..e186b7ac0e5 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARAdminTableViewCell.m @@ -0,0 +1,52 @@ +#import "ARAdminTableViewCell.h" + +CGFloat ARTableViewCellSettingsHeight = 60; + +@implementation ARAdminTableViewCell + +CGFloat MainTextVerticalOffset = 4; +CGFloat DetailTextVerticalOffset = 6; + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (!self) { return nil; } + + self.useSerifFont = YES; + UIView *backgroundView = [[UIView alloc] init]; + backgroundView.backgroundColor = [UIColor artsyLightGrey]; + self.selectedBackgroundView = backgroundView; + self.textLabel.backgroundColor = [UIColor clearColor]; + + return self; +} + + +- (void)setUseSerifFont:(BOOL)newUseSerifFont { + _useSerifFont = newUseSerifFont; + + if (_useSerifFont) { + self.textLabel.font = [UIFont serifFontWithSize:18]; + self.detailTextLabel.font = [UIFont serifFontWithSize:18]; + } else { + self.textLabel.font = [UIFont sansSerifFontWithSize:15]; + self.detailTextLabel.font = [UIFont sansSerifFontWithSize:15]; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (!self.detailTextLabel) { + if (_useSerifFont) { + CGRect frame = self.textLabel.frame; + frame.size.height -= MainTextVerticalOffset; + self.textLabel.frame = frame; + self.textLabel.center = CGPointMake(self.textLabel.center.x, self.textLabel.center.y + MainTextVerticalOffset); + } + } + else{ + self.detailTextLabel.center = CGPointMake(self.detailTextLabel.center.x, self.detailTextLabel.center.y + DetailTextVerticalOffset); + } +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARAnimatedTickView.h b/Artsy/Classes/Views/Table View Cells/ARAnimatedTickView.h new file mode 100644 index 00000000000..aff4e75f9cc --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARAnimatedTickView.h @@ -0,0 +1,8 @@ +#import + +@interface ARAnimatedTickView : UIView +- (id)initWithSelection:(BOOL)selected; + +- (BOOL)selected; +- (void)setSelected:(BOOL)selected animated:(BOOL)animated; +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARAnimatedTickView.m b/Artsy/Classes/Views/Table View Cells/ARAnimatedTickView.m new file mode 100644 index 00000000000..6873003a906 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARAnimatedTickView.m @@ -0,0 +1,160 @@ +#import "ARAnimatedTickView.h" + +#define TICK_DIMENSION 32 + +@interface ARTickViewFrontLayer : CAShapeLayer +@end + +@interface ARTickViewBackLayer : CALayer +@property (nonatomic, assign) CGFloat completion; +@end + +@interface ARAnimatedTickView (){ + ARTickViewBackLayer *_backLayer; +} +@end + +@implementation ARAnimatedTickView + +- (id)initWithFrame:(CGRect)frame { + [NSException raise:NSInvalidArgumentException format:@"NSObject %@[%@]: selector not recognized - use initWithSelection: ", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; + return nil; +} + +- (id)initWithSelection:(BOOL)selected { + self = [super initWithFrame:CGRectMake(0, 0, TICK_DIMENSION, TICK_DIMENSION)]; + if (self) { + self.backgroundColor = [[UIColor artsyLightGrey] colorWithAlphaComponent:0.2]; + + _backLayer = [ARTickViewBackLayer layer]; + _backLayer.completion = 1; + _backLayer.bounds = self.bounds; + _backLayer.position = CGPointMake(TICK_DIMENSION/2, TICK_DIMENSION/2); + + [self.layer addSublayer:_backLayer]; + [self.layer addSublayer:[ARTickViewFrontLayer layer]]; + + [self setSelected:selected animated:NO]; + } + return self; +} + +- (BOOL)selected { + return (_backLayer.completion)? YES : NO; +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated { + if (!animated) { + _backLayer.completion = selected? 1 : 0; + [_backLayer setNeedsDisplay]; + + }else { + CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"completion"]; + positionAnimation.duration = ARAnimationQuickDuration; + positionAnimation.fromValue = @(!selected); + positionAnimation.toValue = @(selected); + positionAnimation.fillMode = kCAFillModeForwards; + positionAnimation.removedOnCompletion = YES; + [_backLayer addAnimation:positionAnimation forKey:@"TickAnimation"]; + + _backLayer.completion = selected? 1 : 0; + } +} + +@end + +@implementation ARTickViewFrontLayer + +// This is essentially the facia behind which the tick selection is drawn + ++ (instancetype)layer { + ARTickViewFrontLayer *layer = [[ARTickViewFrontLayer alloc] init]; + CGMutablePathRef tickPath = CGPathCreateMutable(); + + // Tick with gets diffed on outline // x y + CGPathMoveToPoint(tickPath, NULL, 24.28, 6.62); + CGPathAddLineToPoint(tickPath, NULL, 12.14, 22.07); + CGPathAddLineToPoint(tickPath, NULL, 6.62, 16.55); + CGPathAddLineToPoint(tickPath, NULL, 4.41, 18.76); + CGPathAddLineToPoint(tickPath, NULL, 12.14, 26.48); + CGPathAddLineToPoint(tickPath, NULL, 26.48, 8.83); + CGPathAddLineToPoint(tickPath, NULL, 24.28, 6.62); + CGPathCloseSubpath(tickPath); + + // Outline + CGPathMoveToPoint(tickPath, NULL, 32, 32); + CGPathAddLineToPoint(tickPath, NULL, 0, 32); + CGPathAddLineToPoint(tickPath, NULL, 0, 0); + CGPathAddLineToPoint(tickPath, NULL, 32, 0); + CGPathAddLineToPoint(tickPath, NULL, 32, 32); + CGPathCloseSubpath(tickPath); + + layer.path = tickPath; + CGPathRelease(tickPath); + + layer.fillColor = [UIColor whiteColor].CGColor; + return layer; +} + +@end + +@implementation ARTickViewBackLayer + +// Tell the class if completion changes that needs a redraw +// meaning you can animate the key completion using a CABasicAnimation + ++ (BOOL)needsDisplayForKey:(NSString *)key { + if ([key isEqualToString:@"completion"]) { + return YES; + } + + return [super needsDisplayForKey:key]; +} + +- (void)drawInContext:(CGContextRef)context { + [self drawLowerHalfInContext:context]; + [self drawUpperHalfInContext:context]; +} + +// Top left is 0,0 + +- (void)drawLowerHalfInContext:(CGContextRef)ctx { + CGPoint TL = CGPointMake(6.0, 15.2); + CGPoint BL = CGPointMake(4.1, 19.4); + + // this is double the distance it needs, so that it finished in half-time + + CGPoint TR = CGPointMake(21, 32); + CGPoint BR = CGPointMake(21.1, 34.6); + [self drawStretchyRectWithPointsTL:TL TR:TR BL:BL BR:BR inContext:ctx]; +} + +- (void)drawUpperHalfInContext:(CGContextRef)ctx { + CGPoint TL = CGPointMake(9.4, 24.5); + CGPoint BL = CGPointMake(12.1, 27.1); + + CGPoint TR = CGPointMake(24, 6.8); + CGPoint BR = CGPointMake(27.1, 8.9); + [self drawStretchyRectWithPointsTL:TL TR:TR BL:BL BR:BR inContext:ctx]; +} + +- (void)drawStretchyRectWithPointsTL:(CGPoint)TL TR:(CGPoint)TR BL:(CGPoint)BL BR:(CGPoint)BR inContext:(CGContextRef)ctx { + CGContextMoveToPoint(ctx, TL.x, TL.y); + + // the top right + CGContextAddLineToPoint(ctx, ((TR.x - TL.x) * self.completion) + TL.x, ((TR.y - TL.y) * self.completion) + TL.y); + // bottom right + CGContextAddLineToPoint(ctx, ((BR.x - BL.x) * self.completion ) + BL.x , ((BR.y - BL.y) * self.completion) + BL.y); + + // bottom left + CGContextAddLineToPoint(ctx, BL.x, BL.y); + CGContextClosePath(ctx); + + // Color it + CGContextSetFillColorWithColor(ctx, [UIColor artsyPurple].CGColor); + CGContextSetLineWidth(ctx, 0); + + CGContextDrawPath(ctx, kCGPathFill); +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARAuctionArtworkTableViewCell.h b/Artsy/Classes/Views/Table View Cells/ARAuctionArtworkTableViewCell.h new file mode 100644 index 00000000000..6bab0c21878 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARAuctionArtworkTableViewCell.h @@ -0,0 +1,24 @@ +#import "AuctionLot.h" + +/// A cell for showing an auction result, either from an artwork +/// or from a auction lot. + +@interface ARAuctionArtworkTableViewCell : UITableViewCell + +/// provides the cell with updated information from a lot +- (void)updateWithAuctionResult:(AuctionLot *)auctionLot; + +/// provides the cell with updated information from an artwork +- (void)updateWithArtwork:(Artwork *)artwork; + +/// Provides a rough estimation of the height, doesn't take multi line +/// into account ++ (CGFloat)estimatedHeightWithAuctionLot:(AuctionLot *)auctionLot; + +/// Gets an accurate cell height for the auction lot, takes the expected width ++ (CGFloat)heightWithAuctionLot:(AuctionLot *)auctionLot withWidth:(CGFloat)width; + +/// Gets an accurate cell height for the artwork, takes the expected width ++ (CGFloat)heightWithArtwork:(Artwork *)auctionLot withWidth:(CGFloat)width; + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARAuctionArtworkTableViewCell.m b/Artsy/Classes/Views/Table View Cells/ARAuctionArtworkTableViewCell.m new file mode 100644 index 00000000000..eedcc7c61a8 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARAuctionArtworkTableViewCell.m @@ -0,0 +1,155 @@ +#import "ARAuctionArtworkTableViewCell.h" + +@interface ARAuctionArtworkTableViewCell () +@property (nonatomic, weak) UIView *separator; +@end + +@implementation ARAuctionArtworkTableViewCell + +static const CGFloat ARVerticalMargin = 20; +static const CGFloat ARTextHorizontalMargin = 160; +static const CGFloat ARImageMargin = 20; +static const CGFloat ARImageSize = 60; +static const CGFloat ARTextLineSpacing = 5; + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGFloat yPosition = CGRectGetHeight(self.bounds) - 1; + self.separator.frame = CGRectMake(ARImageMargin, yPosition, CGRectGetWidth(self.bounds) - (ARImageMargin * 2), 1); +} + +- (void)updateWithArtwork:(Artwork *)artwork +{ + [self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self addImagePreviewWithURL:artwork.urlForThumbnail]; + [self addSeparator]; + + NSArray *strings = [self.class _orderedDisplayStringArrayForArtwork:artwork]; + [self addLabelStackWithStrings:strings andTitleAtIndex:1]; +} + +- (void)updateWithAuctionResult:(AuctionLot *)auctionLot +{ + [self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + + [self addImagePreviewWithURL:auctionLot.imageURL]; + [self addSeparator]; + + NSArray *strings = [self.class _orderedDisplayStringArrayForAuctionLot:auctionLot]; + [self addLabelStackWithStrings:strings andTitleAtIndex:0]; +} + +#pragma mark adding views + +- (void)addLabelStackWithStrings:(NSArray *)strings andTitleAtIndex:(NSInteger)index +{ + CGFloat yOffset = ARVerticalMargin; + CGFloat xOffset = ARImageMargin + ARImageSize + ARImageMargin; + CGFloat textWidth = CGRectGetWidth(self.bounds) - ARTextHorizontalMargin; + CGSize maxSize = (CGSize){ textWidth, 600 }; + + for (NSString *string in strings) { + Class klass = ([strings indexOfObject:string] == index) ? [ARArtworkTitleLabel class] : [ARSerifLabel class]; + UILabel *label = [[klass alloc] init]; + + label.text = string; + + CGSize size = [string ar_sizeWithFont:[UIFont serifFontWithSize:17] constrainedToSize:maxSize]; + label.frame = (CGRect){ CGPointMake(xOffset, yOffset), CGSizeMake(textWidth, size.height) }; + + [self.contentView addSubview:label]; + yOffset += (size.height + ARTextLineSpacing); + } +} + +- (void)addImagePreviewWithURL:(NSURL *)url +{ + UIImageView *imagePreviewView = [[UIImageView alloc] initWithFrame:CGRectMake(ARImageMargin, ARImageMargin, 60, 60)]; + imagePreviewView.backgroundColor = [UIColor artsyLightGrey]; + [imagePreviewView ar_setImageWithURL:url]; + [self.contentView addSubview:imagePreviewView]; +} + +- (void)addSeparator +{ + CGFloat height = 2; + CGFloat yPosition = CGRectGetHeight(self.bounds) - height; + CGRect frame = CGRectMake(ARImageMargin, yPosition, CGRectGetWidth(self.bounds) - (ARImageMargin * 2), height); + UIView *separator = [[UIView alloc] initWithFrame:frame]; + separator.backgroundColor = [UIColor artsyLightGrey]; + self.separator = separator; + + [self.contentView addSubview:separator]; +} + +#pragma mark height + ++ (CGFloat)estimatedHeightWithAuctionLot:(AuctionLot *)auctionLot +{ + CGFloat height = 0; + CGFloat heightPerLine = 20 + ARTextLineSpacing; + + NSUInteger stringCount = [self _orderedDisplayStringArrayForAuctionLot:auctionLot].count; + height += stringCount * heightPerLine; + + height += ARVerticalMargin * 2; + return height; +} + ++ (CGFloat)heightWithAuctionLot:(AuctionLot *)auctionLot withWidth:(CGFloat)width +{ + NSArray *strings = [self _orderedDisplayStringArrayForAuctionLot:auctionLot]; + return [self _heightWithStringArray:strings withWidth:width]; +} + ++ (CGFloat)heightWithArtwork:(Artwork *)artwork withWidth:(CGFloat)width +{ + NSArray *strings = [self _orderedDisplayStringArrayForArtwork:artwork]; + return [self _heightWithStringArray:strings withWidth:width]; +} + ++ (CGFloat)_heightWithStringArray:(NSArray *)strings withWidth:(CGFloat)width +{ + CGFloat height = 0; + CGSize maxSize = (CGSize){ width - ARTextHorizontalMargin, 600 }; + UIFont *font = [UIFont serifFontWithSize:17]; + + for (NSString *string in strings) { + height += [string ar_sizeWithFont:font constrainedToSize:maxSize].height; + height += ARTextLineSpacing; + } + + height += ARVerticalMargin * 2; + return height; +} + ++ (NSArray *)_orderedDisplayStringArrayForAuctionLot:(AuctionLot *)auctionLot +{ + NSMutableArray *strings = [NSMutableArray array]; + NSString *dimensions = auctionLot.dimensionsInches.length ? auctionLot.dimensionsInches : auctionLot.dimensionsCM; + + if (auctionLot.title.length) [strings addObject:auctionLot.title]; + if (dimensions.length) [strings addObject:dimensions]; + if (auctionLot.organization.length) [strings addObject:auctionLot.organization]; + if (auctionLot.auctionDateText.length) [strings addObject:auctionLot.auctionDateText]; + if (auctionLot.price.length) [strings addObject:auctionLot.price]; + + return strings; +} + ++ (NSArray *)_orderedDisplayStringArrayForArtwork:(Artwork *)artwork +{ + NSMutableArray *strings = [NSMutableArray array]; + + if (artwork.artist.name.length) [strings addObject:artwork.artist.name]; + if (artwork.title.length) [strings addObject:artwork.title]; + if (artwork.dimensionsInches.length) [strings addObject:artwork.dimensionsInches]; + if (artwork.dimensionsCM.length) [strings addObject:artwork.dimensionsCM]; + if (artwork.price.length) [strings addObject:artwork.price]; + + return strings; +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARButtonWithImage.h b/Artsy/Classes/Views/Table View Cells/ARButtonWithImage.h new file mode 100644 index 00000000000..a5195228fb7 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARButtonWithImage.h @@ -0,0 +1,14 @@ +@interface ARButtonWithImage : UIButton + +@property (readwrite, nonatomic, copy) NSString *title; +@property (readwrite, nonatomic, copy) NSString *subtitle; + +@property (readwrite, nonatomic, strong) UIFont *titleFont; +@property (readwrite, nonatomic, strong) UIFont *subtitleFont; + +@property (readonly, nonatomic, strong) UIImageView *buttonImageView; +@property (readwrite, nonatomic, copy) NSURL *imageURL; +@property (readwrite, nonatomic, copy) NSURL *targetURL; +@property (readwrite, nonatomic, copy) UIImage *image; + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARButtonWithImage.m b/Artsy/Classes/Views/Table View Cells/ARButtonWithImage.m new file mode 100644 index 00000000000..a2d094e6a1a --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARButtonWithImage.m @@ -0,0 +1,144 @@ +#import "ARButtonWithImage.h" +#import "ARFeedImageLoader.h" + +@interface ARButtonWithImage () + +@property (readonly, nonatomic, strong) UIView *contentView; + +@property (readonly, nonatomic, strong) UIView *separatorView; + +@property (readonly, nonatomic, strong) UIView *labelContainer; +@property (readonly, nonatomic, strong) UILabel *actualTitleLabel; +@property (readonly, nonatomic, strong) UILabel *subtitleLabel; + +@property (readonly, nonatomic, strong) UIImageView *buttonArrowView; + +@property (readonly, nonatomic, strong) ARFeedImageLoader *imageLoader; + +@end + +@implementation ARButtonWithImage + +#pragma mark - Lifecycle + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { return nil; } + + _contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))]; + _contentView.userInteractionEnabled = NO; + [self addSubview:_contentView]; + + _separatorView = [[ARSeparatorView alloc] init]; + [self.contentView addSubview:_separatorView]; + + _buttonArrowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"MoreArrow"]]; + _buttonArrowView.contentMode = UIViewContentModeCenter; + _buttonArrowView.backgroundColor = [UIColor whiteColor]; + [self.contentView addSubview:_buttonArrowView]; + + _buttonImageView = [[UIImageView alloc] init]; + _buttonImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:_buttonImageView]; + + _labelContainer = [[UIView alloc] init]; + [self.contentView addSubview:_labelContainer]; + + _actualTitleLabel = [[UILabel alloc] init]; + _actualTitleLabel.font = [UIFont serifFontWithSize:20]; + _actualTitleLabel.numberOfLines = 0; + [self.labelContainer addSubview:_actualTitleLabel]; + + _subtitleLabel = [[UILabel alloc] init]; + _subtitleLabel.font = [UIFont serifFontWithSize:16]; + _subtitleLabel.textColor = [UIColor blackColor]; + _subtitleLabel.userInteractionEnabled = NO; + _subtitleLabel.numberOfLines = 2; + [self.labelContainer addSubview:_subtitleLabel]; + + [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + + _imageLoader = [[ARFeedImageLoader alloc] init]; + + // Autolayout + [self alignToView:self.contentView]; + + // Use bottom:@"0" to align separator with bottom of button. Using `nil` will align it with the top. + [self.separatorView alignTop:nil leading:@"10" bottom:@"0" trailing:@"-10" toView:self.contentView]; + + [self.buttonImageView constrainWidth:@"80"]; + [self.buttonImageView constrainHeight:@"80"]; + self.buttonImageView.contentMode = UIViewContentModeScaleAspectFill; + self.buttonImageView.clipsToBounds = YES; + + [self.buttonImageView alignLeadingEdgeWithView:self.contentView predicate:@"10"]; + [self.buttonImageView alignTopEdgeWithView:self.contentView predicate:@"12"]; + [self.contentView alignBottomEdgeWithView:self.buttonImageView predicate:@"13"]; + + [self.labelContainer constrainLeadingSpaceToView:self.buttonImageView predicate:@"20"]; + [self.labelContainer alignCenterYWithView:self.contentView predicate:nil]; + + [self.labelContainer alignTopEdgeWithView:self.actualTitleLabel predicate:nil]; + [self.actualTitleLabel alignLeadingEdgeWithView:self.labelContainer predicate:nil]; + [self.actualTitleLabel alignTrailingEdgeWithView:self.labelContainer predicate:nil]; + [self.subtitleLabel alignBottomEdgeWithView:self.labelContainer predicate:nil]; + + [self.subtitleLabel constrainTopSpaceToView:self.actualTitleLabel predicate:@"5"]; + + [UIView alignLeadingAndTrailingEdgesOfViews:@[ self.actualTitleLabel, self.subtitleLabel, self.labelContainer ]]; + + [self.buttonArrowView alignCenterYWithView:self.contentView predicate:nil]; + [self.buttonArrowView alignTrailingEdgeWithView:self.contentView predicate:@"-15@1000"]; + [self.buttonArrowView constrainLeadingSpaceToView:self.labelContainer predicate:@">=8@800"]; + + return self; +} + +#pragma mark - Properties + +- (void)setTitle:(NSString *)title +{ + _title = [title copy]; + + self.actualTitleLabel.text = title; +} + +- (void)setSubtitle:(NSString *)subtitle +{ + _subtitle = [subtitle copy]; + + self.subtitleLabel.text = subtitle; +} + +- (void)setImageURL:(NSURL *)imageURL +{ + _imageURL = imageURL; + + if (imageURL) { + [self.imageLoader loadImageAtAddress:imageURL.absoluteString + desiredSize:ARFeedItemImageSizeSmall + forImageView:self.buttonImageView + customPlaceholder:nil]; + } +} + +- (void)setImage:(UIImage *)image +{ + if (image) { + self.buttonImageView.image = image; + } +} + +- (void)setTitleFont:(UIFont *)titleFont +{ + self.actualTitleLabel.font = titleFont; + _titleFont = titleFont; +} + +- (void)setSubtitleFont:(UIFont *)subtitleFont +{ + self.subtitleLabel.font = subtitleFont; + _subtitleFont = subtitleFont; +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARFeedStatusIndicatorTableViewCell.h b/Artsy/Classes/Views/Table View Cells/ARFeedStatusIndicatorTableViewCell.h new file mode 100644 index 00000000000..36e4ba728c2 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARFeedStatusIndicatorTableViewCell.h @@ -0,0 +1,21 @@ +typedef NS_ENUM(NSUInteger, ARFeedStatusState) { + ARFeedStatusStateLoading = 1, + ARFeedStatusStateNetworkError, + ARFeedStatusStateEndOfFeed +}; + +/// Cell for displaying the state of a feed's network activity +/// will change height and colour based on the state property. + +@interface ARFeedStatusIndicatorTableViewCell : UITableViewCell + +/// Preferred initiliser, sets up cell and sets internal state ++ (instancetype)cellWithInitialState:(ARFeedStatusState)state; + +/// Gets the cells generated height based on state ++ (CGFloat)heightForFeedItemWithState:(ARFeedStatusState)state; + +/// Sets the feed status and updates the cell's UI +@property (nonatomic, assign) ARFeedStatusState state; + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARFeedStatusIndicatorTableViewCell.m b/Artsy/Classes/Views/Table View Cells/ARFeedStatusIndicatorTableViewCell.m new file mode 100644 index 00000000000..488c3a381ea --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARFeedStatusIndicatorTableViewCell.m @@ -0,0 +1,66 @@ +#import "ARFeedStatusIndicatorTableViewCell.h" +#import "ARReusableLoadingView.h" +#import "ARNetworkErrorView.h" + +@interface ARFeedStatusIndicatorTableViewCell() +@property (nonatomic, weak) UIView *stateView; +@end + +@implementation ARFeedStatusIndicatorTableViewCell + ++ (CGFloat)heightForFeedItemWithState:(ARFeedStatusState)state +{ + switch (state) { + case ARFeedStatusStateEndOfFeed: + return 80; + case ARFeedStatusStateLoading: + return 60; + case ARFeedStatusStateNetworkError: + return 60; + default: + return 0; + } +} + ++ (instancetype)cellWithInitialState:(ARFeedStatusState)state +{ + ARFeedStatusIndicatorTableViewCell *cell = nil; + cell = [[ARFeedStatusIndicatorTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"FeedStatus"]; + cell.selectedBackgroundView = nil; + cell.state = state; + cell.userInteractionEnabled = NO; + return cell; +} + +- (void)setState:(ARFeedStatusState)state +{ + if (state != _state){ + [self.stateView removeFromSuperview]; + UIView *stateView = nil; + + switch (state){ + case ARFeedStatusStateLoading: + stateView = [[ARReusableLoadingView alloc] initWithFrame:self.contentView.bounds]; + [(ARReusableLoadingView *)stateView startIndeterminateAnimated:YES]; + stateView.backgroundColor = [UIColor whiteColor]; + break; + + case ARFeedStatusStateEndOfFeed: + stateView = [[UIView alloc] initWithFrame:self.contentView.bounds]; + stateView.backgroundColor = [UIColor blackColor]; + break; + + case ARFeedStatusStateNetworkError: + stateView = [[ARNetworkErrorView alloc] initWithFrame:self.contentView.bounds]; + break; + } + + stateView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.contentView addSubview:stateView]; + + self.stateView = stateView; + _state = state; + } +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARGroupedTableViewCell.h b/Artsy/Classes/Views/Table View Cells/ARGroupedTableViewCell.h new file mode 100644 index 00000000000..b3eb3c6a139 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARGroupedTableViewCell.h @@ -0,0 +1,7 @@ +#import "ARTickedTableViewCell.h" + +@interface ARGroupedTableViewCell : ARTickedTableViewCell + +@property (nonatomic, assign) BOOL isTopCell; + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARGroupedTableViewCell.m b/Artsy/Classes/Views/Table View Cells/ARGroupedTableViewCell.m new file mode 100644 index 00000000000..8e14581474c --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARGroupedTableViewCell.m @@ -0,0 +1,36 @@ +#import "ARGroupedTableViewCell.h" + +@implementation ARGroupedTableViewCell + +- (void)drawRect:(CGRect)rect { + CGContextRef context = UIGraphicsGetCurrentContext(); + [[UIColor whiteColor] setFill]; + CGContextFillRect(context, rect); + + CGContextSetLineWidth(context, [[UIScreen mainScreen] scale] * 4); + CGContextSetStrokeColorWithColor(context, [UIColor artsyMediumGrey].CGColor); + + // left + CGContextMoveToPoint(context, 0, 0); + CGContextAddLineToPoint(context, 0, CGRectGetHeight(rect)); + + // bottom + CGContextAddLineToPoint(context, CGRectGetWidth(rect), CGRectGetHeight(rect)); + + // right + CGContextAddLineToPoint(context, CGRectGetWidth(rect), 0); + + // top + if (_isTopCell) { + CGContextAddLineToPoint(context, 0, 0); + } + + CGContextStrokePath(context); +} + +- (void)prepareForReuse { + _isTopCell = NO; + [super prepareForReuse]; +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARSearchTableViewCell.h b/Artsy/Classes/Views/Table View Cells/ARSearchTableViewCell.h new file mode 100644 index 00000000000..e91cf7b7a31 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARSearchTableViewCell.h @@ -0,0 +1,5 @@ +#import + +@interface ARSearchTableViewCell : UITableViewCell + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARSearchTableViewCell.m b/Artsy/Classes/Views/Table View Cells/ARSearchTableViewCell.m new file mode 100644 index 00000000000..26ff00b9de8 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARSearchTableViewCell.m @@ -0,0 +1,25 @@ +#import "ARSearchTableViewCell.h" + +@implementation ARSearchTableViewCell + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (!self) { return nil; } + + self.backgroundColor = [UIColor clearColor]; + self.textLabel.textColor = [UIColor whiteColor]; + self.textLabel.font = [UIFont serifFontWithSize:18]; + self.indentationWidth = 0; + self.selectionStyle = UITableViewCellSelectionStyleNone; + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.textLabel.frame = CGRectMake(52, 8, CGRectGetWidth(self.bounds)- 52 - 8, 32); + self.imageView.frame = CGRectMake(8, 4, 36, 36); +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARSwitchCell.h b/Artsy/Classes/Views/Table View Cells/ARSwitchCell.h new file mode 100644 index 00000000000..b52db0e62e5 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARSwitchCell.h @@ -0,0 +1,5 @@ +#import "FODSwitchCell.h" + +@interface ARSwitchCell : FODSwitchCell + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARSwitchCell.m b/Artsy/Classes/Views/Table View Cells/ARSwitchCell.m new file mode 100644 index 00000000000..1f5465c6e8b --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARSwitchCell.m @@ -0,0 +1,10 @@ +#import "ARSwitchCell.h" + +@implementation ARSwitchCell + +- (void)awakeFromNib { + [self.switchControl setOnTintColor:[UIColor artsyPurple]]; + self.titleLabel.font = [self.titleLabel.font fontWithSize:16]; +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARTextInputCell.h b/Artsy/Classes/Views/Table View Cells/ARTextInputCell.h new file mode 100644 index 00000000000..8da4ab28163 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARTextInputCell.h @@ -0,0 +1,5 @@ +#import "FODTextInputCell.h" + +@interface ARTextInputCell : FODTextInputCell + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARTextInputCell.m b/Artsy/Classes/Views/Table View Cells/ARTextInputCell.m new file mode 100644 index 00000000000..1b3cce26543 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARTextInputCell.m @@ -0,0 +1,13 @@ +#import "ARTextInputCell.h" + +@implementation ARTextInputCell + +- (void) configureCellForRow:(FODFormRow*)row + withDelegate:(id)delegate { + [super configureCellForRow:row withDelegate:delegate]; + self.textField.textColor = [UIColor blackColor]; + self.textField.font = [UIFont serifFontWithSize:16]; + self.titleLabel.font = [self.titleLabel.font fontWithSize:14]; +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARTickedTableViewCell.h b/Artsy/Classes/Views/Table View Cells/ARTickedTableViewCell.h new file mode 100644 index 00000000000..e2f52e7ae17 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARTickedTableViewCell.h @@ -0,0 +1,7 @@ +#import "ARAdminTableViewCell.h" + +@interface ARTickedTableViewCell : ARAdminTableViewCell + +- (void)setTickSelected:(BOOL)selected animated:(BOOL)animated; +- (BOOL)isSelected; +@end diff --git a/Artsy/Classes/Views/Table View Cells/ARTickedTableViewCell.m b/Artsy/Classes/Views/Table View Cells/ARTickedTableViewCell.m new file mode 100644 index 00000000000..a36cc707e33 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/ARTickedTableViewCell.m @@ -0,0 +1,32 @@ +#import "ARTickedTableViewCell.h" +#import "ARAnimatedTickView.h" + +@implementation ARTickedTableViewCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + self.useSerifFont = YES; + self.selectionStyle = UITableViewCellSelectionStyleNone; + self.accessoryView = [[ARAnimatedTickView alloc] initWithSelection:NO]; + self.textLabel.textColor = [UIColor blackColor]; + } + return self; +} + +// Using setSelected comes with too much baggage. Lets simplify. + +- (void)setTickSelected:(BOOL)selected animated:(BOOL)animated { + if ([self.accessoryView isKindOfClass:[ARAnimatedTickView class]]) { + [(ARAnimatedTickView *)self.accessoryView setSelected:selected animated:animated]; + } +} + +- (BOOL)isSelected { + if ([self.accessoryView isKindOfClass:[ARAnimatedTickView class]]) { + return [(ARAnimatedTickView *)self.accessoryView selected]; + } + return NO; +} + +@end diff --git a/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARFeedActionFooter.xib b/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARFeedActionFooter.xib new file mode 100644 index 00000000000..e47a0da85df --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARFeedActionFooter.xib @@ -0,0 +1,116 @@ + + + + 1536 + 12A269 + 2835 + 1187 + 624.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 1919 + + + IBProxyObject + IBUIView + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + PluginDependencyRecalculationVersion + + + + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + {{0, 20}, {320, 548}} + + + + + 3 + MQA + + 2 + + + + + IBUIScreenMetrics + + YES + + + + + + {320, 568} + {568, 320} + + + IBCocoaTouchFramework + Retina 4 Full Screen + 2 + + IBCocoaTouchFramework + + + + + + + + 0 + + + + + + 1 + + + + + -1 + + + File's Owner + + + -2 + + + + + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + UIResponder + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + + + 2 + + + 0 + IBCocoaTouchFramework + YES + 3 + YES + 1919 + + diff --git a/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARModernPartnerShowTableViewCell.h b/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARModernPartnerShowTableViewCell.h new file mode 100644 index 00000000000..9a424a40487 --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARModernPartnerShowTableViewCell.h @@ -0,0 +1,28 @@ +#import +#import "ARFeedItem.h" + +/** + The Modern Partner Show TableView Cell is the table view cell + used the show feed, it uses auto-layout and the new AREmbeddedArtwork View Controllers + and provides an example of a lot of the best practices. +*/ + +@class ARModernPartnerShowTableViewCell; + +@protocol ARModernPartnerShowTableViewCellDelegate + +-(void)modernPartnerShowTableViewCell:(ARModernPartnerShowTableViewCell *)cell shouldShowViewController:(UIViewController *)viewController; + +@end + +@interface ARModernPartnerShowTableViewCell : UITableViewCell + +/// Get the pre-generated height for the table view cell ++ (CGFloat)heightForItem:(ARFeedItem *)feedItem; + +/// Configure the cell with the feed item +- (void)configureWithFeedItem:(ARFeedItem *)feedItem; + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARModernPartnerShowTableViewCell.m b/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARModernPartnerShowTableViewCell.m new file mode 100644 index 00000000000..ca86a3bda8b --- /dev/null +++ b/Artsy/Classes/Views/Table View Cells/iPhone Feed Items/ARModernPartnerShowTableViewCell.m @@ -0,0 +1,213 @@ +#import "ARModernPartnerShowTableViewCell.h" +#import "AREmbeddedModelsViewController.h" +#import "ARPartnerShowFeedItem.h" +#import "ARTheme+HeightAdditions.h" +#import "ARArtworkSetViewController.h" +/// The maximum amount of items before switching from Carousel to masonry +static const NSInteger CarouselItemLimit = 4; +static const CGFloat ARPartnerShowCellBottomMargin = 30; +static CGFloat pregeneratedMargins = 0; +static CGFloat ARPartnerShowCellSideMargin; + +@interface ARModernPartnerShowTableViewCell() +@property (nonatomic, strong) AREmbeddedModelsViewController *artworkThumbnailsVC; + +@property (nonatomic, strong) ORStackView *stackView; +@property (nonatomic, strong) PartnerShow *show; + +@property (nonatomic, strong) UILabel *partnerNameLabel; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *ausstellungsdauerLabel; +@property (nonatomic, strong) UIView *separatorView; + +@end + +@implementation ARModernPartnerShowTableViewCell + ++ (void)initialize +{ + ARPartnerShowCellSideMargin = [UIDevice isPad] ? 50 : 20; +} + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(self.class)]; + if (!self) { return nil; } + + ARTheme *theme = [ARTheme themeNamed:@"ShowFeed"]; + ARThemeLayoutVendor *layout = [theme layout]; + + // on iOS7 this is a UITableViewCellScrollView, but on iOS8 + if ([self.contentView.superview isKindOfClass:[UIScrollView class]]) { + UIScrollView *contentViewSuperview = (UIScrollView *) self.contentView.superview; + contentViewSuperview.scrollEnabled = NO; + } + + self.selectionStyle = UITableViewCellSelectionStyleNone; + + _partnerNameLabel = [ARThemedFactory labelForFeedItemHeaders]; + _titleLabel = [ARThemedFactory labelForFeedItemHeaders]; + _ausstellungsdauerLabel = [ARThemedFactory labelForFeedItemSubheadings]; + _ausstellungsdauerLabel.textColor = [UIColor blackColor]; + + _artworkThumbnailsVC = [[AREmbeddedModelsViewController alloc] init]; + _artworkThumbnailsVC.delegate = self; + _separatorView = [[ARSeparatorView alloc] init]; + + _stackView = [[ORStackView alloc] init]; + [self addSubview:_stackView]; + + // Artworks must have a bottom position to generate a height. bottomMarginHeight constrains the artwork collection + // view's bottom to the bottom of the stack view. The stack view's bottom is also aligned with the sparator's top, + // and the separator's bottom has already been aligned to the bottom of the cell view. + + // Subtract one on the bottom because the separatorView's height is 1. + [_stackView alignToView:self]; + [_stackView addSubview:_partnerNameLabel withTopMargin:layout[@"FeedItemTopMargin"] sideMargin:@(ARPartnerShowCellSideMargin *2).stringValue]; + + [_stackView addSubview:_titleLabel withTopMargin:layout[@"ShowFeedItemShowTitleTopMargin"] sideMargin:@(ARPartnerShowCellSideMargin *2).stringValue]; + [_stackView addSubview:_ausstellungsdauerLabel withTopMargin:layout[@"ShowFeedItemSubtitleTopMargin"] sideMargin:@(ARPartnerShowCellSideMargin *2).stringValue]; + [_stackView addSubview:_artworkThumbnailsVC.view withTopMargin:layout[@"ShowFeedItemArtworksTopMargin"] sideMargin:nil]; + [_stackView addSubview:_separatorView withTopMargin:@(ARPartnerShowCellBottomMargin - 1).stringValue sideMargin:@(ARPartnerShowCellSideMargin *2).stringValue]; + + UITapGestureRecognizer *goToPartnerTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openPartner:)]; + [_partnerNameLabel addGestureRecognizer:goToPartnerTapGesture]; + _partnerNameLabel.userInteractionEnabled = YES; + + UITapGestureRecognizer *goToShowsTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openShow:)]; + [_titleLabel addGestureRecognizer:goToShowsTapGesture]; + _titleLabel.userInteractionEnabled = YES; + + return self; +} + +- (void)prepareForReuse +{ + _titleLabel.text = nil; + _ausstellungsdauerLabel.text = nil; + _show = nil; + + [_artworkThumbnailsVC viewWillDisappear:NO]; + [_artworkThumbnailsVC viewDidDisappear:NO]; +} + ++ (NSString *)subtitleForItem:(ARPartnerShowFeedItem *)feedItem +{ + PartnerShow *item = feedItem.show; + return item.ausstellungsdauerAndLocation; +} + ++ (CGFloat)heightForItem:(ARPartnerShowFeedItem *)feedItem +{ + CGFloat height = 0; + PartnerShow *item = feedItem.show; + + ARTheme *theme = [ARTheme themeNamed:@"ShowFeed"]; + + CGSize screenSize = [UIScreen mainScreen].bounds.size; + CGFloat width = UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]) ? screenSize.width : screenSize.height; + width -= (2 * ARPartnerShowCellSideMargin); + NSString *partnerName = item.partner.name; + CGSize size = [partnerName ar_sizeWithFont:theme.fonts[@"FeedHeaderTitle"] constrainedToSize:CGSizeMake(width, MAXFLOAT)]; + height += size.height; + + NSString *title = [item.subtitle stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + size = [title ar_sizeWithFont:theme.fonts[@"FeedHeaderTitle"] constrainedToSize:CGSizeMake(width, MAXFLOAT)]; + height += size.height; + + NSString *subtitleText = [self subtitleForItem:feedItem]; + size = [subtitleText ar_sizeWithFont:theme.fonts[@"FeedHeaderSubtitle"] constrainedToSize:CGSizeMake(width, MAXFLOAT)]; + height += size.height; + + CGFloat artworkHeight = 0; + BOOL useMasonry = [self shouldUseMultipleRowsForItem:feedItem.show]; + + ARArtworkMasonryLayout layout = (useMasonry) ? ARArtworkMasonryLayout2Row : ARArtworkMasonryLayout1Row; + artworkHeight = [ARArtworkMasonryModule intrinsicHeightForHorizontalLayout:layout]; + height += (artworkHeight + ARPartnerShowCellBottomMargin); + if (pregeneratedMargins == 0) { + NSArray *verticalMargins = @[ + @"FeedItemTopMargin", + @"ShowFeedItemShowTitleTopMargin", + @"ShowFeedItemSubtitleTopMargin", + @"ShowFeedItemArtworksTopMargin", + ]; + + pregeneratedMargins = [[ARTheme themeNamed:@"ShowFeed"] combinedFloatValueOfLayoutElementsWithKeys:verticalMargins]; + } + + height += pregeneratedMargins; + + return height; +} + ++ (BOOL)shouldUseMultipleRowsForItem:(PartnerShow *)item +{ + return (![UIDevice isPad]) && item.artworks.count > CarouselItemLimit; +} + +- (void)configureWithFeedItem:(ARPartnerShowFeedItem *)feedItem +{ + PartnerShow *item = feedItem.show; + + self.show = item; + self.partnerNameLabel.text = item.title; + self.titleLabel.text = [item.subtitle stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + self.ausstellungsdauerLabel.text = [self.class subtitleForItem:feedItem]; + + BOOL useMasonry = [[self class] shouldUseMultipleRowsForItem:item]; + if (useMasonry) { + id module = [ARArtworkMasonryModule masonryModuleWithLayout:ARArtworkMasonryLayout2Row andStyle:[self presentationStyle]]; + self.artworkThumbnailsVC.activeModule = module; + + } else { + id module = [ARArtworkMasonryModule masonryModuleWithLayout:ARArtworkMasonryLayout1Row andStyle:[self presentationStyle]]; + self.artworkThumbnailsVC.activeModule = module; + } + + [self.artworkThumbnailsVC appendItems:item.artworks]; +} + ++ (AREmbeddedArtworkPresentationStyle)presentationStyle +{ + return [UIDevice isPad] ? AREmbeddedArtworkPresentationStyleArtworkMetadata : AREmbeddedArtworkPresentationStyleArtworkOnly; +} + +- (AREmbeddedArtworkPresentationStyle)presentationStyle +{ + return [[self class] presentationStyle]; +} + +- (void)openShow:(UITapGestureRecognizer *)gesture +{ + ARFairShowViewController *viewController = [[ARSwitchBoard sharedInstance] loadShow:self.show]; + [self.delegate modernPartnerShowTableViewCell:self shouldShowViewController:(id)viewController]; +} + +- (void)openPartner:(UITapGestureRecognizer *)gesture +{ + if ([AROptions boolForOption:AROptionsTappingPartnerSendsToPartner]) { + + UIViewController *viewController = [[ARSwitchBoard sharedInstance] loadPartnerWithID:self.show.partner.partnerID]; + [self.delegate modernPartnerShowTableViewCell:self shouldShowViewController:(id)viewController]; + + } else { + [self openShow:gesture]; + } +} + +#pragma mark - AREmbeddedModelsDelegate + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller shouldPresentViewController:(UIViewController *)viewController +{ + [self.delegate modernPartnerShowTableViewCell:self shouldShowViewController:viewController]; +} + +- (void)embeddedModelsViewController:(AREmbeddedModelsViewController *)controller didTapItemAtIndex:(NSUInteger)index +{ + ARArtworkSetViewController *viewController = [ARSwitchBoard.sharedInstance loadArtworkSet:self.artworkThumbnailsVC.items inFair:nil atIndex:index]; + [self.delegate modernPartnerShowTableViewCell:self shouldShowViewController:viewController]; +} + +@end diff --git a/Artsy/Classes/mail.html b/Artsy/Classes/mail.html new file mode 100644 index 00000000000..20c69f87924 --- /dev/null +++ b/Artsy/Classes/mail.html @@ -0,0 +1,12 @@ + + +

Thanks for helping us improve Artsy.

+ + +

 

+

 

+ + +

Device : {{Device}} - {{iOS Version}}
Version : {{Version}}

+ + \ No newline at end of file diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Contents.json b/Artsy/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..bb157669606 --- /dev/null +++ b/Artsy/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,64 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-Small-40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 00000000000..0939da02b2e Binary files /dev/null and b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Icon-76.png b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 00000000000..0411b0679d3 Binary files /dev/null and b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 00000000000..ab352293522 Binary files /dev/null and b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png new file mode 100644 index 00000000000..2084d59473e Binary files /dev/null and b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png differ diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png new file mode 100644 index 00000000000..b543a27ec30 Binary files /dev/null and b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png differ diff --git a/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png new file mode 100644 index 00000000000..b543a27ec30 Binary files /dev/null and b/Artsy/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Contents.json b/Artsy/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000000..ba7be721957 --- /dev/null +++ b/Artsy/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,81 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "8.0", + "subtype" : "736h", + "scale" : "3x" + }, + { + "orientation" : "landscape", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "8.0", + "subtype" : "736h", + "scale" : "3x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "8.0", + "subtype" : "667h", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default@2x~iphone.png", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "Default-568h@2x~iphone.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Portrait~ipad.png", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Landscape~ipad.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Portrait@2x~ipad.png", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Landscape@2x~ipad.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Default-568h@2x~iphone.png b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-568h@2x~iphone.png new file mode 100644 index 00000000000..cf350f0684f Binary files /dev/null and b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-568h@2x~iphone.png differ diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x~ipad.png b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x~ipad.png new file mode 100644 index 00000000000..e95b51f8eff Binary files /dev/null and b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x~ipad.png differ diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Landscape~ipad.png b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Landscape~ipad.png new file mode 100644 index 00000000000..f6d9e7822ee Binary files /dev/null and b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Landscape~ipad.png differ diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x~ipad.png b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x~ipad.png new file mode 100644 index 00000000000..c0a4a272337 Binary files /dev/null and b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x~ipad.png differ diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Portrait~ipad.png b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Portrait~ipad.png new file mode 100644 index 00000000000..145aa8fb772 Binary files /dev/null and b/Artsy/Images.xcassets/LaunchImage.launchimage/Default-Portrait~ipad.png differ diff --git a/Artsy/Images.xcassets/LaunchImage.launchimage/Default@2x~iphone.png b/Artsy/Images.xcassets/LaunchImage.launchimage/Default@2x~iphone.png new file mode 100644 index 00000000000..29b0a1ff36d Binary files /dev/null and b/Artsy/Images.xcassets/LaunchImage.launchimage/Default@2x~iphone.png differ diff --git a/Artsy/Resources/ImageAssets/ActionButton@2x.png b/Artsy/Resources/ImageAssets/ActionButton@2x.png new file mode 100644 index 00000000000..68f2e633969 Binary files /dev/null and b/Artsy/Resources/ImageAssets/ActionButton@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Artwork_Icon_Share.png b/Artsy/Resources/ImageAssets/Artwork_Icon_Share.png new file mode 100644 index 00000000000..e498c51f233 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Artwork_Icon_Share.png differ diff --git a/Artsy/Resources/ImageAssets/Artwork_Icon_Share@2x.png b/Artsy/Resources/ImageAssets/Artwork_Icon_Share@2x.png new file mode 100644 index 00000000000..464b1ea44fa Binary files /dev/null and b/Artsy/Resources/ImageAssets/Artwork_Icon_Share@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Artwork_Icon_VIR.png b/Artsy/Resources/ImageAssets/Artwork_Icon_VIR.png new file mode 100644 index 00000000000..fce4b514db6 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Artwork_Icon_VIR.png differ diff --git a/Artsy/Resources/ImageAssets/Artwork_Icon_VIR@2x.png b/Artsy/Resources/ImageAssets/Artwork_Icon_VIR@2x.png new file mode 100644 index 00000000000..3333738b887 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Artwork_Icon_VIR@2x.png differ diff --git a/Artsy/Resources/ImageAssets/BackArrow@2x.png b/Artsy/Resources/ImageAssets/BackArrow@2x.png new file mode 100644 index 00000000000..45db8e2ebdb Binary files /dev/null and b/Artsy/Resources/ImageAssets/BackArrow@2x.png differ diff --git a/Artsy/Resources/ImageAssets/BackArrow_Highlighted@2x.png b/Artsy/Resources/ImageAssets/BackArrow_Highlighted@2x.png new file mode 100644 index 00000000000..454a9b1dcdf Binary files /dev/null and b/Artsy/Resources/ImageAssets/BackArrow_Highlighted@2x.png differ diff --git a/Artsy/Resources/ImageAssets/CloseButtonLarge@2x.png b/Artsy/Resources/ImageAssets/CloseButtonLarge@2x.png new file mode 100644 index 00000000000..9a0b6396259 Binary files /dev/null and b/Artsy/Resources/ImageAssets/CloseButtonLarge@2x.png differ diff --git a/Artsy/Resources/ImageAssets/CloseButtonLargeHighlighted@2x.png b/Artsy/Resources/ImageAssets/CloseButtonLargeHighlighted@2x.png new file mode 100644 index 00000000000..b5abfb0e2f8 Binary files /dev/null and b/Artsy/Resources/ImageAssets/CloseButtonLargeHighlighted@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Default-568h@2x~iphone.png b/Artsy/Resources/ImageAssets/Default-568h@2x~iphone.png new file mode 100644 index 00000000000..cf350f0684f Binary files /dev/null and b/Artsy/Resources/ImageAssets/Default-568h@2x~iphone.png differ diff --git a/Artsy/Resources/ImageAssets/Default-Landscape@2x~ipad.png b/Artsy/Resources/ImageAssets/Default-Landscape@2x~ipad.png new file mode 100644 index 00000000000..e95b51f8eff Binary files /dev/null and b/Artsy/Resources/ImageAssets/Default-Landscape@2x~ipad.png differ diff --git a/Artsy/Resources/ImageAssets/Default-Landscape~ipad.png b/Artsy/Resources/ImageAssets/Default-Landscape~ipad.png new file mode 100644 index 00000000000..f6d9e7822ee Binary files /dev/null and b/Artsy/Resources/ImageAssets/Default-Landscape~ipad.png differ diff --git a/Artsy/Resources/ImageAssets/Default-Portrait@2x~ipad.png b/Artsy/Resources/ImageAssets/Default-Portrait@2x~ipad.png new file mode 100644 index 00000000000..c0a4a272337 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Default-Portrait@2x~ipad.png differ diff --git a/Artsy/Resources/ImageAssets/Default-Portrait~ipad.png b/Artsy/Resources/ImageAssets/Default-Portrait~ipad.png new file mode 100644 index 00000000000..145aa8fb772 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Default-Portrait~ipad.png differ diff --git a/Artsy/Resources/ImageAssets/Default@2x~iphone.png b/Artsy/Resources/ImageAssets/Default@2x~iphone.png new file mode 100644 index 00000000000..29b0a1ff36d Binary files /dev/null and b/Artsy/Resources/ImageAssets/Default@2x~iphone.png differ diff --git a/Artsy/Resources/ImageAssets/FollowCheckmark.png b/Artsy/Resources/ImageAssets/FollowCheckmark.png new file mode 100644 index 00000000000..22ee1dac926 Binary files /dev/null and b/Artsy/Resources/ImageAssets/FollowCheckmark.png differ diff --git a/Artsy/Resources/ImageAssets/FollowCheckmark@2x.png b/Artsy/Resources/ImageAssets/FollowCheckmark@2x.png new file mode 100644 index 00000000000..75165108051 Binary files /dev/null and b/Artsy/Resources/ImageAssets/FollowCheckmark@2x.png differ diff --git a/Artsy/Resources/ImageAssets/FooterBackground@2x.png b/Artsy/Resources/ImageAssets/FooterBackground@2x.png new file mode 100644 index 00000000000..7a361dd053c Binary files /dev/null and b/Artsy/Resources/ImageAssets/FooterBackground@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Heart_Black@2x.png b/Artsy/Resources/ImageAssets/Heart_Black@2x.png new file mode 100644 index 00000000000..75554959609 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Heart_Black@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Heart_White@2x.png b/Artsy/Resources/ImageAssets/Heart_White@2x.png new file mode 100644 index 00000000000..0ca1ee5cfc9 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Heart_White@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Image_Shadow_Overlay@2x.png b/Artsy/Resources/ImageAssets/Image_Shadow_Overlay@2x.png new file mode 100644 index 00000000000..ef7d9218871 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Image_Shadow_Overlay@2x.png differ diff --git a/Artsy/Resources/ImageAssets/MapButtonAction@2x.png b/Artsy/Resources/ImageAssets/MapButtonAction@2x.png new file mode 100644 index 00000000000..3a17b536c56 Binary files /dev/null and b/Artsy/Resources/ImageAssets/MapButtonAction@2x.png differ diff --git a/Artsy/Resources/ImageAssets/MapIcon@2x.png b/Artsy/Resources/ImageAssets/MapIcon@2x.png new file mode 100644 index 00000000000..9a040dce704 Binary files /dev/null and b/Artsy/Resources/ImageAssets/MapIcon@2x.png differ diff --git a/Artsy/Resources/ImageAssets/MenuButtonBG@2x.png b/Artsy/Resources/ImageAssets/MenuButtonBG@2x.png new file mode 100644 index 00000000000..9eb9b6b2135 Binary files /dev/null and b/Artsy/Resources/ImageAssets/MenuButtonBG@2x.png differ diff --git a/Artsy/Resources/ImageAssets/MenuClose@2x.png b/Artsy/Resources/ImageAssets/MenuClose@2x.png new file mode 100644 index 00000000000..b7d6d896520 Binary files /dev/null and b/Artsy/Resources/ImageAssets/MenuClose@2x.png differ diff --git a/Artsy/Resources/ImageAssets/MenuHamburger@2x.png b/Artsy/Resources/ImageAssets/MenuHamburger@2x.png new file mode 100644 index 00000000000..03d08fc4a9e Binary files /dev/null and b/Artsy/Resources/ImageAssets/MenuHamburger@2x.png differ diff --git a/Artsy/Resources/ImageAssets/MoreArrow.png b/Artsy/Resources/ImageAssets/MoreArrow.png new file mode 100644 index 00000000000..2cbb38374d0 Binary files /dev/null and b/Artsy/Resources/ImageAssets/MoreArrow.png differ diff --git a/Artsy/Resources/ImageAssets/MoreArrow@2x.png b/Artsy/Resources/ImageAssets/MoreArrow@2x.png new file mode 100644 index 00000000000..c4967d35989 Binary files /dev/null and b/Artsy/Resources/ImageAssets/MoreArrow@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Parallax_Overlay_Bottom@2x.png b/Artsy/Resources/ImageAssets/Parallax_Overlay_Bottom@2x.png new file mode 100644 index 00000000000..9ccb5629b04 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Parallax_Overlay_Bottom@2x.png differ diff --git a/Artsy/Resources/ImageAssets/Parallax_Overlay_Top@2x.png b/Artsy/Resources/ImageAssets/Parallax_Overlay_Top@2x.png new file mode 100644 index 00000000000..c41467aad40 Binary files /dev/null and b/Artsy/Resources/ImageAssets/Parallax_Overlay_Top@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SearchIcon_HeavyGrey@2x.png b/Artsy/Resources/ImageAssets/SearchIcon_HeavyGrey@2x.png new file mode 100644 index 00000000000..9244a80c0f6 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SearchIcon_HeavyGrey@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SearchIcon_LightGrey@2x.png b/Artsy/Resources/ImageAssets/SearchIcon_LightGrey@2x.png new file mode 100644 index 00000000000..8b1f2135f7a Binary files /dev/null and b/Artsy/Resources/ImageAssets/SearchIcon_LightGrey@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SearchIcon_MediumGrey@2x.png b/Artsy/Resources/ImageAssets/SearchIcon_MediumGrey@2x.png new file mode 100644 index 00000000000..edb084c46c0 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SearchIcon_MediumGrey@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SearchIcon_White@2x.png b/Artsy/Resources/ImageAssets/SearchIcon_White@2x.png new file mode 100644 index 00000000000..097c95b48e4 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SearchIcon_White@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SearchThumb_HeavyGrey@2x.png b/Artsy/Resources/ImageAssets/SearchThumb_HeavyGrey@2x.png new file mode 100644 index 00000000000..fdccc34b8ea Binary files /dev/null and b/Artsy/Resources/ImageAssets/SearchThumb_HeavyGrey@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SearchThumb_LightGrey@2x.png b/Artsy/Resources/ImageAssets/SearchThumb_LightGrey@2x.png new file mode 100644 index 00000000000..033b7cda4d0 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SearchThumb_LightGrey@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SettingsButton@2x.png b/Artsy/Resources/ImageAssets/SettingsButton@2x.png new file mode 100644 index 00000000000..0ad9e944bdc Binary files /dev/null and b/Artsy/Resources/ImageAssets/SettingsButton@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SettingsButtonBlackBG@2x.png b/Artsy/Resources/ImageAssets/SettingsButtonBlackBG@2x.png new file mode 100644 index 00000000000..90b3dcff85e Binary files /dev/null and b/Artsy/Resources/ImageAssets/SettingsButtonBlackBG@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SidebarButtonBG@2x.png b/Artsy/Resources/ImageAssets/SidebarButtonBG@2x.png new file mode 100644 index 00000000000..23423627681 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SidebarButtonBG@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SidebarButtonHighlightBG@2x.png b/Artsy/Resources/ImageAssets/SidebarButtonHighlightBG@2x.png new file mode 100644 index 00000000000..d3edaeef25a Binary files /dev/null and b/Artsy/Resources/ImageAssets/SidebarButtonHighlightBG@2x.png differ diff --git a/Artsy/Resources/ImageAssets/SmallMoreVerticalArrow.png b/Artsy/Resources/ImageAssets/SmallMoreVerticalArrow.png new file mode 100644 index 00000000000..2cbb38374d0 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SmallMoreVerticalArrow.png differ diff --git a/Artsy/Resources/ImageAssets/SmallMoreVerticalArrow@2x.png b/Artsy/Resources/ImageAssets/SmallMoreVerticalArrow@2x.png new file mode 100644 index 00000000000..df6a96bef02 Binary files /dev/null and b/Artsy/Resources/ImageAssets/SmallMoreVerticalArrow@2x.png differ diff --git a/Artsy/Resources/ImageAssets/TextfieldClearButton.png b/Artsy/Resources/ImageAssets/TextfieldClearButton.png new file mode 100644 index 00000000000..e0706ffeb59 Binary files /dev/null and b/Artsy/Resources/ImageAssets/TextfieldClearButton.png differ diff --git a/Artsy/Resources/ImageAssets/fbp b/Artsy/Resources/ImageAssets/fbp new file mode 100644 index 00000000000..49cd080909c --- /dev/null +++ b/Artsy/Resources/ImageAssets/fbp @@ -0,0 +1 @@ +]ULVJBKKVAACH \ No newline at end of file diff --git a/Artsy/Resources/ImageAssets/fbs b/Artsy/Resources/ImageAssets/fbs new file mode 100644 index 00000000000..3324bd1e7e6 --- /dev/null +++ b/Artsy/Resources/ImageAssets/fbs @@ -0,0 +1 @@ +ZT@TBCGAWELEM \ No newline at end of file diff --git a/Artsy/Resources/ImageAssets/full_logo_white_large@2x.png b/Artsy/Resources/ImageAssets/full_logo_white_large@2x.png new file mode 100644 index 00000000000..fe1887075f9 Binary files /dev/null and b/Artsy/Resources/ImageAssets/full_logo_white_large@2x.png differ diff --git a/Artsy/Resources/ImageAssets/full_logo_white_small@2x.png b/Artsy/Resources/ImageAssets/full_logo_white_small@2x.png new file mode 100644 index 00000000000..04f742daec9 Binary files /dev/null and b/Artsy/Resources/ImageAssets/full_logo_white_small@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Anchor@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Anchor@2x.png new file mode 100644 index 00000000000..98f556d3be7 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Anchor@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Arrow@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Arrow@2x.png new file mode 100644 index 00000000000..d6ec38b189a Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Arrow@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Partner@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Partner@2x.png new file mode 100644 index 00000000000..033b7cda4d0 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotationCallout_Partner@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Artsy@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Artsy@2x.png new file mode 100644 index 00000000000..e6af4fd2c54 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Artsy@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_CoatCheck@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_CoatCheck@2x.png new file mode 100644 index 00000000000..9bcacbb5484 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_CoatCheck@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Default@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Default@2x.png new file mode 100644 index 00000000000..18105dc843e Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Default@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Drink@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Drink@2x.png new file mode 100644 index 00000000000..7d8c02713f5 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Drink@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Food@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Food@2x.png new file mode 100644 index 00000000000..8644c3be762 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Food@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_GenericEvent@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_GenericEvent@2x.png new file mode 100644 index 00000000000..2d15242274a Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_GenericEvent@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Highlighted@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Highlighted@2x.png new file mode 100644 index 00000000000..a524844d326 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Highlighted@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Info@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Info@2x.png new file mode 100644 index 00000000000..5768dcb288c Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Info@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Installation@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Installation@2x.png new file mode 100644 index 00000000000..62799eb1d69 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Installation@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Lounge@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Lounge@2x.png new file mode 100644 index 00000000000..30166a29799 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Lounge@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Restroom@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Restroom@2x.png new file mode 100644 index 00000000000..5cb4e55d6f8 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Restroom@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Saved@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Saved@2x.png new file mode 100644 index 00000000000..1f9a8358b03 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Saved@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Search@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Search@2x.png new file mode 100644 index 00000000000..5a12c80f9a3 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Search@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_Transport@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_Transport@2x.png new file mode 100644 index 00000000000..ecf7299e892 Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_Transport@2x.png differ diff --git a/Artsy/Resources/MapAnnotations/MapAnnotation_VIP@2x.png b/Artsy/Resources/MapAnnotations/MapAnnotation_VIP@2x.png new file mode 100644 index 00000000000..68fc624499d Binary files /dev/null and b/Artsy/Resources/MapAnnotations/MapAnnotation_VIP@2x.png differ diff --git a/Artsy/Resources/ViewInRoom/ViewInRoom_Base@2x.png b/Artsy/Resources/ViewInRoom/ViewInRoom_Base@2x.png new file mode 100644 index 00000000000..500b009b660 Binary files /dev/null and b/Artsy/Resources/ViewInRoom/ViewInRoom_Base@2x.png differ diff --git a/Artsy/Resources/ViewInRoom/ViewInRoom_BaseNoBench@2x.png b/Artsy/Resources/ViewInRoom/ViewInRoom_BaseNoBench@2x.png new file mode 100644 index 00000000000..ac1185d1f63 Binary files /dev/null and b/Artsy/Resources/ViewInRoom/ViewInRoom_BaseNoBench@2x.png differ diff --git a/Artsy/Resources/ViewInRoom/ViewInRoom_Bench@2x.png b/Artsy/Resources/ViewInRoom/ViewInRoom_Bench@2x.png new file mode 100644 index 00000000000..f614e0aaaad Binary files /dev/null and b/Artsy/Resources/ViewInRoom/ViewInRoom_Bench@2x.png differ diff --git a/Artsy/Resources/ViewInRoom/ViewInRoom_Man_3@2x.png b/Artsy/Resources/ViewInRoom/ViewInRoom_Man_3@2x.png new file mode 100644 index 00000000000..191c5101a64 Binary files /dev/null and b/Artsy/Resources/ViewInRoom/ViewInRoom_Man_3@2x.png differ diff --git a/Artsy/Resources/ViewInRoom/ViewInRoom_Wall@2x.png b/Artsy/Resources/ViewInRoom/ViewInRoom_Wall@2x.png new file mode 100644 index 00000000000..b63f89bb627 Binary files /dev/null and b/Artsy/Resources/ViewInRoom/ViewInRoom_Wall@2x.png differ diff --git a/Artsy/Resources/ViewInRoom/ViewInRoom_Wall_Right@2x.png b/Artsy/Resources/ViewInRoom/ViewInRoom_Wall_Right@2x.png new file mode 100644 index 00000000000..d41d4ef8679 Binary files /dev/null and b/Artsy/Resources/ViewInRoom/ViewInRoom_Wall_Right@2x.png differ diff --git a/Artsy/Resources/onboarding/onboard_1@2x~ipad.jpg b/Artsy/Resources/onboarding/onboard_1@2x~ipad.jpg new file mode 100644 index 00000000000..3101f50376a Binary files /dev/null and b/Artsy/Resources/onboarding/onboard_1@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/onboard_1@2x~iphone.jpg b/Artsy/Resources/onboarding/onboard_1@2x~iphone.jpg new file mode 100644 index 00000000000..ec91b6b3fd3 Binary files /dev/null and b/Artsy/Resources/onboarding/onboard_1@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/onboard_2@2x~ipad.jpg b/Artsy/Resources/onboarding/onboard_2@2x~ipad.jpg new file mode 100644 index 00000000000..62fff9c014a Binary files /dev/null and b/Artsy/Resources/onboarding/onboard_2@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/onboard_2@2x~iphone.jpg b/Artsy/Resources/onboarding/onboard_2@2x~iphone.jpg new file mode 100644 index 00000000000..f448b18cf6c Binary files /dev/null and b/Artsy/Resources/onboarding/onboard_2@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/onboard_3@2x~ipad.jpg b/Artsy/Resources/onboarding/onboard_3@2x~ipad.jpg new file mode 100644 index 00000000000..565db3aa459 Binary files /dev/null and b/Artsy/Resources/onboarding/onboard_3@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/onboard_3@2x~iphone.jpg b/Artsy/Resources/onboarding/onboard_3@2x~iphone.jpg new file mode 100644 index 00000000000..11fa1587aad Binary files /dev/null and b/Artsy/Resources/onboarding/onboard_3@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/splash_1@2x~ipad.jpg b/Artsy/Resources/onboarding/splash_1@2x~ipad.jpg new file mode 100644 index 00000000000..085f637e9b3 Binary files /dev/null and b/Artsy/Resources/onboarding/splash_1@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/splash_1@2x~iphone.jpg b/Artsy/Resources/onboarding/splash_1@2x~iphone.jpg new file mode 100644 index 00000000000..43e6ae3093f Binary files /dev/null and b/Artsy/Resources/onboarding/splash_1@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/splash_2@2x~ipad.jpg b/Artsy/Resources/onboarding/splash_2@2x~ipad.jpg new file mode 100644 index 00000000000..010f6ec66cf Binary files /dev/null and b/Artsy/Resources/onboarding/splash_2@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/splash_2@2x~iphone.jpg b/Artsy/Resources/onboarding/splash_2@2x~iphone.jpg new file mode 100644 index 00000000000..fbff51c1015 Binary files /dev/null and b/Artsy/Resources/onboarding/splash_2@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/splash_3@2x~ipad.jpg b/Artsy/Resources/onboarding/splash_3@2x~ipad.jpg new file mode 100644 index 00000000000..29247dbc552 Binary files /dev/null and b/Artsy/Resources/onboarding/splash_3@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/splash_3@2x~iphone.jpg b/Artsy/Resources/onboarding/splash_3@2x~iphone.jpg new file mode 100644 index 00000000000..deccea1ac5f Binary files /dev/null and b/Artsy/Resources/onboarding/splash_3@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/splash_4@2x~ipad.jpg b/Artsy/Resources/onboarding/splash_4@2x~ipad.jpg new file mode 100644 index 00000000000..b62905a99a4 Binary files /dev/null and b/Artsy/Resources/onboarding/splash_4@2x~ipad.jpg differ diff --git a/Artsy/Resources/onboarding/splash_4@2x~iphone.jpg b/Artsy/Resources/onboarding/splash_4@2x~iphone.jpg new file mode 100644 index 00000000000..673242da563 Binary files /dev/null and b/Artsy/Resources/onboarding/splash_4@2x~iphone.jpg differ diff --git a/Artsy/Resources/onboarding/splash_5@2x~ipad.jpg b/Artsy/Resources/onboarding/splash_5@2x~ipad.jpg new file mode 100644 index 00000000000..aea5ec48d37 Binary files /dev/null and b/Artsy/Resources/onboarding/splash_5@2x~ipad.jpg differ diff --git a/Artsy/en.lproj/InfoPlist.strings b/Artsy/en.lproj/InfoPlist.strings new file mode 100644 index 00000000000..477b28ff8f8 --- /dev/null +++ b/Artsy/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..ce21c8d477d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,519 @@ +## 1.6.0 (05/01/2015) + +* New Fair Guide view - ashfurrow +* New App Navigation System - orta +* Fair Search - ashfurrow & 1aurabrown +* New App Search - 1aurabrown +* Support artsy:// routing - orta +* Fixed hero units not loading on first launch - 1aurabrown & orta +* Fixed feature routing was erroring - 1aurabrown + +## 1.5.1 (16/07/2014) + +* Crash Fix for favourite Artist / Genes - 1aurabrown + +## 1.5.0 (10/07/2014) + +* Updated Artsy numbers in signup splash, now over 160,000 artworks from 2,500 partners - dblock +* Support e-commerce, including buying works in auctions - dblock +* Favorites now have a tabbed layout with Artworks, Artists and Categories - 1aurabrown +* Fair map is now its own view, removed parallax - ashfurrow +* Fair map on the personalized guide displays by dragging view down - ashfurrow +* Fair map annotations use short partner names - dblock +* Tapping on an item on a fair map displays a callouts with location information - dblock +* Do not display 'No Reserve' for auction works for sale - dblock +* Do not display map icon with an artist at a fair that doesn't have a map - dblock +* Support tapping the titles on show feed items - orta +* Support searching and browsing past fairs - orta +* Added admin option for allowing tapping on partner names on show feed items - orta +* Fixed crash when inquirying on an artwork without a title - dblock +* Fixed leaking of X-Access-Token and X-Xapp-Token headers to external websites - dblock +* Fixed blurry images on main feed - 1aurabrown +* Fixed margin issues with internal browser - dblock +* Fixed artworks disappearing on artist's page - dblock +* Cleaned up display of artist with no artworks - dblock +* Web views with links to log-in or sign-up are captured in the app - dblock +* New Fair Overview view - ashfurrow + +## 1.4.3 (4/29/2014) + +* Fixed search on 3.5'' iPhone, search bar cannot be clicked - dblock + +## 1.4.2 (4/21/2014) + +* External links prompt and open in a browser in demo mode - orta +* Updated Artsy color scheme - katarinabatina +* Fixed thumbnail when sharing via Twitter or Facebook - dblock +* Fixed sharing via AirDrop - dblock +* Fixed crash on artwork view - dblock +* Fixed settings labs feature - dblock +* Fixed beta auto-update - ashfurrow + +## 1.4.1 (4/11/2014) + +* Redesigned artwork inquiry workflow - katarinabatina +* Updated signup splash assets, moved logo - robertlenne +* Updated Artsy numbers in signup splash, now over 125,000 artworks from 1,500 partners - dblock +* Added ability to inquire on works for logged out users - dblock +* Added ability to contact Artsy specialists for logged out users - dblock +* Added artwork More Info page - 1aurabrown +* Added an error message trying to reach Artsy on splash - dblock +* Added ASCII art mode - dblock +* Added fade-in transition for splash screen after slideshow - dblock +* Added ability for users to re-login after expired or corrupted access tokens - dblock +* Improved performance and responsiveness of in-fair maps - dblock, orta +* Do not display Back button on fair overview when coming from global nav - 1aurabrown +* Do not display Contact Seller for artworks in auction - dblock +* Removed white glow from white hero units - 1aurabrown +* Removed shadowing of navigation buttons during transitions - dblock +* Fixed display of dollar amounts for auction price estimates - orta +* Fixed background color when pulling down on splash - dblock +* Fixed display of artworks for sale count on artist pages - dblock +* Fixed prematurely closing auctions by using server-side time - dblock +* Fixed display of artwork dimensions in both cm and inches - orta +* Fixed crash selecting a search result while still searching - dblock +* Fixed crash when loading home feed - dblock +* Fixed highlighting shows that have been previously favorited on fair map - dblock +* Fixed some partners not linked to their profile and cannot be followed in fairs - 1aurabrown +* Fixed web views incorrectly navigating to root of m.artsy.net instead of app nav - 1aurabrown +* Fixed crash displaying artworks with diameter - dblock +* Fixed crash after returning from a partner website from an artwork at a fair - dblock +* Fixed map items not tappable unless zoomed all the way in - 1aurabrown +* Fixed dark background in signup splash back from login or signup - dblock +* Fixed social signup that may not always display errors - dblock +* Fixed user favorites layout with different cases of favorited artists, artworks and genes - dblock +* Fixed signup and login password dots displaying in different font size when focused - dblock +* Fixed total artwork count on artist view - dblock +* Fixed line spacing in artwork auction results - dblock +* Fixed separator color between posts - dblock +* Fixed layout of gene pages without a description - dblock + +## 1.3.1 (3/4/2014) + +* Added map annotation analytics - dblock +* Fixed scolling on fairs without maps - orta +* Fixed crash upgrading from a previous version for logged in users - dblock +* Fixed displaying show at fair from A-Z list - dblock +* Fixed unfollowing a gallery doesn't update the map status - 1aurabrown + +## 1.3.0 (3/3/2014) + +* Added native fair experience with search, map, guide, features and posts - orta, robb, dblock, 1aurabrown +* New home screen search UI - dblock +* Display and edit user settings in labs - 1aurabrown +* Display related posts under artists and artworks - dblock +* Added search activity spinner and improved results fading - dblock +* Push notifications slide down and are no longer displayed as a popup - dblock +* Opening a notification when the app is in the background will not re-display the message in a popup - dblock +* Fixed `first_user_install` metric incorrectly reported - dblock +* Fixed hero units appearing outside of their start/end dates - dblock +* Measure initial feed load time with `initial_feed_load_time` metric - dblock +* Fixed intermittent logout on app restart - orta + +## 1.1.0.2 (11/12/2013) + +* Fixed truncation of artist artworks on 3.5" screens - dstnbrkr +* Allow blank inquiries - dstnbrkr +* Fixed crash: don't load stored, fetched items - dstnbrkr + +## 1.1.0.1 (11/12/2013) + +* Updates to design of auction price elements +* Prepend new feed items (were previously appended) +* Remove passive network error alert +* Include device type in User-Agent string +* Graphical improvements to search results +* Animate appearance of bidder status banners +* Move Artsy specialist icon above the subtitle +* Disable background fetch + +## 1.1.0.0 (11/6/2013) + +* Add live auction support to artwork view +* Add native auction results for artworks +* Add staging switch to admin menu +* Show auction-related works when work is in an auction +* Refactor internal URL handling +* Add profiles to search results + +## 1.0.2.2 (10/14/2013) + +* Fix nav button / menu overlap +* Fix related artists headline when no content + +## 1.0.2.1 (10/14/2013) + +* Added auction results button to artwork screen +* Added spinner to forgot password request +* Fixed crash when userdata filepath is nil +* Fixed crash when twitter auth is cancelled +* Fixed twitter login +* Fixed scroll-to-top in artwork view +* Turn on iPad support for all but debug builds +* Require all users to re-upload APNS device tokens + +## 1.0.2.0 (10/14/2013) + +* Improved performance of show feed load +* Use background fetch to load show feed +* Initial auction results API +* Remove extra loading of related works +* Fix loading/displaying of favorites + +## 1.0.0.154 (10/7/2013) + +* Improved internal app routing +* The artwork transitions are back to working state +* Builds as a 64 bit app for the new iPhone + +## 1.0.0.148 (9/23/2013) + +* Initial iPad support on feed and browse screens + +## 1.0.0.147 (9/20/2013) + +* Add void transition to last step of onboarding +* Misc fixes to sharing behavior +* Fix view in room scale when dimensions are in cm +* Update to SDWebImage 3.4 + +## 1.0.0.146 (9/20/2013) + +* Support markdown in artwork blurbs +* Potential fix for crash during facebook authentication +* Potential fix for gesture delegate crash +* Improve metrics + +## 1.0.0.145 (9/18/2013) + +* Can now go back to search results +* Fix jumpiness in artwork detail render (show a spinner while info loads) +* Disable contact buttons when offline +* Improve scroll performance by reducing layer composting +* Misc graphical fixes + +## 1.0.0.142 (9/12/2013) + +* Artists artworks fade in and out when selection changes +* Fix to send push notification tokens to the website for later use +* Get feed data whilst the app is idle on onboarding +* Fixes for redirects in external sites +* Consolidation of keyboard colors + +# 1.0 (9/11/2013) + +* Initial public release to the App Store + +## 0.0.140 (9/11/2013) + +* Added a placeholder thumbnail for search results w/o an image +* Improved onboarding metrics +* Added preliminary auction results button +* Design improvements on login controller + +## 0.0.133 (9/9/2013) + +* Fixed handling of stale credentials +* Made ARTextView HTML parsing async +* Added additional analytics +* Improved user identification in analytics +* Misc fixes for feed links + +## 0.0.130 (9/9/2013) + +* Fixes to authentication on fresh installs +* Redirect users trying to sign in when they have accounts to the log in page +* Improved navigation button logic + +## 0.0.129 (9/6/2013) + +* Fix crash on related artworks + +## 0.0.121 (9/5/2013) + +* Corrected scale in View In Room +* Back button shows in View in Room portrait after switching to landscape and going back +* Gene titles dont overlap the selected tick +* Retina share icon +* Fixes to having multiple cancels in the menu +* Removed artwork spinner +* Disable for sale button on artist when artist has no for sale works +* Featured links come from the server +* More protection against nav buttons hiding +* Removed white bar above artworks +* Better handling of artworks with no prices + +## 0.0.119 (9/4/2013) + +* Improved analytics +* Real links for home feed +* Improved pricing on artworks + +## 0.0.118 (9/4/2013) + +* Fixes an issue with connecting to the right push notification server + +## 0.0.117 (9/4/2013) + +* Potential Artwork Zoom memory fixes +* Initial work on notifications in app +* Artwork pricing styling matches other sites + +## 0.0.113 (9/3/2013) + +* Matched the same categories as the website in browse +* Protection for people changing their minds mid-way through twitter signin and signup +* Improved typography around the app, notably page titles and hero units +* Shouldn't hide nav buttons if you are at the top of a view +* Search has a information label letting you know what you can search for +* Collector Level & Price Range are sent up to artsy so we can keep track of pricing +* Improvements to the layout in artist views + +## 0.0.111 (9/2/2013) + +* Brings back some missing assets, notably the back & close button icons +* Hides the artist bio if the artsy doesn't have a bio +* Related artist names are now clickable + +## 0.0.110 (9/2/2013) + +* Facebook / Twitter will log you in if you try to sign up with an existing account on artsy +* More asset compression to speed up app +* Added trial view thresholding to encourage sign in +* Added more precautions against rotating in the wrong place +* Featured Links added to the top of show feed + +## 0.0.108 (9/1/2013) + +* Fixes for 4" devices + +## 0.0.103 (9/1/2013) + +* Artwork zoom transition takes the scrolling offset into account +* Uses new faster blur API +* Internal browser links respect the edge swipe +* Fixes to single lines of artist artworks + +## 0.0.102 (9/1/2013) + +* Initial recommended genes are taken from the server, with a local backup +* Don't show partner chevron if there's no profile / website +* Navigation buttons respect the 3 second waiting period +* Initial work on an extremely simple push notification support + +## 0.0.101 (8/31/2013) + +* Fix for the 3.5" screen being reported for 4" devices + +## 0.0.99 (8/31/2013) + +* Added in-app analytics +* Loading artwork shows a spinner +* No more white lines on the artwork view +* Added forgot password workflow +* Show location now takes into account location in fairs +* Partner link in Artwork now takes you to the right partner profile + +## 0.0.95 (8/30/2013) + +* Fixed strange scrolling on artworks +* Better splash fadeout animation +* Showing the trial splash with an action (eg. favoriting a work) will do the action after sign up +* Fixes to twitter sign in on splash, and dealing with 3.5" screen +* Uses existing email if available in sign up with social + +## 0.0.93 (8/30/2013) + +* Login or Sign up via Facebook or Twitter +* Add personalize, including following artists and genes during signup +* Fix: heart button doesn't show grey border +* Fix: trial button doesn't flash on load +* Improvements to the behavior of nav buttons +* Fixed crash when scrolling through hundreds of artworks on artists and genes + +## 0.0.92 (8/29/2013) + +* Having no artworks in Favorites is now dealt with elegantly, with a hint of playfulness +* Creating a new user will keep you logged in +* Added terms & conditions, and privacy policy to sign up pages +* Added network failsafes for generating trial tokens +* Artist heart buttons now enabled by default so trial users can trigger them +* Improvements to the favorites loading indicator +* Personalize section of onboarding has a black background + +## 0.0.91 (8/29/2013) + +* Fix for View In Room Logic +* Edge swipes in Onboarding work +* Nav buttons are now top aligned +* Hero units are loaded when signing in / up +* Potential fix for iPhone 5 slideshow animation bug + +## 0.0.90 (8/29/2013) + +* Menu & Back buttons are at the bottom +* Menu & Back buttons will disappear depending on scroll direction +* Parallax direction is reveresed +* Trial sign up/in splash animates into view +* Fixed accidental jump back to the show feed +* Revised Menu, also is now insta-blur +* The app will now requesting rating after 10 days of usage + +## 0.0.88 (8/28/2013) + +* Adjustments to Onboarding, slideshow images, and the app menu parallax + +## 0.0.86 (8/28/2013) + +* New onboarding process that lets users create an account when trying to do things like favoriting +* Multiple artwork view fixes +* Added labs features for different navigation styles +* New menu style +* Multiple View in Room improvements + +## 0.0.81 (8/27/2013) + +* Added support for Guest log-in +* Improved Artwork View scrolling speed +* Improved shows view performance +* JSON parsing for most models is done off them main thread +* Added a non-interactive transition for edge swiping on every view +* Substantial Gene page improvements +* Added navigation options in the options panel, available via rage-shake + +## 0.0.80 (8/31/2013) + +* Display artwork Buy buttons for purchaseable works +* Display image rights when available +* Partner links are clickable +* Collection Institutions are shown and are linkable +* Inqiries are now sending live inquiries +* Loading footers on Gene Favorites +* Removed the triple tap on the hero units +* Added an option for the zoom tap to push in, instead of popping out +* Fixed layout with upside-down phones +* Don't show related artworks when not available +* Tapping the artwork in View in Room exits View in Room + +## 0.0.79 + +* Artwork view auto layout +* Artwork view's related artworks show artist and artwork name +* Artwork view shows a minimum of ~30 pixels of artwork info, enough to show the tops of the action buttons +* Genes / Favorites views have been restructured for significantly lower memory consumption +* Back button works in full screen artwork zoom +* Removed instances of artworks accidentally scrolling +* Consolidated title positions throughout app +* Used correct image thumbnails in masonry height +* Hide back button in VIR when rotated +* Unpublished search results are now visually different +* Menu state only shows the selection if you open the menu from that view +* Fixed dark, pushed back root view controller + +## 0.0.78 + +* Added grey artist chevron +* New search UI +* Added drop shadow to hero units, see in the rage-shake menu for a subtler alternative +* Edge swipe added to all views to go back +* New navigation aligments +* Added support for multi-line titles + +## 0.0.71 + +* Improved logic for Back / Menu button +* For Sale Artworks on the Artist page are now generated from the All Published Artworks, loading instantly +* Hearts now flip vertically instead of the coin dropping animation +* Using new Unfollow API +* Artist names, and bio buttons are hidden when not there's nothing to show +* The rage-shake menu now contains an option to start onboarding +* Added option to make loading screens semi-transparent, so you can see what's happening behind the scenes +* Search requests that are cancelled are not reported as network errors +* Related artists appear under artists +* Post button is hidden on the menu +* Added protection against rotation on most views + +## 0.0.68 + +* New nav concept +* Favorites now have a design +* Favorites now show artists +* Favorites now show genes +* Two-column masonry views now use full width +* Gene pages have an expandable description +* Internal (m.artsy) links are given the full navigation stack +* Website have black background for void-y-ness +* Artist Bios now show fully linked texts +* Fixed awkward scrolling in the Gene page +* Artists can be followed +* Genes can be followed + +## 0.0.65 + +* API requests to artsy.net instead of artsyapi +* Force mobile pages to load without HTTPS + +## 0.0.64 + +* Add Follow Artists +* Add Follow Genes +* Reduced App File Size by optimizing VIR images +* Improved typography on artwork page +* More loading screens when needed +* Supports internal / external links +* Hero Units are clickable +* Fixes to height in Show Feed +* Sharing copy improvements + +## 0.0.63 + +* Genes now have a view +* Genes can be loaded from search, or through browse + +## 0.0.62 + +* Added Browse Genes +* Added Search, including artists and artworks + +## 0.0.61 + +* Artist View has more Polish, including share, heart and progress indicators +* Artists now have a bio page +* Display specific errors on API failures +* Improved general network error message for passive network issues + +## 0.0.59 + +* Artist View; shows Artworks, artist info, and for sale only artworks +* Artist View is accessible view from tapping on an Artwork's artist +* Buttons for contact / inquire are now based on the artwork itself +* Indicators for Hero Units are now overlaid on the Units + + +## 0.0.54 + +* Smarter status bar positioning +* Rewrote transition to be faster & to use CALayer transforms, no view manipulation. +* Don’t change status bar in menu +* Inquiry uses garamond 16, has purple selection state +* Define a minimum artwork info size for artworkVC +* Fix related showing over full screen image +* Favorites don’t do animation & hero units can’t crash when dealing with empty state restoration +* Hero unit cleanup: shadow moved to above the views so no overlap & no more dodgy cropping when moving +* View in room now uses a different guy, walls meet at an alignment +* Remove all options; show VIR parallax by default, show title & show headlines +* Inquiry cancels by dropping, and background is faded in +* Black status bar + +## 0.0.51 + +* Contact Gallery / Representative now has logic determining whether to show or not - speednoisemovement +* Shows load faster in the feed - speednoisemovement +* Menu only shows what's relevant - orta +* Menu will have correct item highlighted - orta +* Favorites has semi-built ability to show artwork metadata - orta +* Applied easing to enquiry form ( still stubbed. ) - orta +* Feeds are smarter about what data to reload - orta +* Statusbar is hidden when going into an artwork set - orta +* Feed & Favorites have a full screen loading view - orta +* Memory fixes for Masonry view - orta diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000000..c9cd3ccc25d --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +gem "cocoapods", '0.36.0.beta.1' +gem "cocoapods-keys", :git => "git://github.com/orta/cocoapods-keys" + +gem 'fui', '~> 0.3.0' +gem 'xcpretty' +gem 'second_curtain', '~> 0.2.3' +gem 'shenzhen' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000000..5dcdc92963c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,120 @@ +GIT + remote: git://github.com/orta/cocoapods-keys + revision: 9107f957bb079cbfa1d4fa27b2d9cb91afc80ec8 + specs: + cocoapods-keys (1.0.0) + osx_keychain + +GEM + remote: https://rubygems.org/ + specs: + RubyInline (3.12.3) + ZenTest (~> 4.3) + ZenTest (4.11.0) + activesupport (4.2.0) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + aws-sdk (1.61.0) + aws-sdk-v1 (= 1.61.0) + aws-sdk-v1 (1.61.0) + json (~> 1.4) + nokogiri (>= 1.4.4) + claide (0.8.0) + cocoapods (0.36.0.beta.1) + activesupport (>= 3.2.15) + claide (~> 0.8.0) + cocoapods-core (= 0.36.0.beta.1) + cocoapods-downloader (~> 0.8.1) + cocoapods-plugins (~> 0.4.0) + cocoapods-trunk (~> 0.5.0) + cocoapods-try (~> 0.4.3) + colored (~> 1.2) + escape (~> 0.0.4) + molinillo (~> 0.2.0) + nap (~> 0.8) + open4 (~> 1.3) + xcodeproj (~> 0.21.0) + cocoapods-core (0.36.0.beta.1) + activesupport (>= 3.2.15) + fuzzy_match (~> 2.0.4) + nap (~> 0.8.0) + cocoapods-downloader (0.8.1) + cocoapods-plugins (0.4.0) + nap + cocoapods-trunk (0.5.0) + nap (>= 0.8) + netrc (= 0.7.8) + cocoapods-try (0.4.3) + colored (1.2) + commander (4.2.1) + highline (~> 1.6.11) + dotenv (0.11.1) + dotenv-deployment (~> 0.0.2) + dotenv-deployment (0.0.2) + escape (0.0.4) + faraday (0.8.9) + multipart-post (~> 1.2.0) + faraday_middleware (0.9.1) + faraday (>= 0.7.4, < 0.10) + fui (0.3.0) + gli + fuzzy_match (2.0.4) + gli (2.12.2) + highline (1.6.21) + i18n (0.7.0) + json (1.8.2) + mini_portile (0.6.2) + minitest (5.5.1) + molinillo (0.2.0) + multipart-post (1.2.0) + mustache (0.99.8) + nap (0.8.0) + net-sftp (2.1.2) + net-ssh (>= 2.6.5) + net-ssh (2.9.2) + netrc (0.7.8) + nokogiri (1.6.5) + mini_portile (~> 0.6.0) + open4 (1.3.4) + osx_keychain (1.0.0) + RubyInline (~> 3) + plist (3.1.0) + rubyzip (1.1.6) + second_curtain (0.2.4) + aws-sdk-v1 (~> 1.52) + mustache (~> 0.99) + security (0.1.3) + shenzhen (0.10.3) + aws-sdk (~> 1.0) + commander (~> 4.1) + dotenv (~> 0.7) + faraday (~> 0.8.9) + faraday_middleware (~> 0.9) + json (~> 1.8) + net-sftp (~> 2.1.2) + plist (~> 3.1.0) + rubyzip (~> 1.1) + security (~> 0.1.3) + terminal-table (~> 1.4.5) + terminal-table (1.4.5) + thread_safe (0.3.4) + tzinfo (1.2.2) + thread_safe (~> 0.1) + xcodeproj (0.21.0) + activesupport (>= 3) + colored (~> 1.2) + xcpretty (0.1.7) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (= 0.36.0.beta.1) + cocoapods-keys! + fui (~> 0.3.0) + second_curtain (~> 0.2.3) + shenzhen + xcpretty diff --git a/HACKS.md b/HACKS.md new file mode 100644 index 00000000000..d41c8300502 --- /dev/null +++ b/HACKS.md @@ -0,0 +1,76 @@ +Gene + Contains the Link set stuff for browse - this makes the gene id needlessly complex + +AREMbeddedArtworkVC + POTENTIALLY CREAKY HACK - Headers. Fix by adding an ORStackView to the top of the embedded VC! + +ArtsyAPI + the whole Browse section is pretty hacky + the ARRouter needs docs! + Naming of things around the concept of favouriting + +Show Feed + Progress dots aren't the right size + Height for the hero units is based on two values when it should be one + +Artwork + Deleting all subviews before layout + Tap gesture on artist name + +Artist + YOLO'd all over the switch. It's not re-usable at all anywhere else. + Manual positioning for many many views. + +Flow Layout + Only bothered with the 2 columns + +Misc + ARShadowView should die + +Martsy + Forces http, not https + +ARTextView + Paragraph / line spacing is a hack + +ARUserManager + DRY + + +-- Robb feedback + +Parallax: + Have VC that is the root of the app, that contains a header view, and *that* controls buttons and nav buttons. This negates the need for a scrollcheif. Can add an ar_rootNavController etc. Only way to get smooth and catch all edge cases. E.g. the show feed headers could be a + +ORStackView: + leaks FLKAutoLayout + be more view controller friendly + ORStackViewController (deferring view will appear etc could work) + addvc with title + +Avoid `if (! foo)` bang space + +Interactive transitions: + don't translate to something with a header overall from the parallax, but centralizing will mean that they all can work again. + perhaps artwork zoom back interactive + rotation of artwork VIR could be a nice zoom via the transitions for rotation + +Model layer: + constants being used once. no point in the router indirection + base model that can bootstrap by an ID, can have consistent API like update. Could be possible to drop promises then. + + +Network: + cleanup the network API, like making the feeds async, it's done outside of the network model + looks at libraries, look at octokit. + robb advises prefixes in model classes + some APIS take ID strings, other takes model objects. Could be unified if there's a base model. + caching? local cache, get cache first then re-run with network layer. + +ARSwitchBoard + should return controllers, not doing the pushing itself, this also is easier re: parallax nav + async view controllers are a problem - could be a quantum view controller pattern for doing this. + +Overall + things hidden in things + martsy: get an API that accepts a URL, it should respond with the model object or nil if noth \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100755 index 00000000000..1697fdc1ad1 --- /dev/null +++ b/Makefile @@ -0,0 +1,140 @@ +WORKSPACE = Artsy.xcworkspace +SCHEME = Artsy +CONFIGURATION = Beta +APP_PLIST = Artsy/App/Artsy-Info.plist +PLIST_BUDDY = /usr/libexec/PlistBuddy +TARGETED_DEVICE_FAMILY = \"1,2\" +DEVICE_HOST = platform='iOS Simulator',OS='7.1',name='iPhone 4s' + +GIT_COMMIT_REV = $(shell git log -n1 --format='%h') +GIT_COMMIT_SHA = $(shell git log -n1 --format='%H') +GIT_REMOTE_ORIGIN_URL = $(shell git config --get remote.origin.url) + +DATE_MONTH = $(shell date "+%e %h" | tr "[:lower:]" "[:upper:]") +DATE_VERSION = $(shell date "+%Y.%m.%d") + +CHANGELOG = CHANGELOG.md +CHANGELOG_SHORT = CHANGELOG_SHORT.md + +IPA = Artsy.ipa +DSYM = Artsy.app.dSYM.zip + +.PHONY: all build ci clean pods test lint + +all: ci + +build: + set -o pipefail && xcodebuild -workspace $(WORKSPACE) -scheme $(SCHEME) -configuration '$(CONFIGURATION)' -sdk iphonesimulator -destination $(DEVICE_HOST) build | bundle exec xcpretty -c + +clean: + xcodebuild -workspace $(WORKSPACE) -scheme $(SCHEME) -configuration '$(CONFIGURATION)' clean + +test: + set -o pipefail && xcodebuild -workspace $(WORKSPACE) -scheme $(SCHEME) -configuration Debug test -sdk iphonesimulator -destination $(DEVICE_HOST) | bundle exec second_curtain | bundle exec xcpretty -c --test + +lint: + bundle exec fui --path Artsy find + +ci: CONFIGURATION = Debug +ci: build +ci: + bundle exec pod keys set "ArtsyAPIClientSecret" "-" Artsy + bundle exec pod keys set "ArtsyAPIClientKey" "-" + bundle exec pod keys set "HockeyProductionSecret" "-" + bundle exec pod keys set "HockeyBetaSecret" "-" + bundle exec pod keys set "MixpanelProductionAPIClientKey" "-" + bundle exec pod keys set "MixpanelStagingAPIClientKey" "-" + bundle exec pod keys set "MixpanelDevAPIClientKey" "-" + bundle exec pod keys set "MixpanelInStoreAPIClientKey" "-" + bundle exec pod keys set "ArtsyFacebookAppID" "-" + bundle exec pod keys set "ArtsyTwitterKey" "-" + bundle exec pod keys set "ArtsyTwitterSecret" "-" + bundle exec pod keys set "ArtsyTwitterStagingKey" "-" + bundle exec pod keys set "ArtsyTwitterStagingSecret "-" + + +remove_debug_pods: + rm -rf Pods + perl -pi -w -e "s{^pod 'Reveal-iOS-SDK'}{# pod 'Reveal-iOS-SDK'}g" Podfile + bundle exec pod install + +add_debug_pods: + rm -rf Pods + perl -pi -w -e "s{^# pod 'Reveal-iOS-SDK'}{pod 'Reveal-iOS-SDK'}g" Podfile + bundle exec pod install + +update_bundle_version: + @printf 'What is the new human-readable release version? '; \ + read HUMAN_VERSION; \ + $(PLIST_BUDDY) -c "Set CFBundleShortVersionString $$HUMAN_VERSION" $(APP_PLIST) + +pods: remove_debug_pods + +bundler: + gem install bundler + bundle install + +pod: + pod update + +ipa: set_git_properties change_version_to_date + $(PLIST_BUDDY) -c "Set CFBundleDisplayName $(BUNDLE_NAME)" $(APP_PLIST) + ipa build --scheme $(SCHEME) --configuration $(CONFIGURATION) -t --verbose + +stamp_date: + config/stamp --input Artsy/Classes/AppIcon_58.png --output Artsy/Classes/AppIcon_58.png --text "$(DATE_MONTH)" + config/stamp --input Artsy/Classes/AppIcon_80.png --output Artsy/Classes/AppIcon_80.png --text "$(DATE_MONTH)" + config/stamp --input Artsy/Classes/AppIcon_120.png --output Artsy/Classes/AppIcon_120.png --text "$(DATE_MONTH)" + +change_version_to_date: + $(PLIST_BUDDY) -c "Set CFBundleVersion $(DATE_VERSION)" $(APP_PLIST) + +set_git_properties: + $(PLIST_BUDDY) -c "Set GITCommitRev $(GIT_COMMIT_REV)" $(APP_PLIST) + $(PLIST_BUDDY) -c "Set GITCommitSha $(GIT_COMMIT_SHA)" $(APP_PLIST) + $(PLIST_BUDDY) -c "Set GITRemoteOriginURL $(GIT_REMOTE_ORIGIN_URL)" $(APP_PLIST) + +set_targeted_device_family: + perl -pi -w -e "s{TARGETED_DEVICE_FAMILY = .*}{TARGETED_DEVICE_FAMILY = $(TARGETED_DEVICE_FAMILY);}g" Artsy.xcodeproj/project.pbxproj + +distribute: + ./config/generate_changelog_short.rb + curl \ + -F repository_url=$(GIT_REMOTE_ORIGIN_URL) \ + -F commit_sha=$(GIT_COMMIT_SHA) \ + -F status=2 \ + -F notify=$(NOTIFY) \ + -F "notes=<$(CHANGELOG_SHORT)" \ + -F notes_type=1 \ + -F ipa=@$(IPA) \ + -F dsym=@$(DSYM) \ + -H 'X-HockeyAppToken: $(HOCKEYAPP_TOKEN)' \ + https://rink.hockeyapp.net/api/2/apps/upload \ + | grep -v "errors" + +appstore: TARGETED_DEVICE_FAMILY = 1 +appstore: remove_debug_pods update_bundle_version set_git_properties change_version_to_date set_targeted_device_family + +appledemo: TARGETED_DEVICE_FAMILY = 1 +appledemo: NOTIFY = 0 +appledemo: CONFIGURATION = "Apple Demo" +appledemo: set_git_properties change_version_to_date remove_debug_pods set_targeted_device_family +appledemo: pods ipa distribute + +next: TARGETED_DEVICE_FAMILY = \"1,2\" +next: add_debug_pods update_bundle_version set_git_properties change_version_to_date remove_debug_pods set_targeted_device_family + +deploy: pods ipa distribute + +alpha: BUNDLE_NAME = 'Artsy α' +alpha: NOTIFY = 0 +alpha: stamp_date deploy + +beta: BUNDLE_NAME = 'Artsy β' +beta: NOTIFY = 1 +beta: stamp_date deploy + +setup: + mkdir -p .git/hooks + cp config/githooks/* .git/hooks/ + chmod +x .git/hooks/* diff --git a/Podfile b/Podfile new file mode 100644 index 00000000000..f7448235341 --- /dev/null +++ b/Podfile @@ -0,0 +1,109 @@ +source 'https://github.com/artsy/Specs.git' +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '7.0' + +# Yep. +inhibit_all_warnings! + +# Allows per-dev overrides +local_podfile = "Podfile.local" +eval(File.open(local_podfile).read) if File.exist? local_podfile + +plugin 'cocoapods-keys', { + :project => "Artsy", + :target => "Artsy", + :keys => [ + "ArtsyAPIClientSecret", + "ArtsyAPIClientKey", + "HockeyProductionSecret", + "HockeyBetaSecret", + "MixpanelProductionAPIClientKey", + "MixpanelStagingAPIClientKey", + "MixpanelDevAPIClientKey", + "MixpanelInStoreAPIClientKey", + "ArtsyFacebookAppID", + "ArtsyTwitterKey", + "ArtsyTwitterSecret", + "ArtsyTwitterStagingKey", + "ArtsyTwitterStagingSecret" + ] +} + +# Core +pod 'Mantle', '1.5.3' +pod 'AFNetworking', '1.3.4' +pod 'AFHTTPRequestOperationLogger', '1.0.0' +pod 'SDWebImage', '3.7.1' +pod 'JLRoutes', '1.5' +pod 'ISO8601DateFormatter', '0.7' +pod 'KSDeferred', '0.2.0' +pod 'JSDecoupledAppDelegate', '1.0.1' +pod 'CocoaLumberjack', '1.8.1' +pod 'FXBlurView', '1.6.1' +pod 'MMMarkdown', '0.3' +pod 'UIAlertView+Blocks', '0.8' +pod 'iRate', '1.10.2' +pod 'MultiDelegate', '0.0.2' +pod 'ReactiveCocoa', '2.3' +pod 'ALPValidator', '0.0.3' +pod 'ORKeyboardReactingApplication', '0.5.3' +pod 'ORStackView', :head +pod 'ARTiledImageView', :git => 'https://github.com/dblock/ARTiledImageView', :commit => '1a31b864d1d56b1aaed0816c10bb55cf2e078bb8' +pod 'ARCollectionViewMasonryLayout', :git => 'https://github.com/ashfurrow/ARCollectionViewMasonryLayout', :commit => '2ee871f509806af147d0529a36f791906997d4b7' +pod 'ARGenericTableViewController', '1.0.2' +pod 'FLKAutoLayout', '0.1.1' +pod 'Artsy+UILabels', '1.1.0' +pod 'Artsy+UIColors' +pod 'Artsy+UIFonts' +pod 'Artsy-UIButtons', :head +pod 'UIView+BooleanAnimations' + + +# Auth +pod 'Facebook-iOS-SDK', '3.14.1' +pod 'AFOAuth1Client', '0.3.3' + +# Language niceities +pod 'ObjectiveSugar', '1.1.0' + +# libextobjc +pod 'libextobjc/EXTKeyPathCoding', '0.4' +pod 'libextobjc/EXTScope', '0.4' + +# Martsy +pod 'TSMiniWebBrowser@dblock', :head + +# Table View simplification +pod 'FODFormKit', :git => 'https://github.com/1aurabrown/FODFormKit.git' + +# Analytics +pod 'HockeySDK', '3.5.0' +pod 'Mixpanel', '2.3.1' +# TODO(AF): Once ARAnalytics is updated, bump this. +pod 'ARAnalytics/Mixpanel', :git => 'https://github.com/orta/ARAnalytics', :commit => '6f1a1c314894437e7e5c09572c276e644dbfb64b' +pod 'ARAnalytics/HockeyApp', :git => 'https://github.com/orta/ARAnalytics', :commit => '6f1a1c314894437e7e5c09572c276e644dbfb64b' +pod 'ARAnalytics/DSL', :git => 'https://github.com/orta/ARAnalytics', :commit => '6f1a1c314894437e7e5c09572c276e644dbfb64b' +pod 'UICKeyChainStore', '1.0.5' + +# Fairs +pod 'NAMapKit', :git => 'https://github.com/neilang/NAMapKit', :commit => '62275386978db91b0e7ed8de755d15cef3e793b4' + +# Developer Pods +# pod 'Reveal-iOS-SDK' +pod 'VCRURLConnection', '0.2.0' +pod 'DHCShakeNotifier', '0.2.0' + +# Easter Eggs +pod 'ARASCIISwizzle', '1.1.0' +pod 'DRKonamiCode', '1.1.0' + +target 'Artsy Tests', :exclusive => true do + pod 'FBSnapshotTestCase', '1.4' + pod 'Expecta+Snapshots', '~> 1.2' + pod 'OHHTTPStubs', '3.1.2' + pod 'XCTest+OHHTTPStubSuiteCleanUp', '1.0.0' + pod 'Specta', :git => 'https://github.com/specta/specta.git', :tag => "v0.3.0.beta1" + pod 'Expecta', '0.3.0' + pod 'OCMock', '2.2.4' +end \ No newline at end of file diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 00000000000..b6b8b7f431a --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,244 @@ +PODS: + - AFHTTPRequestOperationLogger (1.0.0): + - AFNetworking (~> 1.0) + - AFNetworking (1.3.4) + - AFOAuth1Client (0.3.3): + - AFNetworking (~> 1.3) + - ALPValidator (0.0.3) + - ARAnalytics/CoreIOS (2.5.0) + - ARAnalytics/DSL (2.5.0): + - ReactiveCocoa (= 2.3) + - RSSwizzle (= 0.1.0) + - ARAnalytics/HockeyApp (2.5.0): + - ARAnalytics/CoreIOS + - HockeySDK + - ARAnalytics/Mixpanel (2.5.0): + - ARAnalytics/CoreIOS + - Mixpanel + - ARASCIISwizzle (1.1.0) + - ARCollectionViewMasonryLayout (2.0.0) + - ARGenericTableViewController (1.0.2) + - ARTiledImageView (1.2): + - SDWebImage/Core + - Artsy+UIColors (1.0.0): + - EDColor (~> 0.4) + - Artsy+UIFonts (1.0.0) + - Artsy+UILabels (1.1.0): + - Artsy+UIColors + - Artsy+UIFonts + - Artsy-UIButtons (HEAD based on 1.3.0): + - Artsy+UIColors + - UIView+BooleanAnimations + - Bolts (1.1.3) + - CocoaLumberjack (1.8.1): + - CocoaLumberjack/Extensions (= 1.8.1) + - CocoaLumberjack/Core (1.8.1) + - CocoaLumberjack/Extensions (1.8.1): + - CocoaLumberjack/Core + - DHCShakeNotifier (0.2.0) + - DRKonamiCode (1.1.0) + - EDColor (0.4.0) + - Expecta (0.3.0) + - Expecta+Snapshots (1.3.0): + - Expecta + - FBSnapshotTestCase + - Facebook-iOS-SDK (3.14.1): + - Bolts + - FBSnapshotTestCase (1.4) + - FLKAutoLayout (0.1.1) + - FODFormKit (0.1.1) + - FXBlurView (1.6.1) + - HockeySDK (3.5.0) + - iRate (1.10.2) + - ISO8601DateFormatter (0.7) + - JLRoutes (1.5) + - JSDecoupledAppDelegate (1.0.1) + - KSDeferred (0.2.0) + - libextobjc/EXTKeyPathCoding (0.4): + - libextobjc/RuntimeExtensions + - libextobjc/EXTScope (0.4): + - libextobjc/RuntimeExtensions + - libextobjc/RuntimeExtensions (0.4) + - Mantle (1.5.3): + - Mantle/extobjc (= 1.5.3) + - Mantle/extobjc (1.5.3) + - Mixpanel (2.3.1) + - MMMarkdown (0.3) + - MultiDelegate (0.0.2) + - NAMapKit (3.1.1): + - ARTiledImageView + - SDWebImage + - ObjectiveSugar (1.1.0) + - OCMock (2.2.4) + - OHHTTPStubs (3.1.2) + - ORKeyboardReactingApplication (0.5.3) + - ORStackView (HEAD based on 2.0.0): + - FLKAutoLayout (~> 0.1) + - ReactiveCocoa (2.3): + - ReactiveCocoa/UI (= 2.3) + - ReactiveCocoa/Core (2.3): + - ReactiveCocoa/no-arc + - ReactiveCocoa/no-arc (2.3) + - ReactiveCocoa/UI (2.3): + - ReactiveCocoa/Core + - RSSwizzle (0.1.0) + - SDWebImage (3.7.1): + - SDWebImage/Core (= 3.7.1) + - SDWebImage/Core (3.7.1) + - Specta (0.3.0.beta1) + - TSMiniWebBrowser@dblock (HEAD based on 1.1) + - UIAlertView+Blocks (0.8) + - UICKeyChainStore (1.0.5) + - UIView+BooleanAnimations (1.0.2) + - VCRURLConnection (0.2.0) + - XCTest+OHHTTPStubSuiteCleanUp (1.0.0): + - OHHTTPStubs + +DEPENDENCIES: + - AFHTTPRequestOperationLogger (= 1.0.0) + - AFNetworking (= 1.3.4) + - AFOAuth1Client (= 0.3.3) + - ALPValidator (= 0.0.3) + - ARAnalytics/DSL (from `https://github.com/orta/ARAnalytics`, commit `6f1a1c314894437e7e5c09572c276e644dbfb64b`) + - ARAnalytics/HockeyApp (from `https://github.com/orta/ARAnalytics`, commit `6f1a1c314894437e7e5c09572c276e644dbfb64b`) + - ARAnalytics/Mixpanel (from `https://github.com/orta/ARAnalytics`, commit `6f1a1c314894437e7e5c09572c276e644dbfb64b`) + - ARASCIISwizzle (= 1.1.0) + - ARCollectionViewMasonryLayout (from `https://github.com/ashfurrow/ARCollectionViewMasonryLayout`, commit `2ee871f509806af147d0529a36f791906997d4b7`) + - ARGenericTableViewController (= 1.0.2) + - ARTiledImageView (from `https://github.com/dblock/ARTiledImageView`, commit `1a31b864d1d56b1aaed0816c10bb55cf2e078bb8`) + - Artsy+UIColors + - Artsy+UIFonts + - Artsy+UILabels (= 1.1.0) + - Artsy-UIButtons (HEAD) + - CocoaLumberjack (= 1.8.1) + - DHCShakeNotifier (= 0.2.0) + - DRKonamiCode (= 1.1.0) + - Expecta (= 0.3.0) + - Expecta+Snapshots (~> 1.2) + - Facebook-iOS-SDK (= 3.14.1) + - FBSnapshotTestCase (= 1.4) + - FLKAutoLayout (= 0.1.1) + - FODFormKit (from `https://github.com/1aurabrown/FODFormKit.git`) + - FXBlurView (= 1.6.1) + - HockeySDK (= 3.5.0) + - iRate (= 1.10.2) + - ISO8601DateFormatter (= 0.7) + - JLRoutes (= 1.5) + - JSDecoupledAppDelegate (= 1.0.1) + - KSDeferred (= 0.2.0) + - libextobjc/EXTKeyPathCoding (= 0.4) + - libextobjc/EXTScope (= 0.4) + - Mantle (= 1.5.3) + - Mixpanel (= 2.3.1) + - MMMarkdown (= 0.3) + - MultiDelegate (= 0.0.2) + - NAMapKit (from `https://github.com/neilang/NAMapKit`, commit `62275386978db91b0e7ed8de755d15cef3e793b4`) + - ObjectiveSugar (= 1.1.0) + - OCMock (= 2.2.4) + - OHHTTPStubs (= 3.1.2) + - ORKeyboardReactingApplication (= 0.5.3) + - ORStackView (HEAD) + - ReactiveCocoa (= 2.3) + - SDWebImage (= 3.7.1) + - Specta (from `https://github.com/specta/specta.git`, tag `v0.3.0.beta1`) + - TSMiniWebBrowser@dblock (HEAD) + - UIAlertView+Blocks (= 0.8) + - UICKeyChainStore (= 1.0.5) + - UIView+BooleanAnimations + - VCRURLConnection (= 0.2.0) + - XCTest+OHHTTPStubSuiteCleanUp (= 1.0.0) + +EXTERNAL SOURCES: + ARAnalytics: + :commit: 6f1a1c314894437e7e5c09572c276e644dbfb64b + :git: https://github.com/orta/ARAnalytics + ARCollectionViewMasonryLayout: + :commit: 2ee871f509806af147d0529a36f791906997d4b7 + :git: https://github.com/ashfurrow/ARCollectionViewMasonryLayout + ARTiledImageView: + :commit: 1a31b864d1d56b1aaed0816c10bb55cf2e078bb8 + :git: https://github.com/dblock/ARTiledImageView + FODFormKit: + :git: https://github.com/1aurabrown/FODFormKit.git + NAMapKit: + :commit: 62275386978db91b0e7ed8de755d15cef3e793b4 + :git: https://github.com/neilang/NAMapKit + Specta: + :git: https://github.com/specta/specta.git + :tag: v0.3.0.beta1 + +CHECKOUT OPTIONS: + ARAnalytics: + :commit: 6f1a1c314894437e7e5c09572c276e644dbfb64b + :git: https://github.com/orta/ARAnalytics + ARCollectionViewMasonryLayout: + :commit: 2ee871f509806af147d0529a36f791906997d4b7 + :git: https://github.com/ashfurrow/ARCollectionViewMasonryLayout + ARTiledImageView: + :commit: 1a31b864d1d56b1aaed0816c10bb55cf2e078bb8 + :git: https://github.com/dblock/ARTiledImageView + FODFormKit: + :commit: 4607940c98a3200fbc0414577b74f511c9a5774d + :git: https://github.com/1aurabrown/FODFormKit.git + NAMapKit: + :commit: 62275386978db91b0e7ed8de755d15cef3e793b4 + :git: https://github.com/neilang/NAMapKit + Specta: + :git: https://github.com/specta/specta.git + :tag: v0.3.0.beta1 + +SPEC CHECKSUMS: + AFHTTPRequestOperationLogger: 890afab1adb178314d33c4de9d2aa5a42cc594b9 + AFNetworking: 80c4e0652b08eb34e25b9c0ff3c82556fe5967b4 + AFOAuth1Client: 53c76d9cadd73142a9f73854456d5d970bdc147a + ALPValidator: 33ec78108e2389b903f066179973dbb1679d82c1 + ARAnalytics: 4626cd58b4ab08ca4857da8ecf02e47c6f6bdefa + ARASCIISwizzle: 01fc71ffac975f18079a249b0608c9769a9fc4b6 + ARCollectionViewMasonryLayout: 164b82010cf8ec99bc7a38cffe59a179d7e5a116 + ARGenericTableViewController: f40b437b8abb2e2de7c98f1f35f0487ec9087ead + ARTiledImageView: 000af4093f9bdcd0f754e6ebd964c90641389f90 + Artsy+UIColors: d1d5e084a0e542d310c507acb5446bae6a322241 + Artsy+UIFonts: fb0a70f93ca888d45a3af751707701fe8cbeae2a + Artsy+UILabels: 83b261e0a9ee1f113ad9443f80bafe3a870bcfa9 + Artsy-UIButtons: be101574d2d61d310620319252c56ec98d5181c3 + Bolts: 3db0a875235e0c9042b013af13af0f00f79b102c + CocoaLumberjack: 9d198d6e19909b3b113a38c5f06f7bb8eedd0427 + DHCShakeNotifier: d5e1130243096e9488e0ecce8e6e268647357b4b + DRKonamiCode: 17a4e3a8bf527b62c245c511656e4f4e70bcf518 + EDColor: bcdb8600b7a456f4408ce1d7e7fc001588919254 + Expecta: ce8a51b9fad15a2cd573b291cb2909aaed865350 + Expecta+Snapshots: 0ec8bd543329a3944f5e5dc2a5cb220c3dde4ab0 + Facebook-iOS-SDK: e9b3d5ac54ee3e5e693f4eef470b97911b3d076d + FBSnapshotTestCase: f9f225b5ba11c8d8c09075590490df16314e4d62 + FLKAutoLayout: 9fa39aac2903b274fbeed6be74154cdb937a04fc + FODFormKit: c3feff74feb08c54ce2d4a27d67f2e089f0b231d + FXBlurView: 1d01b96076d65027314d1be024c7506c292392aa + HockeySDK: 85c058a5300638a2e4fc98658f90b76df96c9f46 + iRate: 7d204971bab2f727195b868e32e05216fe257681 + ISO8601DateFormatter: 59731cd880cf87e71b4fa95f0d6b713dcbc4cbce + JLRoutes: 1a32dbc34b8af9c2a0174c4799afdc72ad5fe447 + JSDecoupledAppDelegate: cff5663e495456300b1ca2c9479d13d44f43e3e0 + KSDeferred: 33361e3e63ebe90210ea94d7f6469c1c2b874763 + libextobjc: ba42e4111f433273a886cd54f0ddaddb7f62f82f + Mantle: 8d84cacd6c2a69ff6fbce985a2b51298a5495de3 + Mixpanel: 9cc470a58346f60da8ce2982096bcc72b5a7372b + MMMarkdown: 95af8920c647512d1efa2ac8ac1932baf3266e2c + MultiDelegate: 30ea47d00d916390e7b53ba8dc240b8751d19107 + NAMapKit: 4f57bea305c1795d2d6b8a6d6b31c59b1a08114f + ObjectiveSugar: f48793f5902c77cfc02a4ce46d2e8ecddcad4dc2 + OCMock: 6db79185520e24f9f299548f2b8b07e41d881bd5 + OHHTTPStubs: c1c2007839687eaf9b80f061ce84440515080ebb + ORKeyboardReactingApplication: 9e9543372960d3a4994f0ee12ebf125d0ffb2012 + ORStackView: b6ccb2d7a1730548631a8a2749ba3c1bf12168db + ReactiveCocoa: 2066583826c17dde8e90c41e489f682048cc57c9 + RSSwizzle: ab39962b7ec56d19c2318ec1127e253797121363 + SDWebImage: 116e88633b5b416ea0ca4b334a4ac59cf72dd38d + Specta: d2158a483ad179e5f8d3a29f75ff4fc0ff6fab16 + TSMiniWebBrowser@dblock: 4e73ee955fcf2e9f5d98f7a17460454ac059bc37 + UIAlertView+Blocks: 90d7f6b2853404277c94f6284c2b8c16028272e6 + UICKeyChainStore: 4f9719600cc70025064e26f28f1210bcfc3b0c35 + UIView+BooleanAnimations: 48cc98d5469ced7f7654310bff47c8ddba72c8f4 + VCRURLConnection: ad8bfbd376ad44a7f5c48358f3ee96a40d4e683e + XCTest+OHHTTPStubSuiteCleanUp: 16f0278ca4d489896745e6f4f71b8b3efa93cba8 + +COCOAPODS: 0.36.0.beta.1 diff --git a/README.md b/README.md new file mode 100644 index 00000000000..f630d354535 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +![Artsy](AppIcon_114.png "Artsy") + +## Artsy Eigen + +[![Build Status](https://magnum.travis-ci.com/artsy/eigen.svg)](https://magnum.travis-ci.com/artsy/eigen) + +I'm in your phone letting you browse the arts. + +Get setup [here](docs/getting_started.md). Further documentation can be found in the [documentation folder](docs#readme). + +Want API documentation, go through this [guide in gravity](https://github.com/artsy/gravity/blob/master/doc/Api.md#playing-with-the-api) replacing `localhost:4000` with `artsy.net`. + +**Copyright**: 2012-2015, Artsy diff --git a/config/generate_changelog_short.rb b/config/generate_changelog_short.rb new file mode 100644 index 00000000000..beaaaedc02d --- /dev/null +++ b/config/generate_changelog_short.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +readme = File.read("./CHANGELOG.md") +beta_readme = File.read("./BETA_CHANGELOG.md") + +mini_readme = readme.split("\n## ")[0..1].join("\n## ") +generated_changelog = beta_readme + "\n\n-------\n" + mini_readme + +File.open("CHANGELOG_SHORT.md", 'w') { |f| f.write(generated_changelog) } \ No newline at end of file diff --git a/config/stamp b/config/stamp new file mode 100755 index 00000000000..2b6bb631562 Binary files /dev/null and b/config/stamp differ diff --git a/docs/BETA_CHANGELOG.md b/docs/BETA_CHANGELOG.md new file mode 100644 index 00000000000..9d85f362d1b --- /dev/null +++ b/docs/BETA_CHANGELOG.md @@ -0,0 +1,3 @@ +## 2014.12.24 + +* Facebook auth fix - 1aurabrown \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..e0cd5f3baf3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,24 @@ +![Artsy](../AppIcon_114.png "Artsy") + +## Artsy Eigen + +Quick Links +----------- + +* [Hockey App](http://hockeyapp.net) +* [Beta Link](http://artsy.net/supersecretbeta) +* [Dealing with Certificates](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/Introduction/Introduction.html) + +Dev Information +--------------------- + +* [Getting Started](getting_started.md) +* [Getting Confident](getting_confident.md) +* [Eigen Dev Tips](eigen_tips.md) + +* [General Overview](overview.md) + +* [App Store Deployment](deploy_to_app_store.md) +* [Beta Deployment](deploy_to_beta.md) +* [Certificates & Code Signing](certs.md) +* [Notifications](notifications/notifications.md) diff --git a/docs/certs.md b/docs/certs.md new file mode 100644 index 00000000000..de9e31bec2c --- /dev/null +++ b/docs/certs.md @@ -0,0 +1,21 @@ +### Certificates & Profiles +##### Orta Therox - Wed 4 Sep 2013 + +### I can't get my provisional profiles set up + +I've not had to do this from scratch in so long that I can't give any good advice. Xcode 5 should be able to do this automatically. + +### What is the correct setup in Xcode for Code Signing + +Note: This is on the _Project_ not the _Target_ +![](screenshots/code-signing.png) + +So, Code Signing identiy is all automatic. This means that it will base the identity off the Provisioning profile below. + +So for Debug it should take your personal account +![](screenshots/debug-sign.png) + +And for Release it should use the Artsy Enterprise iPhone Distribution +![](screenshots/release-sign.png) + +The Xcode-generated `iOS Team Provisioning Profile: XXX` profiles are Dev profiles, and will not work with enterprise distribution. We have a `Artsy Enterprise Wildcard Dev Profile` profile which until we added notifications worked great, but as it is a wildcard you cannot use it with notifications. If you're writing in-house apps, this is what you want to use for simplicities sake. \ No newline at end of file diff --git a/docs/deploy_to_app_store.md b/docs/deploy_to_app_store.md new file mode 100644 index 00000000000..38693594d9f --- /dev/null +++ b/docs/deploy_to_app_store.md @@ -0,0 +1,55 @@ +## Deploy to App Store + +### TODOs for anyone before deploying + +1. Check out eigen Artsy master. +1. Run `make appstore`. This removes Reveal, runs `pod install` and prompts for a release version number. +1. Update CHANGELOG with the release number. +1. Add and commit the changed files, typically with `-m "Preparing for the next release, version X.Y.Z."`. + +### Provisioning Profiles + +Open the project in XCode, click on the Artsy project. + +1. Change iOS Deployment Target to "iPhone only" in Info, Deployment Target. +2. Verify that the provisioning profiles under Code Signing are displayed as below in Build Settings. + +IMPORTANT: We use the "Artsy Inc Account" not "ARTSY INC" - which is our enterprise account. + +![](../Web/prov-profiles.png) + +The provisioning profile should be _"Artsy Mobile - App Store DistrProfile"_ and when the Code Signing ( which should be automatic ) is clicked it should show "_iPhone Distribution : Art.sy Inc"_ + +If you don't see the "Artsy Mobile - App Store DistrProfile" in the options above, import the Dev/Apple/Artsy AppStore Identities from the Artsy Engineering Operations 1Password vault. + +If you cannot set Code Signing Identity to "Automatic", under which you'll find "iPhone Distribution: Artsy Inc.", open XCode Preferences and add it@artsymail.com as an apple account. Also, choose the it@artsymail.com apple ID, click "Artsy Inc." and then refresh until you see two iOS Distribution signing identities, one that says "iOS Distribution (2)". + +See [certs.md](certs.md) for more info on certificates. + +### Prepare in iTunes Connect + +1. You need to have copy for the next release, for minor releases this is just a list of notable changes. +1. Log in to [iTunes Connect](https://itunesconnect.apple.com) as it@artsymail.com ( team _Art.sy Inc_ ). +1. Manage Your Apps > Which-ever app > Add new version. +1. Fill in the copy. +1. Go back in to the new version and in the top right hit _Mark for Upload_. + +### Release to AppStore + +1. Install HockeyApp from http://hockeyapp.net/apps and run it. +1. In XCode, change the target device to _iOS Device_. +1. In Xcode, hold alt (`⌥`) and go to the menu, hit _Product_ and then _Archive..._. +1. Check that the Build Configuration is set to _Store_. +1. Hit _Archive_. +1. Hit _Distribute_, it will run validations and submit. + +### Upon Successful Submission + +1. HockeyApp will automatically see your new archive. Push Archived build to HockeyApp as a live build. +1. Make a git tag for the version with `git tag x.y.z`. Push the tags to `artsy/eigen` with `git push --tags`. + +### Prepare for the Next Release + +1. Run `make next`. This re-adds Reveal, runs `pod install` and prompts for the next version number. +1. Add a new section to CHANGELOG called _Next_. +1. Add and commit the changed files, typically with `-m "Preparing for development, version X.Y.Z."`. diff --git a/docs/deploy_to_beta.md b/docs/deploy_to_beta.md new file mode 100644 index 00000000000..59e9b39b59e --- /dev/null +++ b/docs/deploy_to_beta.md @@ -0,0 +1,6 @@ +## Installation + +Install [HockeyApp for Mac](http://hockeyapp.net/apps) on the build machine. You will be asked to login to the HockeyApp with *it@artsymail.com* and to install the helper extension. + +You can make beta builds with `make beta`. Make sure you have HOCKEYAPP_TOKEN set in your environment variables. + diff --git a/docs/eigen_tips.md b/docs/eigen_tips.md new file mode 100644 index 00000000000..bbe1d307f85 --- /dev/null +++ b/docs/eigen_tips.md @@ -0,0 +1,31 @@ +Eigen Tips +============== + +Use Quicksilver +--------------- + +Quickly load any type of our primitives ( shows / artworks / artists etc ) by pressing enter on the keyboard when the app loads, typing in your changes and then pressing enter again. + +Use your `.eigen` file +----------------------- + +Authentication is a lot easier when you don't type so much, create a file in your home directory called `.eigen` and it takes a collection of `key:value` lines to have the username and password set for you in the `ARLoginViewController`. You can use the `ARDeveloperOptions` class to react to the key value store. + +Use the developer springboard function +-------------------------------------- + +Edit `Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.m` with any custom code that you would like to run on application startup. For example, you may want to load a specific Fair with the following code. + +```objc +- (void)runDeveloperExtras +{ + [ARSwitchBoard loadFairWithID:@"the-armory-show-2014"]; +} +``` + +Run `git update-index --assume-unchanged "Artsy/Classes/View Controllers/ARTopMenuViewController+DeveloperExtras.m"` to ignore changes on this file. + +Use the offline mode for extra party/speed +------------------------------------------ + +Dustin left us with the amazing `NSVCRURLConnection`, which allows you to save an entire HTTP session for either working offline, or for getting back _faster_ http requests. Load the debug menu and hit the "Start" option to make it record, then go save when you're done and it will have it copied. diff --git a/docs/getting_confident.md b/docs/getting_confident.md new file mode 100644 index 00000000000..a604696abee --- /dev/null +++ b/docs/getting_confident.md @@ -0,0 +1,35 @@ +Getting Confident +================ +##### Orta Therox - Wed 28 Jan 2014 + +Once you've got a feel for the language, and some of the patterns here's some advice to start speeding you up in getting around the systems. + + +Use Reveal +---------------- +[Reveal](http://revealapp.com) is a tool for inspecting the view hierarchy. Folio is _featured_ on their website as an example of how pretty it looks. You should have Reveal installed, and when our app is loaded you select the simulator type in the top left of Reveal. It should come in automatically through CocoaPods. + +Use AppCode +--------------- +AppCode is Xcode but with a lot of extra goodies because they don't have to say no to everything. It's a Java app and I _orta_ can understand when you wince but I can promise you that the 1%ers use it. You should too. + +My install settings are open source at [orta/AppCode](https://github.com/orta/AppCode). They are a mix of TextMate + Xcode. Also check out the [QuickStart Guide](http://www.jetbrains.com/objc/quickstart/). + +Use Base & Chairs +--------------- +Core Data is nice to introspect as it's just a sqlite database, use this to your advantage by installing my gem _chairs_ to open the app's build folder then you can look in the Documents folder and open the DB in [Base](http://menial.co.uk) (or similar) for inspection. + +Use Dash +--------------- +Documentation is essential. Having quick access is even more useful. With Dash I partially type the class then guess the method name after pressing space. [Dash](http://kapeli.com) makes this very very fast. + +If you insist on using Xcode +--------------- +I kinda understand, it's worth the switch. Anyway. + +* Use Xcode Plugins. The Git & FuzzyAutoComplete ones especially. Get [Alcatraz](https://github.com/mneorr/Alcatraz/) for this. +* Make "Open Quickly" `cmd + t` in Xcode to quickly open files like TextMate +* Make "Standard Editor > Show Document Items" `cmd + shift + t` in Xcode to make it quickly jump to methods in the file like TextMate +* `Edit all in Scope` is a great way to rename things: `cmd + ctrl + e` +* If you have no idea what's wrong, and it should work clean your derived data with `cmd + alt + shift + k` +* Use the git commit sheet in Xcode, it's better than AppCode's: `cmd + alt + c` diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 00000000000..fd6f534f9d4 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,57 @@ +## Getting Setup + +### Fork and Clone + +Fork https://github.com/artsy/eigen and clone it locally. + +### Ruby dependencies + +Install the CocoaPods ruby gem. + +``` +bundle install +``` + +Add the Artsy podspec repo. + +``` +pod repo add artsy https://github.com/artsy/Specs.git +``` + +Now run `bundle exec pod install` in the root directory. This will grab all our external libraries. + +Once the `pod install` is complete, it will create the `Artsy.xcworkspace` file that you should open in Xcode. +Workspaces hold Projects, and we have two projects; one for Artsy and one for CocoaPods. + + +### Certificates + +Login as **it@artsymail.com** to [Certificates, Identities and Profiles](https://developer.apple.com/account/overview.action). + +* Invite yourself as a new admin member of the team from the [Member Center](https://developer.apple.com/membercenter/index.action#allpeople). + +Obtain a certificate for iOS development: + +* Open Keychain Tool, Certificate Assisistant, Request a Certificate from a Certification Authority. +* Save the request to disk. +* [Create a new Certificate](https://developer.apple.com/account/ios/certificate/certificateCreate.action), use the CSR generated above. +* Have the admin approve your certificate. + +Connect a device. + +XCode will prompt you to join a team, then to enable the device for development. When prompted, choose the uppercase *ART.SY INC.* team. + +Choose Preferences, Accounts, which should list your Apple ID and membership. + +Choose Window, Organizer, which should list your device. The provisioning profile in the Provisioning Profiles tab under the device should include *iOS Team Provisioning Profile: net.artsy.artsy.dev* and *net.artsy.artsy.beta*. You may need to download these from Certificaties, Identifiers && Profiles. + +### Run Tests + +In Xcode/AppCode: +Tap `cmd + u` to run all tests, use `ctrl + alt + cmd + g` to run the last set you clicked on via the GUI. + +Command line: +``` +make clean +make +``` diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 00000000000..ef45d9cb889 --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,20 @@ +## Notifications (APN) +##### Orta Therox - Wed 4 Sep 2013 + +Out dev setup is a bit different because we throw the enterprise certs into the mix. We have three states, and two developer accounts. + + +| Dev | Beta | Production | +| ---------- | ----------- | ------------ | +| Uses ARTSY INC| | Uses ARTSY INC | Uses Artsy Inc | +| Considered a Dev APN Account | Considered a Production APN Account | Considered a Production APN Account | +| net.artsy.artsy.dev | net.artsy.artsy.beta | net.artsy.artsy | +| iOS Team Provisioning Profile: net.artsy.artsy.dev | Artsy Beta Enterprise Provisioning Profile | Art.sy Inc. | + +You can get access to all of the keys, requests and certificates required to set up the APN servers ( ATM only Mixpanel ) from the Engineering 1password. The key is freshly minted for this task and clumsily named `Artsy (orta created) Key`. If you're renewing these certs, don't forget that you ensure the right cert is used for generating signing requests by right clicking in keychain. + +![](../screenshots/keychain-new-push.png) + +and finally, if you want to know what it should look like if you've got everything set up locally: + +![](../screenshots/keychain-push.png) diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000000..813245e7224 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,81 @@ +Overview of Eigen +========== +##### Orta Therox - Wed 28 Jan 2014 + +#### App wide View Controller Hierarchy + +There are two navigation controllers used in the app. There is one that is presented during onboarding, and one used throughout the app. + +The key navigation controller in-app is a _child_ view controller of the `ARTopMenuViewController`. This is how we could abstract out the specific navigation contexts and I expect will be useful in porting to iPad. + +#### Navigational buttons + +The control of menu buttons like the back / menu button are controlled in the `ARTopMenuViewController`, for whom they are views that sit above the normal navigational flow. A lot of the work on showing and hiding is dealt with by the `ARScrollNavigationChief`. + +#### Switchboard + +The switchboard is an abstraction that passes data between view controllers, it can deal with paths like the artsy website or be given an object and a class and it will push the corrosponding view controller on to the nav stack. + +#### ARRouter + +The ARRouter is the class in-between the app and the API. Providing a way to generate URLRequests with an objective-c feel which the ArtsyAPI object will then use to create methods for dealing with the network requests. + +## Creating + +#### Showing Artwork Thumbnail + +##### If you need an infinite scroll + +Have a `AREmbeddedModelsViewController` child view controller, and use the `headerView` and `headerHeight` properties of it to show views above. See `ARFavoritesViewController` for an example. + +##### If you want to show some artworks + +Have a `AREmbeddedModelsViewController` child view controller. It works with `ARArtworkCollectionViewModule` subclasses like `ARArtworkFlowModule` to provide a single way to present different types of artworks. If there isn't a way to produce the style you want add a new modules. + +## Aspirational + +#### View Controllers in general + +Try keep view controllers small, there are a lot of design patterns to help simplify the process of reducing VC-specific code. Use ORStackViews, use UICollectionView subclasses and try to use child view controllers to reuse. + +#### Networking + +The app as it currently is is a mix of three design patterns for networking. This is mainly due to the original time constraints, the three patterns are: + +* `KSPromise`s - found in `Artwork`. Uses defers & promises to provide a way to give a lot of recievers news that an artwork has been updated from the server. Allows multiple data sources to trigger the promise completion. + +* Raw access of `[ArtsyAPI getBlah]`. Happens around the app in places like `ARFavoritesArtistViewController`. Ideally these should be moved into network-models like `ARGeneArtworkCollection`. + +* Model based network abstractions are just an abstraction on the raw API access inside of the model instead of in a view controller or elsewhere. + +Ideally most of of our network interaction should be moved into network-models. + + +#### ScrollNavigationChief + +In order to provide a consistent navigation button experience a lot of view controllers with scroll views set the `UIScrollView` `delegate` to the ScrollNavigationChief singleton. +If a view needs to override some of the methods it's possible to pass on the messages on too. + +#### StackViews + +Most view controllers show a collection of views that are stacked on top of each other. `ORStackView` is an abstraction that means you can let the stackview deal with positioning and setting height. If want to deal with an order that is not the same as the insertion order there is a subclass that orders by view tag. Remember our mantra: `Everything is a stack`. + +#### Network Errors + +We aim to only provide network error messages if there was an active user engagement like tapping "follow" and that request failing. + +#### Theming / Views + +There is no over-arching methodology for theming in the app. Originally `ARTheme` was used but we don't have designers wanting to tweak all the settings themselves and its coupling is a bit too loose for tooling. It should not be used. + +There are a lot of common use-case views though, like `ARPageSubTitleView` or `ARTextView` for use in StackViews and there are categories for doing some more advanced typographical tricks. + +#### Constants + +Constants should be prefixed with `AR`, `#defines` should be migrated to that if they are still in the codebase. + +If a few exist they should get their own file like `ARFeedConstants`. + +#### AROptions + +Features that aren't ready for alpha/beta users should be hidden behind an `AROption` which defaults to off. This means there is an interface for beta users to toggle it. \ No newline at end of file diff --git a/docs/screenshots/code-signing.png b/docs/screenshots/code-signing.png new file mode 100644 index 00000000000..26804a19044 Binary files /dev/null and b/docs/screenshots/code-signing.png differ diff --git a/docs/screenshots/debug-sign.png b/docs/screenshots/debug-sign.png new file mode 100644 index 00000000000..77d1ba101ee Binary files /dev/null and b/docs/screenshots/debug-sign.png differ diff --git a/docs/screenshots/keychain-new-push.png b/docs/screenshots/keychain-new-push.png new file mode 100644 index 00000000000..72d79d03f58 Binary files /dev/null and b/docs/screenshots/keychain-new-push.png differ diff --git a/docs/screenshots/keychain-push.png b/docs/screenshots/keychain-push.png new file mode 100644 index 00000000000..acaacfccf15 Binary files /dev/null and b/docs/screenshots/keychain-push.png differ diff --git a/docs/screenshots/release-sign.png b/docs/screenshots/release-sign.png new file mode 100644 index 00000000000..ff13f747358 Binary files /dev/null and b/docs/screenshots/release-sign.png differ diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000000..909851628b3 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,15 @@ +### Troubleshooting +##### Orta Therox - Wed 4 Sep 2013 + +### CocoaPods +If a pod complains about not finding `/Developer`, you may need to update your location for Xcode. You can check which version your computer thinks is the latest by running `xcode-select -p` if it's not what you expect: + +``` +# Note that you are referencing the Xcode bundle you want to use. +sudo xcode-select -switch /Applications/Xcode5-DP6.app/Contents/Developer +``` + +If you have run `pod install` in the past, you may need to update: +``` +pod update +```