├── .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 | 
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
--------------------------------------------------------------------------------