├── .gitignore ├── LICENSE ├── README.md ├── SF iOS ├── SF iOS.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── SF iOS │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-512x512@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── Contents.json │ │ ├── close-button.imageset │ │ │ ├── Contents.json │ │ │ └── close-button.pdf │ │ ├── coffee-location-icon.imageset │ │ │ ├── Contents.json │ │ │ └── coffee-location-icon.pdf │ │ ├── icon-launch.imageset │ │ │ ├── Contents.json │ │ │ └── icon-app.pdf │ │ ├── icon-transport-type-automobile.imageset │ │ │ ├── Contents.json │ │ │ └── icon-transport-type-automobile.pdf │ │ ├── icon-transport-type-lyft.imageset │ │ │ ├── Contents.json │ │ │ └── icon-transport-type-lyft.pdf │ │ ├── icon-transport-type-transit.imageset │ │ │ ├── Contents.json │ │ │ └── icon-transport-type-transit.pdf │ │ ├── icon-transport-type-uber.imageset │ │ │ ├── Contents.json │ │ │ └── icon-transport-type-uber.pdf │ │ ├── icon-transport-type-walking.imageset │ │ │ ├── Contents.json │ │ │ └── icon-transport-type-walking.pdf │ │ └── user-location-icon.imageset │ │ │ ├── Contents.json │ │ │ └── Current Location Icon.pdf │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── EventDetails │ │ ├── DirectionsRequestHandler.h │ │ ├── EventDetailsViewController.h │ │ ├── EventDetailsViewController.m │ │ ├── NSAttributedString+EventDetails.h │ │ ├── NSAttributedString+EventDetails.m │ │ ├── TravelTimeView.h │ │ ├── TravelTimeView.m │ │ ├── TravelTimesView.h │ │ └── TravelTimesView.m │ ├── Feed │ │ ├── EventsFeedViewController.h │ │ ├── EventsFeedViewController.m │ │ ├── FeedItem.h │ │ ├── FeedItem.m │ │ ├── FeedItemCell.h │ │ └── FeedItemCell.m │ ├── HTTPRequestOperation │ │ ├── HTTPRequestAsyncOperation.h │ │ └── HTTPRequestAsyncOperation.m │ ├── Helpers │ │ ├── Application │ │ │ ├── UIApplication+Metadata.h │ │ │ └── UIApplication+Metadata.m │ │ ├── Concurrency │ │ │ ├── AsyncBlockOperation.h │ │ │ ├── AsyncBlockOperation.m │ │ │ ├── AsyncOperation.h │ │ │ └── AsyncOperation.m │ │ ├── Errors │ │ │ ├── NSError+Constructor.h │ │ │ └── NSError+Constructor.m │ │ ├── MapCameraOverlookingLocations │ │ │ ├── MKMapCamera+OverlookingLocations.h │ │ │ └── MKMapCamera+OverlookingLocations.m │ │ ├── NSAttributedStringKerning │ │ │ ├── NSAttributedString+Kerning.h │ │ │ └── NSAttributedString+Kerning.m │ │ ├── NSDateUtilities │ │ │ ├── NSDate+Utilities.h │ │ │ └── NSDate+Utilities.m │ │ ├── StackViews │ │ │ ├── UIStackView+ConvenienceInitializer.h │ │ │ └── UIStackView+ConvenienceInitializer.m │ │ ├── StatusBarBackgroung │ │ │ ├── UIViewController+StatusBarBackground.h │ │ │ └── UIViewController+StatusBarBackground.m │ │ └── Styles │ │ │ ├── UIColor+SFiOSColors.h │ │ │ └── UIColor+SFiOSColors.m │ ├── ImageStore │ │ ├── ImageStore.h │ │ └── ImageStore.m │ ├── Info.plist │ ├── Location │ │ ├── DirectionsRequest.h │ │ ├── DirectionsRequest.m │ │ ├── MapView.h │ │ ├── MapView.m │ │ ├── TransportType.h │ │ ├── TravelTime │ │ │ ├── LyftTravelTimeEstimateOperation.h │ │ │ ├── LyftTravelTimeEstimateOperation.m │ │ │ ├── TravelTime+Arrival.h │ │ │ ├── TravelTime+Arrival.m │ │ │ ├── TravelTime.h │ │ │ ├── TravelTime.m │ │ │ ├── TravelTimeCalculationCompletion.h │ │ │ ├── TravelTimeService.h │ │ │ ├── TravelTimeService.m │ │ │ ├── UberTravelTimeEstimateOperation.h │ │ │ └── UberTravelTimeEstimateOperation.m │ │ ├── UserLocation.h │ │ └── UserLocation.m │ ├── Models │ │ ├── ApplicationEventNotifications │ │ │ ├── NSNotification+ApplicationEventNotifications.h │ │ │ └── NSNotification+ApplicationEventNotifications.m │ │ ├── CloudKitDerivedRecord.h │ │ ├── CloudKitDerivedRecord.m │ │ ├── Event.h │ │ ├── Event.m │ │ ├── EventDataSource.h │ │ ├── EventDataSource.m │ │ ├── EventType.h │ │ ├── Location.h │ │ └── Location.m │ ├── RemoteUIImage │ │ ├── UIImage+URL.h │ │ └── UIImage+URL.m │ ├── SF iOS-Debug.entitlements │ ├── SF iOS.entitlements │ ├── Secrets │ │ ├── SecretsStore.h │ │ ├── SecretsStore.m │ │ └── secrets-example.plist │ └── main.m ├── SF iOSTests │ ├── DateTests.m │ ├── EventDataSourceTests.m │ ├── FeedItemTests.m │ ├── Info.plist │ └── TravelTime+ArrivalTests.m └── SF iOSUITests │ ├── Info.plist │ └── SF_iOSUITests.m ├── Sketch-IOS-Icons-1.0.1.sketch ├── screenshots.jpg └── sf-ios.sketch /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots 56 | fastlane/test_output 57 | 58 | # Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | 65 | # macOS 66 | .DS_Store 67 | 68 | # Secrets 69 | SF\ iOS/SF\ iOS/Secrets/secrets.plist 70 | 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Amit Jain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![screenshot](https://github.com/gravicle/sf-ios/blob/master/screenshots.jpg) 2 | 3 | # sf-ios 4 | An app for #sf-coffee and #sf-beer events from the iOS Folks Slack. 5 | 6 | ## Why Obj-C? 7 | For interviews, I needed to become comfortable again with Obj-C. What better way than to write an app? 8 | 9 | ## Getting the app 10 | 11 | Join the TestFlight: [SF iOS testflight](https://sf-ios-testflight.herokuapp.com) 12 | 13 | ## Building from source 14 | 15 | The app has no external dependencies. However, for travel time estimates, it uses Uber and Lyft REST APIs and the corresponding credentials are stored in `Secrets/secrets.plist` file. This file, for obvious reasons, is not checked-in into version control. To build and run: 16 | 17 | 1. Duplicate `Secrets/secrets-example.plist` and rename it to `secrets.plist`. you can build the project now but Uber and Lyft travel times will not be available. 18 | 2. Get [Uber](https://auth.uber.com/login/?next_url=https%3A%2F%2Fdeveloper.uber.com%2Fdashboard%2F&state=jZgX3-jJNzOiN57ly8Tv0uY0ArFXStNvQsjM_mzcYdg%3D) and [Lyft](https://www.lyft.com/developers/manage) `client-id` and `server-token` and populate `secrets.plist`. 19 | 20 | The app uses CloudKit as its backend and you will need to replicate the required models in your CloudKit dev environemnt. 21 | -------------------------------------------------------------------------------- /SF iOS/SF iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/28/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/28/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "EventDataSource.h" 11 | #import "EventsFeedViewController.h" 12 | #import "NSNotification+ApplicationEventNotifications.h" 13 | @import CloudKit; 14 | 15 | @interface AppDelegate () 16 | 17 | @property (nonatomic) EventDataSource *dataSource; 18 | 19 | @end 20 | 21 | @implementation AppDelegate 22 | 23 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 24 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 25 | [self.window makeKeyAndVisible]; 26 | 27 | EventDataSource *datasource = [[EventDataSource alloc] initWithEventType:EventTypeSFCoffee database:[[CKContainer defaultContainer] publicCloudDatabase]]; 28 | EventsFeedViewController *feedController = [[EventsFeedViewController alloc] initWithDataSource:datasource]; 29 | 30 | self.window.rootViewController = feedController; 31 | 32 | return true; 33 | } 34 | 35 | 36 | - (void)applicationWillResignActive:(UIApplication *)application { 37 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 38 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 39 | } 40 | 41 | 42 | - (void)applicationDidEnterBackground:(UIApplication *)application { 43 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | 48 | - (void)applicationWillEnterForeground:(UIApplication *)application { 49 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 50 | } 51 | 52 | 53 | - (void)applicationDidBecomeActive:(UIApplication *)application { 54 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 55 | [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.applicationBecameActiveNotification object:nil]; 56 | } 57 | 58 | 59 | - (void)applicationWillTerminate:(UIApplication *)application { 60 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 61 | } 62 | 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "size" : "29x29", 15 | "idiom" : "iphone", 16 | "filename" : "Icon-App-29x29@2x.png", 17 | "scale" : "2x" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "idiom" : "iphone", 22 | "filename" : "Icon-App-29x29@3x.png", 23 | "scale" : "3x" 24 | }, 25 | { 26 | "size" : "40x40", 27 | "idiom" : "iphone", 28 | "filename" : "Icon-App-40x40@2x.png", 29 | "scale" : "2x" 30 | }, 31 | { 32 | "size" : "40x40", 33 | "idiom" : "iphone", 34 | "filename" : "Icon-App-40x40@3x.png", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "size" : "60x60", 39 | "idiom" : "iphone", 40 | "filename" : "Icon-App-60x60@2x.png", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "size" : "60x60", 45 | "idiom" : "iphone", 46 | "filename" : "Icon-App-60x60@3x.png", 47 | "scale" : "3x" 48 | }, 49 | { 50 | "idiom" : "ipad", 51 | "size" : "20x20", 52 | "scale" : "1x" 53 | }, 54 | { 55 | "idiom" : "ipad", 56 | "size" : "20x20", 57 | "scale" : "2x" 58 | }, 59 | { 60 | "idiom" : "ipad", 61 | "size" : "29x29", 62 | "scale" : "1x" 63 | }, 64 | { 65 | "idiom" : "ipad", 66 | "size" : "29x29", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "idiom" : "ipad", 71 | "size" : "40x40", 72 | "scale" : "1x" 73 | }, 74 | { 75 | "idiom" : "ipad", 76 | "size" : "40x40", 77 | "scale" : "2x" 78 | }, 79 | { 80 | "size" : "76x76", 81 | "idiom" : "ipad", 82 | "filename" : "Icon-App-76x76@1x.png", 83 | "scale" : "1x" 84 | }, 85 | { 86 | "size" : "76x76", 87 | "idiom" : "ipad", 88 | "filename" : "Icon-App-76x76@2x.png", 89 | "scale" : "2x" 90 | }, 91 | { 92 | "size" : "83.5x83.5", 93 | "idiom" : "ipad", 94 | "filename" : "Icon-App-83.5x83.5@2x.png", 95 | "scale" : "2x" 96 | }, 97 | { 98 | "size" : "1024x1024", 99 | "idiom" : "ios-marketing", 100 | "filename" : "Icon-App-512x512@2x.png", 101 | "scale" : "1x" 102 | } 103 | ], 104 | "info" : { 105 | "version" : 1, 106 | "author" : "xcode" 107 | } 108 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-512x512@2x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/close-button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "close-button.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/close-button.imageset/close-button.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/close-button.imageset/close-button.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/coffee-location-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "coffee-location-icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/coffee-location-icon.imageset/coffee-location-icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/coffee-location-icon.imageset/coffee-location-icon.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-launch.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-app.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-launch.imageset/icon-app.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/icon-launch.imageset/icon-app.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-automobile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-transport-type-automobile.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-automobile.imageset/icon-transport-type-automobile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/icon-transport-type-automobile.imageset/icon-transport-type-automobile.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-lyft.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-transport-type-lyft.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-lyft.imageset/icon-transport-type-lyft.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/icon-transport-type-lyft.imageset/icon-transport-type-lyft.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-transit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-transport-type-transit.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-transit.imageset/icon-transport-type-transit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/icon-transport-type-transit.imageset/icon-transport-type-transit.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-uber.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-transport-type-uber.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-uber.imageset/icon-transport-type-uber.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/icon-transport-type-uber.imageset/icon-transport-type-uber.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-walking.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-transport-type-walking.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/icon-transport-type-walking.imageset/icon-transport-type-walking.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/icon-transport-type-walking.imageset/icon-transport-type-walking.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/user-location-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Current Location Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /SF iOS/SF iOS/Assets.xcassets/user-location-icon.imageset/Current Location Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/SF iOS/SF iOS/Assets.xcassets/user-location-icon.imageset/Current Location Icon.pdf -------------------------------------------------------------------------------- /SF iOS/SF iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/DirectionsRequestHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // DirectionsRequestHandler.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #ifndef DirectionsRequestHandler_h 10 | #define DirectionsRequestHandler_h 11 | 12 | typedef void(^DirectionsRequestHandler)(TransportType transportType); 13 | 14 | #endif /* DirectionsRequestHandler_h */ 15 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/EventDetailsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // EventDetailsViewController.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Event.h" 11 | #import "UserLocation.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface EventDetailsViewController : UIViewController 15 | 16 | - (instancetype)initWithEvent:(Event *)event userLocationService:(nullable UserLocation *)userLocation NS_DESIGNATED_INITIALIZER; 17 | 18 | @end 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/EventDetailsViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // EventDetailsViewController.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "EventDetailsViewController.h" 10 | #import "UIStackView+ConvenienceInitializer.h" 11 | #import "UIColor+SFiOSColors.h" 12 | #import "NSAttributedString+EventDetails.h" 13 | #import "NSDate+Utilities.h" 14 | #import "MapView.h" 15 | #import "TravelTimeService.h" 16 | #import "TravelTimesView.h" 17 | #import "DirectionsRequest.h" 18 | #import "UIViewController+StatusBarBackground.h" 19 | @import MapKit; 20 | 21 | NS_ASSUME_NONNULL_BEGIN 22 | @interface EventDetailsViewController () 23 | 24 | @property (nonatomic) Event *event; 25 | @property (nonatomic) MapView *mapView; 26 | @property (nonatomic) UIStackView *containerStack; 27 | @property (nonatomic) TravelTimeService *travelTimeService; 28 | @property (nonatomic) TravelTimesView *travelTimesView; 29 | @property (nullable, nonatomic) UserLocation *userLocationService; 30 | 31 | @end 32 | NS_ASSUME_NONNULL_END 33 | 34 | @implementation EventDetailsViewController 35 | 36 | - (instancetype)initWithEvent:(Event *)event userLocationService:(UserLocation *)userLocation { 37 | if (self = [super initWithNibName:nil bundle:nil]) { 38 | self.event = event; 39 | self.travelTimeService = [TravelTimeService new]; 40 | self.userLocationService = userLocation; 41 | } 42 | return self; 43 | } 44 | 45 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 46 | NSAssert(false, @"Use -initWithEvent"); 47 | return [self initWithEvent:[Event new] userLocationService:[UserLocation new]]; 48 | } 49 | 50 | - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 51 | NSAssert(false, @"Use -initWithEvent"); 52 | return [self initWithEvent:[Event new] userLocationService:[UserLocation new]]; 53 | } 54 | 55 | - (void)viewDidLoad { 56 | [super viewDidLoad]; 57 | 58 | self.view.backgroundColor = [UIColor whiteColor]; 59 | self.extendedLayoutIncludesOpaqueBars = true; 60 | 61 | UILabel *titleLabel = [UILabel new]; 62 | titleLabel.text = self.event.location.name; 63 | titleLabel.font = [UIFont systemFontOfSize:28 weight:UIFontWeightSemibold]; 64 | titleLabel.textColor = [UIColor blackColor]; 65 | 66 | UILabel *subtitleLabel = [UILabel new]; 67 | subtitleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]; 68 | subtitleLabel.textColor = [UIColor abbey]; 69 | subtitleLabel.attributedText = [NSAttributedString attributedDetailsStringFromEvent:self.event]; 70 | 71 | UIStackView *titleStack = [[UIStackView alloc] initWithArrangedSubviews:@[titleLabel, subtitleLabel] 72 | axis:UILayoutConstraintAxisVertical 73 | distribution:UIStackViewDistributionEqualSpacing 74 | alignment:UIStackViewAlignmentFill 75 | spacing:9 76 | margins:UIEdgeInsetsMake(18, 21, 0, 21)]; 77 | [titleStack setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical]; 78 | 79 | self.mapView = [[MapView alloc] init]; 80 | [self.mapView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical]; 81 | 82 | self.travelTimesView = [[TravelTimesView alloc] initWithDirectionsRequestHandler:^(TransportType transportType) { 83 | [DirectionsRequest requestDirectionsToLocation:self.event.location.location 84 | withName:self.event.location.name 85 | usingTransportType:transportType]; 86 | }]; 87 | self.travelTimesView.layoutMargins = UIEdgeInsetsMake(32, 21, 21, 21); 88 | self.travelTimesView.translatesAutoresizingMaskIntoConstraints = false; 89 | [self.travelTimesView.heightAnchor constraintGreaterThanOrEqualToConstant:141].active = true; 90 | 91 | self.containerStack = [[UIStackView alloc] initWithArrangedSubviews:@[self.mapView, titleStack, self.travelTimesView] 92 | axis:UILayoutConstraintAxisVertical 93 | distribution:UIStackViewDistributionFill 94 | alignment:UIStackViewAlignmentFill 95 | spacing:0 96 | margins:UIEdgeInsetsZero]; 97 | self.containerStack.translatesAutoresizingMaskIntoConstraints = false; 98 | 99 | [self.view addSubview:self.containerStack]; 100 | // Extend under status bar 101 | [self.containerStack.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:-self.statusBarHeight].active = true; 102 | [self.containerStack.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = true; 103 | [self.containerStack.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = true; 104 | [self.containerStack.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = true; 105 | 106 | UIButton *closeButton = [UIButton new]; 107 | [closeButton setImage:[UIImage imageNamed:@"close-button"] forState:UIControlStateNormal]; 108 | [closeButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; 109 | closeButton.translatesAutoresizingMaskIntoConstraints = false; 110 | [self.view addSubview:closeButton]; 111 | [closeButton.widthAnchor constraintEqualToConstant:44].active = true; 112 | [closeButton.heightAnchor constraintEqualToConstant:44].active = true; 113 | [closeButton.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:32].active = true; 114 | [closeButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-12].active = true; 115 | 116 | [self addStatusBarBlurBackground]; 117 | 118 | [self.mapView setDestinationToLocation:self.event.location.location withAnnotationImage:self.event.annotationImage]; 119 | [self getTravelTimes]; 120 | } 121 | 122 | // MARK: - Travel Times 123 | 124 | - (void)getTravelTimes { 125 | if (!self.userLocationService) { 126 | return; 127 | } 128 | 129 | self.travelTimesView.loading = true; 130 | 131 | __weak typeof(self) welf = self; 132 | [self.userLocationService requestWithCompletionHandler:^(CLLocation * _Nullable currentLocation, NSError * _Nullable error) { 133 | if (!currentLocation || error) { 134 | NSLog(@"Could not get travel times: %@", error); 135 | self.travelTimesView.loading = false; 136 | return; 137 | } 138 | 139 | [welf.travelTimeService calculateTravelTimesFromLocation:currentLocation toLocation:welf.event.location.location withCompletionHandler:^(NSArray * _Nonnull travelTimes) { 140 | welf.travelTimesView.loading = false; 141 | 142 | if (travelTimes.count > 0) { 143 | [welf.travelTimesView configureWithTravelTimes:travelTimes eventStartDate:welf.event.date endDate:welf.event.endDate]; 144 | [UIView animateWithDuration:0.3 animations:^{ 145 | [welf.mapView layoutIfNeeded]; 146 | [welf.containerStack layoutIfNeeded]; 147 | }]; 148 | } 149 | }]; 150 | }]; 151 | } 152 | 153 | // --- 154 | 155 | - (void)dismiss { 156 | [self dismissViewControllerAnimated:true completion:nil]; 157 | } 158 | 159 | @end 160 | 161 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/NSAttributedString+EventDetails.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+EventDetails.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Event.h" 11 | 12 | @interface NSAttributedString (EventDetails) 13 | 14 | + (NSAttributedString *)attributedDetailsStringFromEvent:(Event *)event; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/NSAttributedString+EventDetails.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+EventDetails.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "NSAttributedString+EventDetails.h" 10 | #import "NSDate+Utilities.h" 11 | #import "NSAttributedString+Kerning.h" 12 | 13 | @implementation NSAttributedString (EventDetails) 14 | 15 | + (NSAttributedString *)attributedDetailsStringFromEvent:(Event *)event { 16 | NSDate *eventDate = event.date; 17 | NSDate *currentDate = [NSDate new]; 18 | NSString *timeLabelText; 19 | 20 | // If the event is ongoing: "Ends in 45mins" 21 | if ([currentDate isBetweenEarlierDate:eventDate laterDate:event.endDate]) { 22 | NSString *remainingtime = [event.endDate abbreviatedTimeintervalFromNow]; 23 | timeLabelText = [NSString stringWithFormat:@"Ends in %@", remainingtime]; 24 | } 25 | // If the event is upcoming today: "in 32min" 26 | else if (eventDate.isInFuture && eventDate.isToday) { 27 | timeLabelText = [NSString stringWithFormat:@"in %@", eventDate.abbreviatedTimeintervalFromNow]; 28 | } 29 | // If the event is upcoming this week: "Wednesday 8:30 - 10:00am" 30 | else if (eventDate.isInFuture && eventDate.isThisWeek) { 31 | NSString *weekday = [eventDate weekdayName]; 32 | NSString *time = [NSDate timeslotStringFromStartDate:eventDate duration:event.duration]; 33 | timeLabelText = [NSString stringWithFormat:@"%@ %@", weekday, time]; 34 | } 35 | // Otherwise: "Oct 11 8:30 - 10:00am" 36 | else { 37 | NSString *date = [eventDate stringWithformat:@"MMM d"]; 38 | NSString *time = [NSDate timeslotStringFromStartDate:eventDate duration:event.duration]; 39 | timeLabelText = [NSString stringWithFormat:@"%@ %@", date, time]; 40 | } 41 | 42 | NSString *detailsString = [NSString stringWithFormat:@"%@, %@", event.location.streetAddress, timeLabelText]; 43 | return [NSAttributedString kernedStringFromString:[detailsString uppercaseString]]; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/TravelTimeView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimeView.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TravelTime.h" 11 | #import "DirectionsRequestHandler.h" 12 | #import "TravelTime+Arrival.h" 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | @interface TravelTimeView : UIControl 16 | 17 | - (instancetype)initWithTravelTime:(TravelTime *)travelTime arrival:(Arrival)arrival directionsRequestHandler:(DirectionsRequestHandler)directionsRequestHandler NS_DESIGNATED_INITIALIZER; 18 | 19 | @end 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/TravelTimeView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimeView.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTimeView.h" 10 | #import "UIColor+SFiOSColors.h" 11 | #import "UIStackView+ConvenienceInitializer.h" 12 | 13 | @interface TravelTimeView () 14 | 15 | @property (nonatomic) UIImageView *iconView; 16 | @property (nonatomic) UILabel *timeLabel; 17 | @property (nonatomic) TransportType transportType; 18 | @property (nonatomic, copy) DirectionsRequestHandler directionsRequestHandler; 19 | 20 | @end 21 | 22 | @implementation TravelTimeView 23 | 24 | - (instancetype)initWithTravelTime:(TravelTime *)travelTime arrival:(Arrival)arrival directionsRequestHandler:(DirectionsRequestHandler)directionsRequestHandler { 25 | if (self = [super initWithFrame:CGRectZero]) { 26 | [self setup]; 27 | self.iconView.image = travelTime.icon; 28 | self.transportType = travelTime.transportType; 29 | self.directionsRequestHandler = directionsRequestHandler; 30 | 31 | self.timeLabel.text = travelTime.travelTimeEstimateString; 32 | switch (arrival) { 33 | case ArrivalOnTime: 34 | self.timeLabel.textColor = [UIColor atlantis]; 35 | break; 36 | 37 | case ArrivalDuringEvent: 38 | self.timeLabel.textColor = [UIColor saffron]; 39 | break; 40 | 41 | case ArrivalAfterEvent: 42 | self.timeLabel.textColor = [UIColor mandy]; 43 | break; 44 | 45 | default: 46 | break; 47 | } 48 | } 49 | return self; 50 | } 51 | 52 | - (instancetype)initWithFrame:(CGRect)frame { 53 | NSAssert(false, @"Use initWithTravelTime:"); 54 | return [self initWithTravelTime:[TravelTime new] arrival:ArrivalOnTime directionsRequestHandler:^(TransportType transportType) {}]; 55 | } 56 | 57 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 58 | NSAssert(false, @"Use initWithTravelTime:"); 59 | return [self initWithTravelTime:[TravelTime new] arrival:ArrivalOnTime directionsRequestHandler:^(TransportType transportType) {}]; 60 | } 61 | 62 | - (void)setup { 63 | self.backgroundColor = [UIColor whiteColor]; 64 | self.layer.cornerRadius = 8; 65 | self.layer.shadowColor = [UIColor nobel].CGColor; 66 | self.layer.shadowOpacity = 0.5; 67 | self.layer.shadowOffset = CGSizeMake(0, 2); 68 | self.layer.shadowRadius = 8; 69 | self.clipsToBounds = false; 70 | 71 | self.timeLabel = [UILabel new]; 72 | self.timeLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; 73 | self.timeLabel.textColor = [UIColor atlantis]; 74 | self.timeLabel.numberOfLines = 1; 75 | self.timeLabel.userInteractionEnabled = false; 76 | 77 | self.iconView = [UIImageView new]; 78 | self.iconView.contentMode = UIViewContentModeScaleAspectFit; 79 | [self.iconView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; 80 | 81 | UIStackView *contentStack = [[UIStackView alloc] initWithArrangedSubviews:@[self.iconView, self.timeLabel] 82 | axis:UILayoutConstraintAxisHorizontal 83 | distribution:UIStackViewDistributionFill 84 | alignment:UIStackViewAlignmentFill 85 | spacing:10 86 | margins:UIEdgeInsetsMake(10, 10, 10, 10)]; 87 | contentStack.userInteractionEnabled = false; 88 | contentStack.translatesAutoresizingMaskIntoConstraints = false; 89 | [contentStack setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; 90 | [self addSubview:contentStack]; 91 | [contentStack.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = true; 92 | [contentStack.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = true; 93 | [contentStack.topAnchor constraintEqualToAnchor:self.topAnchor].active = true; 94 | [contentStack.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = true; 95 | [contentStack.heightAnchor constraintEqualToConstant:36].active = true; 96 | 97 | [self addTarget:self action:@selector(requestdirections) forControlEvents:UIControlEventTouchUpInside]; 98 | } 99 | 100 | //MARK: - Touch Handling 101 | 102 | - (void)requestdirections { 103 | if (self.directionsRequestHandler) { 104 | self.directionsRequestHandler(self.transportType); 105 | } 106 | } 107 | 108 | - (void)setHighlighted:(BOOL)highlighted { 109 | [super setHighlighted:highlighted]; 110 | CGAffineTransform transform = highlighted ? CGAffineTransformScale(CGAffineTransformIdentity, 1.1, 1.1) : CGAffineTransformIdentity; 111 | [UIView animateWithDuration:0.15 animations:^{ 112 | self.transform = transform; 113 | }]; 114 | } 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/TravelTimesView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimesView.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TravelTime.h" 11 | #import "DirectionsRequestHandler.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface TravelTimesView : UIStackView 15 | 16 | @property (nonatomic, assign) BOOL loading; 17 | 18 | - (instancetype)initWithDirectionsRequestHandler:(DirectionsRequestHandler)directionsRequestHandler NS_DESIGNATED_INITIALIZER; 19 | 20 | - (void)configureWithTravelTimes:(NSArray *)travelTimes eventStartDate:(NSDate *)startDate endDate:(NSDate *)endDate; 21 | 22 | @end 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/EventDetails/TravelTimesView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimesView.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTimesView.h" 10 | #import "TravelTimeView.h" 11 | #import "UIStackView+ConvenienceInitializer.h" 12 | 13 | typedef NS_ENUM(NSUInteger, TravelTimeType) { 14 | TravelTimeTypeRegular = 0, 15 | TravelTimeTypeRideSharing = 1 16 | }; 17 | 18 | @interface TravelTimesView () 19 | 20 | @property (nonatomic) UIStackView *regularStack; 21 | @property (nonatomic) UIStackView *ridesharingStack; 22 | @property (nonatomic) UIActivityIndicatorView *loadingIndicator; 23 | @property (copy, nonatomic) DirectionsRequestHandler directionsRequestHandler; 24 | 25 | @end 26 | 27 | @implementation TravelTimesView 28 | 29 | - (instancetype)initWithDirectionsRequestHandler:(DirectionsRequestHandler)directionsRequestHandler { 30 | if (self = [super initWithFrame:CGRectZero]) { 31 | self.directionsRequestHandler = directionsRequestHandler; 32 | [self setup]; 33 | } 34 | return self; 35 | } 36 | 37 | - (instancetype)initWithCoder:(NSCoder *)coder { 38 | NSAssert(false, @"use initWithDirectionsRequestHandler:"); 39 | return [self initWithDirectionsRequestHandler:^(TransportType transportType) {}]; 40 | } 41 | 42 | - (instancetype)initWithFrame:(CGRect)frame { 43 | NSAssert(false, @"use initWithDirectionsRequestHandler:"); 44 | return [self initWithDirectionsRequestHandler:^(TransportType transportType) {}]; 45 | } 46 | 47 | - (void)setLoading:(BOOL)loading { 48 | _loading = loading; 49 | loading ? [self.loadingIndicator startAnimating] : [self.loadingIndicator stopAnimating]; 50 | } 51 | 52 | - (void)configureWithTravelTimes:(NSArray *)travelTimes eventStartDate:(NSDate *)startDate endDate:(NSDate *)endDate { 53 | [self.loadingIndicator stopAnimating]; 54 | 55 | [self.regularStack removeAllArrangedSubviews]; 56 | [self.ridesharingStack removeAllArrangedSubviews]; 57 | 58 | NSDictionary *categorizedTravelTimes = [self categorizedTravelTimesFromArray:travelTimes]; 59 | NSArray *regularTimes = categorizedTravelTimes[@(TravelTimeTypeRegular)]; 60 | if (regularTimes) { 61 | [self populateTravelTimeViewsInStack:self.regularStack withTimes:regularTimes startDate:startDate endDate:endDate]; 62 | } 63 | 64 | NSArray *rideSharingTimes = categorizedTravelTimes[@(TravelTimeTypeRideSharing)]; 65 | if (rideSharingTimes) { 66 | [self populateTravelTimeViewsInStack:self.ridesharingStack withTimes:rideSharingTimes startDate:startDate endDate:endDate]; 67 | } 68 | 69 | [self animateInTravelTimeViews]; 70 | } 71 | 72 | - (void)setup { 73 | self.backgroundColor = [UIColor whiteColor]; 74 | self.clipsToBounds = false; 75 | 76 | self.axis = UILayoutConstraintAxisVertical; 77 | self.distribution = UIStackViewDistributionEqualSpacing; 78 | self.alignment = UIStackViewAlignmentLeading; 79 | self.spacing = 16; 80 | self.layoutMargins = UIEdgeInsetsZero; 81 | self.layoutMarginsRelativeArrangement = true; 82 | 83 | self.regularStack = [self timesStackView]; 84 | [self addArrangedSubview:self.regularStack]; 85 | self.ridesharingStack = [self timesStackView]; 86 | [self addArrangedSubview:self.ridesharingStack]; 87 | 88 | self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; 89 | self.loadingIndicator.hidesWhenStopped = true; 90 | self.loadingIndicator.translatesAutoresizingMaskIntoConstraints = false; 91 | [self addSubview:self.loadingIndicator]; 92 | [self.loadingIndicator.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = true; 93 | [self.loadingIndicator.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = true; 94 | } 95 | 96 | - (void)populateTravelTimeViewsInStack:(nonnull UIStackView *)stack withTimes:(nonnull NSArray *)travelTimes startDate:(NSDate *)startDate endDate:(NSDate *)endDate { 97 | for (TravelTime *time in travelTimes) { 98 | Arrival arrival = [time arrivalToEventWithStartDate:startDate endDate:endDate]; 99 | TravelTimeView *view = [[TravelTimeView alloc] initWithTravelTime:time arrival:arrival directionsRequestHandler:self.directionsRequestHandler]; 100 | [stack addArrangedSubview:view]; 101 | } 102 | } 103 | 104 | - (UIStackView *)timesStackView { 105 | return [[UIStackView alloc] initWithArrangedSubviews:@[] axis:UILayoutConstraintAxisHorizontal 106 | distribution:UIStackViewDistributionEqualSpacing 107 | alignment:UIStackViewAlignmentLeading 108 | spacing:16 109 | margins:UIEdgeInsetsZero]; 110 | } 111 | 112 | - (void)animateInTravelTimeViews { 113 | if (self.regularStack.arrangedSubviews.count == 0 && self.ridesharingStack.arrangedSubviews.count == 0) { 114 | return; 115 | } 116 | 117 | NSTimeInterval stagger = 0.1; 118 | CGAffineTransform transform = CGAffineTransformTranslate(CGAffineTransformIdentity, [[UIScreen mainScreen] bounds].size.width, 0); 119 | 120 | NSArray *views = [self.regularStack.arrangedSubviews arrayByAddingObjectsFromArray:self.ridesharingStack.arrangedSubviews]; 121 | [views enumerateObjectsUsingBlock:^(UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) { 122 | view.transform = transform; 123 | view.alpha = 0; 124 | [UIView 125 | animateWithDuration:0.3 126 | delay:stagger * (idx + 1) 127 | usingSpringWithDamping:0.85 128 | initialSpringVelocity:0 129 | options:0 130 | animations:^{ 131 | view.alpha = 1; 132 | view.transform = CGAffineTransformIdentity; 133 | } 134 | completion:nil]; 135 | }]; 136 | } 137 | 138 | // MARK: - Binning 139 | 140 | - (NSDictionary *)categorizedTravelTimesFromArray:(NSArray *)array { 141 | NSMutableArray *regular = [NSMutableArray new]; 142 | NSMutableArray *ridesharing = [NSMutableArray new]; 143 | 144 | for (TravelTime *time in array) { 145 | switch (time.transportType) { 146 | case TransportTypeUber: 147 | [ridesharing addObject: time]; 148 | break; 149 | case TransportTypeLyft: 150 | [ridesharing addObject: time]; 151 | break; 152 | case TransportTypeWalking: 153 | [regular addObject: time]; 154 | break; 155 | case TransportTypeAutomobile: 156 | [regular addObject: time]; 157 | break; 158 | case TransportTypeTransit: 159 | [regular addObject: time]; 160 | break; 161 | default: 162 | break; 163 | } 164 | } 165 | 166 | return @{@(TravelTimeTypeRegular) : regular, @(TravelTimeTypeRideSharing) : ridesharing}; 167 | } 168 | 169 | @end 170 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Feed/EventsFeedViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // EventsFeedViewController.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "EventDataSource.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | @interface EventsFeedViewController : UIViewController 14 | 15 | - (instancetype)initWithDataSource:(EventDataSource *)dataSource NS_DESIGNATED_INITIALIZER; 16 | 17 | @end 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Feed/EventsFeedViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // EventsFeedViewController.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "EventsFeedViewController.h" 10 | #import "FeedItemCell.h" 11 | #import "FeedItem.h" 12 | #import "UserLocation.h" 13 | #import "EventDetailsViewController.h" 14 | #import "UIViewController+StatusBarBackground.h" 15 | #import "UIImage+URL.h" 16 | #import "ImageStore.h" 17 | 18 | NS_ASSUME_NONNULL_BEGIN 19 | @interface EventsFeedViewController () 20 | 21 | @property (nonatomic) EventDataSource *dataSource; 22 | @property (nullable, nonatomic) UserLocation *userLocationService; 23 | @property (nonatomic) UITableView *tableView; 24 | @property (nonatomic, assign) BOOL firstLoad; 25 | @property (nonatomic) ImageStore *imageStore; 26 | @property (nonatomic) NSOperationQueue *imageFetchQueue; 27 | 28 | @end 29 | NS_ASSUME_NONNULL_END 30 | 31 | @implementation EventsFeedViewController 32 | 33 | - (instancetype)initWithDataSource:(EventDataSource *)dataSource { 34 | if (self = [super initWithNibName:nil bundle:nil]) { 35 | self.dataSource = dataSource; 36 | dataSource.delegate = self; 37 | self.userLocationService = [UserLocation new]; 38 | self.imageFetchQueue = [[NSOperationQueue alloc] init]; 39 | self.imageFetchQueue.name = @"Image Fetch Queue"; 40 | self.imageStore = [[ImageStore alloc] init]; 41 | self.firstLoad = true; 42 | } 43 | 44 | return self; 45 | } 46 | 47 | - (instancetype)initWithStyle:(UITableViewStyle)style { 48 | NSAssert(false, @"Use -initWithDataSource"); 49 | self = [self initWithDataSource:[EventDataSource new]]; 50 | return self; 51 | } 52 | 53 | - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 54 | NSAssert(false, @"Use -initWithDataSource"); 55 | self = [self initWithDataSource:[EventDataSource new]]; 56 | return self; 57 | } 58 | 59 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 60 | NSAssert(false, @"Use -initWithDataSource"); 61 | self = [self initWithDataSource:[EventDataSource new]]; 62 | return self; 63 | } 64 | 65 | - (void)viewDidLoad { 66 | [super viewDidLoad]; 67 | 68 | self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; 69 | self.tableView.dataSource = self; 70 | self.tableView.delegate = self; 71 | [self.tableView registerClass:self.feedItemCellClass forCellReuseIdentifier:NSStringFromClass(self.feedItemCellClass)]; 72 | self.tableView.rowHeight = self.cellHeight; 73 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 74 | self.tableView.contentInset = UIEdgeInsetsMake(40, 0, 0, 0); 75 | self.tableView.translatesAutoresizingMaskIntoConstraints = false; 76 | [self.view addSubview:self.tableView]; 77 | [self.tableView.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = true; 78 | [self.tableView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = true; 79 | [self.tableView.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = true; 80 | [self.tableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = true; 81 | [self.tableView.widthAnchor constraintEqualToAnchor:self.view.widthAnchor].active = true; 82 | 83 | self.tableView.refreshControl = [[UIRefreshControl alloc] init]; 84 | [self.tableView.refreshControl addTarget:self.dataSource action:@selector(refresh) forControlEvents:UIControlEventValueChanged]; 85 | 86 | if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) { 87 | [self registerForPreviewingWithDelegate:self sourceView:self.tableView]; 88 | } 89 | 90 | [self addStatusBarBlurBackground]; 91 | 92 | [self.dataSource refresh]; 93 | } 94 | 95 | - (void)viewDidAppear:(BOOL)animated { 96 | [super viewDidAppear:animated]; 97 | [self requestLocationPermission]; 98 | } 99 | 100 | - (void)viewDidDisappear:(BOOL)animated { 101 | [self.tableView.refreshControl endRefreshing]; 102 | [self.imageFetchQueue cancelAllOperations]; 103 | 104 | [super viewDidDisappear:animated]; 105 | } 106 | 107 | //MARK: - UITableViewDataSource 108 | 109 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 110 | return 1; 111 | } 112 | 113 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 114 | return self.dataSource.numberOfEvents; 115 | } 116 | 117 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 118 | FeedItemCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(self.feedItemCellClass) forIndexPath:indexPath]; 119 | if (!cell) { 120 | NSAssert(false, @"cell couldn't be dequeued"); 121 | } 122 | 123 | FeedItem *item = [[FeedItem alloc] initWithEvent:[self.dataSource eventAtIndex:indexPath.row]]; 124 | [cell configureWithFeedItem:item]; 125 | 126 | UIImage *image = [self.imageStore imageForKey:item.coverImageFileURL]; 127 | if (image) { 128 | [cell setCoverToImage:image]; 129 | } else { 130 | __weak typeof(self) welf = self; 131 | [UIImage 132 | fetchImageFromFileURL:item.coverImageFileURL 133 | onQueue: self.imageFetchQueue 134 | withCompletionHandler:^(UIImage * _Nullable image, NSError * _Nullable error) { 135 | if (!image || error) { 136 | NSLog(@"Error decoding image: %@", error); 137 | return; 138 | } 139 | 140 | [welf.imageStore storeImage:image forKey:item.coverImageFileURL]; 141 | 142 | // Fetch the cell again, if it exists as the original instance of cell might have been 143 | // dequeued by now. If the cell does not exist, setting the image will silently fail. 144 | [(FeedItemCell *)[tableView cellForRowAtIndexPath:indexPath] setCoverToImage:image]; 145 | }]; 146 | } 147 | 148 | return cell; 149 | } 150 | 151 | - (Class)feedItemCellClass { 152 | return [FeedItemCell class]; 153 | } 154 | 155 | //MARK: - UITableViewDelegate 156 | 157 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 158 | EventDetailsViewController *vc = [self eventDetailsViewControllerForEventAtIndexPath:indexPath]; 159 | [self presentViewController:vc animated:true completion:nil]; 160 | } 161 | 162 | //MARK: - 3D Touch Peek & Pop 163 | 164 | - (UIViewController *)previewingContext:(id)previewingContext viewControllerForLocation:(CGPoint)location { 165 | NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location]; 166 | if (!indexPath) { return nil; } 167 | 168 | UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; 169 | if (!cell || ![cell isKindOfClass:[self.feedItemCellClass class]]) { 170 | return nil; 171 | } 172 | 173 | previewingContext.sourceRect = [(FeedItemCell *)cell contentFrame]; 174 | 175 | return [self eventDetailsViewControllerForEventAtIndexPath:indexPath]; 176 | } 177 | 178 | - (void)previewingContext:(id)previewingContext commitViewController:(UIViewController *)viewControllerToCommit { 179 | [self presentViewController:viewControllerToCommit animated:true completion:nil]; 180 | } 181 | 182 | //MARK: - EventDataSourceDelegate 183 | 184 | - (void)willUpdateDataSource:(EventDataSource *)datasource { 185 | [self.tableView.refreshControl beginRefreshing]; 186 | } 187 | 188 | - (void)didUpdateDataSource:(EventDataSource *)datasource withNewData:(BOOL)hasNewData error:(NSError *)error { 189 | [self.tableView.refreshControl endRefreshing]; 190 | 191 | if (hasNewData) { 192 | [self refresh]; 193 | } 194 | 195 | if (error) { 196 | [self handleError:error]; 197 | } 198 | } 199 | 200 | //MARK: - Details View 201 | 202 | - (EventDetailsViewController *)eventDetailsViewControllerForEventAtIndexPath:(NSIndexPath *)indexPath { 203 | Event *event = [self.dataSource eventAtIndex:indexPath.row]; 204 | return [[EventDetailsViewController alloc] initWithEvent:event userLocationService:self.userLocationService]; 205 | } 206 | 207 | //MARK: - Location Permission 208 | 209 | - (void)requestLocationPermission { 210 | [self.userLocationService requestLocationPermission]; 211 | } 212 | 213 | //MARK: - Cell Dimensions 214 | 215 | static CGFloat const eventCellAspectRatio = 1.352; 216 | 217 | - (CGFloat)cellHeight{ 218 | return [UIScreen mainScreen].bounds.size.width * eventCellAspectRatio; 219 | } 220 | 221 | //MARK: - Error Handling 222 | 223 | - (void)handleError:(NSError *)error { 224 | NSLog(@"Error fetching events: %@", error); 225 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error Fetching Events" message:@"There was an error fetching events. Please try again." preferredStyle:UIAlertControllerStyleAlert]; 226 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 227 | [alert dismissViewControllerAnimated:true completion:nil]; 228 | }]; 229 | [alert addAction:okAction]; 230 | 231 | [self presentViewController:alert animated:true completion:nil]; 232 | } 233 | 234 | //MARK: - First Load 235 | 236 | - (void)refresh { 237 | if (!self.firstLoad) { 238 | [self.tableView reloadData]; 239 | return; 240 | } 241 | 242 | [self animateFirstLoad]; 243 | self.firstLoad = false; 244 | } 245 | 246 | - (void)animateFirstLoad { 247 | [self.tableView reloadData]; 248 | 249 | if (self.dataSource.indexOfCurrentEvent != NSNotFound) { 250 | NSIndexPath *nextEventIndexPath = [NSIndexPath indexPathForRow:self.dataSource.indexOfCurrentEvent inSection:0]; 251 | [self.tableView scrollToRowAtIndexPath:nextEventIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:false]; 252 | } 253 | 254 | NSTimeInterval stagger = 0.2; 255 | [self.tableView.visibleCells enumerateObjectsUsingBlock:^(__kindof UITableViewCell * _Nonnull cell, NSUInteger idx, BOOL * _Nonnull stop) { 256 | cell.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, 0, [UIScreen mainScreen].bounds.size.height); 257 | cell.alpha = 0; 258 | 259 | [UIView 260 | animateWithDuration:0.8 261 | delay:stagger * (idx + 1) 262 | usingSpringWithDamping:0.8 263 | initialSpringVelocity:0 264 | options:0 265 | animations:^{ 266 | cell.alpha = 1; 267 | cell.transform = CGAffineTransformIdentity; 268 | } 269 | completion:nil]; 270 | }]; 271 | } 272 | 273 | @end 274 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Feed/FeedItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItem.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Event.h" 11 | @import UIKit; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface FeedItem : NSObject 15 | 16 | @property (nonatomic) NSString *dateString; 17 | @property (nonatomic) NSString *title; 18 | @property (nonatomic) NSAttributedString *subtitle; 19 | @property (nonatomic) UIImage *annotationImage; 20 | @property (readonly, assign, nonatomic) BOOL isActive; 21 | @property (nullable, nonatomic) NSURL *coverImageFileURL; 22 | @property (nonatomic) CLLocation *location; 23 | 24 | - (instancetype)initWithEvent:(Event *)event; 25 | 26 | @end 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Feed/FeedItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItem.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "FeedItem.h" 10 | #import "NSDate+Utilities.h" 11 | #import "NSAttributedString+Kerning.h" 12 | 13 | @interface FeedItem () 14 | 15 | @property (readwrite, assign, nonatomic) BOOL isActive; 16 | 17 | @end 18 | 19 | @implementation FeedItem 20 | 21 | - (instancetype)initWithEvent:(Event *)event { 22 | self = [super init]; 23 | if (!self) { 24 | return nil; 25 | } 26 | self.dateString = [self dateStringFromDate:event.date]; 27 | self.title = event.location.name; 28 | self.isActive = event.isActive; 29 | self.coverImageFileURL = event.location.imageFileURL; 30 | self.location = event.location.location; 31 | self.annotationImage = event.annotationImage; 32 | 33 | NSString *location = event.location.streetAddress; 34 | NSString *time; 35 | 36 | if ([[NSDate new] isBetweenEarlierDate:event.date laterDate:event.endDate]) { 37 | time = @"Now"; 38 | } else { 39 | time = [NSDate timeslotStringFromStartDate:event.date duration:event.duration]; 40 | } 41 | 42 | NSString *subtite = [NSString stringWithFormat:@"%@, %@", location, time]; 43 | self.subtitle = [NSAttributedString kernedStringFromString:[subtite uppercaseString]]; 44 | 45 | return self; 46 | } 47 | 48 | //MARK: - Time Representation 49 | 50 | - (NSString *)dateStringFromDate:(NSDate *)date { 51 | NSString *relativeDate = date.relativeDayRepresentation; 52 | if (relativeDate) { 53 | return relativeDate; 54 | } else if (date.isThisYear) { 55 | return [date stringWithformat:@"MMM d"]; 56 | } else { 57 | return [date stringWithformat:@"MMM d, yyyy"]; 58 | } 59 | } 60 | 61 | - (BOOL)directionsAreRelevantForEventWithDate:(NSDate *)date { 62 | if (date.isToday || date.isInFuture) { 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Feed/FeedItemCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItemCell.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "FeedItem.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | @interface FeedItemCell : UITableViewCell 14 | 15 | @property (readonly, assign, nonatomic) CGRect contentFrame; 16 | @property (readonly, assign, nonatomic) CGSize coverImageSize; 17 | 18 | - (void)configureWithFeedItem:(FeedItem *)item; 19 | - (void)setCoverToImage:(UIImage *)image; 20 | 21 | @end 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Feed/FeedItemCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItemCell.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "FeedItemCell.h" 10 | #import "UIStackView+ConvenienceInitializer.h" 11 | #import "UIColor+SFiOSColors.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface FeedItemCell () 15 | 16 | @property (nonatomic) UIStackView *containerStack; 17 | @property (nonatomic) UIView *detailStackContainer; 18 | @property (nonatomic) UILabel *timeLabel; 19 | @property (nonatomic) UILabel *titleLabel; 20 | @property (nonatomic) UILabel *subtitleLabel; 21 | @property (nonatomic) UIStackView *itemImageStack; 22 | @property (nonatomic) UIImageView *coverImageView; 23 | 24 | @end 25 | NS_ASSUME_NONNULL_END 26 | 27 | @implementation FeedItemCell 28 | 29 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 30 | if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { 31 | [self setup]; 32 | } 33 | return self; 34 | } 35 | 36 | - (CGRect)contentFrame { 37 | UIView *superView = [self superview]; 38 | if (!superView) { 39 | return CGRectZero; 40 | } 41 | 42 | return [superView convertRect:self.detailStackContainer.frame fromView:self.containerStack]; 43 | } 44 | 45 | - (CGSize)coverImageSize { 46 | return self.itemImageStack.bounds.size; 47 | } 48 | 49 | //MARK: - Configuration 50 | 51 | - (void)configureWithFeedItem:(FeedItem *)item { 52 | self.timeLabel.text = item.dateString; 53 | self.timeLabel.alpha = item.isActive ? 1 : 0.2; 54 | 55 | self.titleLabel.text = item.title; 56 | self.subtitleLabel.attributedText = item.subtitle; 57 | } 58 | 59 | - (void)setCoverToImage:(UIImage *)image { 60 | self.coverImageView.image = image; 61 | } 62 | 63 | - (void)layoutSubviews { 64 | [super layoutSubviews]; 65 | 66 | if (@available(iOS 11.0, *)) { 67 | // set using corner mask API 68 | } else { 69 | UIBezierPath *path = [UIBezierPath 70 | bezierPathWithRoundedRect:self.coverImageView.bounds 71 | byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight 72 | cornerRadii:CGSizeMake(15, 15)]; 73 | 74 | CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; 75 | maskLayer.frame = self.coverImageView.bounds; 76 | maskLayer.path = path.CGPath; 77 | 78 | self.coverImageView.layer.mask = maskLayer; 79 | } 80 | } 81 | 82 | //MARK: - Setup 83 | 84 | - (void)setup { 85 | self.selectionStyle = UITableViewCellSelectionStyleNone; 86 | self.contentView.backgroundColor = [UIColor whiteColor]; 87 | 88 | [self setupContainerStack]; 89 | 90 | self.timeLabel = [UILabel new]; 91 | self.timeLabel.font = [UIFont systemFontOfSize:34 weight:UIFontWeightBold]; 92 | self.titleLabel.textColor = [UIColor blackColor]; 93 | [self.timeLabel setTranslatesAutoresizingMaskIntoConstraints:false]; 94 | [self.containerStack addArrangedSubview:self.timeLabel]; 95 | 96 | [self setupDetailsStack]; 97 | } 98 | 99 | - (void)setupContainerStack { 100 | self.containerStack = [[UIStackView alloc] 101 | initWithArrangedSubviews:nil 102 | axis:UILayoutConstraintAxisVertical 103 | distribution:UIStackViewDistributionFill 104 | alignment:UIStackViewAlignmentFill 105 | spacing:13 106 | margins:UIEdgeInsetsMake(0, 20, 40, 20)]; 107 | [self.contentView addSubview:self.containerStack]; 108 | [self.containerStack setTranslatesAutoresizingMaskIntoConstraints:false]; 109 | [[self.containerStack.leftAnchor 110 | constraintEqualToAnchor:self.contentView.leftAnchor] setActive:true]; 111 | [[self.containerStack.rightAnchor 112 | constraintEqualToAnchor:self.contentView.rightAnchor] setActive:true]; 113 | [[self.containerStack.topAnchor 114 | constraintEqualToAnchor:self.contentView.topAnchor] setActive:true]; 115 | [[self.containerStack.bottomAnchor 116 | constraintEqualToAnchor:self.contentView.bottomAnchor] setActive:true]; 117 | } 118 | 119 | - (void)setupDetailsStack { 120 | CGFloat cornerRadius = 15; 121 | 122 | self.coverImageView = [UIImageView new]; 123 | self.coverImageView.contentMode = UIViewContentModeScaleAspectFill; 124 | self.coverImageView.backgroundColor = [UIColor alabaster]; 125 | 126 | if (@available(iOS 11.0, *)) { 127 | self.coverImageView.layer.cornerRadius = cornerRadius; 128 | self.coverImageView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner; 129 | } else { 130 | // round in layoutSubviews: 131 | } 132 | 133 | self.coverImageView.clipsToBounds = true; 134 | self.coverImageView.translatesAutoresizingMaskIntoConstraints = false; 135 | [self.coverImageView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical]; 136 | 137 | self.itemImageStack = [[UIStackView alloc] 138 | initWithArrangedSubviews:@[ self.coverImageView ] 139 | axis:UILayoutConstraintAxisVertical 140 | distribution:UIStackViewDistributionFill 141 | alignment:UIStackViewAlignmentFill 142 | spacing:0 143 | margins:UIEdgeInsetsZero]; 144 | self.itemImageStack.translatesAutoresizingMaskIntoConstraints = false; 145 | 146 | self.titleLabel = [UILabel new]; 147 | self.titleLabel.font = [UIFont systemFontOfSize:28 weight:UIFontWeightSemibold]; 148 | self.titleLabel.textColor = [UIColor blackColor]; 149 | self.titleLabel.translatesAutoresizingMaskIntoConstraints = false; 150 | 151 | self.subtitleLabel = [UILabel new]; 152 | self.subtitleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]; 153 | self.subtitleLabel.textColor = [UIColor abbey]; 154 | self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = false; 155 | 156 | UIStackView *titleStack = [[UIStackView alloc] 157 | initWithArrangedSubviews:@[ self.subtitleLabel, self.titleLabel ] 158 | axis:UILayoutConstraintAxisVertical 159 | distribution:UIStackViewDistributionEqualSpacing 160 | alignment:UIStackViewAlignmentLeading 161 | spacing:6 162 | margins:UIEdgeInsetsMake(19, 19, 19, 19)]; 163 | [titleStack 164 | setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh 165 | forAxis:UILayoutConstraintAxisVertical]; 166 | 167 | UIStackView *detailsStack = [[UIStackView alloc] 168 | initWithArrangedSubviews:@[ self.itemImageStack, titleStack ] 169 | axis:UILayoutConstraintAxisVertical 170 | distribution:UIStackViewDistributionFill 171 | alignment:UIStackViewAlignmentFill 172 | spacing:0 173 | margins:UIEdgeInsetsZero]; 174 | detailsStack.translatesAutoresizingMaskIntoConstraints = false; 175 | 176 | self.detailStackContainer = [UIView new]; 177 | self.detailStackContainer.backgroundColor = [UIColor whiteColor]; 178 | self.detailStackContainer.layer.cornerRadius = 15; 179 | self.detailStackContainer.layer.shadowOpacity = 0.22; 180 | self.detailStackContainer.layer.shadowRadius = 14; 181 | self.detailStackContainer.layer.shadowColor = [UIColor blackColor].CGColor; 182 | self.detailStackContainer.layer.shadowOffset = CGSizeMake(0, 12); 183 | self.detailStackContainer.clipsToBounds = false; 184 | self.detailStackContainer.translatesAutoresizingMaskIntoConstraints = false; 185 | [self.detailStackContainer addSubview:detailsStack]; 186 | [detailsStack.leftAnchor constraintEqualToAnchor:self.detailStackContainer.leftAnchor].active = true; 187 | [detailsStack.rightAnchor constraintEqualToAnchor:self.detailStackContainer.rightAnchor].active = true; 188 | [detailsStack.topAnchor constraintEqualToAnchor:self.detailStackContainer.topAnchor].active = true; 189 | [detailsStack.bottomAnchor constraintEqualToAnchor:self.detailStackContainer.bottomAnchor].active = true; 190 | 191 | [self.containerStack addArrangedSubview:self.detailStackContainer]; 192 | } 193 | 194 | @end 195 | 196 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/HTTPRequestOperation/HTTPRequestAsyncOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestAsyncOperation.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "AsyncOperation.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface HTTPRequestAsyncOperation : AsyncOperation 13 | 14 | typedef NSDictionary JSON; 15 | 16 | typedef void(^HTTPRequestCompletionHandler)(JSON * _Nullable json, NSURLResponse * _Nullable response, NSError * _Nullable error); 17 | 18 | - (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(HTTPRequestCompletionHandler)completionHandler; 19 | 20 | @end 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/HTTPRequestOperation/HTTPRequestAsyncOperation.m: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestAsyncOperation.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "HTTPRequestAsyncOperation.h" 10 | 11 | @interface HTTPRequestAsyncOperation () 12 | 13 | @property (nonatomic) NSURLSessionDataTask *task; 14 | 15 | @end 16 | 17 | @implementation HTTPRequestAsyncOperation 18 | 19 | - (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(HTTPRequestCompletionHandler)completionHandler { 20 | self = [super init]; 21 | if (!self) { 22 | return nil; 23 | } 24 | 25 | __weak typeof(self) welf = self; 26 | self.task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { 27 | if (self.isCancelled) { return; } 28 | 29 | if (!error && data) { 30 | NSError *jsonParsingError; 31 | NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonParsingError]; 32 | 33 | if (!jsonParsingError && json) { 34 | completionHandler(json, response, nil); 35 | } else { 36 | completionHandler(nil, response, jsonParsingError); 37 | } 38 | } else { 39 | completionHandler(nil, response, error); 40 | } 41 | 42 | [welf finish]; 43 | }]; 44 | 45 | return self; 46 | } 47 | 48 | - (void)start { 49 | [super start]; 50 | [self.task resume]; 51 | } 52 | 53 | - (void)cancel { 54 | [self.task cancel]; 55 | [super cancel]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Application/UIApplication+Metadata.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+Metadata.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIApplication (Metadata) 12 | 13 | @property (nonnull, nonatomic, readonly) NSString *bundleIdentifier; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Application/UIApplication+Metadata.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+Metadata.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UIApplication+Metadata.h" 10 | 11 | @implementation UIApplication (Metadata) 12 | 13 | - (NSString *)bundleIdentifier { 14 | return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Concurrency/AsyncBlockOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockOperation.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | // https://stackoverflow.com/a/26895501/2671390 10 | 11 | #import 12 | #import "AsyncOperation.h" 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | typedef void(^AsyncBlock)(dispatch_block_t completionHandler); 17 | 18 | @interface AsyncBlockOperation : AsyncOperation 19 | 20 | @property (nonatomic, readonly, copy) AsyncBlock block; 21 | 22 | + (instancetype)asyncBlockOperationWithBlock:(AsyncBlock)block; 23 | - (instancetype)initWithAsyncBlock:(AsyncBlock)block; 24 | 25 | @end 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Concurrency/AsyncBlockOperation.m: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockOperation.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "AsyncBlockOperation.h" 10 | 11 | @interface AsyncBlockOperation () 12 | 13 | @property (nonatomic, copy) AsyncBlock block; 14 | 15 | @end 16 | 17 | 18 | @implementation AsyncBlockOperation 19 | 20 | + (instancetype)asyncBlockOperationWithBlock:(AsyncBlock)block { 21 | return [[AsyncBlockOperation alloc] initWithAsyncBlock:block]; 22 | } 23 | 24 | - (instancetype)initWithAsyncBlock:(AsyncBlock)block { 25 | if (self = [super init]) { 26 | self.block = block; 27 | } 28 | return self; 29 | } 30 | 31 | - (void)start { 32 | [super start]; 33 | 34 | self.block(^{ 35 | [self finish]; 36 | }); 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Concurrency/AsyncOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncOperation.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface AsyncOperation : NSOperation 13 | 14 | - (void)finish; 15 | 16 | @end 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Concurrency/AsyncOperation.m: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncOperation.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "AsyncOperation.h" 10 | 11 | @interface AsyncOperation () { 12 | BOOL _finished; 13 | BOOL _executing; 14 | } 15 | 16 | @end 17 | 18 | 19 | @implementation AsyncOperation 20 | 21 | - (void)start { 22 | [self willChangeValueForKey:@"isExecuting"]; 23 | _executing = YES; 24 | [self didChangeValueForKey:@"isExecuting"]; 25 | } 26 | 27 | - (void)finish { 28 | [self willChangeValueForKey:@"isExecuting"]; 29 | _executing = NO; 30 | [self didChangeValueForKey:@"isExecuting"]; 31 | 32 | [self willChangeValueForKey:@"isFinished"]; 33 | _finished = YES; 34 | [self didChangeValueForKey:@"isFinished"]; 35 | } 36 | 37 | - (BOOL)isFinished { 38 | return _finished; 39 | } 40 | 41 | - (BOOL)isExecuting { 42 | return _executing; 43 | } 44 | 45 | - (BOOL)isAsynchronous { 46 | return YES; 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Errors/NSError+Constructor.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSError+Constructor.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/30/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSError (Constructor) 12 | 13 | + (nonnull NSError *)appErrorWithDescription:(nonnull NSString *)description; 14 | + (nonnull NSError *)appErrorWithDescription:(nonnull NSString *)description errorCode:(NSInteger)code; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Errors/NSError+Constructor.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSError+Constructor.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/30/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "NSError+Constructor.h" 10 | 11 | @implementation NSError (Constructor) 12 | 13 | + (nonnull NSError *)appErrorWithDescription:(nonnull NSString *)description { 14 | return [NSError appErrorWithDescription:description errorCode:0]; 15 | } 16 | 17 | + (NSError *)appErrorWithDescription:(NSString *)description errorCode:(NSInteger)code { 18 | return [[NSError alloc] initWithDomain:[NSBundle mainBundle].bundleIdentifier 19 | code:code 20 | userInfo:@{NSLocalizedDescriptionKey : NSLocalizedString(description, nil)}]; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/MapCameraOverlookingLocations/MKMapCamera+OverlookingLocations.h: -------------------------------------------------------------------------------- 1 | // 2 | // MKMapCamera+OverlookingLocations.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface MKMapCamera (OverlookingLocations) 13 | 14 | + (MKMapCamera *)cameraOverlookingLocation1:(CLLocation *)location1 location2:(CLLocation *)location2 withPadding:(double)padding; 15 | 16 | @end 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/MapCameraOverlookingLocations/MKMapCamera+OverlookingLocations.m: -------------------------------------------------------------------------------- 1 | // 2 | // MKMapCamera+OverlookingLocations.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "MKMapCamera+OverlookingLocations.h" 10 | 11 | @implementation MKMapCamera (OverlookingLocations) 12 | 13 | // https://stackoverflow.com/a/21034410/2671390 14 | static double const mapApertureInRadians = (30 * M_PI) / 180; 15 | 16 | + (MKMapCamera *)cameraOverlookingLocation1:(CLLocation *)location1 location2:(CLLocation *)location2 withPadding:(double)padding { 17 | CLLocationCoordinate2D coordinate1 = location1.coordinate; 18 | CLLocationCoordinate2D coordinate2 = location2.coordinate; 19 | CLLocationCoordinate2D centerCoordinate = CLLocationCoordinate2DMake((coordinate1.latitude + coordinate2.latitude) / 2, (coordinate1.longitude + coordinate2.longitude) / 2); 20 | 21 | double span = [location1 distanceFromLocation:location2] / 2; 22 | double altitude = span / tan(mapApertureInRadians / 2); 23 | double altitudeAdjustedForPadding = altitude + (altitude * padding); 24 | 25 | return [MKMapCamera cameraLookingAtCenterCoordinate:centerCoordinate fromDistance:altitudeAdjustedForPadding pitch:0 heading:0]; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/NSAttributedStringKerning/NSAttributedString+Kerning.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Kerning.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSAttributedString (Kerning) 12 | 13 | + (NSAttributedString *)kernedStringFromString:(NSString *)address; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/NSAttributedStringKerning/NSAttributedString+Kerning.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Kerning.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "NSAttributedString+Kerning.h" 10 | @import UIKit; 11 | 12 | @implementation NSAttributedString (Kerning) 13 | 14 | + (NSAttributedString *)kernedStringFromString:(NSString *)string { 15 | NSDictionary *kerning = @{NSKernAttributeName : @(0.82)}; 16 | return [[NSAttributedString alloc] initWithString:string attributes:kerning]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/NSDateUtilities/NSDate+Utilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+Utilities.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface NSDate (Utilities) 13 | 14 | @property (nonatomic, readonly, assign) BOOL isYesterday; 15 | @property (nonatomic, readonly, assign) BOOL isToday; 16 | @property (nonatomic, readonly, assign) BOOL isTomorrow; 17 | @property (nonatomic, readonly, assign) BOOL isThisWeek; 18 | @property (nonatomic, readonly, assign) BOOL isThisYear; 19 | @property (nonatomic, readonly, assign) BOOL isInFuture; 20 | @property (nullable, nonatomic, readonly) NSString *relativeDayRepresentation; 21 | 22 | @property (nonatomic, readonly) NSString *abbreviatedTimeintervalFromNow; 23 | 24 | + (NSString *)abbreviatedTimeIntervalForTimeInterval:(NSTimeInterval)timeInterval; 25 | 26 | + (NSString *)timeslotStringFromStartDate:(NSDate *)startDate duration:(NSTimeInterval)duration; 27 | 28 | - (NSString *)stringWithformat:(NSString *)format; 29 | 30 | - (NSString *)weekdayName; 31 | 32 | - (BOOL)isLaterThanDate:(NSDate *)date; 33 | - (BOOL)isEarlierThanDate:(NSDate *)date; 34 | - (BOOL)isBetweenEarlierDate:(NSDate *)earlierDate laterDate:(NSDate *)laterDate; 35 | 36 | @end 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/NSDateUtilities/NSDate+Utilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+Utilities.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "NSDate+Utilities.h" 10 | 11 | @implementation NSDate (Utilities) 12 | 13 | - (BOOL)isYesterday { 14 | return [[NSCalendar currentCalendar] isDateInYesterday:self]; 15 | } 16 | 17 | - (BOOL)isToday { 18 | return [[NSCalendar currentCalendar] isDateInToday:self]; 19 | } 20 | 21 | - (BOOL)isTomorrow { 22 | return [[NSCalendar currentCalendar] isDateInTomorrow:self]; 23 | } 24 | 25 | -(NSString *)relativeDayRepresentation { 26 | if (self.isYesterday) { 27 | return @"Yesterday"; 28 | } else if (self.isToday) { 29 | return @"Today"; 30 | } else if (self.isTomorrow) { 31 | return @"Tomorrow"; 32 | } else { 33 | return nil; 34 | } 35 | } 36 | 37 | - (BOOL)isThisWeek { 38 | return [self isInThisCalendarUnit:NSCalendarUnitYear] && [self isInThisCalendarUnit:NSCalendarUnitWeekOfYear]; 39 | } 40 | 41 | - (BOOL)isThisYear { 42 | return [self isInThisCalendarUnit:NSCalendarUnitYear]; 43 | } 44 | 45 | - (BOOL)isInThisCalendarUnit:(NSCalendarUnit)unit { 46 | NSInteger dateUnit = [[NSCalendar currentCalendar] component:unit fromDate:self]; 47 | NSInteger currentUnit = [[NSCalendar currentCalendar] component:unit fromDate:[NSDate new]]; 48 | return dateUnit == currentUnit; 49 | } 50 | 51 | - (BOOL)isInFuture { 52 | return [self compare:[NSDate new]] == NSOrderedDescending; 53 | } 54 | 55 | - (NSString *)abbreviatedTimeintervalFromNow { 56 | NSTimeInterval difference = [self timeIntervalSinceNow]; 57 | return [NSDate abbreviatedTimeIntervalForTimeInterval:difference]; 58 | } 59 | 60 | + (NSString *)abbreviatedTimeIntervalForTimeInterval:(NSTimeInterval)timeInterval { 61 | NSDateComponentsFormatter *formatter = [NSDateComponentsFormatter new]; 62 | formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleAbbreviated; 63 | 64 | if (timeInterval > 86400) { 65 | formatter.allowedUnits = NSCalendarUnitDay | NSCalendarUnitHour; 66 | } else { 67 | formatter.allowedUnits = NSCalendarUnitHour | NSCalendarUnitMinute; 68 | } 69 | 70 | return [formatter stringFromTimeInterval:timeInterval]; 71 | } 72 | 73 | + (NSString *)timeslotStringFromStartDate:(NSDate *)startDate duration:(NSTimeInterval)duration { 74 | NSDate *endDate = [startDate dateByAddingTimeInterval:duration]; 75 | NSCalendar *calendar = [NSCalendar currentCalendar]; 76 | 77 | BOOL startDateIsInAM = [calendar component:NSCalendarUnitHour fromDate:startDate] < 12; 78 | BOOL endDateIsInAM = [calendar component:NSCalendarUnitHour fromDate:endDate] < 12; 79 | BOOL shouldShowPeriodInStartDate = (startDateIsInAM != endDateIsInAM); // show period when start and end are in different periods 80 | 81 | NSString *timeFormat = @"h:mm"; 82 | NSString *timeFormatWithPeriod = @"h:mma"; 83 | NSString *startTime = [startDate stringWithformat:shouldShowPeriodInStartDate ? timeFormatWithPeriod : timeFormat]; 84 | NSString *endTime = [endDate stringWithformat:timeFormatWithPeriod]; 85 | 86 | return [@[startTime, endTime] componentsJoinedByString:@" - "]; 87 | } 88 | 89 | - (NSString *)stringWithformat:(NSString *)format { 90 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 91 | [formatter setDateFormat:format]; 92 | return [formatter stringFromDate:self]; 93 | } 94 | 95 | - (NSString *)weekdayName { 96 | return [self stringWithformat:@"E"]; 97 | } 98 | 99 | - (BOOL)isLaterThanDate:(NSDate *)date { 100 | return [self compare:date] == NSOrderedDescending; 101 | } 102 | 103 | - (BOOL)isEarlierThanDate:(NSDate *)date { 104 | return [self compare:date] == NSOrderedAscending; 105 | } 106 | 107 | - (BOOL)isBetweenEarlierDate:(NSDate *)earlierDate laterDate:(NSDate *)laterDate { 108 | return [self isLaterThanDate:earlierDate] && [self isEarlierThanDate:laterDate]; 109 | } 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/StackViews/UIStackView+ConvenienceInitializer.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+ConvenienceInitializer.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface UIStackView (ConvenienceInitializer) 13 | 14 | - (nonnull instancetype)initWithArrangedSubviews:(nullable NSArray<__kindof UIView *> *)views 15 | axis:(UILayoutConstraintAxis)axis 16 | distribution:(UIStackViewDistribution)distribution 17 | alignment:(UIStackViewAlignment)alignment 18 | spacing:(CGFloat)spacing 19 | margins:(UIEdgeInsets)margins; 20 | 21 | - (void)removeAllArrangedSubviews; 22 | 23 | @end 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/StackViews/UIStackView+ConvenienceInitializer.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+ConvenienceInitializer.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UIStackView+ConvenienceInitializer.h" 10 | 11 | @implementation UIStackView (ConvenienceInitializer) 12 | 13 | - (instancetype)initWithArrangedSubviews:(NSArray<__kindof UIView *> *)views 14 | axis:(UILayoutConstraintAxis)axis 15 | distribution:(UIStackViewDistribution)distribution 16 | alignment:(UIStackViewAlignment)alignment 17 | spacing:(CGFloat)spacing 18 | margins:(UIEdgeInsets)margins { 19 | self = [self initWithArrangedSubviews:views ? views : [NSArray new]]; 20 | if (!self) { 21 | return nil; 22 | } 23 | 24 | self.axis = axis; 25 | self.distribution = distribution; 26 | self.alignment = alignment; 27 | self.spacing = spacing; 28 | self.layoutMargins = margins; 29 | self.layoutMarginsRelativeArrangement = true; 30 | 31 | return self; 32 | } 33 | 34 | - (void)removeAllArrangedSubviews { 35 | for (UIView *view in self.arrangedSubviews) { 36 | [self removeArrangedSubview:view]; 37 | [view removeFromSuperview]; 38 | } 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/StatusBarBackgroung/UIViewController+StatusBarBackground.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+StatusBarBackground.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIViewController (StatusBarBackground) 12 | 13 | - (void)addStatusBarBlurBackground; 14 | - (CGFloat)statusBarHeight; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/StatusBarBackgroung/UIViewController+StatusBarBackground.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+StatusBarBackground.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+StatusBarBackground.h" 10 | 11 | @implementation UIViewController (StatusBarBackground) 12 | 13 | - (void)addStatusBarBlurBackground { 14 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; 15 | UIVisualEffectView *statusBarBackground = [[UIVisualEffectView alloc] initWithEffect:effect]; 16 | statusBarBackground.translatesAutoresizingMaskIntoConstraints = false; 17 | [self.view addSubview:statusBarBackground]; 18 | [statusBarBackground.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = true; 19 | [statusBarBackground.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = true; 20 | [statusBarBackground.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = true; 21 | [statusBarBackground.heightAnchor constraintEqualToConstant:20].active = true; 22 | } 23 | 24 | 25 | - (CGFloat)statusBarHeight { 26 | return [UIApplication sharedApplication].statusBarFrame.size.height; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Styles/UIColor+SFiOSColors.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+SFiOSColors.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIColor (SFiOSColors) 12 | 13 | /// #58595E 14 | + (UIColor *)abbey; 15 | 16 | ///#FEFEFE 17 | + (UIColor *)alabaster; 18 | 19 | ///#8CD838 20 | + (UIColor *)atlantis; 21 | 22 | ///#F6AE36 23 | + (UIColor *)saffron; 24 | 25 | ///#E75D5A 26 | + (UIColor *)mandy; 27 | 28 | ///#B7B7B7 29 | + (UIColor *)nobel; 30 | 31 | ///#A6A6A6 32 | +(UIColor *)boulder; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Helpers/Styles/UIColor+SFiOSColors.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+SFiOSColors.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UIColor+SFiOSColors.h" 10 | 11 | @implementation UIColor (SFiOSColors) 12 | 13 | + (UIColor *)abbey { 14 | return [UIColor colorWithRed:0.35 green:0.35 blue:0.37 alpha:1.0]; 15 | } 16 | 17 | + (UIColor *)alabaster { 18 | return [UIColor colorWithRed:0.98 green:0.98 blue:0.98 alpha:1.0]; 19 | } 20 | 21 | + (UIColor *)atlantis { 22 | return [UIColor colorWithRed:0.55 green:0.85 blue:0.22 alpha:1.0]; 23 | } 24 | 25 | + (UIColor *)saffron { 26 | return [UIColor colorWithRed:0.96 green:0.68 blue:0.21 alpha:1.0]; 27 | } 28 | 29 | + (UIColor *)mandy { 30 | return [UIColor colorWithRed:0.91 green:0.36 blue:0.35 alpha:1.0]; 31 | } 32 | 33 | + (UIColor *)nobel { 34 | return [UIColor colorWithRed:0.72 green:0.72 blue:0.72 alpha:1.0]; 35 | } 36 | 37 | + (UIColor *)boulder { 38 | return [UIColor colorWithRed:0.49 green:0.49 blue:0.49 alpha:1.0];; 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/ImageStore/ImageStore.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImageStore.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/14/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | @import UIKit; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface ImageStore : NSObject 15 | 16 | - (void)storeImage:(UIImage *)image forKey:(id)key; 17 | - (nullable UIImage *)imageForKey:(id)key; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/ImageStore/ImageStore.m: -------------------------------------------------------------------------------- 1 | // 2 | // ImageStore.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/14/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "ImageStore.h" 10 | #import "UIImage+URL.h" 11 | #import "UIApplication+Metadata.h" 12 | 13 | @interface ImageStore () 14 | 15 | @property (nonatomic) NSCache *cache; 16 | 17 | @end 18 | 19 | @implementation ImageStore 20 | 21 | - (instancetype)init { 22 | if (self = [super init]) { 23 | self.cache = [[NSCache alloc] init]; 24 | self.cache.countLimit = 20; 25 | self.cache.name = [NSString stringWithFormat:@"%@.image-cache", [UIApplication sharedApplication].bundleIdentifier]; 26 | } 27 | 28 | return self; 29 | } 30 | 31 | - (void)storeImage:(UIImage *)image forKey:(id)key { 32 | [self.cache setObject:image forKey:key]; 33 | } 34 | 35 | - (UIImage *)imageForKey:(id)key { 36 | return [self.cache objectForKey:key]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | MKDirectionsRequest 12 | LSItemContentTypes 13 | 14 | com.apple.maps.directionsrequest 15 | 16 | 17 | 18 | CFBundleExecutable 19 | $(EXECUTABLE_NAME) 20 | CFBundleIdentifier 21 | $(PRODUCT_BUNDLE_IDENTIFIER) 22 | CFBundleInfoDictionaryVersion 23 | 6.0 24 | CFBundleName 25 | $(PRODUCT_NAME) 26 | CFBundlePackageType 27 | APPL 28 | CFBundleShortVersionString 29 | 1.0.2 30 | CFBundleVersion 31 | 12 32 | ITSAppUsesNonExemptEncryption 33 | 34 | LSApplicationQueriesSchemes 35 | 36 | uber 37 | lyft 38 | 39 | LSRequiresIPhoneOS 40 | 41 | MKDirectionsApplicationSupportedModes 42 | 43 | MKDirectionsModeBike 44 | MKDirectionsModeBus 45 | MKDirectionsModeCar 46 | MKDirectionsModeFerry 47 | MKDirectionsModeOther 48 | MKDirectionsModePedestrian 49 | MKDirectionsModePlane 50 | MKDirectionsModeRideShare 51 | MKDirectionsModeStreetCar 52 | MKDirectionsModeSubway 53 | MKDirectionsModeTaxi 54 | MKDirectionsModeTrain 55 | 56 | NSLocationWhenInUseUsageDescription 57 | Your location is used to get directions to events. 58 | UILaunchStoryboardName 59 | LaunchScreen 60 | UIRequiredDeviceCapabilities 61 | 62 | armv7 63 | 64 | UISupportedInterfaceOrientations 65 | 66 | UIInterfaceOrientationPortrait 67 | 68 | UISupportedInterfaceOrientations~ipad 69 | 70 | UIInterfaceOrientationPortrait 71 | UIInterfaceOrientationPortraitUpsideDown 72 | UIInterfaceOrientationLandscapeLeft 73 | UIInterfaceOrientationLandscapeRight 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/DirectionsRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // DirectionsRequest.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TransportType.h" 11 | @import CoreLocation; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface DirectionsRequest : NSObject 15 | 16 | + (void)requestDirectionsToLocation:(CLLocation *)destination withName:(nullable NSString *)name usingTransportType:(TransportType)transportType; 17 | 18 | @end 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/DirectionsRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // DirectionsRequest.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "DirectionsRequest.h" 10 | #import "SecretsStore.h" 11 | @import MapKit; 12 | 13 | @implementation DirectionsRequest 14 | 15 | + (void)requestDirectionsToLocation:(CLLocation *)destination withName:(NSString *)name usingTransportType:(TransportType)transportType { 16 | switch (transportType) { 17 | case TransportTypeTransit: 18 | [self requestDirectionsToLocation:destination withName:name usingMode:MKLaunchOptionsDirectionsModeTransit]; 19 | break; 20 | 21 | case TransportTypeAutomobile: 22 | [self requestDirectionsToLocation:destination withName:name usingMode:MKLaunchOptionsDirectionsModeDriving]; 23 | break; 24 | 25 | case TransportTypeWalking: 26 | [self requestDirectionsToLocation:destination withName:name usingMode:MKLaunchOptionsDirectionsModeWalking]; 27 | break; 28 | 29 | case TransportTypeUber: 30 | [self requestUberRideToLocation:destination withName:name]; 31 | break; 32 | 33 | case TransportTypeLyft: 34 | [self requestLyftRideToLocation:destination]; 35 | break; 36 | 37 | default: 38 | break; 39 | } 40 | } 41 | 42 | + (void)requestDirectionsToLocation:(CLLocation *)destination withName:(NSString *)name usingMode:(NSString *)mode { 43 | MKPlacemark *placemark = [[MKPlacemark alloc] initWithCoordinate:destination.coordinate]; 44 | MKMapItem *mapItem = [[MKMapItem alloc] initWithPlacemark:placemark]; 45 | mapItem.name = name; 46 | [mapItem openInMapsWithLaunchOptions:@{MKLaunchOptionsDirectionsModeKey : mode}]; 47 | } 48 | 49 | + (void)requestUberRideToLocation:(CLLocation *)destination withName:(NSString *)name { 50 | SecretsStore *store = [[SecretsStore alloc] init]; 51 | NSString *clientID = store.uberClientID; 52 | NSString *escapedName = [name stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; 53 | 54 | // https://stackoverflow.com/questions/26049950/uber-deeplinking-on-ios 55 | NSString *query = [NSString stringWithFormat:@"?client_id=%@&action=setPickup&pickup=my_location&dropoff[latitude]=%f&dropoff[longitude]=%f&dropoff[nickname]=%@", clientID, destination.coordinate.latitude, destination.coordinate.longitude, escapedName]; 56 | 57 | [self requestRideWithURLScheme:@"uber://" httpHost:@"https://m.uber.com/ul/" queryFragment:query]; 58 | } 59 | 60 | + (void)requestLyftRideToLocation:(CLLocation *)destination { 61 | SecretsStore *store = [[SecretsStore alloc] init]; 62 | NSString *clientID = store.lyftClientID; 63 | 64 | NSString *query = [NSString stringWithFormat:@"?id=lyft&partner=%@&destination[latitude]=%f&destination[longitude]=%f", clientID, destination.coordinate.latitude, destination.coordinate.longitude]; 65 | 66 | [self requestRideWithURLScheme:@"lyft://ridetype" httpHost:@"https://www.lyft.com/ride" queryFragment:query]; 67 | } 68 | 69 | + (void)requestRideWithURLScheme:(NSString *)urlScheme httpHost:(NSString *)httpHost queryFragment:(NSString *)query { 70 | NSString *path; 71 | if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:urlScheme]]) { 72 | path = [NSString stringWithFormat:@"%@%@", urlScheme, query]; 73 | } else { 74 | path = [NSString stringWithFormat:@"%@%@", httpHost, query]; 75 | } 76 | 77 | NSURL *url = [NSURL URLWithString:path]; 78 | NSAssert(url != nil, @"Failed to construct URL from path: %@", path); 79 | 80 | [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; 81 | } 82 | 83 | @end 84 | 85 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/MapView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MapView.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | @import CoreLocation; 11 | @import MapKit; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface MapView : UIView 15 | 16 | typedef void(^UserLocationObserverBlock)(CLLocation *_Nullable userLocation); 17 | @property (nullable, copy, nonatomic) UserLocationObserverBlock userLocationObserver; 18 | 19 | - (void)setDestinationToLocation:(CLLocation *)destination withAnnotationImage:(UIImage *)annotationImage; 20 | 21 | @end 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/MapView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MapView.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "MapView.h" 10 | #import "MKMapCamera+OverlookingLocations.h" 11 | 12 | static NSString * const destAnnotationIdentifier = @"destinationAnnotationidentifier"; 13 | 14 | @interface MapView () 15 | 16 | @property (nonatomic) MKMapView *mapView; 17 | @property (nullable, nonatomic) MKPointAnnotation *destinationAnnotation; 18 | @property (nullable, nonatomic) UIImage *annotationImage; 19 | @property (nullable, nonatomic) CLLocation *destination; 20 | @property (nullable, nonatomic) CLLocation *userLocation; 21 | @property (nonatomic, assign) BOOL cameraHasBeenSet; 22 | 23 | @end 24 | 25 | @implementation MapView 26 | 27 | - (instancetype)init { 28 | if (self = [super initWithFrame:CGRectZero]) { 29 | [self setup]; 30 | } 31 | return self; 32 | } 33 | 34 | - (void)setDestinationToLocation:(CLLocation *)destination withAnnotationImage:(UIImage *)annotationImage { 35 | self.destination = destination; 36 | self.annotationImage = annotationImage; 37 | 38 | [self updateDestinationAnnotation]; 39 | } 40 | 41 | - (void)setup { 42 | self.mapView = [MKMapView new]; 43 | self.mapView.delegate = self; 44 | if (@available(iOS 11.0, *)) { 45 | self.mapView.mapType = MKMapTypeMutedStandard; 46 | } else { 47 | self.mapView.mapType = MKMapTypeStandard; 48 | } 49 | self.mapView.showsTraffic = true; 50 | self.mapView.showsUserLocation = true; 51 | if (@available(iOS 11.0, *)) { 52 | [self.mapView registerClass:[MKAnnotationView class] forAnnotationViewWithReuseIdentifier:destAnnotationIdentifier]; 53 | } 54 | 55 | self.mapView.translatesAutoresizingMaskIntoConstraints = false; 56 | [self addSubview:self.mapView]; 57 | [self.mapView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = true; 58 | [self.mapView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = true; 59 | [self.mapView.topAnchor constraintEqualToAnchor:self.topAnchor].active = true; 60 | [self.mapView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = true; 61 | 62 | [self setCameraOnSanFrancisco]; 63 | } 64 | 65 | //MARK: - Annotations 66 | 67 | - (void)updateDestinationAnnotation { 68 | [self.mapView removeAnnotation:self.destinationAnnotation]; 69 | self.destinationAnnotation = [MKPointAnnotation new]; 70 | self.destinationAnnotation.coordinate = self.destination.coordinate; 71 | [self.mapView addAnnotation:self.destinationAnnotation]; 72 | 73 | [self setCameraOverlookingDestinationAndUserLocation]; 74 | } 75 | 76 | - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation { 77 | if ([annotation isKindOfClass:[MKUserLocation class]]) { 78 | return nil; 79 | } 80 | 81 | MKAnnotationView *dest; 82 | if (@available(iOS 11.0, *)) { 83 | dest = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:destAnnotationIdentifier forAnnotation:annotation]; 84 | 85 | } else { 86 | dest = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:destAnnotationIdentifier]; 87 | } 88 | 89 | if (!dest) { 90 | dest = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:destAnnotationIdentifier]; 91 | dest.canShowCallout = false; 92 | } 93 | 94 | dest.annotation = annotation; 95 | dest.image = self.annotationImage; 96 | return dest; 97 | } 98 | 99 | //MARK: - UserLocation 100 | 101 | - (void)setUserLocation:(CLLocation *)userLocation { 102 | if ([self location:userLocation isSameAsLocation:_userLocation]) { 103 | return; 104 | } 105 | 106 | _userLocation = userLocation; 107 | if (self.userLocationObserver) { 108 | self.userLocationObserver(userLocation); 109 | } 110 | 111 | if (self.cameraHasBeenSet) { return; } 112 | if (!userLocation) { 113 | [self setCameraOnDestination]; 114 | } else { 115 | [self setCameraOverlookingDestinationAndUserLocation]; 116 | } 117 | self.cameraHasBeenSet = true; 118 | } 119 | 120 | - (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation { 121 | self.userLocation = userLocation.location; 122 | } 123 | 124 | - (void)mapView:(MKMapView *)mapView didFailToLocateUserWithError:(NSError *)error { 125 | NSLog(@"Failed to locate user in MapView: %@", error); 126 | self.userLocation = nil; 127 | } 128 | 129 | //MARK: - Camera 130 | 131 | - (void)setCameraOverlookingDestinationAndUserLocation { 132 | if (!self.userLocation) { 133 | [self setCameraOnDestination]; 134 | return; 135 | } else if (!self.destination) { 136 | return; 137 | } 138 | 139 | MKMapCamera *camera = [MKMapCamera cameraOverlookingLocation1:self.userLocation location2:self.destination withPadding:0.8]; 140 | [self.mapView setCamera:camera animated:true]; 141 | } 142 | 143 | - (void)setCameraOnDestination { 144 | if (!self.destination) { 145 | return; 146 | } 147 | 148 | MKMapCamera *camera = [MKMapCamera cameraLookingAtCenterCoordinate:self.destination.coordinate fromDistance:6000 pitch:0 heading:0]; 149 | [self.mapView setCamera:camera animated:true]; 150 | } 151 | 152 | - (void)setCameraOnSanFrancisco { 153 | CLLocationCoordinate2D sanFrancisco = CLLocationCoordinate2DMake(37.749576, -122.442606); 154 | MKMapCamera *camera = [MKMapCamera cameraLookingAtCenterCoordinate:sanFrancisco fromDistance:10000 pitch:0 heading:0]; 155 | [self.mapView setCamera:camera animated:false]; 156 | } 157 | 158 | //MARK: - Location Comparison 159 | 160 | - (BOOL)location:(CLLocation *)lhs isSameAsLocation:(CLLocation *)rhs { 161 | return lhs.coordinate.latitude == rhs.coordinate.latitude && lhs.coordinate.longitude == rhs.coordinate.longitude; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TransportType.h: -------------------------------------------------------------------------------- 1 | // 2 | // TransportType.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #ifndef TransportType_h 10 | #define TransportType_h 11 | 12 | typedef NS_ENUM(NSUInteger, TransportType) { 13 | TransportTypeTransit, 14 | TransportTypeWalking, 15 | TransportTypeAutomobile, 16 | TransportTypeUber, 17 | TransportTypeLyft 18 | }; 19 | 20 | #endif /* TransportType_h */ 21 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/LyftTravelTimeEstimateOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // LyftTravelTimeEstimateOperation.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "HTTPRequestAsyncOperation.h" 10 | #import "TravelTimeCalculationCompletion.h" 11 | @import CoreLocation; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface LyftTravelTimeEstimateOperation : HTTPRequestAsyncOperation 15 | 16 | - (instancetype)initWithSourceLocation:(CLLocation *)sourceLocation destinationLocation:(CLLocation *)destinationLocation completionHandler:(TravelTimeCalculationCompletion)completionHandler; 17 | 18 | @end 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/LyftTravelTimeEstimateOperation.m: -------------------------------------------------------------------------------- 1 | // 2 | // LyftTravelTimeEstimateOperation.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "LyftTravelTimeEstimateOperation.h" 10 | #import "TravelTime.h" 11 | #import "SecretsStore.h" 12 | 13 | @implementation LyftTravelTimeEstimateOperation 14 | 15 | - (id)initWithSourceLocation:(CLLocation *)sourceLocation destinationLocation:(CLLocation *)destinationLocation completionHandler:(TravelTimeCalculationCompletion)completionHandler { 16 | CLLocationCoordinate2D start = sourceLocation.coordinate; 17 | CLLocationCoordinate2D end = destinationLocation.coordinate; 18 | NSString *path = [NSString stringWithFormat:@"https://api.lyft.com/v1/cost?start_lat=%f&start_lng=%f&end_lat=%f&end_lng=%f", start.latitude, start.longitude, end.latitude, end.longitude]; 19 | 20 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path]]; 21 | request.HTTPMethod = @"GET"; 22 | 23 | NSString *token = [NSString stringWithFormat:@"Bearer %@", [[SecretsStore alloc] init].lyftServerToken]; 24 | [request addValue:token forHTTPHeaderField:@"Authorization"]; 25 | [request addValue:@"en_US" forHTTPHeaderField:@"Accept-Language"]; 26 | [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 27 | 28 | __weak typeof(self) welf = self; 29 | return [super initWithRequest:request completionHandler:^(JSON * _Nullable json, NSURLResponse * _Nullable response, NSError * _Nullable error) { 30 | if (error || !json) { 31 | completionHandler(nil, error); 32 | } else { 33 | TravelTime *travelTime = [welf shortestTravelTimeFromResponseJSON:json]; 34 | completionHandler(travelTime, nil); 35 | } 36 | }]; 37 | } 38 | 39 | - (nullable TravelTime *)shortestTravelTimeFromResponseJSON:(JSON *)json { 40 | NSMutableArray *estimates = [NSMutableArray arrayWithArray:json[@"cost_estimates"]]; 41 | [estimates sortUsingComparator:^NSComparisonResult(JSON *_Nonnull obj1, JSON *_Nonnull obj2) { 42 | NSNumber *duration1 = (NSNumber *)obj1[@"estimated_duration_seconds"]; 43 | NSNumber *duration2 = (NSNumber *)obj2[@"estimated_duration_seconds"]; 44 | return [duration1 compare:duration2]; 45 | }]; 46 | 47 | NSNumber *estimate = estimates.firstObject[@"estimated_duration_seconds"]; 48 | return estimate ? [[TravelTime alloc] initWithTransportType:TransportTypeLyft travelTime:estimate.doubleValue] : nil; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTime+Arrival.h: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTime+Arrival.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTime.h" 10 | 11 | typedef NS_ENUM(NSUInteger, Arrival) { 12 | ArrivalOnTime, 13 | ArrivalDuringEvent, 14 | ArrivalAfterEvent, 15 | }; 16 | 17 | @interface TravelTime (Arrival) 18 | 19 | - (Arrival)arrivalToEventWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTime+Arrival.m: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTime+Arrival.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTime+Arrival.h" 10 | #import "NSDate+Utilities.h" 11 | 12 | @implementation TravelTime (Arrival) 13 | 14 | - (Arrival)arrivalToEventWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate { 15 | // if the event is in the past, magnitudes do not matter 16 | if ([endDate isEarlierThanDate:[NSDate new]]) { 17 | return ArrivalOnTime; 18 | } 19 | 20 | NSDate *arrivalDate = [NSDate dateWithTimeIntervalSinceNow:self.travelTime]; 21 | 22 | // arrive on time 23 | if ([arrivalDate isEarlierThanDate:startDate]) { 24 | return ArrivalOnTime; 25 | } 26 | // arrive before event end 27 | else if ([arrivalDate isEarlierThanDate:endDate]) { 28 | return ArrivalDuringEvent; 29 | } else { 30 | return ArrivalAfterEvent; 31 | } 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTime.h: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTime.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TransportType.h" 11 | @import MapKit; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | @interface TravelTime : NSObject 15 | 16 | @property (nonatomic, assign) TransportType transportType; 17 | @property (nonatomic, assign) NSTimeInterval travelTime; 18 | @property (nonatomic, readonly) NSString *travelTimeEstimateString; 19 | @property (nonatomic) UIImage *icon; 20 | 21 | - (instancetype)initWithTransportType:(TransportType)transportType travelTime:(NSTimeInterval)travelTime; 22 | - (instancetype)initWithMKDirectionsTransportType:(MKDirectionsTransportType)mkTransportType travelTime:(NSTimeInterval)travelTime; 23 | 24 | @end 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTime.m: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTime.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTime.h" 10 | #import "NSDate+Utilities.h" 11 | 12 | @implementation TravelTime 13 | 14 | - (instancetype)initWithTransportType:(TransportType)transportType travelTime:(NSTimeInterval)travelTime { 15 | if (self = [super init]) { 16 | self.transportType = transportType; 17 | self.travelTime = travelTime; 18 | 19 | switch (self.transportType) { 20 | case TransportTypeTransit: 21 | self.icon = [UIImage imageNamed:@"icon-transport-type-transit"]; 22 | break; 23 | 24 | case TransportTypeWalking: 25 | self.icon = [UIImage imageNamed:@"icon-transport-type-walking"]; 26 | break; 27 | 28 | case TransportTypeAutomobile: 29 | self.icon = [UIImage imageNamed:@"icon-transport-type-automobile"]; 30 | break; 31 | 32 | case TransportTypeUber: 33 | self.icon = [UIImage imageNamed:@"icon-transport-type-uber"]; 34 | break; 35 | 36 | case TransportTypeLyft: 37 | self.icon = [UIImage imageNamed:@"icon-transport-type-lyft"]; 38 | break; 39 | 40 | default: 41 | break; 42 | } 43 | } 44 | 45 | return self; 46 | } 47 | 48 | - (instancetype)initWithMKDirectionsTransportType:(MKDirectionsTransportType)mkTransportType travelTime:(NSTimeInterval)travelTime { 49 | TransportType transportType; 50 | switch (mkTransportType) { 51 | case MKDirectionsTransportTypeTransit: 52 | transportType = TransportTypeTransit; 53 | break; 54 | 55 | case MKDirectionsTransportTypeWalking: 56 | transportType = TransportTypeWalking; 57 | break; 58 | 59 | case MKDirectionsTransportTypeAutomobile: 60 | transportType = TransportTypeAutomobile; 61 | break; 62 | 63 | default: 64 | NSAssert(false, @"Unsupported transport type: %lu", mkTransportType); 65 | transportType = INT_MAX; 66 | break; 67 | } 68 | 69 | return [self initWithTransportType:transportType travelTime:travelTime]; 70 | } 71 | 72 | - (NSString *)travelTimeEstimateString { 73 | return [NSDate abbreviatedTimeIntervalForTimeInterval:self.travelTime]; 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTimeCalculationCompletion.h: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimeCalculationCompletion.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTime.h" 10 | 11 | typedef void(^TravelTimeCalculationCompletion)(TravelTime *_Nullable travelTime, NSError *_Nullable error); 12 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTimeService.h: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimeService.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TravelTime.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | @interface TravelTimeService : NSObject 14 | 15 | typedef void(^TravelTimeCompletionHandler)(NSArray *travelTimes); 16 | - (void)calculateTravelTimesFromLocation:(CLLocation *)sourceLocation toLocation:(CLLocation *)destinationLocation withCompletionHandler:(TravelTimeCompletionHandler)completionHandler; 17 | 18 | @end 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/TravelTimeService.m: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTimeService.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/3/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "TravelTimeService.h" 10 | #import "AsyncBlockOperation.h" 11 | #import "UberTravelTimeEstimateOperation.h" 12 | #import "LyftTravelTimeEstimateOperation.h" 13 | @import MapKit; 14 | 15 | @interface TravelTimeService () 16 | 17 | @property (nonatomic) NSOperationQueue *travelTimeCalculationQueue; 18 | 19 | @end 20 | 21 | @implementation TravelTimeService 22 | 23 | - (instancetype)init { 24 | if (self = [super init]) { 25 | self.travelTimeCalculationQueue = [NSOperationQueue new]; 26 | } 27 | return self; 28 | } 29 | 30 | - (void)calculateTravelTimesFromLocation:(CLLocation *)sourceLocation toLocation:(CLLocation *)destinationLocation withCompletionHandler:(TravelTimeCompletionHandler)completionHandler { 31 | [self.travelTimeCalculationQueue cancelAllOperations]; 32 | 33 | __block NSMutableArray *travelTimes = [NSMutableArray new]; 34 | 35 | MKDirectionsRequest *transitRequest = [self requestFromLocation:sourceLocation toLocation:destinationLocation usingTransporationType:MKDirectionsTransportTypeTransit]; 36 | MKDirectionsRequest *walkingRequest = [self requestFromLocation:sourceLocation toLocation:destinationLocation usingTransporationType:MKDirectionsTransportTypeWalking]; 37 | MKDirectionsRequest *drivingRequest = [self requestFromLocation:sourceLocation toLocation:destinationLocation usingTransporationType:MKDirectionsTransportTypeAutomobile]; 38 | NSArray *requests = @[transitRequest, walkingRequest, drivingRequest]; 39 | 40 | NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{ 41 | dispatch_async(dispatch_get_main_queue(), ^{ 42 | [travelTimes sortUsingComparator:^NSComparisonResult(TravelTime *_Nonnull obj1, TravelTime *_Nonnull obj2) { 43 | return obj1.travelTime > obj2.travelTime; 44 | }]; 45 | completionHandler(travelTimes); 46 | }); 47 | }]; 48 | 49 | for (MKDirectionsRequest *request in requests) { 50 | AsyncOperation *travelTimeCalculation = [self travelTimeCalculationWithRequest:request completionHandler:^(TravelTime * _Nullable travelTime) { 51 | if (travelTime) { 52 | [travelTimes addObject:travelTime]; 53 | } 54 | }]; 55 | [completionOperation addDependency:travelTimeCalculation]; 56 | [self.travelTimeCalculationQueue addOperation:travelTimeCalculation]; 57 | } 58 | 59 | UberTravelTimeEstimateOperation *uberTimeCalculation = [[UberTravelTimeEstimateOperation alloc] initWithSourceLocation:sourceLocation destinationLocation:destinationLocation completionHandler:^(TravelTime * _Nullable travelTime, NSError * _Nullable error) { 60 | if (!travelTime) { 61 | NSLog(@"Could not retrieve uber travel time: %@", error); 62 | return; 63 | } 64 | [travelTimes addObject:travelTime]; 65 | }]; 66 | [completionOperation addDependency:uberTimeCalculation]; 67 | [self.travelTimeCalculationQueue addOperation:uberTimeCalculation]; 68 | 69 | LyftTravelTimeEstimateOperation *lyftTimeCalculation = [[LyftTravelTimeEstimateOperation alloc] initWithSourceLocation:sourceLocation destinationLocation:destinationLocation completionHandler:^(TravelTime * _Nullable travelTime, NSError * _Nullable error) { 70 | if (!travelTime) { 71 | NSLog(@"Could not retrieve lyft travel time: %@", error); 72 | return; 73 | } 74 | [travelTimes addObject:travelTime]; 75 | }]; 76 | [completionOperation addDependency:lyftTimeCalculation]; 77 | [self.travelTimeCalculationQueue addOperation:lyftTimeCalculation]; 78 | 79 | [self.travelTimeCalculationQueue addOperation:completionOperation]; 80 | } 81 | 82 | - (MKDirectionsRequest *)requestFromLocation:(CLLocation *)sourceLocation toLocation:(CLLocation *)destinationLocation usingTransporationType:(MKDirectionsTransportType)transportationType { 83 | MKDirectionsRequest *request = [MKDirectionsRequest new]; 84 | request.source = [[MKMapItem alloc] initWithPlacemark: [[MKPlacemark alloc] initWithCoordinate:sourceLocation.coordinate]]; 85 | request.destination = [[MKMapItem alloc] initWithPlacemark: [[MKPlacemark alloc] initWithCoordinate:destinationLocation.coordinate]]; 86 | request.requestsAlternateRoutes = false; 87 | request.transportType = transportationType; 88 | return request; 89 | } 90 | 91 | - (AsyncBlockOperation *)travelTimeCalculationWithRequest:(MKDirectionsRequest *)request completionHandler:(void(^)(TravelTime * _Nullable travelTime))resultHandler { 92 | return [[AsyncBlockOperation alloc] initWithAsyncBlock:^(dispatch_block_t _Nonnull completionHandler) { 93 | MKDirections *direction = [[MKDirections alloc] initWithRequest:request]; 94 | [direction calculateETAWithCompletionHandler:^(MKETAResponse * _Nullable response, NSError * _Nullable error) { 95 | if (!response) { 96 | NSLog(@"Could not get travel time for request:\n%@\n%@", request, error); 97 | resultHandler(nil); 98 | completionHandler(); 99 | } 100 | TravelTime *result = [[TravelTime alloc] initWithMKDirectionsTransportType:request.transportType travelTime:response.expectedTravelTime]; 101 | resultHandler(result); 102 | completionHandler(); 103 | }]; 104 | }]; 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/UberTravelTimeEstimateOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // UberTravelTimeEstimateOperation.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "HTTPRequestAsyncOperation.h" 10 | #import "TravelTime.h" 11 | #import "TravelTimeCalculationCompletion.h" 12 | @import CoreLocation; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | @interface UberTravelTimeEstimateOperation : HTTPRequestAsyncOperation 16 | 17 | - (instancetype)initWithSourceLocation:(CLLocation *)sourceLocation destinationLocation:(CLLocation *)destinationLocation completionHandler:(TravelTimeCalculationCompletion)completionHandler; 18 | 19 | @end 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/TravelTime/UberTravelTimeEstimateOperation.m: -------------------------------------------------------------------------------- 1 | // 2 | // UberTravelTimeEstimateOperation.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UberTravelTimeEstimateOperation.h" 10 | #import "HTTPRequestAsyncOperation.h" 11 | #import "SecretsStore.h" 12 | 13 | @implementation UberTravelTimeEstimateOperation 14 | 15 | - (instancetype)initWithSourceLocation:(CLLocation *)sourceLocation destinationLocation:(CLLocation *)destinationLocation completionHandler:(TravelTimeCalculationCompletion)completionHandler { 16 | CLLocationCoordinate2D start = sourceLocation.coordinate; 17 | CLLocationCoordinate2D end = destinationLocation.coordinate; 18 | NSString *path = [NSString stringWithFormat:@"https://api.uber.com/v1.2/estimates/price?start_latitude=%f&start_longitude=%f&end_latitude=%f&end_longitude=%f", start.latitude, start.longitude, end.latitude, end.longitude]; 19 | 20 | NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path]]; 21 | request.HTTPMethod = @"GET"; 22 | 23 | NSString *token = [NSString stringWithFormat:@"Token %@", [[SecretsStore alloc] init].uberServerToken]; 24 | [request addValue:token forHTTPHeaderField:@"Authorization"]; 25 | [request addValue:@"en_US" forHTTPHeaderField:@"Accept-Language"]; 26 | [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 27 | 28 | __weak typeof(self) welf = self; 29 | return [super initWithRequest:request completionHandler:^(NSDictionary * _Nullable json, NSURLResponse * _Nullable response, NSError * _Nullable error) { 30 | if (error || !json) { 31 | completionHandler(nil, error); 32 | } else { 33 | TravelTime *travelTime = [welf shortestTravelTimeFromResponseJSON:json]; 34 | completionHandler(travelTime, nil); 35 | } 36 | }]; 37 | } 38 | 39 | - (nullable TravelTime *)shortestTravelTimeFromResponseJSON:(nonnull JSON *)json { 40 | NSMutableArray *estimates = [NSMutableArray arrayWithArray:json[@"prices"]]; 41 | [estimates sortUsingComparator:^NSComparisonResult(JSON *_Nonnull obj1, JSON *_Nonnull obj2) { 42 | NSNumber *duration1 = (NSNumber *)obj1[@"duration"]; 43 | NSNumber *duration2 = (NSNumber *)obj2[@"duration"]; 44 | return [duration1 compare:duration2]; 45 | }]; 46 | 47 | NSNumber *estimate = [(JSON *)estimates.firstObject objectForKey:@"duration"]; 48 | return estimate ? [[TravelTime alloc] initWithTransportType:TransportTypeUber travelTime:estimate.doubleValue] : nil; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/UserLocation.h: -------------------------------------------------------------------------------- 1 | // 2 | // UserLocation.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/1/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | @import CoreLocation; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | @interface UserLocation : NSObject 14 | 15 | - (BOOL)canRequestUserLocation; 16 | - (BOOL)canRequestLocationPermission; 17 | - (void)requestLocationPermission; 18 | 19 | typedef void(^UserLocationRequestCompletionHandler)(CLLocation *_Nullable currentLocation, NSError *_Nullable error); 20 | - (void)requestWithCompletionHandler:(UserLocationRequestCompletionHandler)completionHandler; 21 | 22 | @end 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Location/UserLocation.m: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentLocation.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/1/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UserLocation.h" 10 | #import "NSError+Constructor.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | @interface UserLocation () 14 | 15 | @property (nonatomic) CLLocationManager *locationManager; 16 | 17 | // Support multiple requests backed by a single location manager 18 | @property (nullable, nonatomic) NSMutableArray *requestCompletionHandlers; 19 | 20 | @end 21 | NS_ASSUME_NONNULL_END 22 | 23 | @implementation UserLocation 24 | 25 | - (instancetype)init { 26 | if (self = [super init]) { 27 | self.requestCompletionHandlers = [NSMutableArray new]; 28 | self.locationManager = [CLLocationManager new]; 29 | self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters; 30 | self.locationManager.delegate = self; 31 | } 32 | return self; 33 | } 34 | 35 | - (BOOL)canRequestUserLocation { 36 | BOOL locationServicesEnabled = [CLLocationManager locationServicesEnabled]; 37 | 38 | CLAuthorizationStatus permission = [CLLocationManager authorizationStatus]; 39 | BOOL locationCanBeAccessed = permission == kCLAuthorizationStatusAuthorizedWhenInUse || permission == kCLAuthorizationStatusAuthorizedAlways; 40 | 41 | return locationServicesEnabled && locationCanBeAccessed; 42 | } 43 | 44 | - (BOOL)canRequestLocationPermission { 45 | BOOL locationServicesEnabled = [CLLocationManager locationServicesEnabled]; 46 | 47 | CLAuthorizationStatus permission = [CLLocationManager authorizationStatus]; 48 | BOOL permissionHasBeenDenied = permission == kCLAuthorizationStatusDenied ||permission == kCLAuthorizationStatusRestricted; 49 | BOOL permissionHasNotBeenDenied = !permissionHasBeenDenied; 50 | 51 | return locationServicesEnabled && permissionHasNotBeenDenied; 52 | } 53 | 54 | - (void)requestLocationPermission { 55 | if (self.canRequestLocationPermission) { 56 | [self.locationManager requestWhenInUseAuthorization]; 57 | } 58 | } 59 | 60 | -(void)requestWithCompletionHandler:(UserLocationRequestCompletionHandler)completionHandler { 61 | if (!self.canRequestUserLocation) { 62 | completionHandler(nil, [NSError appErrorWithDescription:@"Access to location has not been granted."]); 63 | return; 64 | } 65 | 66 | [self.requestCompletionHandlers addObject:completionHandler]; 67 | [self.locationManager requestLocation]; 68 | } 69 | 70 | //MARK: - CLLocationManagerDelegate 71 | 72 | - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { 73 | if (locations.count == 0) { 74 | [self callCompletionHandlersWithLocation:nil error:[NSError appErrorWithDescription:@"Locations array came in empty."]]; 75 | return; 76 | } 77 | 78 | [self callCompletionHandlersWithLocation:locations.firstObject error:nil]; 79 | } 80 | 81 | - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { 82 | [self callCompletionHandlersWithLocation:nil error:error]; 83 | } 84 | 85 | // MARK - Completion Handlers 86 | 87 | - (void)callCompletionHandlersWithLocation:(nullable CLLocation *)location error:(nullable NSError *)error { 88 | for (UserLocationRequestCompletionHandler handler in self.requestCompletionHandlers) { 89 | handler(location, error); 90 | } 91 | [self.requestCompletionHandlers removeAllObjects]; 92 | } 93 | 94 | @end 95 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/ApplicationEventNotifications/NSNotification+ApplicationEventNotifications.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSNotification+ApplicationEventNotifications.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface NSNotification (ApplicationEventNotifications) 13 | 14 | @property (class, nonatomic, readonly) NSNotificationName applicationBecameActiveNotification; 15 | 16 | @end 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/ApplicationEventNotifications/NSNotification+ApplicationEventNotifications.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSNotification+ApplicationEventNotifications.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "NSNotification+ApplicationEventNotifications.h" 10 | #import "UIApplication+Metadata.h" 11 | @import UIKit; 12 | 13 | @implementation NSNotification (ApplicationEventNotifications) 14 | 15 | + (NSNotificationName)applicationBecameActiveNotification { 16 | return [NSString stringWithFormat:@"%@.applicationBecameActiveNotification", [UIApplication sharedApplication].bundleIdentifier]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/CloudKitDerivedRecord.h: -------------------------------------------------------------------------------- 1 | // 2 | // CloudKitDerivedRecord.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/6/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | @import CloudKit; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | @interface CloudKitDerivedRecord : NSObject 14 | 15 | @property (class, nonatomic, readonly) NSString *recordName; 16 | @property (nonatomic) NSString *identifier; 17 | @property (nonatomic) NSDate *modificationDate; 18 | 19 | - (instancetype)initWithRecord:(CKRecord *)record NS_DESIGNATED_INITIALIZER; 20 | 21 | - (BOOL)hasBeenModifiedSinceRecord:(CloudKitDerivedRecord *)cachedRecord; 22 | 23 | @end 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/CloudKitDerivedRecord.m: -------------------------------------------------------------------------------- 1 | // 2 | // CloudKitDerivedRecord.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/6/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "CloudKitDerivedRecord.h" 10 | #import "NSDate+Utilities.h" 11 | 12 | @implementation CloudKitDerivedRecord 13 | 14 | + (NSString *)recordName { 15 | return NSStringFromClass([self class]); 16 | } 17 | 18 | - (instancetype)initWithRecord:(CKRecord *)record { 19 | if (self = [super init]) { 20 | self.identifier = record.recordID.recordName; 21 | self.modificationDate = record.modificationDate; 22 | } 23 | return self; 24 | } 25 | 26 | - (instancetype)init { 27 | NSAssert(false, @"Use initWithRecord:"); 28 | return [self initWithRecord:[CKRecord new]]; 29 | } 30 | 31 | - (BOOL)isEqual:(id)object { 32 | if (![object isKindOfClass:[self class]]) { 33 | return false; 34 | } 35 | return [self.identifier isEqualToString: [(CloudKitDerivedRecord *)object identifier]]; 36 | } 37 | 38 | - (BOOL)hasBeenModifiedSinceRecord:(CloudKitDerivedRecord *)cachedRecord { 39 | if (![cachedRecord isKindOfClass:[self class]]) { return false; } 40 | return [self.modificationDate isLaterThanDate:cachedRecord.modificationDate]; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/Event.h: -------------------------------------------------------------------------------- 1 | // 2 | // Event.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/29/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "EventType.h" 11 | #import "Location.h" 12 | #import "CloudKitDerivedRecord.h" 13 | @import UIKit; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | @interface Event : CloudKitDerivedRecord 17 | 18 | @property (nonatomic, assign) EventType type; 19 | @property (nonatomic) NSDate* date; 20 | @property (nonatomic, assign) NSTimeInterval duration; 21 | @property (nonatomic) Location* location; 22 | @property (nonatomic, readonly) UIImage *annotationImage; 23 | @property (nonatomic, readonly) NSDate *endDate; 24 | @property (nonatomic, readonly, assign) BOOL isActive; 25 | 26 | - (instancetype)initWithRecord:(CKRecord *)record location:(Location *)location; 27 | 28 | @end 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/Event.m: -------------------------------------------------------------------------------- 1 | // 2 | // Event.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/29/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "Event.h" 10 | #import "NSDate+Utilities.h" 11 | @import CloudKit; 12 | 13 | @implementation Event 14 | 15 | - (instancetype)initWithRecord:(CKRecord *)record location:(Location *)location { 16 | if (self = [super initWithRecord:record]) { 17 | self.type = [(NSNumber *)record[@"eventType"] integerValue]; 18 | self.date = record[@"eventDate"]; 19 | self.duration = [(NSNumber *)record[@"duration"] doubleValue]; 20 | self.location = location; 21 | } 22 | 23 | return self; 24 | } 25 | 26 | - (UIImage *)annotationImage { 27 | switch (self.type) { 28 | case EventTypeSFCoffee: 29 | return [UIImage imageNamed:@"coffee-location-icon"]; 30 | break; 31 | default: 32 | break; 33 | } 34 | } 35 | 36 | - (NSDate *)endDate { 37 | return [self.date dateByAddingTimeInterval:self.duration]; 38 | } 39 | 40 | - (BOOL)isActive { 41 | return self.endDate.isInFuture; 42 | } 43 | 44 | - (BOOL)hasBeenModifiedSinceRecord:(CloudKitDerivedRecord *)cachedRecord { 45 | BOOL isEventModified = [super hasBeenModifiedSinceRecord:cachedRecord]; 46 | BOOL isLocationModified = [self.location hasBeenModifiedSinceRecord:[(Event *)cachedRecord location]]; 47 | return isEventModified || isLocationModified; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/EventDataSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // EventDataSOurce.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/29/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Event.h" 11 | @import CloudKit; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @class EventDataSource; 16 | 17 | @protocol EventDataSourceDelegate 18 | - (void)willUpdateDataSource:(EventDataSource *)datasource; 19 | - (void)didUpdateDataSource:(EventDataSource *)datasource withNewData:(BOOL)hasNewData error:(nullable NSError *)error; 20 | @end 21 | 22 | @interface EventDataSource : NSObject 23 | 24 | @property (nonatomic, weak) id delegate; 25 | @property (nonatomic, assign) BOOL hasMoreEvents; 26 | @property (nonatomic, readonly, assign) NSUInteger numberOfEvents; 27 | 28 | /// Index of the next upcoming event. If not found, returns NSNotFound 29 | @property (nonatomic, readonly, assign) NSUInteger indexOfCurrentEvent; 30 | 31 | - (instancetype)initWithEventType:(EventType)eventType database:(CKDatabase *)database; 32 | - (void)refresh; 33 | - (Event *)eventAtIndex:(NSUInteger)index; 34 | 35 | @end 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/EventDataSource.m: -------------------------------------------------------------------------------- 1 | // 2 | // EventDataSOurce.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/29/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "EventDataSource.h" 10 | #import "Event.h" 11 | #import "NSDate+Utilities.h" 12 | #import "NSError+Constructor.h" 13 | #import "NSNotification+ApplicationEventNotifications.h" 14 | 15 | @interface EventDataSource () 16 | 17 | @property (nonatomic) CKDatabase *database; 18 | @property (nonatomic, assign) EventType eventType; 19 | @property (nonatomic) NSMutableArray *events; 20 | 21 | @end 22 | 23 | @implementation EventDataSource 24 | 25 | - (instancetype)initWithEventType:(EventType)eventType database:(CKDatabase *)database { 26 | if (self = [super init]) { 27 | self.eventType = eventType; 28 | self.database = database; 29 | self.events = [NSMutableArray new]; 30 | [self observeAppActivationEvents]; 31 | } 32 | return self; 33 | } 34 | 35 | - (void)refresh { 36 | __weak typeof(self) welf = self; 37 | __block NSArray *eventRecords; 38 | 39 | CKFetchRecordsOperation *locationsOperation = [self locationRecordsFetchOperationWithCompletionHandler:^(NSDictionary * _Nullable recordsByRecordID, NSError * _Nullable error) { 40 | if (error) { 41 | dispatch_async(dispatch_get_main_queue(), ^{ 42 | [welf.delegate didUpdateDataSource:welf withNewData:false error:error]; 43 | }); 44 | return; 45 | } 46 | 47 | NSArray *newEvents = [welf eventsFromEventRecords:eventRecords locationRecordsByID:recordsByRecordID]; 48 | dispatch_async(dispatch_get_main_queue(), ^{ 49 | BOOL updatedEvents = [welf reconcileNewEvents:newEvents]; 50 | [welf.delegate didUpdateDataSource:welf withNewData:updatedEvents error:nil]; 51 | }); 52 | }]; 53 | 54 | CKQueryOperation *eventRecordsOperation = [self eventRecordsQueryOperationForEventsOfType:self.eventType withCompletionHandler:^(CKQueryCursor *cursor, NSArray *records, NSError *error) { 55 | if (error) { 56 | dispatch_async(dispatch_get_main_queue(), ^{ 57 | [welf.delegate didUpdateDataSource:welf withNewData:false error:error]; 58 | }); 59 | return; 60 | } 61 | 62 | eventRecords = records; 63 | 64 | locationsOperation.recordIDs = [welf locationRecordIDsFromEventRecords:eventRecords]; 65 | [self.database addOperation:locationsOperation]; 66 | }]; 67 | 68 | [self.delegate willUpdateDataSource:self]; 69 | [self.database addOperation:eventRecordsOperation]; 70 | } 71 | 72 | - (Event *)eventAtIndex:(NSUInteger)index { 73 | return self.events[index]; 74 | } 75 | 76 | - (NSUInteger)numberOfEvents { 77 | return self.events.count; 78 | } 79 | 80 | 81 | - (NSUInteger)indexOfCurrentEvent { 82 | // index of the first future event 83 | for (Event *event in self.events.reverseObjectEnumerator) { 84 | // Basing on end-date allows ongoing event to show up first 85 | if (event.isActive) { 86 | return [self.events indexOfObjectIdenticalTo:event]; 87 | } 88 | } 89 | 90 | return NSNotFound; 91 | } 92 | 93 | // MARK: - CloudKit Operations 94 | 95 | - (CKQueryOperation *)eventRecordsQueryOperationForEventsOfType:(EventType)eventType withCompletionHandler: (void (^)(CKQueryCursor *cursor, NSArray *records, NSError *error))completionHandler { 96 | NSString *recordType = Event.recordName; 97 | 98 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"eventType == %u", eventType]; 99 | CKQuery *query = [[CKQuery alloc] initWithRecordType:recordType predicate:predicate]; 100 | query.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:@"eventDate" ascending:false]]; 101 | CKQueryOperation *operation = [[CKQueryOperation alloc] initWithQuery:query]; 102 | 103 | __block NSMutableArray *records = [NSMutableArray new]; 104 | operation.recordFetchedBlock = ^(CKRecord * _Nonnull record) { 105 | if (![record.recordType isEqualToString:recordType]) { 106 | NSAssert(false, @"Received a record of unexpected type: %@", record.recordType); 107 | return; 108 | } 109 | [records addObject:record]; 110 | }; 111 | operation.queryCompletionBlock = ^(CKQueryCursor * _Nullable cursor, NSError * _Nullable operationError) { 112 | if (operationError != nil) { 113 | completionHandler(nil, nil, operationError); 114 | return; 115 | } 116 | 117 | completionHandler(cursor, records, nil); 118 | }; 119 | 120 | return operation; 121 | } 122 | 123 | - (CKFetchRecordsOperation *)locationRecordsFetchOperationWithCompletionHandler:(void (^)(NSDictionary * _Nullable recordsByRecordID, NSError * _Nullable error))completionHandler { 124 | NSMutableArray *locationRecords = [NSMutableArray new]; 125 | CKFetchRecordsOperation *operation = [CKFetchRecordsOperation new]; 126 | operation.perRecordCompletionBlock = ^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error) { 127 | if (record == nil) { 128 | NSError *fallbackError = [NSError appErrorWithDescription:@"Record of type %@ with id %@ could not be found."]; 129 | completionHandler(nil, error ? error : fallbackError); 130 | return; 131 | } 132 | 133 | [locationRecords addObject:record]; 134 | }; 135 | 136 | operation.fetchRecordsCompletionBlock = completionHandler; 137 | 138 | return operation; 139 | } 140 | 141 | // MARK: - Records Parsing 142 | 143 | - (NSArray *)eventsFromEventRecords:(NSArray *)eventRecords locationRecordsByID:(NSDictionary *) locationRecordsByRecordID { 144 | NSMutableArray *events = [NSMutableArray new]; 145 | for (CKRecord *eventRecord in eventRecords) { 146 | CKRecordID *locationRecordID = [self locationRecordIDFromEventRecord:eventRecord]; 147 | if (!locationRecordID) { 148 | NSAssert(false, @"Location corresponding to Event does not exist\n%@", eventRecord); 149 | break; 150 | } 151 | Location *location = [[Location alloc] initWithRecord:locationRecordsByRecordID[locationRecordID]]; 152 | Event *event = [[Event alloc] initWithRecord:eventRecord location:location]; 153 | [events addObject:event]; 154 | } 155 | 156 | return events; 157 | } 158 | 159 | - (CKRecordID *)locationRecordIDFromEventRecord:(CKRecord *)eventRecord { 160 | CKReference *locationReference = eventRecord[@"location"]; 161 | return locationReference.recordID; 162 | } 163 | 164 | - (NSArray *)locationRecordIDsFromEventRecords:(NSArray *)eventRecords { 165 | NSMutableArray *recordIDs = [NSMutableArray new]; 166 | for (CKRecord *eventRecord in eventRecords) { 167 | [recordIDs addObject:[self locationRecordIDFromEventRecord:eventRecord]]; 168 | } 169 | return recordIDs; 170 | } 171 | 172 | //MARK: - Respond To app Events 173 | 174 | - (void)observeAppActivationEvents { 175 | __weak typeof(self) welf = self; 176 | [[NSNotificationCenter defaultCenter] addObserverForName:NSNotification.applicationBecameActiveNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { 177 | [welf refresh]; 178 | }]; 179 | } 180 | 181 | // MARK: - Bookeeping 182 | 183 | - (BOOL)reconcileNewEvents:(NSArray *)newEvents { 184 | BOOL updatedEvents = false; 185 | NSMutableArray *newUniqueEvents = [NSMutableArray new]; 186 | for (Event *event in newEvents) { 187 | NSUInteger index = [self.events indexOfObject:event]; 188 | if (index == NSNotFound) { 189 | [newUniqueEvents addObject:event]; 190 | } else { 191 | Event *exsistingEvent = self.events[index]; 192 | if ([event hasBeenModifiedSinceRecord:exsistingEvent]) { 193 | updatedEvents = true; 194 | [self.events replaceObjectAtIndex:index withObject:event]; 195 | } 196 | } 197 | } 198 | 199 | if (newUniqueEvents.count > 0) { 200 | updatedEvents = true; 201 | [self.events addObjectsFromArray:newUniqueEvents]; 202 | } 203 | 204 | if (updatedEvents) { 205 | [self.events sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { 206 | return [[(Event *)obj2 date] compare:[(Event *)obj1 date]]; 207 | }]; 208 | } 209 | 210 | return updatedEvents; 211 | } 212 | 213 | @end 214 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/EventType.h: -------------------------------------------------------------------------------- 1 | // 2 | // EventType.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/29/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | typedef NS_ENUM(NSUInteger, EventType) { 10 | EventTypeSFCoffee = 0 11 | }; 12 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/Location.h: -------------------------------------------------------------------------------- 1 | // 2 | // Location.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/28/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CloudKitDerivedRecord.h" 11 | @import CoreLocation; 12 | @import CloudKit; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | @interface Location : CloudKitDerivedRecord 16 | 17 | @property (nonatomic) NSString *name; 18 | @property (nonatomic) NSString *streetAddress; 19 | @property (nonatomic) CLLocation *location; 20 | @property (nullable, nonatomic) NSURL *imageFileURL; 21 | 22 | @end 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Models/Location.m: -------------------------------------------------------------------------------- 1 | // 2 | // Location.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/28/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "Location.h" 10 | 11 | @implementation Location 12 | 13 | - (instancetype)initWithRecord:(CKRecord *)record { 14 | if (self = [super initWithRecord:record]) { 15 | self.name = record[@"name"]; 16 | self.streetAddress = record[@"streetAddress"]; 17 | self.location = record[@"location"]; 18 | self.imageFileURL = [(CKAsset *)record[@"image"] fileURL]; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/RemoteUIImage/UIImage+URL.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+URL.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | @interface UIImage (URL) 13 | 14 | typedef void(^ImageDownloadCompletionHandler)(UIImage *_Nullable image, NSError *_Nullable error); 15 | 16 | + (void)fetchImageFromFileURL:(NSURL *)fileURL onQueue:(nullable NSOperationQueue *)queue withCompletionHandler:(ImageDownloadCompletionHandler)completionHandler; 17 | 18 | @end 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/RemoteUIImage/UIImage+URL.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+URL.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/2/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "UIImage+URL.h" 10 | #import "NSError+Constructor.h" 11 | 12 | @implementation UIImage (URL) 13 | 14 | + (void)fetchImageFromFileURL:(NSURL *)fileURL onQueue:(nullable NSOperationQueue *)queue withCompletionHandler:(ImageDownloadCompletionHandler)completionHandler { 15 | NSAssert(fileURL.isFileURL, @"%@ is not a file URL", fileURL); 16 | 17 | if (!queue) { 18 | queue = [NSOperationQueue new]; 19 | } 20 | 21 | [queue addOperationWithBlock:^{ 22 | NSData *data = [NSData dataWithContentsOfURL:fileURL]; 23 | UIImage *image = nil; 24 | if (data) { 25 | image = [UIImage imageWithData:data]; 26 | } 27 | 28 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 29 | completionHandler(image, nil); 30 | }]; 31 | }]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/SF iOS-Debug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.icloud-container-environment 8 | Production 9 | com.apple.developer.icloud-container-identifiers 10 | 11 | iCloud.$(CFBundleIdentifier) 12 | 13 | com.apple.developer.icloud-services 14 | 15 | CloudKit 16 | 17 | com.apple.developer.ubiquity-kvstore-identifier 18 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 19 | 20 | 21 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/SF iOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.$(CFBundleIdentifier) 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | com.apple.developer.ubiquity-kvstore-identifier 16 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 17 | 18 | 19 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Secrets/SecretsStore.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecretsStore.h 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | static NSString *const secretsFileName = @"secrets"; 14 | 15 | /** 16 | An access wrapper for the Secrets/secrets.plist. 17 | If you are seeing warnings about missing secrets, and correspondingly, 18 | missing functionality, please follow instructions in Readme to setup secrets. 19 | */ 20 | @interface SecretsStore : NSObject 21 | 22 | @property (readonly, nonatomic) NSString *uberClientID; 23 | @property (readonly, nonatomic) NSString *uberServerToken; 24 | @property (readonly, nonatomic) NSString *lyftClientID; 25 | @property (readonly, nonatomic) NSString *lyftServerToken; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Secrets/SecretsStore.m: -------------------------------------------------------------------------------- 1 | // 2 | // SecretsStore.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 8/5/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import "SecretsStore.h" 10 | 11 | @interface SecretsStore () 12 | 13 | @property (nonatomic) NSDictionary *secrets; 14 | 15 | @end 16 | 17 | @implementation SecretsStore 18 | 19 | - (instancetype)init { 20 | if (self = [super init]) { 21 | NSString *plistPath = [[NSBundle mainBundle] pathForResource:secretsFileName ofType:@"plist"]; 22 | self.secrets = [[NSDictionary alloc] initWithContentsOfFile:plistPath]; 23 | NSAssert(self.secrets != nil, @"Configure secrets by following instructions in Readme."); 24 | } 25 | return self; 26 | } 27 | 28 | - (NSString *)uberClientID { 29 | return self.secrets[@"uber-client-id"]; 30 | } 31 | 32 | - (NSString *)uberServerToken { 33 | return self.secrets[@"uber-server-token"]; 34 | } 35 | 36 | - (NSString *)lyftClientID { 37 | return self.secrets[@"lyft-client-id"]; 38 | } 39 | 40 | -(NSString *)lyftServerToken { 41 | return self.secrets[@"lyft-server-token"]; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/Secrets/secrets-example.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | uber-server-token 6 | 7 | uber-client-id 8 | 9 | lyft-client-secret 10 | 11 | lyft-client-id 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SF iOS/SF iOS/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // SF iOS 4 | // 5 | // Created by Amit Jain on 7/28/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SF iOS/SF iOSTests/DateTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // DateTests.m 3 | // SF iOSTests 4 | // 5 | // Created by Amit Jain on 8/6/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NSDate+Utilities.h" 11 | 12 | @interface DateTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation DateTests 17 | 18 | - (void)testLaterThanDate { 19 | NSDate *now = [NSDate new]; 20 | NSDate *later = [now dateByAddingTimeInterval:1000]; 21 | XCTAssertTrue([later isLaterThanDate:now]); 22 | } 23 | 24 | - (void)testBefioreThanDate { 25 | NSDate *now = [NSDate new]; 26 | NSDate *earlier = [now dateByAddingTimeInterval:-01000]; 27 | XCTAssertTrue([earlier isEarlierThanDate:now]); 28 | } 29 | 30 | - (void)testTimeSlotWhenStartAndEndDatesAreInTheSamePeriod { 31 | NSDate *start = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate new]]; 32 | NSString *timesot = [NSDate timeslotStringFromStartDate:start duration:1800]; 33 | 34 | XCTAssertTrue([@"12:00 - 12:30AM" isEqualToString:timesot]); 35 | } 36 | 37 | - (void)testTimeSlotWhenStartAndEndDatesAreInDifferentPeriod { 38 | NSDate *start = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate new]]; 39 | NSString *timesot = [NSDate timeslotStringFromStartDate:start duration:13 * 60 * 60]; // 13hrs later 40 | 41 | XCTAssertTrue([@"12:00AM - 1:00PM" isEqualToString:timesot]); 42 | } 43 | 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /SF iOS/SF iOSTests/EventDataSourceTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // EventDataSourceTests.m 3 | // SF iOSTests 4 | // 5 | // Created by Amit Jain on 7/30/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "EventDataSource.h" 11 | @import CloudKit; 12 | 13 | @interface EventDataSourceTests : XCTestCase 14 | 15 | @property (nonatomic) CKDatabase *database; 16 | 17 | @end 18 | 19 | @implementation EventDataSourceTests 20 | 21 | - (void)setUp { 22 | [super setUp]; 23 | 24 | self.database = [CKContainer defaultContainer].publicCloudDatabase; 25 | } 26 | 27 | 28 | /** 29 | This test will start failing as soon as more events are added. Figure out a better way to test CloudKit! 30 | The limitation is that w/o mocking the only way to test is with live records in the dev enviornment. 31 | */ 32 | //- (void)testFetchingCoffeeEvents { 33 | // XCTestExpectation *exp = [self expectationWithDescription:@"Wait for events"]; 34 | // 35 | // EventDataSource *dataSource = [[EventDataSource alloc] initWithEventType:EventTypeSFCoffee database:self.database]; 36 | // [dataSource fetchPreviousEventsWithCompletionHandler:^(BOOL didUpdate, NSError * _Nullable error) { 37 | // if (error) { 38 | // XCTAssertFalse(didUpdate); 39 | // XCTFail(@"Error fetching events: %@", error); 40 | // } else { 41 | // XCTAssertTrue(didUpdate); 42 | // XCTAssertEqual(dataSource.numberOfEvents, 2); 43 | // } 44 | // 45 | // [exp fulfill]; 46 | // }]; 47 | // 48 | // [self waitForExpectationsWithTimeout:2.0 handler:nil]; 49 | //} 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /SF iOS/SF iOSTests/FeedItemTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItemTests.m 3 | // SF iOSTests 4 | // 5 | // Created by Amit Jain on 7/31/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Event.h" 11 | #import "FeedItem.h" 12 | #import "Location.h" 13 | 14 | @interface FeedItemTests : XCTestCase 15 | 16 | @property (nonatomic) Event *event; 17 | 18 | @end 19 | 20 | @implementation FeedItemTests 21 | 22 | - (void)setUp { 23 | [super setUp]; 24 | 25 | CKRecord *locRecord = [[CKRecord alloc] initWithRecordType:Location.recordName]; 26 | Location *location = [[Location alloc] initWithRecord:locRecord]; 27 | location.name = @"Test Event"; 28 | location.streetAddress = @"600 Post St."; 29 | location.location = [[CLLocation alloc] initWithLatitude:37.7564388 longitude:-122.4213833]; 30 | 31 | CKRecord *eventRecord = [[CKRecord alloc] initWithRecordType:Event.recordName]; 32 | self.event = [[Event alloc] initWithRecord:eventRecord location:location]; 33 | self.event.type = EventTypeSFCoffee; 34 | self.event.date = [NSDate new]; 35 | } 36 | 37 | - (void)tearDown { 38 | // Put teardown code here. This method is called after the invocation of each test method in the class. 39 | [super tearDown]; 40 | } 41 | 42 | - (void)testEventInTomorrow { 43 | NSDate *eventDate = [self dateByAddingUnit:NSCalendarUnitDay value:1 toDate:[NSDate new]]; 44 | self.event.date = eventDate; 45 | FeedItem *item = [[FeedItem alloc] initWithEvent:self.event]; 46 | 47 | XCTAssertTrue([item.dateString isEqualToString:@"Tomorrow"]); 48 | XCTAssertTrue(item.isActive); 49 | } 50 | 51 | - (void)testEventInToday { 52 | NSDate *eventDate = [self dateByAddingUnit:NSCalendarUnitHour value:1 toDate:[NSDate new]]; 53 | self.event.date = eventDate; 54 | FeedItem *item = [[FeedItem alloc] initWithEvent:self.event]; 55 | 56 | XCTAssertTrue([item.dateString isEqualToString:@"Today"]); 57 | XCTAssertTrue(item.isActive); 58 | } 59 | 60 | - (void)testEventInPastButStillInToday { 61 | NSDate *eventDate = [self dateByAddingUnit:NSCalendarUnitHour value:-1 toDate:[NSDate new]]; 62 | self.event.date = eventDate; 63 | FeedItem *item = [[FeedItem alloc] initWithEvent:self.event]; 64 | 65 | XCTAssertTrue([item.dateString isEqualToString:@"Today"]); 66 | XCTAssertFalse(item.isActive); 67 | } 68 | 69 | - (void)testEventInYesterday { 70 | NSDate *eventDate = [self dateByAddingUnit:NSCalendarUnitDay value:-1 toDate:[NSDate new]]; 71 | self.event.date = eventDate; 72 | FeedItem *item = [[FeedItem alloc] initWithEvent:self.event]; 73 | 74 | XCTAssertTrue([item.dateString isEqualToString:@"Yesterday"]); 75 | XCTAssertEqual(item.isActive, false); 76 | } 77 | 78 | - (void)testEventInLastMonth { 79 | NSDate *eventDate = [self dateByAddingUnit:NSCalendarUnitMonth value:-1 toDate:[NSDate new]]; 80 | self.event.date = eventDate; 81 | FeedItem *item = [[FeedItem alloc] initWithEvent:self.event]; 82 | 83 | NSDateFormatter *formatter = [NSDateFormatter new]; 84 | formatter.dateFormat = @"MMM d"; 85 | NSString *expctedTime = [formatter stringFromDate:eventDate]; 86 | 87 | XCTAssertTrue([item.dateString isEqualToString:expctedTime]); 88 | XCTAssertFalse(item.isActive); 89 | } 90 | 91 | - (NSDate *)dateByAddingUnit:(NSCalendarUnit)calendarUnit value:(NSInteger)value toDate:(NSDate *)date { 92 | return [[NSCalendar currentCalendar] dateByAddingUnit:calendarUnit value:value toDate:date options:0]; 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /SF iOS/SF iOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SF iOS/SF iOSTests/TravelTime+ArrivalTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TravelTime+ArrivalTests.m 3 | // SF iOSTests 4 | // 5 | // Created by Amit Jain on 8/8/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TravelTime+Arrival.h" 11 | 12 | @interface TravelTime_ArrivalTests : XCTestCase 13 | 14 | @property (nonatomic) TravelTime *traveltime; 15 | 16 | @end 17 | 18 | @implementation TravelTime_ArrivalTests 19 | 20 | - (void)setUp { 21 | [super setUp]; 22 | self.traveltime = [TravelTime new]; 23 | self.traveltime.transportType = TransportTypeTransit; 24 | self.traveltime.travelTime = 1800; //30min 25 | } 26 | 27 | - (void)testArrivalWhenEventIsInFuture { 28 | NSDate *startDate = [self dateWithHoursSinceNow:3]; 29 | NSDate *endDate = [self dateWithHoursSinceNow:4]; 30 | 31 | Arrival arrival = [self.traveltime arrivalToEventWithStartDate:startDate endDate:endDate]; 32 | XCTAssertEqual(arrival, ArrivalOnTime); 33 | } 34 | 35 | - (void)testArrivalWhenArrivalDateIsDuringEvent { 36 | NSDate *startDate = [self dateWithHoursSinceNow:-1]; 37 | NSDate *endDate = [self dateWithHoursSinceNow:1]; 38 | 39 | Arrival arrival = [self.traveltime arrivalToEventWithStartDate:startDate endDate:endDate]; 40 | XCTAssertEqual(arrival, ArrivalDuringEvent); 41 | } 42 | 43 | - (void)testArrivalWhenArrivalDateIsAfterEventEnd { 44 | NSDate *startDate = [self dateWithHoursSinceNow:-1]; 45 | NSDate *endDate = [self dateWithHoursSinceNow:0.25]; 46 | 47 | Arrival arrival = [self.traveltime arrivalToEventWithStartDate:startDate endDate:endDate]; 48 | XCTAssertEqual(arrival, ArrivalAfterEvent); 49 | } 50 | 51 | - (NSDate *)dateWithHoursSinceNow:(double)numberOfHours { 52 | return [NSDate dateWithTimeIntervalSinceNow:[self timeIntervalForHours:numberOfHours]]; 53 | } 54 | 55 | - (NSTimeInterval)timeIntervalForHours:(double)numberOfHours { 56 | return 3600 * numberOfHours; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /SF iOS/SF iOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SF iOS/SF iOSUITests/SF_iOSUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SF_iOSUITests.m 3 | // SF iOSUITests 4 | // 5 | // Created by Amit Jain on 7/28/17. 6 | // Copyright © 2017 Amit Jain. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SF_iOSUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation SF_iOSUITests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | 22 | // In UI tests it is usually best to stop immediately when a failure occurs. 23 | self.continueAfterFailure = NO; 24 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 25 | [[[XCUIApplication alloc] init] launch]; 26 | 27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 28 | } 29 | 30 | - (void)tearDown { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | [super tearDown]; 33 | } 34 | 35 | - (void)testExample { 36 | // Use recording to get started writing UI tests. 37 | // Use XCTAssert and related functions to verify your tests produce the correct results. 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Sketch-IOS-Icons-1.0.1.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/Sketch-IOS-Icons-1.0.1.sketch -------------------------------------------------------------------------------- /screenshots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/screenshots.jpg -------------------------------------------------------------------------------- /sf-ios.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravicle/sf-ios/d6af00487a9b2dc71fefd7bce823884681babd3c/sf-ios.sketch --------------------------------------------------------------------------------