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