├── .gitignore ├── CONTRIBUTING.md ├── FriendlyEats.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── FriendlyEats.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── FriendlyEats ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── logo_firebase_color_1x_ios_133in167dp.png │ │ ├── logo_firebase_color_1x_ios_23in29dp.png │ │ ├── logo_firebase_color_1x_ios_32in40dp-1.png │ │ ├── logo_firebase_color_1x_ios_32in40dp-2.png │ │ ├── logo_firebase_color_1x_ios_32in40dp.png │ │ ├── logo_firebase_color_1x_ios_48in60dp.png │ │ ├── logo_firebase_color_1x_ios_61in76dp.png │ │ ├── logo_firebase_color_2x_ios_23in29dp-1.png │ │ ├── logo_firebase_color_2x_ios_23in29dp.png │ │ ├── logo_firebase_color_2x_ios_32in40dp-1.png │ │ ├── logo_firebase_color_2x_ios_32in40dp.png │ │ ├── logo_firebase_color_2x_ios_48in60dp.png │ │ ├── logo_firebase_color_2x_ios_61in76dp.png │ │ ├── logo_firebase_color_3x_ios_23in29dp.png │ │ ├── logo_firebase_color_3x_ios_32in40dp.png │ │ └── logo_firebase_color_3x_ios_48in60dp.png │ ├── Contents.json │ ├── ic_keyboard_arrow_left.imageset │ │ ├── Contents.json │ │ ├── ic_keyboard_arrow_left.png │ │ ├── ic_keyboard_arrow_left_2x.png │ │ └── ic_keyboard_arrow_left_3x.png │ ├── ic_keyboard_arrow_right.imageset │ │ ├── Contents.json │ │ ├── ic_keyboard_arrow_right.png │ │ ├── ic_keyboard_arrow_right_2x.png │ │ └── ic_keyboard_arrow_right_3x.png │ ├── ic_person.imageset │ │ ├── Contents.json │ │ ├── ic_person_black_1x_ios_24dp.png │ │ ├── ic_person_black_2x_ios_24dp.png │ │ └── ic_person_black_3x_ios_24dp.png │ ├── ic_restaurant.imageset │ │ ├── Contents.json │ │ ├── ic_restaurant_black_1x_ios_24dp.png │ │ ├── ic_restaurant_black_2x_ios_24dp.png │ │ └── ic_restaurant_black_3x_ios_24dp.png │ └── pizza-monster.imageset │ │ ├── Contents.json │ │ └── pizza-monster.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Common │ ├── Firestore+Populate.swift │ ├── LocalCollection.swift │ ├── RestaurantTableViewDataSource.swift │ ├── ReviewTableViewCell.swift │ ├── ReviewTableViewDataSource.swift │ ├── StarsView.swift │ └── Utils.swift ├── Info.plist ├── Models │ ├── Restaurant.swift │ ├── Review.swift │ ├── User.swift │ └── Yum.swift ├── Profile │ ├── AddRestaurantViewController.swift │ ├── MyRestaurantsViewController.swift │ └── ProfileViewController.swift ├── Restaurants │ ├── BasicRestaurantsTableViewController.swift │ ├── EditRestaurantViewController.swift │ ├── FiltersViewController.swift │ ├── HackPageViewController.swift │ ├── NewReviewViewController.swift │ ├── RestaurantDetailViewController.swift │ ├── RestaurantTableViewCell.swift │ └── RestaurantsTableViewController.swift └── pizza-monster.png ├── FriendlyEatsTests ├── FriendlyEatsTests.swift └── Info.plist ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── firebase.json ├── functions ├── package-lock.json ├── package.json ├── src │ └── index.ts ├── tsconfig.json └── tslint.json └── package-lock.json /.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 | ## Functions-related 21 | functions/etc/ 22 | functions/lib/ 23 | functions/node_modules/ 24 | .firebaserc 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | .DS_Store 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # 44 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | .build/ 48 | 49 | # CocoaPods 50 | # 51 | # We recommend against adding the Pods directory to your .gitignore. However 52 | # you should judge for yourself, the pros and cons are mentioned at: 53 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 54 | # 55 | GoogleService-Info.plist 56 | 57 | # Carthage 58 | # 59 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 60 | # Carthage/Checkouts 61 | 62 | Carthage/Build 63 | 64 | # fastlane 65 | # 66 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 67 | # screenshots whenever they are needed. 68 | # For more information about the recommended setup visit: 69 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 70 | 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | /default.profraw 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to friendlyeats-ios 2 | 3 | We'd love for you to contribute to our source code and to make this project even better than it is today! Here are the guidelines we'd like you to follow: 4 | 5 | - [Code of Conduct](#coc) 6 | - [Question or Problem?](#question) 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Submission Guidelines](#submit) 10 | - [Coding Rules](#rules) 11 | - [Signing the CLA](#cla) 12 | 13 | ## Code of Conduct 14 | 15 | As contributors and maintainers of the friendlyeats-ios project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 16 | 17 | Communication through any of Firebase's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 18 | 19 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same. 20 | 21 | If any member of the community violates this code of conduct, the maintainers of the friendlyeats-ios project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 22 | 23 | If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at samstern@google.com. 24 | 25 | ## Got a Question or Problem? 26 | 27 | If you have questions about how to use the friendlyeats-ios, please open a Github issue. If you need help debugging your own app, please please direct these to [StackOverflow][stackoverflow] and use the `firebase` tag. 28 | 29 | If you feel that we're missing an important bit of documentation, feel free to 30 | file an issue so we can help. Here's an example to get you started: 31 | 32 | ``` 33 | What are you trying to do or find out more about? 34 | 35 | Where have you looked? 36 | 37 | Where did you expect to find this information? 38 | ``` 39 | 40 | ## Found an Issue? 41 | If you find a bug in the source code or a mistake in the documentation, you can help us by 42 | submitting an issue. Even better you can submit a Pull Request with a fix. 43 | 44 | See [below](#submit) for some guidelines. 45 | 46 | ## Submission Guidelines 47 | 48 | ### Submitting an Issue 49 | Before you submit your issue search the archive, maybe your question was already answered. 50 | 51 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 52 | Help us to maximize the effort we can spend fixing issues and adding new 53 | features, by not reporting duplicate issues. Providing the following information will increase the 54 | chances of your issue being dealt with quickly: 55 | 56 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 57 | * **Motivation for or Use Case** - explain why this is a bug for you 58 | * **Environment** - is this a problem with all devices or only some? 59 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps. 60 | * **Related Issues** - has a similar issue been reported before? 61 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 62 | causing the problem (line of code or commit) 63 | 64 | **If you get help, help others. Good karma rulez!** 65 | 66 | Here's a template to get you started: 67 | 68 | ``` 69 | Device: 70 | Operating system version: 71 | Firebase SDK version: 72 | 73 | What steps will reproduce the problem: 74 | 1. 75 | 2. 76 | 3. 77 | 78 | What is the expected result? 79 | 80 | What happens instead of that? 81 | 82 | Please provide any other information below, and attach a screenshot if possible. 83 | ``` 84 | 85 | ### Submitting a Pull Request 86 | Before you submit your pull request consider the following guidelines: 87 | 88 | * Search [GitHub](https://github.com/firebase/friendlyeats-ios/pulls) for an open or closed Pull Request 89 | that relates to your submission. You don't want to duplicate effort. 90 | * Please sign our [Contributor License Agreement (CLA)](#cla) before sending pull 91 | requests. We cannot accept code without this. 92 | * Make your changes in a new git branch: 93 | 94 | ```shell 95 | git checkout -b my-fix-branch master 96 | ``` 97 | 98 | * Create your patch, **including appropriate test cases**. 99 | * Avoid checking in files that shouldn't be tracked (e.g `.tmp`, `.idea`). We recommend using a [global](#global-gitignore) gitignore for this. 100 | * Commit your changes using a descriptive commit message. 101 | 102 | ```shell 103 | git commit -a 104 | ``` 105 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 106 | 107 | * Build your changes locally to ensure all the tests pass: 108 | 109 | ```shell 110 | gulp 111 | ``` 112 | 113 | * Push your branch to GitHub: 114 | 115 | ```shell 116 | git push origin my-fix-branch 117 | ``` 118 | 119 | * In GitHub, send a pull request to `friendlyeats-ios:master`. 120 | * If we suggest changes then: 121 | * Make the required updates. 122 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 123 | 124 | ```shell 125 | git rebase master -i 126 | git push origin my-fix-branch -f 127 | ``` 128 | 129 | That's it! Thank you for your contribution! 130 | 131 | #### After your pull request is merged 132 | 133 | After your pull request is merged, you can safely delete your branch and pull the changes 134 | from the main (upstream) repository: 135 | 136 | * Delete the remote branch on GitHub either through the GitHub UI or your local shell as follows: 137 | 138 | ```shell 139 | git push origin --delete my-fix-branch 140 | ``` 141 | 142 | * Check out the master branch: 143 | 144 | ```shell 145 | git checkout master -f 146 | ``` 147 | 148 | * Delete the local branch: 149 | 150 | ```shell 151 | git branch -D my-fix-branch 152 | ``` 153 | 154 | * Update your master with the latest upstream version: 155 | 156 | ```shell 157 | git pull --ff upstream master 158 | ``` 159 | 160 | ## Signing the CLA 161 | 162 | Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code 163 | changes to be accepted, the CLA must be signed. It's a quick process, we promise! 164 | 165 | [github]: https://github.com/firebase/friendyeats-android 166 | [google-cla]: https://cla.developers.google.com 167 | [stackoverflow]: http://stackoverflow.com/questions/tagged/firebase 168 | [global-gitignore]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore 169 | -------------------------------------------------------------------------------- /FriendlyEats.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FriendlyEats.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FriendlyEats.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FriendlyEats/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import Firebase 19 | 20 | @UIApplicationMain 21 | class AppDelegate: UIResponder, UIApplicationDelegate { 22 | 23 | var window: UIWindow? 24 | 25 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 26 | FirebaseApp.configure() 27 | 28 | // Globally set our navigation bar style 29 | let navigationStyles = UINavigationBar.appearance() 30 | navigationStyles.barTintColor = 31 | UIColor(red: 0x3d/0xff, green: 0x5a/0xff, blue: 0xfe/0xff, alpha: 1.0) 32 | navigationStyles.tintColor = UIColor(white: 0.8, alpha: 1.0) 33 | navigationStyles.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white] 34 | return true 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "logo_firebase_color_1x_ios_32in40dp.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "logo_firebase_color_1x_ios_48in60dp.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "logo_firebase_color_2x_ios_23in29dp.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "logo_firebase_color_3x_ios_23in29dp.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "logo_firebase_color_2x_ios_32in40dp.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "logo_firebase_color_2x_ios_48in60dp.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "logo_firebase_color_3x_ios_32in40dp.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "logo_firebase_color_3x_ios_48in60dp.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "size" : "20x20", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "size" : "20x20", 58 | "idiom" : "ipad", 59 | "filename" : "logo_firebase_color_1x_ios_32in40dp-1.png", 60 | "scale" : "2x" 61 | }, 62 | { 63 | "size" : "29x29", 64 | "idiom" : "ipad", 65 | "filename" : "logo_firebase_color_1x_ios_23in29dp.png", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "size" : "29x29", 70 | "idiom" : "ipad", 71 | "filename" : "logo_firebase_color_2x_ios_23in29dp-1.png", 72 | "scale" : "2x" 73 | }, 74 | { 75 | "size" : "40x40", 76 | "idiom" : "ipad", 77 | "filename" : "logo_firebase_color_1x_ios_32in40dp-2.png", 78 | "scale" : "1x" 79 | }, 80 | { 81 | "size" : "40x40", 82 | "idiom" : "ipad", 83 | "filename" : "logo_firebase_color_2x_ios_32in40dp-1.png", 84 | "scale" : "2x" 85 | }, 86 | { 87 | "size" : "76x76", 88 | "idiom" : "ipad", 89 | "filename" : "logo_firebase_color_1x_ios_61in76dp.png", 90 | "scale" : "1x" 91 | }, 92 | { 93 | "size" : "76x76", 94 | "idiom" : "ipad", 95 | "filename" : "logo_firebase_color_2x_ios_61in76dp.png", 96 | "scale" : "2x" 97 | }, 98 | { 99 | "size" : "83.5x83.5", 100 | "idiom" : "ipad", 101 | "filename" : "logo_firebase_color_1x_ios_133in167dp.png", 102 | "scale" : "2x" 103 | }, 104 | { 105 | "idiom" : "ios-marketing", 106 | "size" : "1024x1024", 107 | "scale" : "1x" 108 | } 109 | ], 110 | "info" : { 111 | "version" : 1, 112 | "author" : "xcode" 113 | } 114 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_133in167dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_133in167dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_23in29dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_23in29dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_32in40dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_32in40dp-1.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_32in40dp-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_32in40dp-2.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_32in40dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_32in40dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_48in60dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_48in60dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_61in76dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_1x_ios_61in76dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_23in29dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_23in29dp-1.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_23in29dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_23in29dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_32in40dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_32in40dp-1.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_32in40dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_32in40dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_48in60dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_48in60dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_61in76dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_2x_ios_61in76dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_3x_ios_23in29dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_3x_ios_23in29dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_3x_ios_32in40dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_3x_ios_32in40dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_3x_ios_48in60dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/AppIcon.appiconset/logo_firebase_color_3x_ios_48in60dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_keyboard_arrow_left.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_keyboard_arrow_left_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_keyboard_arrow_left_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/ic_keyboard_arrow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/ic_keyboard_arrow_left.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/ic_keyboard_arrow_left_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/ic_keyboard_arrow_left_2x.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/ic_keyboard_arrow_left_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_keyboard_arrow_left.imageset/ic_keyboard_arrow_left_3x.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_keyboard_arrow_right.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_keyboard_arrow_right_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_keyboard_arrow_right_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right_2x.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_keyboard_arrow_right.imageset/ic_keyboard_arrow_right_3x.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_person.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_person_black_1x_ios_24dp.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_person_black_2x_ios_24dp.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_person_black_3x_ios_24dp.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_person.imageset/ic_person_black_1x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_person.imageset/ic_person_black_1x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_person.imageset/ic_person_black_2x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_person.imageset/ic_person_black_2x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_person.imageset/ic_person_black_3x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_person.imageset/ic_person_black_3x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_restaurant.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_restaurant_black_1x_ios_24dp.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_restaurant_black_2x_ios_24dp.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_restaurant_black_3x_ios_24dp.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_restaurant.imageset/ic_restaurant_black_1x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_restaurant.imageset/ic_restaurant_black_1x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_restaurant.imageset/ic_restaurant_black_2x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_restaurant.imageset/ic_restaurant_black_2x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/ic_restaurant.imageset/ic_restaurant_black_3x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/ic_restaurant.imageset/ic_restaurant_black_3x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/pizza-monster.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "pizza-monster.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /FriendlyEats/Assets.xcassets/pizza-monster.imageset/pizza-monster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/Assets.xcassets/pizza-monster.imageset/pizza-monster.png -------------------------------------------------------------------------------- /FriendlyEats/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 | 46 | -------------------------------------------------------------------------------- /FriendlyEats/Common/Firestore+Populate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseFirestore 18 | 19 | extension Firestore { 20 | 21 | /// Returns a reference to the top-level users collection. 22 | var users: CollectionReference { 23 | return self.collection("users") 24 | } 25 | 26 | /// Returns a reference to the top-level restaurants collection. 27 | var restaurants: CollectionReference { 28 | return self.collection("restaurants") 29 | } 30 | 31 | /// Returns a reference to the top-level reviews collection. 32 | var reviews: CollectionReference { 33 | return self.collection("reviews") 34 | } 35 | 36 | /// Returns a reference to the yums collection for a specific restaurant. 37 | func yums(forReview reviewID: String) -> CollectionReference { 38 | return self.collection("reviews/\(reviewID)/yums") 39 | } 40 | 41 | } 42 | 43 | // MARK: Write operations 44 | 45 | extension Firestore { 46 | 47 | /// Writes a user to the top-level users collection, overwriting data if the 48 | /// user's uid already exists in the collection. 49 | func add(user: User) { 50 | self.users.document(user.documentID).setData(user.documentData) 51 | } 52 | 53 | /// Writes a restaurant to the top-level restaurants collection, overwriting data 54 | /// if the restaurant's ID already exists. 55 | func add(restaurant: Restaurant) { 56 | self.restaurants.document(restaurant.documentID).setData(restaurant.documentData) 57 | } 58 | 59 | /// Writes a review to the top-level reviews collection, overwriting data if the review 60 | /// already exists. 61 | func add(review: Review) { 62 | self.reviews.document(review.documentID).setData(review.documentData) 63 | } 64 | 65 | /// Writes a yum to the yums subcollection for a specific review. 66 | func add(yum: Yum, forReview reviewID: String) { 67 | self.yums(forReview: reviewID).document(yum.documentID).setData(yum.documentData) 68 | } 69 | 70 | } 71 | 72 | extension WriteBatch { 73 | 74 | /// Writes a user to the top-level users collection, overwriting data if the 75 | /// user's uid already exists in the collection. 76 | func add(user: User) { 77 | let document = Firestore.firestore().users.document(user.documentID) 78 | self.setData(user.documentData, forDocument: document) 79 | } 80 | 81 | /// Writes a restaurant to the top-level restaurants collection, overwriting data 82 | /// if the restaurant's ID already exists. 83 | func add(restaurant: Restaurant) { 84 | let document = Firestore.firestore().restaurants.document(restaurant.documentID) 85 | self.setData(restaurant.documentData, forDocument: document) 86 | } 87 | 88 | /// Writes a review to the top-level reviews collection, overwriting data if the review 89 | /// already exists. 90 | func add(review: Review) { 91 | let document = Firestore.firestore().reviews.document(review.documentID) 92 | self.setData(review.documentData, forDocument: document) 93 | } 94 | 95 | /// Writes a yum to the yums subcollection for a specific review. 96 | func add(yum: Yum, toReview: String) { 97 | let document = Firestore.firestore().reviews.document(toReview).collection("yums").document(yum.documentID) 98 | self.setData(yum.documentData, forDocument: document) 99 | } 100 | 101 | } 102 | 103 | // MARK: Pre-populating Firestore data 104 | 105 | extension Firestore { 106 | 107 | /// Returns a tuple of arrays containing sample data to populate the app. 108 | func sampleData() -> (users: [User], restaurants: [Restaurant], reviews: [Review], yums: [(String, Yum)]) { 109 | let userCount = 15 110 | let restaurantCount = 15 111 | let reviewCountPerRestaurant = 3 112 | 113 | // This must be less than or equal to the number of users, 114 | // since yums are unique per user per review. If this number 115 | // exceeds the number of users, the code will likely crash 116 | // when generating likes. 117 | let maxYumCountPerReview = 3 118 | 119 | 120 | // Users must be created first, since Restaurants have dependencies on users, 121 | // Reviews depend on both Users and Restaurants, and Yums depend on Reviews and Users. 122 | // The users generated here will not be backed by real users in Auth, but that's ok. 123 | // User IDs aren't generated by Firestore, since in the real app they'll be pulled from 124 | // an authentication provider (like Firebase Auth) instead of being generated. 125 | let users: [User] = (0 ..< userCount).map { _ in 126 | let uid = UUID().uuidString 127 | let userName = User.randomUsername() 128 | return User(userID: uid, name: userName, photoURL: nil) 129 | } 130 | 131 | func randomUser() -> User { return users[Int(arc4random_uniform(UInt32(userCount)))] } 132 | 133 | var restaurants: [Restaurant] = (0 ..< restaurantCount).map { _ in 134 | let ownerID = randomUser().userID 135 | let name = Restaurant.randomName() 136 | let category = Restaurant.randomCategory() 137 | let city = Restaurant.randomCity() 138 | let price = Restaurant.randomPrice() 139 | let photoURL = Restaurant.randomPhotoURL() 140 | 141 | return Restaurant(ownerID: ownerID, 142 | name: name, 143 | category: category, 144 | city: city, 145 | price: price, 146 | reviewCount: 0, // This is modified later when generating reviews. 147 | averageRating: 0, // This is modified later when generating reviews. 148 | photoURL: photoURL) 149 | } 150 | 151 | var reviews: [Review] = [] 152 | for i in 0 ..< restaurants.count { 153 | var restaurant = restaurants[i] 154 | reviews += (0 ..< reviewCountPerRestaurant).map { _ in 155 | let rating = RandomUniform(5) + 1 156 | let reviewNum = RandomUniform(3) + 1 157 | let userInfo = randomUser() 158 | let text: String 159 | let date = Date() 160 | let restaurantID = restaurant.documentID 161 | 162 | switch (rating, reviewNum) { 163 | case (5, 3): 164 | text = "Amazing!!" 165 | case (5, 2): 166 | text = "This was my favorite meal ever!!" 167 | case (5, 1): 168 | text = "Great service, great food. This is my new favorite restaurant !" 169 | case (4, 3): 170 | text = "Tasty restaurant, would recommend" 171 | case (4, 2): 172 | text = "Really good food for the price" 173 | case (4, 1): 174 | text = "I'd come back here again." 175 | case (3, 3): 176 | text = "Food was good but the service was slow" 177 | case (3, 2): 178 | text = "Pretty average. Nothing to write home about." 179 | case (3, 1): 180 | text = "It was a decent meal, but nothing too memorable." 181 | case (2, 3): 182 | text = "The ketchup was too spicy" 183 | case (2, 2): 184 | text = "The food was cold when it came out" 185 | case (2, 1): 186 | text = "The service was rude." 187 | case (1, 3): 188 | text = "There was a bug in my soup" 189 | case (1, 2): 190 | text = "I'd rather eat a shoe than another meal here." 191 | case (1, 1): 192 | text = "Food was bad, service was slow, place was too loud." 193 | case _: 194 | fatalError("Unreachable code. If the app breaks here, check the call to RandomUniform above.") 195 | } 196 | 197 | // Compute the new average after the review is created. This adds side effects to the map 198 | // statement, angering programmers all over the world 199 | restaurant.averageRating = 200 | (restaurant.averageRating * Double(restaurant.reviewCount) + Double(rating)) 201 | / Double(restaurant.reviewCount + 1) 202 | restaurant.reviewCount += 1 203 | 204 | // Since everything here is value types, we need to explicitly write back to the array. 205 | restaurants[i] = restaurant 206 | 207 | return Review(restaurantID: restaurantID, 208 | restaurantName: restaurant.name, 209 | rating: rating, 210 | userInfo: userInfo, 211 | text: text, 212 | date: date, 213 | yumCount: 0) // This will be modified later when generating Yums. 214 | } 215 | } 216 | 217 | var yums: [(String, Yum)] = [] 218 | for i in 0 ..< reviews.count { 219 | var review = reviews[i] 220 | let numYums = RandomUniform(maxYumCountPerReview) 221 | if numYums == 0 { continue } 222 | 223 | yums += (0 ..< numYums).map { index in 224 | let reviewID = review.documentID 225 | 226 | // index is guaranteed to be less than the number of users. 227 | // Use an index here instead of a random users so users don't 228 | // double-like restaurants, since that's supposed to be illegal. 229 | let userID = users[index].userID 230 | let username = users[index].name 231 | 232 | review.yumCount += 1 233 | reviews[i] = review 234 | 235 | return (reviewID, Yum(documentID: userID, username: username)) 236 | } 237 | } 238 | 239 | return ( 240 | users: users, 241 | restaurants: restaurants, 242 | reviews: reviews, 243 | yums: yums 244 | ) 245 | } 246 | 247 | // Writes data directly to the Firestore root. Useful for populating the app with sample data. 248 | func prepopulate(users: [User], restaurants: [Restaurant], reviews: [Review], yums: [(String, Yum)]) { 249 | let batch = self.batch() 250 | 251 | users.forEach { batch.add(user: $0) } 252 | restaurants.forEach { batch.add(restaurant: $0) } 253 | reviews.forEach { batch.add(review: $0) } 254 | yums.forEach { tuple in 255 | let restaurantID = tuple.0 256 | let yum = tuple.1 257 | batch.add(yum: yum, toReview: restaurantID) 258 | } 259 | 260 | batch.commit { error in 261 | if let error = error { 262 | print("Error populating Firestore: \(error)") 263 | } else { 264 | print("Batch committed!") 265 | } 266 | } 267 | } 268 | 269 | // Pre-populates the app with sample data. 270 | func prepopulate() { 271 | let data = sampleData() 272 | prepopulate(users: data.users, 273 | restaurants: data.restaurants, 274 | reviews: data.reviews, 275 | yums: data.yums) 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /FriendlyEats/Common/LocalCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseFirestore 18 | 19 | /// A type that can be initialized from a Firestore document. 20 | public protocol DocumentSerializable { 21 | 22 | /// Initializes an instance from a Firestore document. May fail if the 23 | /// document is missing required fields. 24 | init?(document: QueryDocumentSnapshot) 25 | 26 | /// Initializes an instance from a Firestore document. May fail if the 27 | /// document does not exist or is missing required fields. 28 | init?(document: DocumentSnapshot) 29 | 30 | /// The documentID of the object in Firestore. 31 | var documentID: String { get } 32 | 33 | /// The representation of a document-serializable object in Firestore. 34 | var documentData: [String: Any] { get } 35 | 36 | } 37 | 38 | final class LocalCollection { 39 | 40 | private(set) var items: [T] 41 | private(set) var documents: [DocumentSnapshot] = [] 42 | let query: Query 43 | 44 | private let updateHandler: ([DocumentChange]) -> () 45 | 46 | private var listener: ListenerRegistration? { 47 | didSet { 48 | oldValue?.remove() 49 | } 50 | } 51 | 52 | var count: Int { 53 | return self.items.count 54 | } 55 | 56 | subscript(index: Int) -> T { 57 | return self.items[index] 58 | } 59 | 60 | init(query: Query, updateHandler: @escaping ([DocumentChange]) -> ()) { 61 | self.items = [] 62 | self.query = query 63 | self.updateHandler = updateHandler 64 | } 65 | 66 | func index(of document: DocumentSnapshot) -> Int? { 67 | for i in 0 ..< documents.count { 68 | if documents[i].documentID == document.documentID { 69 | return i 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func listen() { 77 | guard listener == nil else { return } 78 | listener = query.addSnapshotListener { [unowned self] querySnapshot, error in 79 | guard let snapshot = querySnapshot else { 80 | print("Error fetching snapshot results: \(error!)") 81 | return 82 | } 83 | let models = snapshot.documents.map { (document) -> T in 84 | if let model = T(document: document) { 85 | return model 86 | } else { 87 | // handle error 88 | fatalError("Unable to initialize type \(T.self) with dictionary \(document.data())") 89 | } 90 | } 91 | self.items = models 92 | self.documents = snapshot.documents 93 | self.updateHandler(snapshot.documentChanges) 94 | } 95 | } 96 | 97 | func stopListening() { 98 | listener?.remove() 99 | listener = nil 100 | } 101 | 102 | deinit { 103 | stopListening() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /FriendlyEats/Common/RestaurantTableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import FirebaseFirestore 19 | 20 | /// A class that populates a table view using RestaurantTableViewCell cells 21 | /// with restaurant data from a Firestore query. Consumers should update the 22 | /// table view with new data from Firestore in the updateHandler closure. 23 | @objc class RestaurantTableViewDataSource: NSObject, UITableViewDataSource { 24 | 25 | private var restaurants: [Restaurant] = [] 26 | 27 | public init(query: Query, 28 | updateHandler: @escaping ([DocumentChange]) -> ()) { 29 | fatalError("Unimplemented") 30 | } 31 | 32 | 33 | // Pull data from Firestore 34 | 35 | /// Starts listening to the Firestore query and invoking the updateHandler. 36 | public func startUpdates() { 37 | fatalError("Unimplemented") 38 | } 39 | 40 | /// Stops listening to the Firestore query. updateHandler will not be called unless startListening 41 | /// is called again. 42 | public func stopUpdates() { 43 | fatalError("Unimplemented") 44 | } 45 | 46 | /// Returns the restaurant at the given index. 47 | subscript(index: Int) -> Restaurant { 48 | return restaurants[index] 49 | } 50 | 51 | /// The number of items in the data source. 52 | public var count: Int { 53 | return restaurants.count 54 | } 55 | 56 | // MARK: - UITableViewDataSource 57 | 58 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 59 | return count 60 | } 61 | 62 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 63 | let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", 64 | for: indexPath) as! RestaurantTableViewCell 65 | let restaurant = restaurants[indexPath.row] 66 | cell.populate(restaurant: restaurant) 67 | return cell 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /FriendlyEats/Common/ReviewTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import Firebase 19 | 20 | class ReviewTableViewCell: UITableViewCell { 21 | 22 | @IBOutlet var usernameLabel: UILabel? 23 | @IBOutlet var reviewContentsLabel: UILabel! 24 | @IBOutlet var starsView: ImmutableStarsView! 25 | @IBOutlet weak var yumsLabel: UILabel! 26 | @IBOutlet weak var userIcon: UIImageView? 27 | @IBOutlet weak var yumButton: UIButton! 28 | @IBOutlet weak var restaurantNameLabel: UILabel? 29 | 30 | var review: Review! 31 | 32 | func populate(review: Review) { 33 | 34 | } 35 | 36 | @IBAction func yumWasTapped(_ sender: Any) { 37 | // TODO: Let's increment the yumCount! 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /FriendlyEats/Common/ReviewTableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import FirebaseFirestore 19 | 20 | /// A class that populates a table view using ReviewTableViewCell cells 21 | /// with review data from a Firestore query. Consumers should update the 22 | /// table view with new data from Firestore in the updateHandler closure. 23 | @objc class ReviewTableViewDataSource: NSObject, UITableViewDataSource { 24 | 25 | private let reviews: LocalCollection 26 | var sectionTitle: String? 27 | 28 | /// Returns an instance of ReviewTableViewDataSource. Consumers should update the 29 | /// table view with new data from Firestore in the updateHandler closure. 30 | public init(reviews: LocalCollection) { 31 | self.reviews = reviews 32 | } 33 | 34 | /// Returns an instance of ReviewTableViewDataSource. Consumers should update the 35 | /// table view with new data from Firestore in the updateHandler closure. 36 | public convenience init(query: Query, updateHandler: @escaping ([DocumentChange]) -> ()) { 37 | let collection = LocalCollection(query: query, updateHandler: updateHandler) 38 | self.init(reviews: collection) 39 | } 40 | 41 | /// Starts listening to the Firestore query and invoking the updateHandler. 42 | public func startUpdates() { 43 | reviews.listen() 44 | } 45 | 46 | /// Stops listening to the Firestore query. updateHandler will not be called unless startListening 47 | /// is called again. 48 | public func stopUpdates() { 49 | reviews.stopListening() 50 | } 51 | 52 | 53 | /// Returns the review at the given index. 54 | subscript(index: Int) -> Review { 55 | return reviews[index] 56 | } 57 | 58 | /// The number of items in the data source. 59 | public var count: Int { 60 | return reviews.count 61 | } 62 | 63 | // MARK: - UITableViewDataSource 64 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 65 | return sectionTitle 66 | } 67 | 68 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 69 | return count 70 | } 71 | 72 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 73 | let cell = tableView.dequeueReusableCell(withIdentifier: "ReviewTableViewCell", 74 | for: indexPath) as! ReviewTableViewCell 75 | let review = reviews[indexPath.row] 76 | cell.populate(review: review) 77 | return cell 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /FriendlyEats/Common/StarsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | class RatingView: UIControl { 20 | 21 | var highlightedColor: CGColor = Constants.highlightedColorOrange 22 | 23 | var rating: Int? { 24 | didSet { 25 | if let value = rating { 26 | setHighlighted(index: value - 1) 27 | } else { 28 | clearAll() 29 | } 30 | 31 | // highlight the appropriate amount of stars. 32 | sendActions(for: .valueChanged) 33 | } 34 | } 35 | 36 | private let starLayers: [CAShapeLayer] 37 | 38 | override init(frame: CGRect) { 39 | starLayers = (0 ..< 5).map { 40 | let layer = RatingView.starLayer() 41 | layer.frame = CGRect(x: $0 * 55, y: 0, width: 25, height: 25) 42 | return layer 43 | } 44 | super.init(frame: frame) 45 | 46 | starLayers.forEach { 47 | layer.addSublayer($0) 48 | } 49 | } 50 | 51 | private var starWidth: CGFloat { 52 | return intrinsicContentSize.width / 5 53 | } 54 | 55 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 56 | super.touchesBegan(touches, with: event) 57 | guard let touch = touches.first else { return } 58 | let point = touch.location(in: self) 59 | let index = clamp(Int(point.x / starWidth)) 60 | setHighlighted(index: index) 61 | } 62 | 63 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 64 | super.touchesMoved(touches, with: event) 65 | guard let touch = touches.first else { return } 66 | let point = touch.location(in: self) 67 | let index = clamp(Int(point.x / starWidth)) 68 | setHighlighted(index: index) 69 | } 70 | 71 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 72 | super.touchesEnded(touches, with: event) 73 | guard let touch = touches.first else { return } 74 | let point = touch.location(in: self) 75 | let index = clamp(Int(point.x / starWidth)) 76 | rating = index + 1 // Ratings are 1-indexed; things can be between 1-5 stars. 77 | } 78 | 79 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 80 | super.touchesCancelled(touches, with: event) 81 | guard touches.first != nil else { return } 82 | 83 | // Cancelled touches should preserve the value before the interaction. 84 | if let oldRating = rating { 85 | let oldIndex = oldRating - 1 86 | setHighlighted(index: oldIndex) 87 | } else { 88 | clearAll() 89 | } 90 | } 91 | 92 | /// This is an awful func name. Index must be within 0 ..< 4, or crash. 93 | private func setHighlighted(index: Int) { 94 | // Highlight everything up to and including the star at the index. 95 | (0 ... index).forEach { 96 | let star = starLayers[$0] 97 | star.strokeColor = highlightedColor 98 | star.fillColor = highlightedColor 99 | } 100 | 101 | // Unhighlight everything after the index, if applicable. 102 | guard index < 4 else { return } 103 | ((index + 1) ..< 5).forEach { 104 | let star = starLayers[$0] 105 | star.strokeColor = highlightedColor 106 | star.fillColor = nil 107 | } 108 | } 109 | 110 | /// Unhighlights every star. 111 | private func clearAll() { 112 | (0 ..< 5).forEach { 113 | let star = starLayers[$0] 114 | star.strokeColor = highlightedColor 115 | star.fillColor = nil 116 | } 117 | } 118 | 119 | private func clamp(_ index: Int) -> Int { 120 | if index < 0 { return 0 } 121 | if index > 4 { return 4 } 122 | return index 123 | } 124 | 125 | override var intrinsicContentSize: CGSize { 126 | return CGSize(width: 270, height: 50) 127 | } 128 | 129 | override var isMultipleTouchEnabled: Bool { 130 | get { return false } 131 | set {} 132 | } 133 | 134 | private static func starLayer() -> CAShapeLayer { 135 | let layer = CAShapeLayer() 136 | 137 | let mutablePath = CGMutablePath() 138 | 139 | let outerRadius: CGFloat = 18 140 | let outerPoints = stride(from: CGFloat.pi / -5, to: .pi * 2, by: 2 * .pi / 5).map { 141 | return CGPoint(x: outerRadius * sin($0) + 25, 142 | y: outerRadius * cos($0) + 25) 143 | } 144 | 145 | let innerRadius: CGFloat = 6 146 | let innerPoints = stride(from: 0, to: .pi * 2, by: 2 * .pi / 5).map { 147 | return CGPoint(x: innerRadius * sin($0) + 25, 148 | y: innerRadius * cos($0) + 25) 149 | } 150 | 151 | let points = zip(outerPoints, innerPoints).reduce([CGPoint]()) { (aggregate, pair) -> [CGPoint] in 152 | return aggregate + [pair.0, pair.1] 153 | } 154 | 155 | mutablePath.move(to: points[0]) 156 | points.forEach { 157 | mutablePath.addLine(to: $0) 158 | } 159 | mutablePath.closeSubpath() 160 | 161 | layer.path = mutablePath.copy() 162 | layer.strokeColor = UIColor.gray.cgColor 163 | layer.lineWidth = 1 164 | layer.fillColor = nil 165 | 166 | return layer 167 | } 168 | 169 | @available(*, unavailable) 170 | required convenience init?(coder aDecoder: NSCoder) { 171 | // coder is ignored. 172 | self.init(frame: CGRect(x: 0, y: 0, width: 270, height: 50)) 173 | self.translatesAutoresizingMaskIntoConstraints = false 174 | } 175 | 176 | private enum Constants { 177 | static let unhighlightedColor = UIColor.gray.cgColor 178 | static let highlightedColorOrange = UIColor(red: 255 / 255, green: 179 / 255, blue: 0 / 255, alpha: 1).cgColor 179 | } 180 | 181 | // MARK: Rating View Accessibility 182 | 183 | override var isAccessibilityElement: Bool { 184 | get { return true } 185 | set {} 186 | } 187 | 188 | override var accessibilityValue: String? { 189 | get { 190 | if let rating = rating { 191 | return NSLocalizedString("\(rating) out of 5", comment: "Format string for indicating a variable amount of stars out of five") 192 | } 193 | return NSLocalizedString("No rating", comment: "Read by VoiceOver to vision-impaired users indicating a rating that hasn't been filled out yet") 194 | } 195 | set {} 196 | } 197 | 198 | override var accessibilityTraits: UIAccessibilityTraits { 199 | get { return UIAccessibilityTraits.adjustable } 200 | set {} 201 | } 202 | 203 | override func accessibilityIncrement() { 204 | let currentRatingIndex = (rating ?? 0) - 1 205 | let highlightedIndex = clamp(currentRatingIndex + 1) 206 | rating = highlightedIndex + 1 207 | } 208 | 209 | override func accessibilityDecrement() { 210 | guard let rating = rating else { return } // Doesn't make sense to decrement no rating and get 1. 211 | let currentRatingIndex = rating - 1 212 | let highlightedIndex = clamp(currentRatingIndex - 1) 213 | self.rating = highlightedIndex + 1 214 | } 215 | 216 | } 217 | 218 | // This class is absolutely not immutable, but it's also not user-interactive. 219 | class ImmutableStarsView: UIView { 220 | 221 | override var intrinsicContentSize: CGSize { 222 | get { return CGSize(width: 100, height: 20) } 223 | set {} 224 | } 225 | 226 | var highlightedColor: CGColor = Constants.highlightedColorOrange 227 | 228 | var rating: Int? { 229 | didSet { 230 | if let value = rating { 231 | setHighlighted(index: value - 1) 232 | } else { 233 | clearAll() 234 | } 235 | } 236 | } 237 | 238 | private let starLayers: [CAShapeLayer] 239 | 240 | override init(frame: CGRect) { 241 | starLayers = (0 ..< 5).map { 242 | let layer = ImmutableStarsView.starLayer() 243 | layer.frame = CGRect(x: $0 * 20, y: 0, width: 20, height: 20) 244 | return layer 245 | } 246 | super.init(frame: frame) 247 | 248 | starLayers.forEach { 249 | layer.addSublayer($0) 250 | } 251 | } 252 | 253 | private var starWidth: CGFloat { 254 | return intrinsicContentSize.width / 5 255 | } 256 | 257 | /// This is an awful func name. Index must be within 0 ..< 4, or crash. 258 | private func setHighlighted(index anyIndex: Int) { 259 | if anyIndex < 0 { 260 | clearAll() 261 | return 262 | } 263 | let index = self.clamp(anyIndex) 264 | // Highlight everything up to and including the star at the index. 265 | (0 ... index).forEach { 266 | let star = starLayers[$0] 267 | star.strokeColor = highlightedColor 268 | star.fillColor = highlightedColor 269 | } 270 | 271 | // Unhighlight everything after the index, if applicable. 272 | guard index < 4 else { return } 273 | ((index + 1) ..< 5).forEach { 274 | let star = starLayers[$0] 275 | star.strokeColor = Constants.unhighlightedColor 276 | star.fillColor = nil 277 | } 278 | } 279 | 280 | /// Unhighlights every star. 281 | private func clearAll() { 282 | (0 ..< 5).forEach { 283 | let star = starLayers[$0] 284 | star.strokeColor = Constants.unhighlightedColor 285 | star.fillColor = nil 286 | } 287 | } 288 | 289 | private func clamp(_ index: Int) -> Int { 290 | if index < 0 { return 0 } 291 | if index >= 5 { return 4 } 292 | return index 293 | } 294 | 295 | override var isUserInteractionEnabled: Bool { 296 | get { return false } 297 | set {} 298 | } 299 | 300 | private static func starLayer() -> CAShapeLayer { 301 | let layer = CAShapeLayer() 302 | 303 | let mutablePath = CGMutablePath() 304 | 305 | let outerRadius: CGFloat = 9 306 | let outerPoints = stride(from: CGFloat.pi / -5, to: .pi * 2, by: 2 * .pi / 5).map { 307 | return CGPoint(x: outerRadius * sin($0) + 9, 308 | y: outerRadius * cos($0) + 9) 309 | } 310 | 311 | let innerRadius: CGFloat = 4 312 | let innerPoints = stride(from: 0, to: .pi * 2, by: 2 * .pi / 5).map { 313 | return CGPoint(x: innerRadius * sin($0) + 9, 314 | y: innerRadius * cos($0) + 9) 315 | } 316 | 317 | let points = zip(outerPoints, innerPoints).reduce([CGPoint]()) { (aggregate, pair) -> [CGPoint] in 318 | return aggregate + [pair.0, pair.1] 319 | } 320 | 321 | mutablePath.move(to: points[0]) 322 | points.forEach { 323 | mutablePath.addLine(to: $0) 324 | } 325 | mutablePath.closeSubpath() 326 | 327 | layer.path = mutablePath.copy() 328 | layer.strokeColor = UIColor.gray.cgColor 329 | layer.lineWidth = 1 330 | layer.fillColor = nil 331 | 332 | return layer 333 | } 334 | 335 | @available(*, unavailable) 336 | required convenience init?(coder aDecoder: NSCoder) { 337 | // coder is ignored. 338 | self.init(frame: CGRect(x: 0, y: 0, width: 270, height: 50)) 339 | self.translatesAutoresizingMaskIntoConstraints = false 340 | } 341 | 342 | private enum Constants { 343 | static let unhighlightedColor = UIColor.gray.cgColor 344 | static let highlightedColorOrange = UIColor(red: 255 / 255, green: 179 / 255, blue: 0 / 255, alpha: 1).cgColor 345 | } 346 | 347 | } 348 | -------------------------------------------------------------------------------- /FriendlyEats/Common/Utils.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | 17 | import UIKit 18 | 19 | class Utils: NSObject { 20 | 21 | static func showSimpleAlert(message: String, presentingVC: UIViewController) { 22 | Utils.showSimpleAlert(title: nil, message: message, presentingVC: presentingVC) 23 | } 24 | 25 | static func showSimpleAlert(title: String?, message: String, presentingVC: UIViewController) { 26 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 27 | let okAction = UIAlertAction(title: "OK", style: .default) 28 | alertController.addAction(okAction) 29 | presentingVC.present(alertController, animated: true, completion: nil) 30 | } 31 | 32 | static func priceString(from price: Int) -> String { 33 | return (0 ..< price).reduce("") { s, _ in s + "$" } 34 | } 35 | 36 | static func priceValue(from string: String?) -> Int? { 37 | guard let string = string else { return nil } 38 | // TODO: Maybe ensure that we're only counting dollar signs 39 | return string.count 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /FriendlyEats/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /FriendlyEats/Models/Restaurant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseFirestore 18 | 19 | /// A restaurant, created by a user. 20 | struct Restaurant { 21 | 22 | /// The ID of the restaurant, generated from Firestore. 23 | var documentID: String 24 | 25 | /// The restaurant owner's uid. Corresponds to a user object in the top-level Users collection. 26 | var ownerID: String 27 | 28 | /// The name of the restaurant. 29 | var name: String 30 | 31 | /// The category of the restaurant. 32 | var category: String 33 | 34 | /// The city the restaurant is located in. 35 | var city: String 36 | 37 | /// The price category of the restaurant. Values are clamped between 1 and 3 (inclusive). 38 | var price: Int 39 | 40 | /// The number of reviews that have been left for this restaurant. 41 | var reviewCount: Int 42 | 43 | /// The average rating of all the restaurant's reviews. 44 | var averageRating: Double 45 | 46 | /// The restaurant's photo URL. These are stored as strings in Firestore. 47 | var photoURL: URL 48 | 49 | } 50 | 51 | // MARK: - Firestore interoperability 52 | 53 | extension Restaurant: DocumentSerializable { 54 | 55 | /// Initializes a restaurant with a documentID auto-generated by Firestore. 56 | init(ownerID: String, 57 | name: String, 58 | category: String, 59 | city: String, 60 | price: Int, 61 | reviewCount: Int, 62 | averageRating: Double, 63 | photoURL: URL) { 64 | let document = Firestore.firestore().restaurants.document() 65 | self.init(documentID: document.documentID, 66 | ownerID: ownerID, 67 | name: name, 68 | category: category, 69 | city: city, 70 | price: price, 71 | reviewCount: reviewCount, 72 | averageRating: averageRating, 73 | photoURL: photoURL) 74 | } 75 | 76 | /// Initializes a restaurant from a documentID and some data, ostensibly from Firestore. 77 | private init?(documentID: String, dictionary: [String: Any]) { 78 | guard let ownerID = dictionary["ownerID"] as? String, 79 | let name = dictionary["name"] as? String, 80 | let category = dictionary["category"] as? String, 81 | let city = dictionary["city"] as? String, 82 | let price = dictionary["price"] as? Int, 83 | let reviewCount = dictionary["reviewCount"] as? Int, 84 | let averageRating = dictionary["averageRating"] as? Double, 85 | let photoURLString = dictionary["photoURL"] as? String else { return nil } 86 | 87 | guard let photoURL = URL(string: photoURLString) else { return nil } 88 | 89 | self.init(documentID: documentID, 90 | ownerID: ownerID, 91 | name: name, 92 | category: category, 93 | city: city, 94 | price: price, 95 | reviewCount: reviewCount, 96 | averageRating: averageRating, 97 | photoURL: photoURL) 98 | } 99 | 100 | init?(document: QueryDocumentSnapshot) { 101 | self.init(documentID: document.documentID, dictionary: document.data()) 102 | } 103 | 104 | init?(document: DocumentSnapshot) { 105 | guard let data = document.data() else { return nil } 106 | self.init(documentID: document.documentID, dictionary: data) 107 | } 108 | 109 | /// The dictionary representation of the restaurant for uploading to Firestore. 110 | var documentData: [String: Any] { 111 | return [ 112 | "ownerID": ownerID, 113 | "name": name, 114 | "category": category, 115 | "city": city, 116 | "price": price, 117 | "reviewCount": reviewCount, 118 | "averageRating": averageRating, 119 | "photoURL": photoURL.absoluteString 120 | ] 121 | } 122 | 123 | } 124 | 125 | // MARK: - Data generation 126 | 127 | /// A wrapper of arc4random_uniform, to avoid lots of casting. 128 | func RandomUniform(_ upperBound: Int) -> Int { 129 | return Int(arc4random_uniform(UInt32(upperBound))) 130 | } 131 | 132 | /// A helper for restaurant generation. 133 | extension Restaurant { 134 | 135 | // TODO(morganchen): For non-US audiences, we may want to localize these to non-US cities. 136 | static let cities = [ 137 | "Albuquerque", 138 | "Arlington", 139 | "Atlanta", 140 | "Austin", 141 | "Baltimore", 142 | "Boston", 143 | "Charlotte", 144 | "Chicago", 145 | "Cleveland", 146 | "Colorado Springs", 147 | "Columbus", 148 | "Dallas", 149 | "Denver", 150 | "Detroit", 151 | "El Paso", 152 | "Fort Worth", 153 | "Fresno", 154 | "Houston", 155 | "Indianapolis", 156 | "Jacksonville", 157 | "Kansas City", 158 | "Las Vegas", 159 | "Long Beach", 160 | "Los Angeles", 161 | "Louisville", 162 | "Memphis", 163 | "Mesa", 164 | "Miami", 165 | "Milwaukee", 166 | "Nashville", 167 | "New York", 168 | "Oakland", 169 | "Oklahoma", 170 | "Omaha", 171 | "Philadelphia", 172 | "Phoenix", 173 | "Portland", 174 | "Raleigh", 175 | "Sacramento", 176 | "San Antonio", 177 | "San Diego", 178 | "San Francisco", 179 | "San Jose", 180 | "Tucson", 181 | "Tulsa", 182 | "Virginia Beach", 183 | "Washington" 184 | ] 185 | 186 | static let categories = [ 187 | "Brunch", "Burgers", "Coffee", "Deli", "Dim Sum", "Indian", "Italian", 188 | "Mediterranean", "Mexican", "Pizza", "Ramen", "Sushi" 189 | ] 190 | 191 | static func randomName() -> String { 192 | let prefixes = ["Morgan's", "Jen's", "Todd's", "Best", "Mom's", "Down home", "Fire", "Tasty", "Spicy", "Delish", "Divine", "Scrumptious"] 193 | let suffixes = ["Bar", "House", "Grill", "Drive Thru", "Place", "Spot", "Inn", "Joint", "Diner", "Cafe", "Hideaway"] 194 | let randomIndexes = (RandomUniform(prefixes.count), RandomUniform(suffixes.count)) 195 | return prefixes[randomIndexes.0] + " " + suffixes[randomIndexes.1] 196 | } 197 | 198 | static func randomCategory() -> String { 199 | return Restaurant.categories[RandomUniform(Restaurant.categories.count)] 200 | } 201 | 202 | static func randomCity() -> String { 203 | return Restaurant.cities[RandomUniform(Restaurant.cities.count)] 204 | } 205 | 206 | static func randomPrice() -> Int { 207 | return RandomUniform(3) + 1 208 | } 209 | 210 | static func randomPhotoURL() -> URL { 211 | let number = RandomUniform(22) + 1 212 | let URLString = 213 | "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_\(number).png" 214 | return URL(string: URLString)! 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /FriendlyEats/Models/Review.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseFirestore 18 | 19 | /// A review of a restaurant, created by a user. 20 | struct Review { 21 | 22 | /// A unique ID identifying the review, generated by Firestore. 23 | var documentID: String 24 | 25 | /// The restaurant that this review is reviewing. 26 | var restaurantID: String 27 | 28 | /// The name of the restaurant for this review. 29 | var restaurantName: String 30 | 31 | /// The rating given to the restaurant. Values range between 1 and 5. 32 | var rating: Int 33 | 34 | /// User information duplicated in the review object. 35 | var userInfo: User 36 | 37 | /// The body text of the review, containing the user's comments. 38 | var text: String 39 | 40 | /// The date the review was posted. 41 | var date: Date 42 | 43 | /// The number of yums (likes) that the review has received. 44 | var yumCount: Int 45 | 46 | } 47 | 48 | // MARK: Firestore interoperability 49 | 50 | extension Review: DocumentSerializable { 51 | 52 | /// Initializes a review from a dictionary. Returns nil if any fields are missing, or if 53 | /// the User object is not serializable. 54 | private init?(documentID: String, dictionary: [String : Any]) { 55 | guard let restaurantID = dictionary["restaurantID"] as? String, 56 | let restaurantName = dictionary["restaurantName"] as? String, 57 | let rating = dictionary["rating"] as? Int, 58 | let userInfo = dictionary["userInfo"] as? [String: Any], 59 | let text = dictionary["text"] as? String, 60 | let timestamp = dictionary["date"] as? Timestamp, 61 | let yumCount = dictionary["yumCount"] as? Int else { return nil } 62 | 63 | guard let user = User(dictionary: userInfo) else { return nil } 64 | self.init(documentID: documentID, 65 | restaurantID: restaurantID, 66 | restaurantName: restaurantName, 67 | rating: rating, 68 | userInfo: user, 69 | text: text, 70 | date: timestamp.dateValue(), 71 | yumCount: yumCount) 72 | } 73 | 74 | public init?(document: QueryDocumentSnapshot) { 75 | self.init(documentID: document.documentID, dictionary: document.data()) 76 | } 77 | 78 | public init?(document: DocumentSnapshot) { 79 | guard let data = document.data() else { return nil } 80 | self.init(documentID: document.documentID, dictionary: data) 81 | } 82 | 83 | public init(restaurantID: String, 84 | restaurantName: String, 85 | rating: Int, 86 | userInfo: User, 87 | text: String, 88 | date: Date, 89 | yumCount: Int) { 90 | let document = Firestore.firestore().reviews.document() 91 | self.init(documentID: document.documentID, 92 | restaurantID: restaurantID, 93 | restaurantName: restaurantName, 94 | rating: rating, 95 | userInfo: userInfo, 96 | text: text, 97 | date: date, 98 | yumCount: yumCount) 99 | } 100 | 101 | /// A review's representation in Firestore. 102 | var documentData: [String: Any] { 103 | return [ 104 | "restaurantID": restaurantID, 105 | "restaurantName": restaurantName, 106 | "rating": rating, 107 | "userInfo": userInfo.documentData, 108 | "text": text, 109 | "date": Timestamp(date:date), 110 | "yumCount": yumCount 111 | ] 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /FriendlyEats/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseFirestore 18 | import FirebaseAuth 19 | 20 | /// A user corresponding to a Firebase user. Additional metadata for each user is stored in 21 | /// Firestore. 22 | struct User { 23 | 24 | /// The ID of the user. This corresponds with a Firebase user's uid property. 25 | var userID: String 26 | 27 | /// The display name of the user. Users with unspecified display names are given a default name. 28 | var name: String 29 | 30 | /// A url to the user's profile photo. Users with unspecified profile pictures are given a 31 | /// default profile picture. 32 | var photoURL: URL 33 | 34 | } 35 | 36 | extension User: DocumentSerializable { 37 | 38 | /// All users are stored by their userIDs for easier querying later. 39 | var documentID: String { 40 | return userID 41 | } 42 | 43 | /// The default URL for profile images. 44 | static let defaultPhotoURL = 45 | URL(string: "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_1.png")! 46 | 47 | /// Initializes a User from document snapshot data. 48 | private init?(documentID: String, dictionary: [String : Any]) { 49 | guard let userID = dictionary["userID"] as? String else { return nil } 50 | 51 | // This is something that should be verified on the server using a security rule. 52 | // In order to maintain a consistent database, all users must be stored in the top-level 53 | // users collection by their userID. Some queries are dependent on this consistency. 54 | precondition(userID == documentID) 55 | 56 | self.init(dictionary: dictionary) 57 | } 58 | 59 | /// A convenience initializer for user data that won't be written to the Users collection 60 | /// in Firestore. Unlike the other data types, users aren't dependent on Firestore to 61 | /// generate unique identifiers, since they come with unique identifiers for free. 62 | public init?(dictionary: [String: Any]) { 63 | guard let name = dictionary["name"] as? String, 64 | let userID = dictionary["userID"] as? String, 65 | let photoURLString = dictionary["photoURL"] as? String else { return nil } 66 | guard let photoURL = URL(string: photoURLString) else { return nil } 67 | 68 | self.init(userID: userID, name: name, photoURL: photoURL) 69 | } 70 | 71 | public init?(document: QueryDocumentSnapshot) { 72 | self.init(documentID: document.documentID, dictionary: document.data()) 73 | } 74 | 75 | public init?(document: DocumentSnapshot) { 76 | guard let data = document.data() else { return nil } 77 | self.init(documentID: document.documentID, dictionary: data) 78 | } 79 | 80 | /// Initializes a new User from a Firebase user object. 81 | public init(user: FirebaseAuth.UserInfo) { 82 | self.init(userID: user.uid, 83 | name: user.displayName, 84 | photoURL: user.photoURL) 85 | } 86 | 87 | /// Returns a new User, providing a default name and photoURL if passed nil or left unspecified. 88 | public init(userID: String, 89 | name: String? = "FriendlyEats User", 90 | photoURL: URL? = User.defaultPhotoURL) { 91 | self.init(userID: userID, 92 | name: name ?? "FriendlyEats User", 93 | photoURL: photoURL ?? User.defaultPhotoURL) 94 | } 95 | 96 | /// Returns a randomly-generated User without checking for uid collisions, with the default name 97 | /// and profile picture. 98 | public init() { 99 | let uid = UUID().uuidString 100 | self.init(userID: uid) 101 | } 102 | 103 | /// A user object's representation in Firestore. 104 | public var documentData: [String: Any] { 105 | return [ 106 | "userID": userID, 107 | "name": name, 108 | "photoURL": photoURL.absoluteString 109 | ] 110 | } 111 | 112 | } 113 | 114 | // MARK: - Data generation 115 | 116 | 117 | /// A helper for user generation. 118 | extension User { 119 | 120 | static let firstNames = ["Sophia", "Jackson", "Olivia", "Liam", "Emma", "Noah", "Ava", "Aiden", 121 | "Isabella", "Lucas", "Mia", "Caden", "Aria", "Grayson", "Riley", "Mason"] 122 | 123 | static let lastNames = ["Smith", "Johnson", "Williams", "Jones", "Brown", "Davis", "Miller", "Wilson", 124 | "Moore", "Taylor", "Anderson", "Thomas", "Jackson", "White", "Harris", "Martin", 125 | "Thompson", "Garcia", "Martinez", "Robinson", "Clark", "Rodriguez", "Lewis", "Lee"] 126 | 127 | static func randomUsername() -> String { 128 | let randomIndexes = (RandomUniform(firstNames.count), RandomUniform(lastNames.count)) 129 | return firstNames[randomIndexes.0] + " " + lastNames[randomIndexes.1] 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /FriendlyEats/Models/Yum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseFirestore 18 | 19 | /// A struct corresponding to a 'like' on social media. 20 | struct Yum { 21 | 22 | /// The document ID for this particular yum. This will serve double-duty as 23 | /// the userID of the user who left the review as well, which will help guarantee 24 | /// only one yum per user per review. 25 | var documentID: String 26 | 27 | /// The name of the user who yummed this review. Although this is out of the 28 | /// scope of this sample app, this could be used to build a "This review was yummed by 29 | /// Bob Smith, Alex Avery, and 3 others" kind of message 30 | var username: String 31 | 32 | 33 | } 34 | 35 | // MARK: - Firestore interoperability 36 | 37 | extension Yum: DocumentSerializable { 38 | 39 | /// Initializes a Yum from Firestore document data. 40 | public init?(documentAndUserID: String, dictionary: [String : Any]) { 41 | guard let username = dictionary["username"] as? String else { return nil } 42 | self.init(documentID: documentAndUserID, username: username) 43 | } 44 | 45 | public init?(document: DocumentSnapshot) { 46 | guard let data = document.data() else { return nil } 47 | self.init(documentAndUserID: document.documentID, dictionary: data) 48 | } 49 | 50 | public init?(document: QueryDocumentSnapshot) { 51 | self.init(documentAndUserID: document.documentID, dictionary: document.data()) 52 | } 53 | 54 | /// Returns a dictionary representation of a Yum. 55 | public var documentData: [String: Any] { 56 | return [ 57 | "username": username, 58 | ] 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /FriendlyEats/Profile/AddRestaurantViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | import UIKit 17 | import Firebase 18 | import FirebaseStorage 19 | 20 | class AddRestaurantViewController: UIViewController, UINavigationControllerDelegate, UIPickerViewDataSource, UIPickerViewDelegate { 21 | 22 | // MARK: Properties 23 | 24 | private var user: User! 25 | private lazy var restaurant: Restaurant = { 26 | return Restaurant(ownerID: user.userID, name: "", category: "", city: "", price: 0, reviewCount: 0, averageRating: 0, photoURL: Restaurant.randomPhotoURL()) 27 | }() 28 | private var imagePicker = UIImagePickerController() 29 | private var downloadUrl: String? 30 | 31 | // MARK: Outlets 32 | 33 | @IBOutlet private weak var restaurantImageView: UIImageView! 34 | @IBOutlet private weak var restaurantNameTextField: UITextField! 35 | @IBOutlet private weak var cityTextField: UITextField! { 36 | didSet { 37 | cityTextField.inputView = cityPickerView 38 | } 39 | } 40 | @IBOutlet private weak var categoryTextField: UITextField! { 41 | didSet { 42 | categoryTextField.inputView = categoryPickerView 43 | } 44 | } 45 | @IBOutlet private weak var priceTextField: UITextField! { 46 | didSet { 47 | priceTextField.inputView = pricePickerView 48 | } 49 | } 50 | @IBOutlet fileprivate weak var addPhotoButton: UIButton! 51 | 52 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)) 53 | -> AddRestaurantViewController { 54 | let controller = storyboard.instantiateViewController(withIdentifier: "AddRestaurantViewController") 55 | as! AddRestaurantViewController 56 | return controller 57 | } 58 | 59 | override func viewDidLoad() { 60 | super.viewDidLoad() 61 | user = User(user: Auth.auth().currentUser!) 62 | restaurantImageView.contentMode = .scaleAspectFill 63 | restaurantImageView.clipsToBounds = true 64 | hideKeyboardWhenTappedAround() 65 | } 66 | 67 | func saveChanges() { 68 | guard let name = restaurantNameTextField.text, 69 | let city = cityTextField.text, 70 | let category = categoryTextField.text, 71 | let price = Utils.priceValue(from: priceTextField.text) else { 72 | self.presentInvalidDataAlert(message: "All fields must be filled out.") 73 | return 74 | } 75 | restaurant.name = name 76 | restaurant.city = city 77 | restaurant.category = category 78 | restaurant.price = price 79 | // if photo was changed, add the new url 80 | if let downloadUrl = downloadUrl { 81 | restaurant.photoURL = URL(string: downloadUrl)! 82 | } 83 | print("Going to save document data as \(restaurant.documentData)") 84 | 85 | // TODO: Save the restaurant document to Cloud Firestore 86 | 87 | self.presentDidSaveAlert() 88 | } 89 | 90 | // MARK: Setting up pickers 91 | 92 | private let priceOptions = ["$", "$$", "$$$"] 93 | private let cityOptions = Restaurant.cities 94 | private let categoryOptions = Restaurant.categories 95 | 96 | private lazy var cityPickerView: UIPickerView = { 97 | let pickerView = UIPickerView() 98 | pickerView.dataSource = self 99 | pickerView.delegate = self 100 | return pickerView 101 | }() 102 | 103 | private lazy var categoryPickerView: UIPickerView = { 104 | let pickerView = UIPickerView() 105 | pickerView.dataSource = self 106 | pickerView.delegate = self 107 | return pickerView 108 | }() 109 | 110 | private lazy var pricePickerView: UIPickerView = { 111 | let pickerView = UIPickerView() 112 | pickerView.dataSource = self 113 | pickerView.delegate = self 114 | return pickerView 115 | }() 116 | 117 | // MARK: UIPickerViewDataSource 118 | 119 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 120 | return 1 121 | } 122 | 123 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 124 | switch pickerView { 125 | case pricePickerView: 126 | return priceOptions.count 127 | case cityPickerView: 128 | return cityOptions.count 129 | case categoryPickerView: 130 | return categoryOptions.count 131 | case _: 132 | fatalError("Unhandled picker view: \(pickerView)") 133 | } 134 | } 135 | 136 | // MARK: - UIPickerViewDelegate 137 | 138 | func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent: Int) -> String? { 139 | switch pickerView { 140 | case pricePickerView: 141 | return priceOptions[row] 142 | case cityPickerView: 143 | return cityOptions[row] 144 | case categoryPickerView: 145 | return categoryOptions[row] 146 | case _: 147 | fatalError("Unhandled picker view: \(pickerView)") 148 | } 149 | } 150 | 151 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 152 | switch pickerView { 153 | case pricePickerView: 154 | priceTextField.text = priceOptions[row] 155 | case cityPickerView: 156 | cityTextField.text = cityOptions[row] 157 | case categoryPickerView: 158 | categoryTextField.text = categoryOptions[row] 159 | case _: 160 | fatalError("Unhandled picker view: \(pickerView)") 161 | } 162 | } 163 | 164 | 165 | // MARK: Alert Messages 166 | 167 | func presentDidSaveAlert() { 168 | let message = "Restaurant added successfully!" 169 | let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) 170 | let okAction = UIAlertAction(title: "OK", style: .default) { action in 171 | self.performSegue(withIdentifier: "unwindToMyRestaurantsSegue", sender: self) 172 | } 173 | alertController.addAction(okAction) 174 | self.present(alertController, animated: true, completion: nil) 175 | } 176 | 177 | // If data in text fields isn't valid, give an alert 178 | func presentInvalidDataAlert(message: String) { 179 | Utils.showSimpleAlert(title: "Invalid Input", message: message, presentingVC: self) 180 | } 181 | 182 | func saveImage(photoData: Data) { 183 | let storageRef = Storage.storage().reference(withPath: restaurant.documentID) 184 | storageRef.putData(photoData, metadata: nil) { (metadata, error) in 185 | if let error = error { 186 | print(error) 187 | return 188 | } 189 | storageRef.downloadURL { (url, error) in 190 | if let error = error { 191 | print(error) 192 | } 193 | if let url = url { 194 | self.downloadUrl = url.absoluteString 195 | } 196 | } 197 | } 198 | } 199 | 200 | // MARK: Keyboard functionality 201 | 202 | @objc func inputToolbarDonePressed() { 203 | resignFirstResponder() 204 | } 205 | 206 | @objc func keyboardNextButton() { 207 | if cityTextField.isFirstResponder { 208 | categoryTextField.becomeFirstResponder() 209 | } else if categoryTextField.isFirstResponder { 210 | priceTextField.becomeFirstResponder() 211 | } else if restaurantNameTextField.isFirstResponder { 212 | cityTextField.becomeFirstResponder() 213 | } else { 214 | resignFirstResponder() 215 | } 216 | } 217 | 218 | @objc func keyboardPreviousButton() { 219 | if cityTextField.isFirstResponder { 220 | restaurantNameTextField.becomeFirstResponder() 221 | } else if categoryTextField.isFirstResponder { 222 | cityTextField.becomeFirstResponder() 223 | } else if priceTextField.isFirstResponder { 224 | categoryTextField.becomeFirstResponder() 225 | } else { 226 | resignFirstResponder() 227 | } 228 | } 229 | 230 | lazy var inputToolbar: UIToolbar = { 231 | let toolbar = UIToolbar() 232 | toolbar.barStyle = .default 233 | toolbar.isTranslucent = true 234 | toolbar.sizeToFit() 235 | 236 | var doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(self.inputToolbarDonePressed)) 237 | var flexibleSpaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 238 | var fixedSpaceButton = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) 239 | 240 | var nextButton = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_keyboard_arrow_left"), style: .plain, target: self, action: #selector(self.keyboardPreviousButton)) 241 | nextButton.width = 50.0 242 | var previousButton = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_keyboard_arrow_right"), style: .plain, target: self, action: #selector(self.keyboardNextButton)) 243 | 244 | toolbar.setItems([fixedSpaceButton, nextButton, fixedSpaceButton, previousButton, flexibleSpaceButton, doneButton], animated: false) 245 | toolbar.isUserInteractionEnabled = true 246 | 247 | return toolbar 248 | }() 249 | 250 | // MARK: IBActions 251 | 252 | @IBAction func selectNewImage(_ sender: Any) { 253 | selectImage() 254 | } 255 | 256 | @IBAction func didPressSaveButton(_ sender: Any) { 257 | saveChanges() 258 | } 259 | 260 | } 261 | 262 | extension AddRestaurantViewController: UITextFieldDelegate { 263 | 264 | func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { 265 | textField.inputAccessoryView = inputToolbar 266 | return true 267 | } 268 | 269 | func textFieldDidEndEditing(_ textField: UITextField) { 270 | let trimmed = textField.text?.trimmingCharacters(in: .whitespaces) 271 | textField.text = trimmed 272 | } 273 | 274 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 275 | textField.resignFirstResponder() 276 | return true 277 | } 278 | } 279 | 280 | extension AddRestaurantViewController: UIImagePickerControllerDelegate { 281 | 282 | func selectImage() { 283 | imagePicker.delegate = self 284 | if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum){ 285 | 286 | imagePicker.sourceType = .savedPhotosAlbum; 287 | imagePicker.allowsEditing = false 288 | 289 | self.present(imagePicker, animated: true, completion: nil) 290 | } 291 | } 292 | 293 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 294 | if let photo = info[UIImagePickerController.InfoKey.originalImage] as? UIImage, let photoData = photo.jpegData(compressionQuality: 0.8) { 295 | self.restaurantImageView.image = photo 296 | self.addPhotoButton.titleLabel?.text = "" 297 | self.addPhotoButton.backgroundColor = UIColor.clear 298 | saveImage(photoData: photoData) 299 | } 300 | self.dismiss(animated: true, completion: nil) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /FriendlyEats/Profile/MyRestaurantsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import FirebaseAuth 19 | import FirebaseFirestore 20 | 21 | class MyRestaurantsViewController: UIViewController { 22 | 23 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)) 24 | -> MyRestaurantsViewController { 25 | return storyboard.instantiateViewController(withIdentifier: "MyRestaurantsViewController") 26 | as! MyRestaurantsViewController 27 | } 28 | 29 | private var user: User! 30 | fileprivate var dataSource: RestaurantTableViewDataSource! 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | super.viewWillAppear(animated) 34 | // These should all be nonnull. The user can be signed out by an event 35 | // outside of the app, like a password change, but we're ignoring that case 36 | // for simplicity. In a real-world app, you should dismiss this view controller 37 | // or present a login flow if the user is unexpectedly nil. 38 | user = User(user: Auth.auth().currentUser!) 39 | let query = Firestore.firestore().restaurants.whereField("ownerID", isEqualTo: user.userID) 40 | dataSource = RestaurantTableViewDataSource(query: query) { (changes) in 41 | self.tableView.reloadData() 42 | } 43 | 44 | tableView.dataSource = dataSource 45 | dataSource.startUpdates() 46 | tableView.delegate = self 47 | } 48 | 49 | override func viewWillDisappear(_ animated: Bool) { 50 | super.viewWillDisappear(animated) 51 | dataSource.stopUpdates() 52 | } 53 | 54 | @IBOutlet private var tableView: UITableView! 55 | @IBAction func unwindToMyRestaurants(segue: UIStoryboardSegue) {} 56 | 57 | @IBAction private func didTapAddRestaurantButton(_ sender: Any) { 58 | let controller = AddRestaurantViewController.fromStoryboard() 59 | self.navigationController?.pushViewController(controller, animated: true) 60 | } 61 | 62 | } 63 | 64 | // MARK: - UITableViewDelegate 65 | 66 | extension MyRestaurantsViewController: UITableViewDelegate { 67 | 68 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 69 | tableView.deselectRow(at: indexPath, animated: true) 70 | let restaurant = dataSource[indexPath.row] 71 | let controller = RestaurantDetailViewController.fromStoryboard(restaurant: restaurant) 72 | self.navigationController?.pushViewController(controller, animated: true) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /FriendlyEats/Profile/ProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import FirebaseAuth 19 | import FirebaseUI 20 | import FirebaseFirestore 21 | import SDWebImage 22 | 23 | class ProfileViewController: UIViewController { 24 | 25 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)) 26 | -> ProfileViewController { 27 | return storyboard.instantiateViewController(withIdentifier: "ProfileViewController") 28 | as! ProfileViewController 29 | } 30 | 31 | /// The current user displayed by the controller. Setting this property has side effects. 32 | fileprivate var user: User? = nil { 33 | didSet { 34 | populate(user: user) 35 | if let user = user { 36 | populateReviews(forUser: user) 37 | } else { 38 | dataSource?.stopUpdates() 39 | dataSource = nil 40 | tableView.backgroundView = tableBackgroundLabel 41 | tableView.reloadData() 42 | } 43 | } 44 | } 45 | 46 | lazy private var tableBackgroundLabel: UILabel = { 47 | let label = UILabel(frame: tableView.frame) 48 | label.textAlignment = .center 49 | return label 50 | }() 51 | 52 | private var dataSource: ReviewTableViewDataSource? = nil 53 | private var authListener: AuthStateDidChangeListenerHandle? = nil 54 | 55 | @IBOutlet private var tableView: UITableView! 56 | 57 | @IBOutlet private var profileImageView: UIImageView! 58 | @IBOutlet private var usernameLabel: UILabel! 59 | @IBOutlet private var viewRestaurantsButton: UIButton! 60 | @IBOutlet private var signInButton: UIButton! 61 | // Not weak because we might remove it 62 | @IBOutlet private var signOutButton: UIBarButtonItem! 63 | 64 | override func viewDidLoad() { 65 | super.viewDidLoad() 66 | tableBackgroundLabel.text = "There aren't any reviews here." 67 | tableView.backgroundView = tableBackgroundLabel 68 | } 69 | 70 | override func viewWillAppear(_ animated: Bool) { 71 | super.viewWillAppear(animated) 72 | setUser(firebaseUser: Auth.auth().currentUser) 73 | Auth.auth().addStateDidChangeListener { (auth, newUser) in 74 | self.setUser(firebaseUser: newUser) 75 | } 76 | } 77 | 78 | @IBAction func signInButtonWasTapped(_ sender: Any) { 79 | presentLoginController() 80 | } 81 | 82 | override func viewDidAppear(_ animated: Bool) { 83 | super.viewDidAppear(animated) 84 | } 85 | 86 | override func viewWillDisappear(_ animated: Bool) { 87 | super.viewWillDisappear(animated) 88 | if let listener = authListener { 89 | Auth.auth().removeStateDidChangeListener(listener) 90 | } 91 | } 92 | 93 | fileprivate func setUser(firebaseUser: FirebaseAuth.UserInfo?) { 94 | if let firebaseUser = firebaseUser { 95 | let user = User(user: firebaseUser) 96 | self.user = user 97 | } else { 98 | user = nil 99 | } 100 | } 101 | 102 | fileprivate func populate(user: User?) { 103 | if let user = user { 104 | profileImageView.sd_setImage(with: user.photoURL) 105 | usernameLabel.text = user.name 106 | viewRestaurantsButton.isHidden = false 107 | signInButton.isHidden = true 108 | self.navigationItem.leftBarButtonItem = signOutButton 109 | } else { 110 | profileImageView.image = nil 111 | usernameLabel.text = "Sign in, why don'cha?" 112 | viewRestaurantsButton.isHidden = true 113 | signInButton.isHidden = false 114 | self.navigationItem.leftBarButtonItem = nil 115 | } 116 | } 117 | 118 | fileprivate func populateReviews(forUser user: User) { 119 | 120 | dataSource?.sectionTitle = "My reviews" 121 | dataSource?.startUpdates() 122 | tableView.dataSource = dataSource 123 | } 124 | 125 | fileprivate func presentLoginController() { 126 | guard let authUI = FUIAuth.defaultAuthUI() else { return } 127 | guard authUI.auth?.currentUser == nil else { 128 | print("Attempted to present auth flow while already logged in") 129 | return 130 | } 131 | let emailAuth = FUIEmailAuth(authAuthUI: authUI, 132 | signInMethod: "password", 133 | forceSameDevice: false, 134 | allowNewEmailAccounts: true, 135 | actionCodeSetting: ActionCodeSettings()) 136 | authUI.providers = [emailAuth] 137 | let controller = authUI.authViewController() 138 | self.present(controller, animated: true, completion: nil) 139 | } 140 | 141 | @IBAction private func didTapViewRestaurantsButton(_ sender: Any) { 142 | let controller = MyRestaurantsViewController.fromStoryboard() 143 | self.navigationController?.pushViewController(controller, animated: true) 144 | } 145 | 146 | @IBAction private func didTapSignOutButton(_ sender: Any) { 147 | do { 148 | try Auth.auth().signOut() 149 | } catch let error { 150 | print("Error signing out: \(error)") 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/BasicRestaurantsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import Firebase 19 | 20 | class BasicRestaurantsTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 21 | 22 | @IBOutlet var tableView: UITableView! 23 | // You can ignore these properties. They're used later on in the workshop. 24 | @IBOutlet var activeFiltersStackView: UIStackView! 25 | @IBOutlet var stackViewHeightConstraint: NSLayoutConstraint! 26 | @IBOutlet var cityFilterLabel: UILabel! 27 | @IBOutlet var categoryFilterLabel: UILabel! 28 | @IBOutlet var priceFilterLabel: UILabel! 29 | 30 | 31 | let backgroundView = UIImageView() 32 | var restaurantData: [Restaurant] = [] 33 | var restaurantListener: ListenerRegistration? 34 | 35 | private func startListeningForRestaurants() { 36 | // TODO: Create a listener for the "restaurants" collection and use that data 37 | // to popualte our `restaurantData` model 38 | } 39 | 40 | func tryASampleQuery() { 41 | // TODO: Let's put a sample query here to see how basic data fetching works in 42 | // Cloud Firestore 43 | } 44 | 45 | private func stopListeningForRestaurants() { 46 | // TODO: We should "deactivate" our restaurant listener when this view goes away 47 | } 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | backgroundView.image = UIImage(named: "pizza-monster")! 52 | backgroundView.contentMode = .scaleAspectFit 53 | backgroundView.alpha = 0.5 54 | tableView.backgroundView = backgroundView 55 | tableView.tableFooterView = UIView() 56 | stackViewHeightConstraint.constant = 0 57 | activeFiltersStackView.isHidden = true 58 | tableView.delegate = self 59 | tableView.dataSource = self 60 | tryASampleQuery() 61 | } 62 | 63 | override func viewWillAppear(_ animated: Bool) { 64 | super.viewWillAppear(animated) 65 | setNeedsStatusBarAppearanceUpdate() 66 | startListeningForRestaurants() 67 | } 68 | 69 | override func viewWillDisappear(_ animated: Bool) { 70 | super.viewWillDisappear(animated) 71 | stopListeningForRestaurants() 72 | } 73 | 74 | // MARK: - Table view data source 75 | 76 | func numberOfSections(in tableView: UITableView) -> Int { 77 | return 1 78 | } 79 | 80 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 81 | return restaurantData.count 82 | } 83 | 84 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 85 | let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", 86 | for: indexPath) as! RestaurantTableViewCell 87 | let restaurant = restaurantData[indexPath.row] 88 | cell.populate(restaurant: restaurant) 89 | return cell 90 | } 91 | 92 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 93 | tableView.deselectRow(at: indexPath, animated: true) 94 | let restaurant = restaurantData[indexPath.row] 95 | let controller = RestaurantDetailViewController.fromStoryboard(restaurant: restaurant) 96 | self.navigationController?.pushViewController(controller, animated: true) 97 | } 98 | 99 | @IBAction func didTapPopulateButton(_ sender: Any) { 100 | let confirmationBox = UIAlertController(title: "Populate the database", 101 | message: "This will add populate the database with several new restaurants and reviews. Would you like to proceed?", 102 | preferredStyle: .alert) 103 | confirmationBox.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 104 | confirmationBox.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in 105 | Firestore.firestore().prepopulate() 106 | })) 107 | present(confirmationBox, animated: true) 108 | } 109 | 110 | } 111 | 112 | 113 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/EditRestaurantViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | import UIKit 17 | import Firebase 18 | import FirebaseStorage 19 | 20 | class EditRestaurantViewController: UIViewController, UINavigationControllerDelegate, UIPickerViewDataSource, UIPickerViewDelegate { 21 | 22 | // MARK: Properties 23 | 24 | private var restaurant: Restaurant! 25 | private var imagePicker = UIImagePickerController() 26 | private var downloadUrl: String? 27 | 28 | // MARK: Outlets 29 | 30 | @IBOutlet private weak var restaurantImageView: UIImageView! 31 | @IBOutlet private weak var restaurantNameTextField: UITextField! 32 | @IBOutlet private weak var cityTextField: UITextField! { 33 | didSet { 34 | cityTextField.inputView = cityPickerView 35 | } 36 | } 37 | @IBOutlet private weak var categoryTextField: UITextField! { 38 | didSet { 39 | categoryTextField.inputView = categoryPickerView 40 | } 41 | } 42 | @IBOutlet private weak var priceTextField: UITextField! { 43 | didSet { 44 | priceTextField.inputView = pricePickerView 45 | } 46 | } 47 | 48 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil), 49 | restaurant: Restaurant) -> EditRestaurantViewController { 50 | let controller = storyboard.instantiateViewController(withIdentifier: "EditRestaurantViewController") 51 | as! EditRestaurantViewController 52 | controller.restaurant = restaurant 53 | return controller 54 | } 55 | 56 | override func viewDidLoad() { 57 | super.viewDidLoad() 58 | restaurantImageView.contentMode = .scaleAspectFill 59 | restaurantImageView.clipsToBounds = true 60 | hideKeyboardWhenTappedAround() 61 | if let _ = restaurant { 62 | populateRestaurant() 63 | } 64 | } 65 | 66 | // populate restaurant with current data 67 | func populateRestaurant() { 68 | restaurantNameTextField.text = restaurant.name 69 | cityTextField.text = restaurant.city 70 | categoryTextField.text = restaurant.category 71 | priceTextField.text = Utils.priceString(from: restaurant.price) 72 | restaurantImageView.sd_setImage(with: restaurant.photoURL) 73 | } 74 | 75 | func saveChanges() { 76 | guard let name = restaurantNameTextField.text, 77 | let city = cityTextField.text, 78 | let category = categoryTextField.text, 79 | let price = Utils.priceValue(from: priceTextField.text) else { 80 | self.presentInvalidDataAlert(message: "All fields must be filled out.") 81 | return 82 | } 83 | var data = [ 84 | "name": name, 85 | "city": city, 86 | "category": category, 87 | "price": price 88 | ] as [String : Any] 89 | // if photo was changed, add the new url 90 | if let downloadUrl = downloadUrl { 91 | data["photoURL"] = downloadUrl 92 | } 93 | 94 | 95 | // TODO: Update the restaurant document in Cloud Firestore 96 | } 97 | 98 | // MARK: Setting up pickers 99 | private let priceOptions = ["$", "$$", "$$$"] 100 | private let cityOptions = Restaurant.cities 101 | private let categoryOptions = Restaurant.categories 102 | 103 | private lazy var cityPickerView: UIPickerView = { 104 | let pickerView = UIPickerView() 105 | pickerView.dataSource = self 106 | pickerView.delegate = self 107 | return pickerView 108 | }() 109 | 110 | private lazy var categoryPickerView: UIPickerView = { 111 | let pickerView = UIPickerView() 112 | pickerView.dataSource = self 113 | pickerView.delegate = self 114 | return pickerView 115 | }() 116 | 117 | private lazy var pricePickerView: UIPickerView = { 118 | let pickerView = UIPickerView() 119 | pickerView.dataSource = self 120 | pickerView.delegate = self 121 | return pickerView 122 | }() 123 | 124 | // MARK: UIPickerViewDataSource 125 | 126 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 127 | return 1 128 | } 129 | 130 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 131 | switch pickerView { 132 | case pricePickerView: 133 | return priceOptions.count 134 | case cityPickerView: 135 | return cityOptions.count 136 | case categoryPickerView: 137 | return categoryOptions.count 138 | case _: 139 | fatalError("Unhandled picker view: \(pickerView)") 140 | } 141 | } 142 | 143 | // MARK: - UIPickerViewDelegate 144 | 145 | func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent: Int) -> String? { 146 | switch pickerView { 147 | case pricePickerView: 148 | return priceOptions[row] 149 | case cityPickerView: 150 | return cityOptions[row] 151 | case categoryPickerView: 152 | return categoryOptions[row] 153 | case _: 154 | fatalError("Unhandled picker view: \(pickerView)") 155 | } 156 | } 157 | 158 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 159 | switch pickerView { 160 | case pricePickerView: 161 | priceTextField.text = priceOptions[row] 162 | case cityPickerView: 163 | cityTextField.text = cityOptions[row] 164 | case categoryPickerView: 165 | categoryTextField.text = categoryOptions[row] 166 | case _: 167 | fatalError("Unhandled picker view: \(pickerView)") 168 | } 169 | } 170 | 171 | 172 | // MARK: Alert Messages 173 | 174 | func presentDidSaveAlert() { 175 | let message = "Successfully saved!" 176 | let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) 177 | let okAction = UIAlertAction(title: "OK", style: .default) { action in 178 | self.performSegue(withIdentifier: "unwindToMyRestaurantsSegue", sender: self) 179 | } 180 | alertController.addAction(okAction) 181 | self.present(alertController, animated: true, completion: nil) 182 | } 183 | 184 | func presentWillSaveAlert() { 185 | let message = "Are you sure you want to save changes to this restaurant?" 186 | let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) 187 | let saveAction = UIAlertAction(title: "Save", style: .default) { action in 188 | self.saveChanges() 189 | } 190 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 191 | alertController.addAction(saveAction) 192 | alertController.addAction(cancelAction) 193 | 194 | self.present(alertController, animated: true, completion: nil) 195 | } 196 | 197 | // If data in text fields isn't valid, give an alert 198 | func presentInvalidDataAlert(message: String) { 199 | Utils.showSimpleAlert(title: "Invalid Input", message: message, presentingVC: self) 200 | } 201 | 202 | func saveImage(photoData: Data) { 203 | let storageRef = Storage.storage().reference(withPath: restaurant.documentID) 204 | storageRef.putData(photoData, metadata: nil) { (metadata, error) in 205 | if let error = error { 206 | print(error) 207 | return 208 | } 209 | storageRef.downloadURL { (url, error) in 210 | if let error = error { 211 | print(error) 212 | } 213 | if let url = url { 214 | self.downloadUrl = url.absoluteString 215 | } 216 | } 217 | } 218 | } 219 | 220 | // MARK: Keyboard functionality 221 | 222 | @objc func inputToolbarDonePressed() { 223 | resignFirstResponder() 224 | } 225 | 226 | @objc func keyboardNextButton() { 227 | if cityTextField.isFirstResponder { 228 | categoryTextField.becomeFirstResponder() 229 | } else if categoryTextField.isFirstResponder { 230 | priceTextField.becomeFirstResponder() 231 | } else if restaurantNameTextField.isFirstResponder { 232 | cityTextField.becomeFirstResponder() 233 | } else { 234 | resignFirstResponder() 235 | } 236 | } 237 | 238 | @objc func keyboardPreviousButton() { 239 | if cityTextField.isFirstResponder { 240 | restaurantNameTextField.becomeFirstResponder() 241 | } else if categoryTextField.isFirstResponder { 242 | cityTextField.becomeFirstResponder() 243 | } else if priceTextField.isFirstResponder { 244 | categoryTextField.becomeFirstResponder() 245 | } else { 246 | resignFirstResponder() 247 | } 248 | } 249 | 250 | lazy var inputToolbar: UIToolbar = { 251 | let toolbar = UIToolbar() 252 | toolbar.barStyle = .default 253 | toolbar.isTranslucent = true 254 | toolbar.sizeToFit() 255 | 256 | var doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(self.inputToolbarDonePressed)) 257 | var flexibleSpaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 258 | var fixedSpaceButton = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) 259 | 260 | var nextButton = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_keyboard_arrow_left"), style: .plain, target: self, action: #selector(self.keyboardPreviousButton)) 261 | nextButton.width = 50.0 262 | var previousButton = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_keyboard_arrow_right"), style: .plain, target: self, action: #selector(self.keyboardNextButton)) 263 | 264 | toolbar.setItems([fixedSpaceButton, nextButton, fixedSpaceButton, previousButton, flexibleSpaceButton, doneButton], animated: false) 265 | toolbar.isUserInteractionEnabled = true 266 | 267 | return toolbar 268 | }() 269 | 270 | // MARK: IBActions 271 | 272 | @IBAction func selectNewImage(_ sender: Any) { 273 | selectImage() 274 | } 275 | 276 | @IBAction func didSelectSaveChanges(_ sender: Any) { 277 | presentWillSaveAlert() 278 | } 279 | 280 | } 281 | 282 | extension EditRestaurantViewController: UITextFieldDelegate { 283 | 284 | func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { 285 | textField.inputAccessoryView = inputToolbar 286 | return true 287 | } 288 | 289 | func textFieldDidEndEditing(_ textField: UITextField) { 290 | let trimmed = textField.text?.trimmingCharacters(in: .whitespaces) 291 | textField.text = trimmed 292 | } 293 | 294 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 295 | textField.resignFirstResponder() 296 | return true 297 | } 298 | } 299 | 300 | extension EditRestaurantViewController: UIImagePickerControllerDelegate { 301 | 302 | func selectImage() { 303 | imagePicker.delegate = self 304 | if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum){ 305 | 306 | imagePicker.sourceType = .savedPhotosAlbum; 307 | imagePicker.allowsEditing = false 308 | 309 | self.present(imagePicker, animated: true, completion: nil) 310 | } 311 | } 312 | 313 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 314 | if let photo = info[UIImagePickerController.InfoKey.originalImage] as? UIImage, let photoData = photo.jpegData(compressionQuality: 0.8) { 315 | saveImage(photoData: photoData) 316 | restaurantImageView.image = photo 317 | } 318 | self.dismiss(animated: true, completion: nil) 319 | } 320 | } 321 | 322 | extension UIViewController { 323 | func hideKeyboardWhenTappedAround() { 324 | let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard)) 325 | tap.cancelsTouchesInView = false 326 | view.addGestureRecognizer(tap) 327 | } 328 | 329 | @objc func dismissKeyboard() { 330 | view.endEditing(true) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/FiltersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | class FiltersViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { 20 | 21 | weak var delegate: FiltersViewControllerDelegate? 22 | 23 | static func fromStoryboard(delegate: FiltersViewControllerDelegate? = nil) -> 24 | (navigationController: UINavigationController, filtersController: FiltersViewController) { 25 | let navController = UIStoryboard(name: "Main", bundle: nil) 26 | .instantiateViewController(withIdentifier: "FiltersViewController") 27 | as! UINavigationController 28 | let controller = navController.viewControllers[0] as! FiltersViewController 29 | controller.delegate = delegate 30 | return (navigationController: navController, filtersController: controller) 31 | } 32 | 33 | @IBOutlet var categoryTextField: UITextField! { 34 | didSet { 35 | categoryTextField.inputView = categoryPickerView 36 | } 37 | } 38 | @IBOutlet var cityTextField: UITextField! { 39 | didSet { 40 | cityTextField.inputView = cityPickerView 41 | } 42 | } 43 | @IBOutlet var priceTextField: UITextField! { 44 | didSet { 45 | priceTextField.inputView = pricePickerView 46 | } 47 | } 48 | @IBOutlet var sortByTextField: UITextField! { 49 | didSet { 50 | sortByTextField.inputView = sortByPickerView 51 | } 52 | } 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | } 57 | 58 | private func price(from string: String) -> Int? { 59 | switch string { 60 | case "$": 61 | return 1 62 | case "$$": 63 | return 2 64 | case "$$$": 65 | return 3 66 | 67 | case _: 68 | return nil 69 | } 70 | } 71 | 72 | @IBAction func didTapDoneButton(_ sender: Any) { 73 | let price = priceTextField.text.flatMap { self.price(from: $0) } 74 | delegate?.controller(self, didSelectCategory: categoryTextField.text, 75 | city: cityTextField.text, price: price, sortBy: sortByTextField.text) 76 | navigationController?.dismiss(animated: true, completion: nil) 77 | } 78 | 79 | @IBAction func didTapCancelButton(_ sender: Any) { 80 | navigationController?.dismiss(animated: true, completion: nil) 81 | } 82 | 83 | func clearFilters() { 84 | categoryTextField.text = "" 85 | cityTextField.text = "" 86 | priceTextField.text = "" 87 | sortByTextField.text = "" 88 | } 89 | 90 | private lazy var sortByPickerView: UIPickerView = { 91 | let pickerView = UIPickerView() 92 | pickerView.dataSource = self 93 | pickerView.delegate = self 94 | return pickerView 95 | }() 96 | 97 | private lazy var pricePickerView: UIPickerView = { 98 | let pickerView = UIPickerView() 99 | pickerView.dataSource = self 100 | pickerView.delegate = self 101 | return pickerView 102 | }() 103 | 104 | private lazy var cityPickerView: UIPickerView = { 105 | let pickerView = UIPickerView() 106 | pickerView.dataSource = self 107 | pickerView.delegate = self 108 | return pickerView 109 | }() 110 | 111 | private lazy var categoryPickerView: UIPickerView = { 112 | let pickerView = UIPickerView() 113 | pickerView.dataSource = self 114 | pickerView.delegate = self 115 | return pickerView 116 | }() 117 | 118 | private let sortByOptions = ["name", "category", "city", "price", "averageRating"] 119 | private let priceOptions = ["$", "$$", "$$$"] 120 | private let cityOptions = Restaurant.cities 121 | private let categoryOptions = Restaurant.categories 122 | 123 | // MARK: UIPickerViewDataSource 124 | 125 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 126 | return 1 127 | } 128 | 129 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 130 | switch pickerView { 131 | case sortByPickerView: 132 | return sortByOptions.count 133 | case pricePickerView: 134 | return priceOptions.count 135 | case cityPickerView: 136 | return cityOptions.count 137 | case categoryPickerView: 138 | return categoryOptions.count 139 | 140 | case _: 141 | fatalError("Unhandled picker view: \(pickerView)") 142 | } 143 | } 144 | 145 | // MARK: - UIPickerViewDelegate 146 | 147 | func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent: Int) -> String? { 148 | switch pickerView { 149 | case sortByPickerView: 150 | return sortByOptions[row] 151 | case pricePickerView: 152 | return priceOptions[row] 153 | case cityPickerView: 154 | return cityOptions[row] 155 | case categoryPickerView: 156 | return categoryOptions[row] 157 | 158 | case _: 159 | fatalError("Unhandled picker view: \(pickerView)") 160 | } 161 | } 162 | 163 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 164 | switch pickerView { 165 | case sortByPickerView: 166 | sortByTextField.text = sortByOptions[row] 167 | case pricePickerView: 168 | priceTextField.text = priceOptions[row] 169 | case cityPickerView: 170 | cityTextField.text = cityOptions[row] 171 | case categoryPickerView: 172 | categoryTextField.text = categoryOptions[row] 173 | 174 | case _: 175 | fatalError("Unhandled picker view: \(pickerView)") 176 | } 177 | } 178 | 179 | } 180 | 181 | protocol FiltersViewControllerDelegate: NSObjectProtocol { 182 | 183 | func controller(_ controller: FiltersViewController, 184 | didSelectCategory category: String?, 185 | city: String?, price: Int?, sortBy: String?) 186 | 187 | } 188 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/HackPageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import Firebase 19 | 20 | class HackPageViewController: UIViewController { 21 | @IBOutlet weak var editReviewStatus: UILabel! 22 | @IBOutlet weak var changeRestNameStatus: UILabel! 23 | @IBOutlet weak var changeUserPicStatus: UILabel! 24 | @IBOutlet weak var addFakeReviewStatus: UILabel! 25 | @IBOutlet weak var addBadDataStatus: UILabel! 26 | @IBOutlet weak var giveMeFiveStarsStatus: UILabel! 27 | 28 | let currentUserID = Auth.auth().currentUser?.uid 29 | 30 | 31 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)) -> HackPageViewController { 32 | let controller = storyboard.instantiateViewController(withIdentifier: "HackPageViewController") as! HackPageViewController 33 | return controller 34 | } 35 | 36 | @IBAction func editReviewWasTapped(_ sender: Any) { 37 | Firestore.firestore().collection("reviews").limit(to: 20).getDocuments { (snapshotBlock, error) in 38 | if let error = error { 39 | print("Received fetch error \(error)") 40 | self.editReviewStatus.text = "Fetch error" 41 | return 42 | } 43 | guard let documents = snapshotBlock?.documents else { return } 44 | 45 | // Let's find a review that we didn't write 46 | for reviewDoc in documents { 47 | guard let review = Review(document: reviewDoc) else { continue } 48 | if review.userInfo.userID != self.currentUserID { 49 | self.hackReview(review) 50 | // We only want to hack one review 51 | break 52 | } 53 | } 54 | } 55 | } 56 | 57 | func hackReview(_ review: Review) { 58 | var hackedReview = review 59 | hackedReview.rating = 1 60 | hackedReview.text = "YOU HAVE BEEN HACKED!!!11!" 61 | hackedReview.yumCount = 99 62 | hackedReview.userInfo.name = "ANONYMOUS" 63 | let documentRef = Firestore.firestore().collection("reviews").document(hackedReview.documentID) 64 | documentRef.updateData(hackedReview.documentData) { (error) in 65 | if let error = error { 66 | print("Could not update review: \(error)") 67 | self.editReviewStatus.text = "Hack failed!" 68 | } else { 69 | self.editReviewStatus.text = "Mischief Managed" 70 | } 71 | } 72 | } 73 | 74 | 75 | @IBAction func changeRestNameWasTapped(_ sender: Any) { 76 | Firestore.firestore().collection("restaurants").limit(to: 20).getDocuments { (snapshotBlock, error) in 77 | if let error = error { 78 | print("Received fetch error \(error)") 79 | self.changeRestNameStatus.text = "Fetch error" 80 | return 81 | } 82 | guard let documents = snapshotBlock?.documents else { return } 83 | 84 | // Let's find a restaurant that we don't own 85 | for restaurantDoc in documents { 86 | guard let restaurant = Restaurant(document: restaurantDoc) else { continue } 87 | if restaurant.ownerID != self.currentUserID { 88 | self.hackOthersRestaurant(restaurant) 89 | // We only want to hack one restaurant 90 | break 91 | } 92 | } 93 | } 94 | } 95 | 96 | func hackOthersRestaurant(_ restaurant: Restaurant) { 97 | var hackedRestaurant = restaurant 98 | hackedRestaurant.name = "DON'T EAT HERE" 99 | hackedRestaurant.category = "GARBAGE" 100 | hackedRestaurant.city = "HACKEDVILLE" 101 | hackedRestaurant.averageRating = 1 102 | hackedRestaurant.photoURL = URL(string: "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_garbage.png")! 103 | let documentRef = Firestore.firestore().collection("restaurants").document(hackedRestaurant.documentID) 104 | documentRef.updateData(hackedRestaurant.documentData) { (error) in 105 | if let error = error { 106 | print("Could not update restaurant: \(error)") 107 | self.changeRestNameStatus.text = "Hack failed!" 108 | } else { 109 | self.changeRestNameStatus.text = "Mischief Managed" 110 | } 111 | } 112 | } 113 | 114 | @IBAction func addInvalidResturantData(_ sender: Any) { 115 | let badRestaurantData: [String : Any] = ["averageRating": "Good", 116 | "category": "Sushi", 117 | "city": 42.3, 118 | "name": "a", 119 | "ownerID": currentUserID!, 120 | "photoURL": "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_3.png", 121 | "price": "expensive", 122 | "reviewCount": -3] 123 | Firestore.firestore().collection("restaurants").document("zzzzzzz-BADDATA").setData(badRestaurantData) { (error) in 124 | if let error = error { 125 | print("Could not update restaurant: \(error)") 126 | self.addBadDataStatus.text = "Hack failed!" 127 | } else { 128 | self.addBadDataStatus.text = "Mischief Managed" 129 | } 130 | } 131 | 132 | } 133 | 134 | 135 | @IBAction func changeUserPicWasTapped(_ sender: Any) { 136 | Firestore.firestore().collection("users").limit(to: 20).getDocuments { (snapshotBlock, error) in 137 | if let error = error { 138 | print("Received fetch error \(error)") 139 | self.changeUserPicStatus.text = "Fetch error" 140 | return 141 | } 142 | guard let documents = snapshotBlock?.documents else { return } 143 | // Let's find a user that isn't the current user 144 | for userDoc in documents { 145 | guard let user = User(document: userDoc) else { continue } 146 | if user.userID != self.currentUserID { 147 | self.hackOtherUser(user) 148 | // We only want to hack one restaurant 149 | break 150 | } 151 | } 152 | } 153 | } 154 | 155 | func hackOtherUser(_ user: User) { 156 | var hackedUser = user 157 | hackedUser.name = "JOHNNY MNEMONIC" 158 | hackedUser.photoURL = URL(string: "https://storage.googleapis.com/firestorequickstarts.appspot.com/user_hacker.png")! 159 | let documentRef = Firestore.firestore().collection("users").document(hackedUser.documentID) 160 | documentRef.updateData(hackedUser.documentData) { (error) in 161 | if let error = error { 162 | print("Could not update user: \(error)") 163 | self.changeUserPicStatus.text = "Hack failed!" 164 | } else { 165 | self.changeUserPicStatus.text = "Mischief Managed" 166 | } 167 | } 168 | } 169 | 170 | 171 | @IBAction func addFakeReviewWasTapped(_ sender: Any) { 172 | let myRestaurantQuery = Firestore.firestore().collection("restaurants").limit(to: 3) 173 | myRestaurantQuery.getDocuments { (snapshotBlock, error) in 174 | if let error = error { 175 | print("Received fetch error \(error)") 176 | self.addFakeReviewStatus.text = "Fetch error" 177 | return 178 | } 179 | guard let documents = snapshotBlock?.documents else { return } 180 | // Let's find a restaurant that we don't own 181 | for restaurantDoc in documents { 182 | guard let restaurant = Restaurant(document: restaurantDoc) else { continue } 183 | self.writeFakeReviewFor(restaurant) 184 | } 185 | } 186 | } 187 | 188 | func writeFakeReviewFor(_ restaurant: Restaurant) { 189 | let fakeUser = User(userID: "ABCDEFG", 190 | name: "Jane Fake", 191 | photoURL: URL(string: "https://storage.googleapis.com/firestorequickstarts.appspot.com/user_fake.png")!) 192 | let fakeReview = Review(restaurantID: restaurant.documentID, 193 | restaurantName: restaurant.name, 194 | rating: 5, 195 | userInfo: fakeUser, 196 | text: "This place is great! And I'm totally not making this up because I'm a fake person!", 197 | date: Date(), 198 | yumCount: 80) 199 | Firestore.firestore().collection("reviews").addDocument(data: fakeReview.documentData) { (error) in 200 | if let error = error { 201 | print("Could not update user: \(error)") 202 | self.addFakeReviewStatus.text = "Hack failed!" 203 | } else { 204 | self.addFakeReviewStatus.text = "Mischief Managed" 205 | } 206 | } 207 | } 208 | 209 | @IBAction func giveMeFiveStarsWasTapped(_ sender: Any) { 210 | let myRestaurantQuery = Firestore.firestore().collection("restaurants").whereField("ownerID", isEqualTo: currentUserID!).limit(to: 1) 211 | myRestaurantQuery.getDocuments { (snapshotBlock, error) in 212 | if let error = error { 213 | print("Received fetch error \(error)") 214 | self.giveMeFiveStarsStatus.text = "Fetch error" 215 | return 216 | } 217 | guard let documents = snapshotBlock?.documents else { return } 218 | for restaurantDoc in documents { 219 | guard let restaurant = Restaurant(document: restaurantDoc) else { continue } 220 | self.giveFiveStarsTo(restaurant) 221 | } 222 | } 223 | } 224 | 225 | func giveFiveStarsTo(_ restaurant: Restaurant) { 226 | let newData = ["averageRating": 5, "reviewCount": 100] 227 | Firestore.firestore().collection("restaurants").document(restaurant.documentID).updateData(newData) { (error) in 228 | if let error = error { 229 | print("Could not update restuarant: \(error)") 230 | self.giveMeFiveStarsStatus.text = "Hack failed!" 231 | } else { 232 | self.giveMeFiveStarsStatus.text = "Mischief Managed" 233 | } 234 | } 235 | } 236 | 237 | 238 | override func viewDidLoad() { 239 | super.viewDidLoad() 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/NewReviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import FirebaseFirestore 19 | import FirebaseAuth 20 | 21 | class NewReviewViewController: UIViewController, UITextFieldDelegate { 22 | 23 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil), 24 | forRestaurant restaurant: Restaurant) -> NewReviewViewController { 25 | let controller = storyboard.instantiateViewController(withIdentifier: "NewReviewViewController") as! NewReviewViewController 26 | controller.restaurant = restaurant 27 | return controller 28 | } 29 | 30 | /// The restaurant being reviewed. This must be set when the controller is created. 31 | private var restaurant: Restaurant! 32 | 33 | @IBOutlet var doneButton: UIBarButtonItem! 34 | 35 | @IBOutlet var ratingView: RatingView! { 36 | didSet { 37 | ratingView.addTarget(self, action: #selector(ratingDidChange(_:)), for: .valueChanged) 38 | } 39 | } 40 | 41 | @IBOutlet var reviewTextField: UITextField! { 42 | didSet { 43 | reviewTextField.addTarget(self, action: #selector(textFieldTextDidChange(_:)), for: .editingChanged) 44 | } 45 | } 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | doneButton.isEnabled = false 50 | reviewTextField.delegate = self 51 | } 52 | 53 | @IBAction func cancelButtonPressed(_ sender: Any) { 54 | self.navigationController?.popViewController(animated: true) 55 | } 56 | 57 | @IBAction func doneButtonPressed(_ sender: Any) { 58 | // TODO: handle user not logged in. 59 | guard let user = Auth.auth().currentUser.flatMap(User.init) else { return } 60 | let review = Review(restaurantID: restaurant.documentID, 61 | restaurantName: restaurant.name, 62 | rating: ratingView.rating!, 63 | userInfo: user, 64 | text: reviewTextField.text!, 65 | date: Date(), 66 | yumCount: 0) 67 | 68 | // TODO: Write the review to Firestore. 69 | } 70 | 71 | @objc func ratingDidChange(_ sender: Any) { 72 | updateSubmitButton() 73 | } 74 | 75 | func textFieldIsEmpty() -> Bool { 76 | guard let text = reviewTextField.text else { return true } 77 | return text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 78 | } 79 | 80 | func updateSubmitButton() { 81 | doneButton.isEnabled = (ratingView.rating != nil && !textFieldIsEmpty()) 82 | } 83 | 84 | @objc func textFieldTextDidChange(_ sender: Any) { 85 | updateSubmitButton() 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/RestaurantDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import SDWebImage 19 | import FirebaseFirestore 20 | import Firebase 21 | import FirebaseUI 22 | 23 | class RestaurantDetailViewController: UIViewController { 24 | 25 | private var restaurant: Restaurant! 26 | private var localCollection: LocalCollection! 27 | private var dataSource: ReviewTableViewDataSource? 28 | 29 | private var query: Query? { 30 | didSet { 31 | if let query = query { 32 | localCollection = LocalCollection(query: query) { [unowned self] (changes) in 33 | if self.localCollection.count == 0 { 34 | self.tableView.backgroundView = self.backgroundView 35 | } else { 36 | self.tableView.backgroundView = nil 37 | } 38 | self.tableView.reloadData() 39 | } 40 | 41 | dataSource = ReviewTableViewDataSource(reviews: localCollection) 42 | localCollection.listen() 43 | tableView.dataSource = dataSource 44 | } else { 45 | localCollection.stopListening() 46 | dataSource = nil 47 | tableView.dataSource = nil 48 | } 49 | } 50 | } 51 | 52 | lazy private var baseQuery: Query = { 53 | return fatalError("Unimplemented") 54 | }() 55 | 56 | static func fromStoryboard(_ storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil), 57 | restaurant: Restaurant) -> RestaurantDetailViewController { 58 | let controller = 59 | storyboard.instantiateViewController(withIdentifier: "RestaurantDetailViewController") 60 | as! RestaurantDetailViewController 61 | controller.restaurant = restaurant 62 | return controller 63 | } 64 | 65 | @IBOutlet var tableView: UITableView! 66 | @IBOutlet var titleView: RestaurantTitleView! 67 | @IBOutlet weak var editButton: UIButton! 68 | @IBOutlet weak var bottomToolbar: UIToolbar! 69 | 70 | 71 | let backgroundView = UIImageView() 72 | 73 | override func viewDidLoad() { 74 | super.viewDidLoad() 75 | 76 | self.title = restaurant.name 77 | 78 | backgroundView.image = UIImage(named: "pizza-monster")! 79 | backgroundView.contentScaleFactor = 2 80 | backgroundView.contentMode = .bottom 81 | tableView.backgroundView = backgroundView 82 | tableView.tableFooterView = UIView() 83 | 84 | tableView.rowHeight = UITableView.automaticDimension 85 | tableView.estimatedRowHeight = 120 86 | 87 | // enable edit button if owner of restaurant 88 | editButton.isHidden = true 89 | if restaurant.ownerID == FirebaseAuth.Auth.auth().currentUser?.uid { 90 | editButton.isHidden = false 91 | } 92 | 93 | // Sort by date by default. 94 | query = baseQuery 95 | tableView.dataSource = dataSource 96 | tableView.rowHeight = UITableView.automaticDimension 97 | tableView.estimatedRowHeight = 140 98 | 99 | // Comment out this line to show the toolbar 100 | bottomToolbar.isHidden = true 101 | } 102 | 103 | 104 | deinit { 105 | localCollection.stopListening() 106 | } 107 | 108 | override func viewWillAppear(_ animated: Bool) { 109 | super.viewWillAppear(animated) 110 | localCollection.listen() 111 | titleView.populate(restaurant: restaurant) 112 | } 113 | 114 | override func viewWillDisappear(_ animated: Bool) { 115 | super.viewWillDisappear(animated) 116 | } 117 | 118 | override var preferredStatusBarStyle: UIStatusBarStyle { 119 | set {} 120 | get { 121 | return .lightContent 122 | } 123 | } 124 | 125 | @IBAction func didTapAddButton(_ sender: Any) { 126 | if Auth.auth().currentUser == nil { 127 | Utils.showSimpleAlert(message: "You need to be signed in to add a review.", presentingVC: self) 128 | } else { 129 | let controller = NewReviewViewController.fromStoryboard(forRestaurant: self.restaurant!) 130 | self.navigationController?.pushViewController(controller, animated: true) 131 | } 132 | } 133 | 134 | @IBAction func didTapEditButton(_ sender: Any) { 135 | let controller = 136 | EditRestaurantViewController.fromStoryboard(restaurant: restaurant) 137 | self.navigationController?.pushViewController(controller, animated: true) 138 | } 139 | 140 | @IBAction func sortReviewsWasTapped(_ sender: Any) { 141 | // TODO: Add an action sheet-style alert controller that let our user 142 | // sort reviews by different methods 143 | } 144 | } 145 | 146 | class RestaurantTitleView: UIView { 147 | 148 | @IBOutlet var nameLabel: UILabel! 149 | 150 | @IBOutlet var categoryLabel: UILabel! 151 | 152 | @IBOutlet var cityLabel: UILabel! 153 | 154 | @IBOutlet var priceLabel: UILabel! 155 | 156 | @IBOutlet var starsView: ImmutableStarsView! { 157 | didSet { 158 | starsView.highlightedColor = UIColor.white.cgColor 159 | } 160 | } 161 | 162 | @IBOutlet var titleImageView: UIImageView! { 163 | didSet { 164 | let gradient = CAGradientLayer() 165 | gradient.colors = 166 | [UIColor(red: 0, green: 0, blue: 0, alpha: 0.6).cgColor, UIColor.clear.cgColor] 167 | gradient.locations = [0.0, 1.0] 168 | 169 | gradient.startPoint = CGPoint(x: 0, y: 1) 170 | gradient.endPoint = CGPoint(x: 0, y: 0) 171 | gradient.frame = CGRect(x: 0, 172 | y: 0, 173 | width: UIScreen.main.bounds.width, 174 | height: titleImageView.bounds.height) 175 | 176 | titleImageView.layer.insertSublayer(gradient, at: 0) 177 | titleImageView.contentMode = .scaleAspectFill 178 | titleImageView.clipsToBounds = true 179 | } 180 | } 181 | 182 | func populate(restaurant: Restaurant) { 183 | nameLabel.text = restaurant.name 184 | categoryLabel.text = restaurant.category 185 | cityLabel.text = restaurant.city 186 | priceLabel.text = Utils.priceString(from: restaurant.price) 187 | starsView.rating = Int(restaurant.averageRating.rounded()) 188 | titleImageView.sd_setImage(with: restaurant.photoURL) 189 | } 190 | 191 | } 192 | 193 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/RestaurantTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | import UIKit 17 | 18 | 19 | class RestaurantTableViewCell: UITableViewCell { 20 | 21 | @IBOutlet private var thumbnailView: UIImageView! 22 | 23 | @IBOutlet private var nameLabel: UILabel! 24 | 25 | @IBOutlet var starsView: ImmutableStarsView! 26 | 27 | @IBOutlet private var cityLabel: UILabel! 28 | 29 | @IBOutlet private var categoryLabel: UILabel! 30 | 31 | @IBOutlet private var priceLabel: UILabel! 32 | 33 | func populate(restaurant: Restaurant) { 34 | // TODO: Fill this out 35 | } 36 | 37 | override func prepareForReuse() { 38 | super.prepareForReuse() 39 | thumbnailView.sd_cancelCurrentImageLoad() 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /FriendlyEats/Restaurants/RestaurantsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import Firebase 19 | import SDWebImage 20 | 21 | 22 | class RestaurantsTableViewController: UIViewController, UITableViewDelegate { 23 | 24 | @IBOutlet var tableView: UITableView! 25 | @IBOutlet var activeFiltersStackView: UIStackView! 26 | @IBOutlet var stackViewHeightConstraint: NSLayoutConstraint! 27 | 28 | @IBOutlet var cityFilterLabel: UILabel! 29 | @IBOutlet var categoryFilterLabel: UILabel! 30 | @IBOutlet var priceFilterLabel: UILabel! 31 | 32 | let backgroundView = UIImageView() 33 | 34 | lazy private var dataSource: RestaurantTableViewDataSource = { 35 | fatalError("Unimplemented") 36 | }() 37 | 38 | fileprivate var query: Query? { 39 | didSet { 40 | dataSource.stopUpdates() 41 | tableView.dataSource = nil 42 | if let query = query { 43 | dataSource = dataSourceForQuery(query) 44 | tableView.dataSource = dataSource 45 | dataSource.startUpdates() 46 | } 47 | } 48 | } 49 | 50 | private func dataSourceForQuery(_ query: Query) -> RestaurantTableViewDataSource { 51 | fatalError("Unimplemented") 52 | } 53 | 54 | private lazy var baseQuery: Query = { 55 | fatalError("Unimplemented") 56 | }() 57 | 58 | lazy private var filters: (navigationController: UINavigationController, 59 | filtersController: FiltersViewController) = { 60 | return FiltersViewController.fromStoryboard(delegate: self) 61 | }() 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | backgroundView.image = UIImage(named: "pizza-monster")! 66 | backgroundView.contentMode = .scaleAspectFit 67 | backgroundView.alpha = 0.5 68 | tableView.backgroundView = backgroundView 69 | tableView.tableFooterView = UIView() 70 | stackViewHeightConstraint.constant = 0 71 | activeFiltersStackView.isHidden = true 72 | tableView.delegate = self 73 | // TODO: assign our data source 74 | 75 | self.navigationController?.navigationBar.barStyle = .black 76 | 77 | // Uncomment these two lines to enable SECRET HACKER PAGE!!! 78 | // let omgHAX = UIBarButtonItem(barButtonSystemItem: .bookmarks, target: self, action: #selector(goToHackPage)) 79 | // navigationItem.rightBarButtonItems?.append(omgHAX) 80 | } 81 | 82 | override func viewWillAppear(_ animated: Bool) { 83 | super.viewWillAppear(animated) 84 | self.setNeedsStatusBarAppearanceUpdate() 85 | } 86 | 87 | override func viewWillDisappear(_ animated: Bool) { 88 | super.viewWillDisappear(animated) 89 | } 90 | 91 | deinit { 92 | dataSource.stopUpdates() 93 | } 94 | 95 | @IBAction func didTapPopulateButton(_ sender: Any) { 96 | // Let's confirm that we want to do this 97 | let confirmationBox = UIAlertController(title: "Populate the database", 98 | message: "This will add populate the database with several new restaurants and reviews. Would you like to proceed?", 99 | preferredStyle: .alert) 100 | confirmationBox.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 101 | confirmationBox.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in 102 | Firestore.firestore().prepopulate() 103 | })) 104 | present(confirmationBox, animated: true) 105 | } 106 | 107 | @IBAction func didTapClearButton(_ sender: Any) { 108 | filters.filtersController.clearFilters() 109 | controller(filters.filtersController, didSelectCategory: nil, city: nil, price: nil, sortBy: nil) 110 | } 111 | 112 | @IBAction func didTapFilterButton(_ sender: Any) { 113 | present(filters.navigationController, animated: true, completion: nil) 114 | } 115 | 116 | @objc func goToHackPage(_ sender: Any) { 117 | if Auth.auth().currentUser != nil { 118 | let hackPage = HackPageViewController.fromStoryboard() 119 | self.navigationController?.pushViewController(hackPage, animated: true) 120 | } else { 121 | Utils.showSimpleAlert(message: "You must be signed in to be a 1337 hax0r", presentingVC: self) 122 | } 123 | } 124 | 125 | override var preferredStatusBarStyle: UIStatusBarStyle { 126 | set {} 127 | get { 128 | return .lightContent 129 | } 130 | } 131 | 132 | // MARK: - UITableViewDelegate 133 | 134 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 135 | tableView.deselectRow(at: indexPath, animated: true) 136 | let restaurant = dataSource[indexPath.row] 137 | let controller = RestaurantDetailViewController.fromStoryboard(restaurant: restaurant) 138 | self.navigationController?.pushViewController(controller, animated: true) 139 | } 140 | 141 | } 142 | 143 | extension RestaurantsTableViewController: FiltersViewControllerDelegate { 144 | 145 | func query(withCategory category: String?, city: String?, price: Int?, sortBy: String?) -> Query { 146 | var filtered = baseQuery 147 | 148 | if category == nil && city == nil && price == nil && sortBy == nil { 149 | stackViewHeightConstraint.constant = 0 150 | activeFiltersStackView.isHidden = true 151 | } else { 152 | stackViewHeightConstraint.constant = 44 153 | activeFiltersStackView.isHidden = false 154 | } 155 | 156 | // Sort and Filter data 157 | 158 | return filtered 159 | } 160 | 161 | func controller(_ controller: FiltersViewController, 162 | didSelectCategory category: String?, 163 | city: String?, 164 | price: Int?, 165 | sortBy: String?) { 166 | if category == nil && city == nil && price == nil && sortBy == nil { 167 | stackViewHeightConstraint.constant = 0 168 | activeFiltersStackView.isHidden = true 169 | } else { 170 | stackViewHeightConstraint.constant = 44 171 | activeFiltersStackView.isHidden = false 172 | } 173 | 174 | let filtered = query(withCategory: category, city: city, price: price, sortBy: sortBy) 175 | 176 | if let category = category, !category.isEmpty { 177 | categoryFilterLabel.text = category 178 | categoryFilterLabel.isHidden = false 179 | } else { 180 | categoryFilterLabel.isHidden = true 181 | } 182 | 183 | if let city = city, !city.isEmpty { 184 | cityFilterLabel.text = city 185 | cityFilterLabel.isHidden = false 186 | } else { 187 | cityFilterLabel.isHidden = true 188 | } 189 | 190 | if let price = price { 191 | priceFilterLabel.text = Utils.priceString(from: price) 192 | priceFilterLabel.isHidden = false 193 | } else { 194 | priceFilterLabel.isHidden = true 195 | } 196 | 197 | query = filtered 198 | } 199 | 200 | } 201 | 202 | -------------------------------------------------------------------------------- /FriendlyEats/pizza-monster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/firestore-codelab-extended-swift/2169d31377cc0928c8546afa2ca2481ea348a8a3/FriendlyEats/pizza-monster.png -------------------------------------------------------------------------------- /FriendlyEatsTests/FriendlyEatsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | @testable import FriendlyEats 19 | 20 | class FriendlyEatsTests: XCTestCase { 21 | } 22 | -------------------------------------------------------------------------------- /FriendlyEatsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 1. Definitions. 7 | "License" shall mean the terms and conditions for use, reproduction, 8 | and distribution as defined by Sections 1 through 9 of this document. 9 | "Licensor" shall mean the copyright owner or entity authorized by 10 | the copyright owner that is granting the License. 11 | "Legal Entity" shall mean the union of the acting entity and all 12 | other entities that control, are controlled by, or are under common 13 | control with that entity. For the purposes of this definition, 14 | "control" means (i) the power, direct or indirect, to cause the 15 | direction or management of such entity, whether by contract or 16 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 17 | outstanding shares, or (iii) beneficial ownership of such entity. 18 | "You" (or "Your") shall mean an individual or Legal Entity 19 | exercising permissions granted by this License. 20 | "Source" form shall mean the preferred form for making modifications, 21 | including but not limited to software source code, documentation 22 | source, and configuration files. 23 | "Object" form shall mean any form resulting from mechanical 24 | transformation or translation of a Source form, including but 25 | not limited to compiled object code, generated documentation, 26 | and conversions to other media types. 27 | "Work" shall mean the work of authorship, whether in Source or 28 | Object form, made available under the License, as indicated by a 29 | copyright notice that is included in or attached to the work 30 | (an example is provided in the Appendix below). 31 | "Derivative Works" shall mean any work, whether in Source or Object 32 | form, that is based on (or derived from) the Work and for which the 33 | editorial revisions, annotations, elaborations, or other modifications 34 | represent, as a whole, an original work of authorship. For the purposes 35 | of this License, Derivative Works shall not include works that remain 36 | separable from, or merely link (or bind by name) to the interfaces of, 37 | the Work and Derivative Works thereof. 38 | "Contribution" shall mean any work of authorship, including 39 | the original version of the Work and any modifications or additions 40 | to that Work or Derivative Works thereof, that is intentionally 41 | submitted to Licensor for inclusion in the Work by the copyright owner 42 | or by an individual or Legal Entity authorized to submit on behalf of 43 | the copyright owner. For the purposes of this definition, "submitted" 44 | means any form of electronic, verbal, or written communication sent 45 | to the Licensor or its representatives, including but not limited to 46 | communication on electronic mailing lists, source code control systems, 47 | and issue tracking systems that are managed by, or on behalf of, the 48 | Licensor for the purpose of discussing and improving the Work, but 49 | excluding communication that is conspicuously marked or otherwise 50 | designated in writing by the copyright owner as "Not a Contribution." 51 | "Contributor" shall mean Licensor and any individual or Legal Entity 52 | on behalf of whom a Contribution has been received by Licensor and 53 | subsequently incorporated within the Work. 54 | 2. Grant of Copyright License. Subject to the terms and conditions of 55 | this License, each Contributor hereby grants to You a perpetual, 56 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 57 | copyright license to reproduce, prepare Derivative Works of, 58 | publicly display, publicly perform, sublicense, and distribute the 59 | Work and such Derivative Works in Source or Object form. 60 | 3. Grant of Patent License. Subject to the terms and conditions of 61 | this License, each Contributor hereby grants to You a perpetual, 62 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 63 | (except as stated in this section) patent license to make, have made, 64 | use, offer to sell, sell, import, and otherwise transfer the Work, 65 | where such license applies only to those patent claims licensable 66 | by such Contributor that are necessarily infringed by their 67 | Contribution(s) alone or by combination of their Contribution(s) 68 | with the Work to which such Contribution(s) was submitted. If You 69 | institute patent litigation against any entity (including a 70 | cross-claim or counterclaim in a lawsuit) alleging that the Work 71 | or a Contribution incorporated within the Work constitutes direct 72 | or contributory patent infringement, then any patent licenses 73 | granted to You under this License for that Work shall terminate 74 | as of the date such litigation is filed. 75 | 4. Redistribution. You may reproduce and distribute copies of the 76 | Work or Derivative Works thereof in any medium, with or without 77 | modifications, and in Source or Object form, provided that You 78 | meet the following conditions: 79 | (a) You must give any other recipients of the Work or 80 | Derivative Works a copy of this License; and 81 | (b) You must cause any modified files to carry prominent notices 82 | stating that You changed the files; and 83 | (c) You must retain, in the Source form of any Derivative Works 84 | that You distribute, all copyright, patent, trademark, and 85 | attribution notices from the Source form of the Work, 86 | excluding those notices that do not pertain to any part of 87 | the Derivative Works; and 88 | (d) If the Work includes a "NOTICE" text file as part of its 89 | distribution, then any Derivative Works that You distribute must 90 | include a readable copy of the attribution notices contained 91 | within such NOTICE file, excluding those notices that do not 92 | pertain to any part of the Derivative Works, in at least one 93 | of the following places: within a NOTICE text file distributed 94 | as part of the Derivative Works; within the Source form or 95 | documentation, if provided along with the Derivative Works; or, 96 | within a display generated by the Derivative Works, if and 97 | wherever such third-party notices normally appear. The contents 98 | of the NOTICE file are for informational purposes only and 99 | do not modify the License. You may add Your own attribution 100 | notices within Derivative Works that You distribute, alongside 101 | or as an addendum to the NOTICE text from the Work, provided 102 | that such additional attribution notices cannot be construed 103 | as modifying the License. 104 | You may add Your own copyright statement to Your modifications and 105 | may provide additional or different license terms and conditions 106 | for use, reproduction, or distribution of Your modifications, or 107 | for any such Derivative Works as a whole, provided Your use, 108 | reproduction, and distribution of the Work otherwise complies with 109 | the conditions stated in this License. 110 | 5. Submission of Contributions. Unless You explicitly state otherwise, 111 | any Contribution intentionally submitted for inclusion in the Work 112 | by You to the Licensor shall be under the terms and conditions of 113 | this License, without any additional terms or conditions. 114 | Notwithstanding the above, nothing herein shall supersede or modify 115 | the terms of any separate license agreement you may have executed 116 | with Licensor regarding such Contributions. 117 | 6. Trademarks. This License does not grant permission to use the trade 118 | names, trademarks, service marks, or product names of the Licensor, 119 | except as required for reasonable and customary use in describing the 120 | origin of the Work and reproducing the content of the NOTICE file. 121 | 7. Disclaimer of Warranty. Unless required by applicable law or 122 | agreed to in writing, Licensor provides the Work (and each 123 | Contributor provides its Contributions) on an "AS IS" BASIS, 124 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 125 | implied, including, without limitation, any warranties or conditions 126 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 127 | PARTICULAR PURPOSE. You are solely responsible for determining the 128 | appropriateness of using or redistributing the Work and assume any 129 | risks associated with Your exercise of permissions under this License. 130 | 8. Limitation of Liability. In no event and under no legal theory, 131 | whether in tort (including negligence), contract, or otherwise, 132 | unless required by applicable law (such as deliberate and grossly 133 | negligent acts) or agreed to in writing, shall any Contributor be 134 | liable to You for damages, including any direct, indirect, special, 135 | incidental, or consequential damages of any character arising as a 136 | result of this License or out of the use or inability to use the 137 | Work (including but not limited to damages for loss of goodwill, 138 | work stoppage, computer failure or malfunction, or any and all 139 | other commercial damages or losses), even if such Contributor 140 | has been advised of the possibility of such damages. 141 | 9. Accepting Warranty or Additional Liability. While redistributing 142 | the Work or Derivative Works thereof, You may choose to offer, 143 | and charge a fee for, acceptance of support, warranty, indemnity, 144 | or other liability obligations and/or rights consistent with this 145 | License. However, in accepting such obligations, You may act only 146 | on Your own behalf and on Your sole responsibility, not on behalf 147 | of any other Contributor, and only if You agree to indemnify, 148 | defend, and hold each Contributor harmless for any liability 149 | incurred by, or claims asserted against, such Contributor by reason 150 | of your accepting any such warranty or additional liability. 151 | END OF TERMS AND CONDITIONS 152 | APPENDIX: How to apply the Apache License to your work. 153 | To apply the Apache License to your work, attach the following 154 | boilerplate notice, with the fields enclosed by brackets "[]" 155 | replaced with your own identifying information. (Don't include 156 | the brackets!) The text should be enclosed in the appropriate 157 | comment syntax for the file format. We also recommend that a 158 | file or class name and description of purpose be included on the 159 | same "printed page" as the copyright notice for easier 160 | identification within third-party archives. 161 | Copyright 2015 Google Inc 162 | Licensed under the Apache License, Version 2.0 (the "License"); 163 | you may not use this file except in compliance with the License. 164 | You may obtain a copy of the License at 165 | http://www.apache.org/licenses/LICENSE-2.0 166 | Unless required by applicable law or agreed to in writing, software 167 | distributed under the License is distributed on an "AS IS" BASIS, 168 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 169 | See the License for the specific language governing permissions and 170 | limitations under the License. 171 | All code in any directories or sub-directories that end with *.html or 172 | *.css is licensed under the Creative Commons Attribution International 173 | 4.0 License, which full text can be found here: 174 | https://creativecommons.org/licenses/by/4.0/legalcode. 175 | As an exception to this license, all html or css that is generated by 176 | the software at the direction of the user is copyright the user. The 177 | user has full ownership and control over such content, including 178 | whether and how they wish to license it. 179 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | 3 | target 'FriendlyEats' do 4 | 5 | use_frameworks! 6 | 7 | pod 'SDWebImage' 8 | pod 'Firebase/Core' 9 | pod 'FirebaseUI/Auth', '~> 8.0' 10 | pod 'FirebaseUI/Google' 11 | pod 'FirebaseUI/Email' 12 | pod 'Firebase/Firestore' 13 | pod 'Firebase/Storage' 14 | 15 | target 'FriendlyEatsTests' do 16 | inherit! :search_paths 17 | # Pods for testing 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - BoringSSL-GRPC (0.0.3): 3 | - BoringSSL-GRPC/Implementation (= 0.0.3) 4 | - BoringSSL-GRPC/Interface (= 0.0.3) 5 | - BoringSSL-GRPC/Implementation (0.0.3): 6 | - BoringSSL-GRPC/Interface (= 0.0.3) 7 | - BoringSSL-GRPC/Interface (0.0.3) 8 | - Firebase/Auth (6.2.0): 9 | - Firebase/CoreOnly 10 | - FirebaseAuth (~> 6.1.1) 11 | - Firebase/Core (6.2.0): 12 | - Firebase/CoreOnly 13 | - FirebaseAnalytics (= 6.0.1) 14 | - Firebase/CoreOnly (6.2.0): 15 | - FirebaseCore (= 6.0.2) 16 | - Firebase/Firestore (6.2.0): 17 | - Firebase/CoreOnly 18 | - FirebaseFirestore (~> 1.3.2) 19 | - Firebase/Storage (6.2.0): 20 | - Firebase/CoreOnly 21 | - FirebaseStorage (~> 3.2.1) 22 | - FirebaseAnalytics (6.0.1): 23 | - FirebaseCore (~> 6.0) 24 | - FirebaseInstanceID (~> 4.1) 25 | - GoogleAppMeasurement (= 6.0.1) 26 | - GoogleUtilities/AppDelegateSwizzler (~> 6.0) 27 | - GoogleUtilities/MethodSwizzler (~> 6.0) 28 | - GoogleUtilities/Network (~> 6.0) 29 | - "GoogleUtilities/NSData+zlib (~> 6.0)" 30 | - nanopb (~> 0.3) 31 | - FirebaseAuth (6.1.1): 32 | - FirebaseAuthInterop (~> 1.0) 33 | - FirebaseCore (~> 6.0) 34 | - GoogleUtilities/AppDelegateSwizzler (~> 6.2) 35 | - GoogleUtilities/Environment (~> 6.2) 36 | - GTMSessionFetcher/Core (~> 1.1) 37 | - FirebaseAuthInterop (1.0.0) 38 | - FirebaseCore (6.0.2): 39 | - GoogleUtilities/Environment (~> 6.0) 40 | - GoogleUtilities/Logger (~> 6.0) 41 | - FirebaseFirestore (1.3.2): 42 | - FirebaseAuthInterop (~> 1.0) 43 | - FirebaseCore (~> 6.0) 44 | - FirebaseFirestore/abseil-cpp (= 1.3.2) 45 | - "gRPC-C++ (= 0.0.9)" 46 | - leveldb-library (~> 1.20) 47 | - nanopb (~> 0.3.901) 48 | - Protobuf (~> 3.1) 49 | - FirebaseFirestore/abseil-cpp (1.3.2): 50 | - FirebaseAuthInterop (~> 1.0) 51 | - FirebaseCore (~> 6.0) 52 | - "gRPC-C++ (= 0.0.9)" 53 | - leveldb-library (~> 1.20) 54 | - nanopb (~> 0.3.901) 55 | - Protobuf (~> 3.1) 56 | - FirebaseInstanceID (4.1.1): 57 | - FirebaseCore (~> 6.0) 58 | - GoogleUtilities/Environment (~> 6.0) 59 | - GoogleUtilities/UserDefaults (~> 6.0) 60 | - FirebaseStorage (3.2.1): 61 | - FirebaseAuthInterop (~> 1.0) 62 | - FirebaseCore (~> 6.0) 63 | - GTMSessionFetcher/Core (~> 1.1) 64 | - FirebaseUI/Auth (8.0.2): 65 | - Firebase/Auth (~> 6.0) 66 | - GoogleUtilities/UserDefaults 67 | - FirebaseUI/Email (8.0.2): 68 | - FirebaseUI/Auth 69 | - FirebaseUI/Google (8.0.2): 70 | - FirebaseUI/Auth 71 | - GoogleSignIn (~> 4.0) 72 | - GoogleAppMeasurement (6.0.1): 73 | - GoogleUtilities/AppDelegateSwizzler (~> 6.0) 74 | - GoogleUtilities/MethodSwizzler (~> 6.0) 75 | - GoogleUtilities/Network (~> 6.0) 76 | - "GoogleUtilities/NSData+zlib (~> 6.0)" 77 | - nanopb (~> 0.3) 78 | - GoogleSignIn (4.4.0): 79 | - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" 80 | - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)" 81 | - GTMSessionFetcher/Core (~> 1.1) 82 | - GoogleToolboxForMac/DebugUtils (2.2.1): 83 | - GoogleToolboxForMac/Defines (= 2.2.1) 84 | - GoogleToolboxForMac/Defines (2.2.1) 85 | - "GoogleToolboxForMac/NSDictionary+URLArguments (2.2.1)": 86 | - GoogleToolboxForMac/DebugUtils (= 2.2.1) 87 | - GoogleToolboxForMac/Defines (= 2.2.1) 88 | - "GoogleToolboxForMac/NSString+URLArguments (= 2.2.1)" 89 | - "GoogleToolboxForMac/NSString+URLArguments (2.2.1)" 90 | - GoogleUtilities/AppDelegateSwizzler (6.2.0): 91 | - GoogleUtilities/Environment 92 | - GoogleUtilities/Logger 93 | - GoogleUtilities/Network 94 | - GoogleUtilities/Environment (6.2.0) 95 | - GoogleUtilities/Logger (6.2.0): 96 | - GoogleUtilities/Environment 97 | - GoogleUtilities/MethodSwizzler (6.2.0): 98 | - GoogleUtilities/Logger 99 | - GoogleUtilities/Network (6.2.0): 100 | - GoogleUtilities/Logger 101 | - "GoogleUtilities/NSData+zlib" 102 | - GoogleUtilities/Reachability 103 | - "GoogleUtilities/NSData+zlib (6.2.0)" 104 | - GoogleUtilities/Reachability (6.2.0): 105 | - GoogleUtilities/Logger 106 | - GoogleUtilities/UserDefaults (6.2.0): 107 | - GoogleUtilities/Logger 108 | - "gRPC-C++ (0.0.9)": 109 | - "gRPC-C++/Implementation (= 0.0.9)" 110 | - "gRPC-C++/Interface (= 0.0.9)" 111 | - "gRPC-C++/Implementation (0.0.9)": 112 | - "gRPC-C++/Interface (= 0.0.9)" 113 | - gRPC-Core (= 1.21.0) 114 | - nanopb (~> 0.3) 115 | - "gRPC-C++/Interface (0.0.9)" 116 | - gRPC-Core (1.21.0): 117 | - gRPC-Core/Implementation (= 1.21.0) 118 | - gRPC-Core/Interface (= 1.21.0) 119 | - gRPC-Core/Implementation (1.21.0): 120 | - BoringSSL-GRPC (= 0.0.3) 121 | - gRPC-Core/Interface (= 1.21.0) 122 | - nanopb (~> 0.3) 123 | - gRPC-Core/Interface (1.21.0) 124 | - GTMSessionFetcher/Core (1.2.2) 125 | - leveldb-library (1.20) 126 | - nanopb (0.3.901): 127 | - nanopb/decode (= 0.3.901) 128 | - nanopb/encode (= 0.3.901) 129 | - nanopb/decode (0.3.901) 130 | - nanopb/encode (0.3.901) 131 | - Protobuf (3.8.0) 132 | - SDWebImage (5.0.6): 133 | - SDWebImage/Core (= 5.0.6) 134 | - SDWebImage/Core (5.0.6) 135 | 136 | DEPENDENCIES: 137 | - Firebase/Core 138 | - Firebase/Firestore 139 | - Firebase/Storage 140 | - FirebaseUI/Auth (~> 8.0) 141 | - FirebaseUI/Email 142 | - FirebaseUI/Google 143 | - SDWebImage 144 | 145 | SPEC REPOS: 146 | https://github.com/cocoapods/specs.git: 147 | - BoringSSL-GRPC 148 | - Firebase 149 | - FirebaseAnalytics 150 | - FirebaseAuth 151 | - FirebaseAuthInterop 152 | - FirebaseCore 153 | - FirebaseFirestore 154 | - FirebaseInstanceID 155 | - FirebaseStorage 156 | - FirebaseUI 157 | - GoogleAppMeasurement 158 | - GoogleSignIn 159 | - GoogleToolboxForMac 160 | - GoogleUtilities 161 | - "gRPC-C++" 162 | - gRPC-Core 163 | - GTMSessionFetcher 164 | - leveldb-library 165 | - nanopb 166 | - Protobuf 167 | - SDWebImage 168 | 169 | SPEC CHECKSUMS: 170 | BoringSSL-GRPC: db8764df3204ccea016e1c8dd15d9a9ad63ff318 171 | Firebase: 5965bac23e7fcb5fa6d926ed429c9ecef8a2014e 172 | FirebaseAnalytics: 629301c2b9925f3537d4093a17a72751ae5b7084 173 | FirebaseAuth: 80879f0e9f2c5f40686d5b42f6ccb3cd5dce78ea 174 | FirebaseAuthInterop: 0ffa57668be100582bb7643d4fcb7615496c41fc 175 | FirebaseCore: b0f0262acebfa540e5f97b3832dbb13186980822 176 | FirebaseFirestore: d9c55dfd0cd648f420cc8340c4940dc3eab2d860 177 | FirebaseInstanceID: cdb3827746d53ece7ae87a5c51c25c3055443366 178 | FirebaseStorage: 926d41552072b9fee67aa645760f05f87b7ce604 179 | FirebaseUI: 8734527e38959ba31b433b8115c21941d1b67ec8 180 | GoogleAppMeasurement: 51d8d9ea48f0ca44484d29cfbdef976fbd4fc336 181 | GoogleSignIn: 7ff245e1a7b26d379099d3243a562f5747e23d39 182 | GoogleToolboxForMac: b3553629623a3b1bff17f555e736cd5a6d95ad55 183 | GoogleUtilities: 996e0db07153674fd1b54b220fda3a3dc3547cba 184 | "gRPC-C++": 9dfe7b44821e7b3e44aacad2af29d2c21f7cde83 185 | gRPC-Core: c9aef9a261a1247e881b18059b84d597293c9947 186 | GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23 187 | leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 188 | nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48 189 | Protobuf: 3f617b9a6e73605565086864c9bc26b2bf2dd5a3 190 | SDWebImage: 920f1a2ff1ca8296ad34f6e0510a1ef1d70ac965 191 | 192 | PODFILE CHECKSUM: eaefbe61fd434aa83d26ac415a10d0801a8fe9c9 193 | 194 | COCOAPODS: 1.7.1 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Friendly Eats Extended Codelab 2 | 3 | This is the source code that accompanies the 4 | [Firestore Extended iOS Codelab](https://codelabs.developers.google.com/codelabs/firebase-cloud-firestore-workshop-swift). 5 | 6 | The codelab will walk you through developing a more fully-featured restaurant 7 | recommendation app powered by Cloud Firestore on iOS. 8 | 9 | If you don't want to do the codelab and would rather view the completed sample 10 | code, see the `codelab-complete` branch. 11 | 12 | ## Status 13 | 14 | ![Status: Frozen](https://img.shields.io/badge/Status-Frozen-yellow) 15 | 16 | This repository is no longer under active development. No new features will be added and issues are not actively triaged. Pull Requests which fix bugs are welcome and will be reviewed on a best-effort basis. 17 | 18 | If you maintain a fork of this repository that you believe is healthier than the official version, we may consider recommending your fork. Please open a Pull Request if you believe that is the case. 19 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix $RESOURCE_DIR run lint", 5 | "npm --prefix $RESOURCE_DIR run build" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "./node_modules/.bin/tslint -p tslint.json", 5 | "build": "./node_modules/.bin/tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase experimental:functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "main": "lib/index.js", 13 | "dependencies": { 14 | "firebase-admin": "^11.11.0", 15 | "firebase-functions": "^3.3.0" 16 | }, 17 | "devDependencies": { 18 | "tslint": "^5.20.0", 19 | "typescript": "^2.9.2" 20 | }, 21 | "private": true, 22 | "engines": { 23 | "node": "8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | // TODO(DEVELOPER): Import the Cloud Functions for Firebase and the Firebase Admin modules here. 17 | 18 | // TODO(DEVELOPER): Write the computeAverageReview Function here. 19 | 20 | // TODO(DEVELOPER): Add updateAverage helper function here. 21 | 22 | // TODO(DEVELOPER): Write the updateRest Function here. 23 | 24 | // TODO(DEVELOPER): Add updateRestaurant helper function here. 25 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 69 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 70 | 71 | // Disallow duplicate imports in the same file. 72 | "no-duplicate-imports": true, 73 | 74 | 75 | // -- Strong Warnings -- 76 | // These rules should almost never be needed, but may be included due to legacy code. 77 | // They are left as a warning to avoid frustration with blocked deploys when the developer 78 | // understand the warning and wants to deploy anyway. 79 | 80 | // Warn when an empty interface is defined. These are generally not useful. 81 | "no-empty-interface": {"severity": "warning"}, 82 | 83 | // Warn when an import will have side effects. 84 | "no-import-side-effect": {"severity": "warning"}, 85 | 86 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 87 | // most values and let for values that will change. 88 | "no-var-keyword": {"severity": "warning"}, 89 | 90 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 91 | "triple-equals": {"severity": "warning"}, 92 | 93 | // Warn when using deprecated APIs. 94 | "deprecation": {"severity": "warning"}, 95 | 96 | // -- Light Warnigns -- 97 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 98 | // if TSLint supported such a level. 99 | 100 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 101 | // (Even better: check out utils like .map if transforming an array!) 102 | "prefer-for-of": {"severity": "warning"}, 103 | 104 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 105 | "unified-signatures": {"severity": "warning"}, 106 | 107 | // Prefer const for values that will not change. This better documents code. 108 | "prefer-const": {"severity": "warning"}, 109 | 110 | // Multi-line object liiterals and function calls should have a trailing comma. This helps avoid merge conflicts. 111 | "trailing-comma": {"severity": "warning"} 112 | }, 113 | 114 | "defaultSeverity": "error" 115 | } 116 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | --------------------------------------------------------------------------------