├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── architecture.png ├── banner.jpg ├── carplay-freeway.jpg ├── carplay-message.jpg ├── carplay-simulator.jpg ├── carplay-weather.jpg ├── carplay.jpg ├── subscription.png └── xcode-simulate-location.jpg └── mobile ├── .gitignore ├── amplify ├── AmplifyModels.swift ├── Location+Schema.swift ├── Location.swift ├── Message+Schema.swift ├── Message.swift ├── Place+Schema.swift ├── Place.swift ├── PlaceType.swift ├── Weather+Schema.swift ├── Weather.swift └── schema.graphql ├── mobile.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── mobile ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── icon-1024.png │ ├── Contents.json │ └── poi.imageset │ │ ├── Contents.json │ │ ├── poi-1.png │ │ ├── poi-2.png │ │ └── poi.png ├── CarPlaySceneDelegate.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Services │ ├── DataService.swift │ ├── LocationService.swift │ ├── Message+Extensions.swift │ ├── PlaceType+Extensions.swift │ ├── Queries.swift │ ├── Subscriptions.swift │ └── VehicleMessageService.swift ├── Views │ ├── CarPlayMapView.swift │ ├── ContentView.swift │ ├── FormButton.swift │ ├── LoadingView.swift │ ├── MapButton.swift │ ├── MapView.swift │ ├── MessagesView.swift │ ├── PlacesView.swift │ └── WeatherView.swift ├── mobile.entitlements └── mobileApp.swift └── scripts └── amplify-config.sh /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Full Stack Swift with Apple CarPlay 2 | 3 | ![Image description](images/banner.jpg) 4 | 5 | This application demonstrates a full-stack Apple CarPlay app that uses Swift for both the UI and the backend services in AWS. The app implements the new [AWS SDK for Swift](https://github.com/awslabs/aws-sdk-swift) and latest features of AWS Lambda that allow you to develop and deploy functions written in Swift as Docker images. 6 | 7 | This is important as it allows frontend developers who are proficient in Swift to leverage their skills in building out the backend of their applications. It also enables an entire geneation of Swift engineers to build apps on AWS using their language of choice. 8 | 9 | This sample app is useful for iOS/CarPlay developers learning how to interact with backend services running on AWS. It is also beneficial for customers who want to build their backend infrastructure using Swift, regardless if there is an iOS front end component. Explore the SAM portion of this project to discover how to build and deploy Swift based Lambda functions to AWS. 10 | 11 | ![Image description](images/carplay.jpg) 12 | 13 | The application tracks the user's current location and displays the current weather, air quality, and nearby points of interest such as coffee, food, and fuel. The app also allows you to send messages from the AWS Cloud to the app which are displayed in real-time to the user. 14 | 15 | ## Architecture 16 | 17 | ![Image description](images/architecture.png) 18 | 19 | 1. The Apple CarPlay app is written in Swift and uses AWS Amplify libraries to communicate with services in the AWS Cloud. 20 | 2. All data is served to the client application via an AWS AppSync GraphQL API. As the client changes its location, queries are sent via the API to obtain weather, air quality, and points of interest in the vicinity of the user. 21 | 3. The AWS AppSync GraphQL API uses Lambda functions written in Swift to interact with Amazon Location Service for points of interest. It also communicates with a 3rd party API outside of AWS for weather and air quality. The API key for the 3rd party weather service is stored in AWS Secrets Manager. 22 | 4. The Lambda functions use the new [AWS SDK for Swift](https://github.com/awslabs/aws-sdk-swift) to interact with the AWS services! 23 | 5. The client establishes a subscription to AWS AppSync to receive real-time notifications triggered from the cloud. 24 | 25 | ## Getting Started 26 | 27 | ### **Prerequisites** 28 | 29 | The following software was used in the development of this application. While it may work with alternative versions, we recommend you deploy the specified minimum version. 30 | 31 | 1. An AWS account in which you have Administrator access. 32 | 33 | 2. [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) (^2.19.5) the AWS Command Line Interface (CLI) is used to configure your connection credentials to AWS. These credentials are used by the AWS Serverless Application Model (SAM). 34 | 35 | 3. [Node.js](https://nodejs.org/en/download/current/) (^18.19.0) with NPM (^10.1.0) 36 | 37 | 4. [AWS Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) (^1.127.0) used to generate the AWS services used by the application. 38 | 39 | 5. [IQ Air API Key](https://dashboard.iqair.com/) is a 3rd party API used to obtain weather and air quality for a specified location. Create a free Community Edition API key. 40 | 41 | 6. [Docker Desktop](https://www.docker.com/products/docker-desktop) (^4.36) Docker is used to compile the Swift Lambda functions into a Docker image. 42 | 43 | 7. [Xcode](https://developer.apple.com/xcode/) (^16.2) Xcode is used to build and debug the CarPlay application. You will need iOS Simulator ^18.0 enabled. 44 | 45 | ### **Installation** 46 | 47 | The application utilizes the AWS Serverless Application Model (SAM) and Docker to compile and deploy your Swift based Lambda functions. 48 | 49 | _Make sure you have [configured the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) prior to following these instructions as several steps assume you gave defined credentials for your default AWS account._ 50 | 51 | **Clone this code repository** 52 | 53 | ``` 54 | git clone git@github.com:aws-samples/aws-serverless-fullstack-swift-apple-carplay-example.git 55 | 56 | cd aws-serverless-fullstack-swift-apple-carplay-example 57 | ``` 58 | 59 | **Initialize the Backend** 60 | 61 | First, initialize the SAM project from the public GraphQL template. 62 | 63 | ``` 64 | sam init --location gh:aws-samples/aws-sam-swift 65 | ``` 66 | 67 | Select template 3 _Serverless GraphQL API (GraphQL API using AWS AppSync and Amazon Location Service)_ template. 68 | 69 | Enter **sam** as the project name. 70 | 71 | ```bash 72 | Select a template 73 | 1 - Hello World (Hello World on AWS) 74 | 2 - Serverless REST API (REST API using Amazon API Gateway and Amazon DynamoDB) 75 | 3 - Serverless GraphQL API (GraphQL API using AWS AppSync and Amazon Location Service) 76 | Choose from [1/2/3] (1): 3 77 | 78 | [1/1] project_name (Swift Graphql API on AWS): sam 79 | ``` 80 | 81 | **Build and deploy the SAM project** 82 | 83 | SAM compiles your Swift Lambda functions and prepares your project for deployment. SAM uses Docker on your local machine to compile the Lambda Functions. 84 | 85 | Switch to the **sam** folder and build the project. 86 | 87 | ```bash 88 | cd sam 89 | ``` 90 | 91 | Then use the **swift package archive** command to compile the Swift code for Lambda functions. 92 | 93 | ```bash 94 | swift package archive --allow-network-connections docker 95 | ``` 96 | 97 | Then use the **sam build** command to package your Swift Lambda functions for deployment to AWS. 98 | 99 | ```bash 100 | sam build 101 | ``` 102 | 103 | The _sam deploy_ command deploys your application to AWS. This includes 4 Lambda functions, an Amazon Location Place Index, and a Secrets Manager secret. 104 | 105 | If you are deploying to a region other than _us-east-1_ specify the region when prompted. Accept the default value for all other prompts. 106 | 107 | ```bash 108 | sam deploy --guided 109 | ``` 110 | 111 | **Configure Secrets Manager with the IQ Air API Key** 112 | 113 | SAM created a secret in AWS Secrets Manager to hold the IQ Air API key. Lambda uses this secret to obtain the API Key for the IQ Air API. Use the AWS CLI to update the secret with the API key you obtained from the IQ Air site. 114 | 115 | Replace the values in brackets with your values: 116 | 117 | ```bash 118 | aws secretsmanager put-secret-value --secret-id SwiftAPIWeatherApiKeySecret --secret-string [your IQ Air API key] 119 | ``` 120 | 121 | **Configure the Amplify Libraries** 122 | 123 | The application uses Amplify Swift libraries to communicate with your GraphQL API. You create a configuration file for the application to use your API. 124 | 125 | Switch to the **mobile** folder of the application: 126 | 127 | ```bash 128 | cd ../mobile 129 | ``` 130 | 131 | When you deloyed the SAM application, 2 values were output at the end of the deployment. 132 | 133 | ```bash 134 | Outputs 135 | ------------------------------------------------------------------------------------------------------------ 136 | Key APIEndpointValue 137 | Description AppSync API Endpoint 138 | Value [your-api-endpoint] 139 | 140 | Key ApiKeyValue 141 | Description API Key 142 | Value [your-api-key] 143 | 144 | ------------------------------------------------------------------------------------------------------------ 145 | ``` 146 | 147 | Pass these values, and your AWS region, to the **amplify-config.sh** script. 148 | 149 | ```bash 150 | ./scripts/amplify-config.sh \ 151 | [your-api-endpoint] \ 152 | [your-api-key] \ 153 | [region] 154 | ``` 155 | 156 | For example: 157 | 158 | ```bash 159 | ./scripts/amplify-config.sh \ 160 | https://your-api.appsync-api.us-east-1.amazonaws.com/graphql \ 161 | 1234567890abcdefg \ 162 | us-east-1 163 | ``` 164 | 165 | ## Run the CarPlay app 166 | 167 | From the **mobile** folder of the application open the project in Xcode: 168 | 169 | ```bash 170 | open mobile.xcodeproj 171 | ``` 172 | 173 | _Note - you can also open the project from the Xcode UI_ 174 | 175 | Once the project loads in Xcode, select an iPhone simulator from the menu bar and the "Run" arrow button to start the app. 176 | 177 | Once the iPhone app is running in the iOS Simulator, initiate a "Freeway Drive" to simulate the user driving: 178 | 179 | ![Image description](images/carplay-freeway.jpg) 180 | 181 | As the user's location changes: 182 | 183 | - Select the Weather button at the bottom of the iOS app to view the weather and air quality of the current location. 184 | 185 | - Select the Places button to view coffee, food, and fuel locations in the vicinity of the user. 186 | 187 | - Select the Messages button to view messages sent to the user from the AWS Cloud. Instructions for sending messages are detailed below. 188 | 189 | If the simulator does not display the feature to simulate a Freeway Drive, ensure Location Simulation is enabled in Xcode: 190 | 191 | From the Xcode menu select _Product -> Scheme -> Edit Scheme_ 192 | 193 | Then ensure _Core Location: Allow Location Simulation_ is checked. 194 | 195 | ![Image description](images/xcode-simulate-location.jpg) 196 | 197 | **Start the CarPlay simulator** 198 | 199 | From the Simulator menu select I/O -> External Displays -> CarPlay: 200 | 201 | ![Image description](images/carplay-simulator.jpg) 202 | 203 | When the CarPlay simulator screen launches select the AWS app: 204 | 205 | ![Image description](images/carplay.jpg) 206 | 207 | The app will display a map with the user's current location. Click the map to view the navbar buttons and select the Weather and Places buttons. 208 | 209 | _Note - when selecting a location from the Places screen (Coffee, Food, or Fuel) the screen will display a navigation alert to "Go" to that location. For this sample we have not implemented navigation functionality. That is functionality you may want to add to your version of this app._ 210 | 211 | **Send a real-time message to the application from AWS** 212 | 213 | To send a message to the driver, logon to the AWS Console and navigate to the AppSync service. From there select the **SwiftGraphQLApi** API. 214 | 215 | From the API screen select the **Run a Query** button. Paste the following GraphQL mutation code into the middle query panel and click the orange Run arrow button 216 | 217 | ```bash 218 | mutation MyMutation { 219 | createMessage(text: "Pickup package", recipient: "Vehicle1") { 220 | id 221 | text 222 | recipient 223 | timestamp 224 | } 225 | } 226 | ``` 227 | 228 | Console screenshot: 229 | 230 | ![Image description](images/subscription.png) 231 | 232 | You should see the message delivered to both the CarPlay and iPhone screens. 233 | 234 | ## Cleanup 235 | 236 | Once you are finished working with this project, you may want to delete the resources it created in your AWS account. 237 | 238 | From the **sam** folder delete the resources created by the SAM: 239 | 240 | ```bash 241 | sam delete 242 | ``` 243 | 244 | ## License 245 | 246 | This sample code is made available under a modified MIT-0 license. See the LICENSE file. 247 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/architecture.png -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/banner.jpg -------------------------------------------------------------------------------- /images/carplay-freeway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/carplay-freeway.jpg -------------------------------------------------------------------------------- /images/carplay-message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/carplay-message.jpg -------------------------------------------------------------------------------- /images/carplay-simulator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/carplay-simulator.jpg -------------------------------------------------------------------------------- /images/carplay-weather.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/carplay-weather.jpg -------------------------------------------------------------------------------- /images/carplay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/carplay.jpg -------------------------------------------------------------------------------- /images/subscription.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/subscription.png -------------------------------------------------------------------------------- /images/xcode-simulate-location.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/images/xcode-simulate-location.jpg -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # Amplify 5 | amplifyconfiguration.json 6 | 7 | # VS Code 8 | .vscode 9 | 10 | # XCode 11 | xcuserdata/ 12 | -------------------------------------------------------------------------------- /mobile/amplify/AmplifyModels.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | // Contains the set of classes that conforms to the `Model` protocol. 6 | 7 | final public class AmplifyModels: AmplifyModelRegistration { 8 | public let version: String = "551dbb617314f9244128dd7d925bf72e" 9 | 10 | public func registerModels(registry: ModelRegistry.Type) { 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /mobile/amplify/Location+Schema.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | extension Location { 6 | // MARK: - CodingKeys 7 | public enum CodingKeys: String, ModelKey { 8 | case latitude 9 | case longitude 10 | case name 11 | } 12 | 13 | public static let keys = CodingKeys.self 14 | // MARK: - ModelSchema 15 | 16 | public static let schema = defineSchema { model in 17 | let location = Location.keys 18 | 19 | model.listPluralName = "Locations" 20 | model.syncPluralName = "Locations" 21 | 22 | model.fields( 23 | .field(location.latitude, is: .required, ofType: .double), 24 | .field(location.longitude, is: .required, ofType: .double), 25 | .field(location.name, is: .required, ofType: .string) 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /mobile/amplify/Location.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | public struct Location: Embeddable { 6 | var latitude: Double 7 | var longitude: Double 8 | var name: String 9 | } -------------------------------------------------------------------------------- /mobile/amplify/Message+Schema.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | extension Message { 6 | // MARK: - CodingKeys 7 | public enum CodingKeys: String, ModelKey { 8 | case id 9 | case recipient 10 | case text 11 | case timestamp 12 | } 13 | 14 | public static let keys = CodingKeys.self 15 | // MARK: - ModelSchema 16 | 17 | public static let schema = defineSchema { model in 18 | let message = Message.keys 19 | 20 | model.listPluralName = "Messages" 21 | model.syncPluralName = "Messages" 22 | 23 | model.fields( 24 | .field(message.id, is: .required, ofType: .string), 25 | .field(message.recipient, is: .required, ofType: .string), 26 | .field(message.text, is: .required, ofType: .string), 27 | .field(message.timestamp, is: .required, ofType: .dateTime) 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /mobile/amplify/Message.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | public struct Message: Model { 6 | public let id: String 7 | public var recipient: String 8 | public var timestamp: Temporal.DateTime 9 | public var text: String 10 | 11 | public init(id: String = UUID().uuidString, 12 | recipient: String, 13 | timestamp: Temporal.DateTime, 14 | text: String) { 15 | self.id = id 16 | self.recipient = recipient 17 | self.timestamp = timestamp 18 | self.text = text 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mobile/amplify/Place+Schema.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | extension Place { 6 | // MARK: - CodingKeys 7 | public enum CodingKeys: String, ModelKey { 8 | case address 9 | case latitude 10 | case longitude 11 | case name 12 | case placeType 13 | } 14 | 15 | public static let keys = CodingKeys.self 16 | // MARK: - ModelSchema 17 | 18 | public static let schema = defineSchema { model in 19 | let place = Place.keys 20 | 21 | model.listPluralName = "Places" 22 | model.syncPluralName = "Places" 23 | 24 | model.fields( 25 | .field(place.address, is: .required, ofType: .string), 26 | .field(place.latitude, is: .required, ofType: .double), 27 | .field(place.longitude, is: .required, ofType: .double), 28 | .field(place.name, is: .required, ofType: .string), 29 | .field(place.placeType, is: .required, ofType: .enum(type: PlaceType.self)) 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /mobile/amplify/Place.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | public struct Place: Embeddable { 6 | var address: String 7 | var latitude: Double 8 | var longitude: Double 9 | var name: String 10 | var placeType: PlaceType 11 | } -------------------------------------------------------------------------------- /mobile/amplify/PlaceType.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | public enum PlaceType: String, EnumPersistable { 6 | case coffee 7 | case food 8 | case fuel 9 | } -------------------------------------------------------------------------------- /mobile/amplify/Weather+Schema.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | extension Weather { 6 | // MARK: - CodingKeys 7 | public enum CodingKeys: String, ModelKey { 8 | case aqIndex 9 | case latitude 10 | case longitude 11 | case temperature 12 | } 13 | 14 | public static let keys = CodingKeys.self 15 | // MARK: - ModelSchema 16 | 17 | public static let schema = defineSchema { model in 18 | let weather = Weather.keys 19 | 20 | model.listPluralName = "Weathers" 21 | model.syncPluralName = "Weathers" 22 | 23 | model.fields( 24 | .field(weather.aqIndex, is: .required, ofType: .double), 25 | .field(weather.latitude, is: .required, ofType: .double), 26 | .field(weather.longitude, is: .required, ofType: .double), 27 | .field(weather.temperature, is: .required, ofType: .double) 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /mobile/amplify/Weather.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | import Amplify 3 | import Foundation 4 | 5 | public struct Weather: Embeddable { 6 | var aqIndex: Double 7 | var latitude: Double 8 | var longitude: Double 9 | var temperature: Double 10 | } -------------------------------------------------------------------------------- /mobile/amplify/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | subscription: Subscription 5 | } 6 | 7 | type Location { 8 | latitude: Float! 9 | longitude: Float! 10 | name: String! 11 | } 12 | 13 | type Message { 14 | id: ID! 15 | recipient: String! 16 | text: String! 17 | timestamp: AWSDateTime! 18 | } 19 | 20 | type Mutation { 21 | createMessage(recipient: String!, text: String!): Message 22 | } 23 | 24 | type Place { 25 | address: String! 26 | latitude: Float! 27 | longitude: Float! 28 | name: String! 29 | placeType: PlaceType! 30 | } 31 | 32 | type Query { 33 | getCity(latitude: Float!, longitude: Float!): Location 34 | getPlaces(latitude: Float!, longitude: Float!, maxResults: Int!, placeType: PlaceType!): [Place] 35 | getWeather(latitude: Float!, longitude: Float!): Weather 36 | } 37 | 38 | type Subscription { 39 | onCreateMessage(recipient: String!): Message @aws_subscribe(mutations : ["createMessage"]) 40 | } 41 | 42 | type Weather { 43 | aqIndex: Float! 44 | latitude: Float! 45 | longitude: Float! 46 | temperature: Float! 47 | } 48 | 49 | enum PlaceType { 50 | coffee 51 | food 52 | fuel 53 | } 54 | -------------------------------------------------------------------------------- /mobile/mobile.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1C30CF7A2D120B19003B08E8 /* AWSAPIPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 1C30CF792D120B19003B08E8 /* AWSAPIPlugin */; }; 11 | 1C30CF7C2D120B19003B08E8 /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 1C30CF7B2D120B19003B08E8 /* Amplify */; }; 12 | 1C9F4B272B65D8D20013245C /* mobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B262B65D8D20013245C /* mobileApp.swift */; }; 13 | 1C9F4B292B65D8D20013245C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B282B65D8D20013245C /* ContentView.swift */; }; 14 | 1C9F4B2B2B65D8D30013245C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1C9F4B2A2B65D8D30013245C /* Assets.xcassets */; }; 15 | 1C9F4B2E2B65D8D30013245C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1C9F4B2D2B65D8D30013245C /* Preview Assets.xcassets */; }; 16 | 1C9F4B4C2B65D98E0013245C /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B402B65D98E0013245C /* Location.swift */; }; 17 | 1C9F4B4D2B65D98E0013245C /* Weather+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B412B65D98E0013245C /* Weather+Schema.swift */; }; 18 | 1C9F4B4E2B65D98E0013245C /* schema.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 1C9F4B422B65D98E0013245C /* schema.graphql */; }; 19 | 1C9F4B4F2B65D98E0013245C /* AmplifyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B432B65D98E0013245C /* AmplifyModels.swift */; }; 20 | 1C9F4B502B65D98E0013245C /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B442B65D98E0013245C /* Message.swift */; }; 21 | 1C9F4B512B65D98E0013245C /* Place.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B452B65D98E0013245C /* Place.swift */; }; 22 | 1C9F4B522B65D98E0013245C /* Place+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B462B65D98E0013245C /* Place+Schema.swift */; }; 23 | 1C9F4B532B65D98E0013245C /* PlaceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B472B65D98E0013245C /* PlaceType.swift */; }; 24 | 1C9F4B542B65D98E0013245C /* Location+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B482B65D98E0013245C /* Location+Schema.swift */; }; 25 | 1C9F4B552B65D98E0013245C /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B492B65D98E0013245C /* Weather.swift */; }; 26 | 1C9F4B562B65D98E0013245C /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 1C9F4B4A2B65D98E0013245C /* amplifyconfiguration.json */; }; 27 | 1C9F4B572B65D98E0013245C /* Message+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B4B2B65D98E0013245C /* Message+Schema.swift */; }; 28 | 1C9F4B5B2B65DB220013245C /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9F4B5A2B65DB220013245C /* MapView.swift */; }; 29 | 1CC5A6872B681A0500F7419A /* DataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6862B681A0500F7419A /* DataService.swift */; }; 30 | 1CC5A6892B681A5400F7419A /* Queries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6882B681A5400F7419A /* Queries.swift */; }; 31 | 1CC5A68B2B681B1500F7419A /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A68A2B681B1500F7419A /* LocationService.swift */; }; 32 | 1CC5A68D2B68219A00F7419A /* MapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A68C2B68219A00F7419A /* MapButton.swift */; }; 33 | 1CC5A68F2B68222100F7419A /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A68E2B68222100F7419A /* WeatherView.swift */; }; 34 | 1CC5A6912B68224A00F7419A /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6902B68224A00F7419A /* LoadingView.swift */; }; 35 | 1CC5A6932B68227400F7419A /* FormButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6922B68227400F7419A /* FormButton.swift */; }; 36 | 1CC5A6952B68242800F7419A /* PlacesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6942B68242800F7419A /* PlacesView.swift */; }; 37 | 1CC5A6972B68245F00F7419A /* PlaceType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6962B68245F00F7419A /* PlaceType+Extensions.swift */; }; 38 | 1CC5A6992B68261700F7419A /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6982B68261700F7419A /* MessagesView.swift */; }; 39 | 1CC5A69B2B6826C400F7419A /* VehicleMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A69A2B6826C400F7419A /* VehicleMessageService.swift */; }; 40 | 1CC5A69D2B6826D200F7419A /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A69C2B6826D200F7419A /* Subscriptions.swift */; }; 41 | 1CC5A69F2B682A0900F7419A /* Message+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A69E2B682A0900F7419A /* Message+Extensions.swift */; }; 42 | 1CC5A6A12B682FBA00F7419A /* CarPlayMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6A02B682FBA00F7419A /* CarPlayMapView.swift */; }; 43 | 1CC5A6A52B68338900F7419A /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC5A6A42B68338900F7419A /* CarPlaySceneDelegate.swift */; }; 44 | /* End PBXBuildFile section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 1C9F4B232B65D8D20013245C /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 1C9F4B262B65D8D20013245C /* mobileApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mobileApp.swift; sourceTree = ""; }; 49 | 1C9F4B282B65D8D20013245C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50 | 1C9F4B2A2B65D8D30013245C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 1C9F4B2D2B65D8D30013245C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 52 | 1C9F4B402B65D98E0013245C /* Location.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 53 | 1C9F4B412B65D98E0013245C /* Weather+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Weather+Schema.swift"; sourceTree = ""; }; 54 | 1C9F4B422B65D98E0013245C /* schema.graphql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.graphql; sourceTree = ""; }; 55 | 1C9F4B432B65D98E0013245C /* AmplifyModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplifyModels.swift; sourceTree = ""; }; 56 | 1C9F4B442B65D98E0013245C /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 57 | 1C9F4B452B65D98E0013245C /* Place.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Place.swift; sourceTree = ""; }; 58 | 1C9F4B462B65D98E0013245C /* Place+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Place+Schema.swift"; sourceTree = ""; }; 59 | 1C9F4B472B65D98E0013245C /* PlaceType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceType.swift; sourceTree = ""; }; 60 | 1C9F4B482B65D98E0013245C /* Location+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Location+Schema.swift"; sourceTree = ""; }; 61 | 1C9F4B492B65D98E0013245C /* Weather.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; 62 | 1C9F4B4A2B65D98E0013245C /* amplifyconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = amplifyconfiguration.json; sourceTree = ""; }; 63 | 1C9F4B4B2B65D98E0013245C /* Message+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Message+Schema.swift"; sourceTree = ""; }; 64 | 1C9F4B5A2B65DB220013245C /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; 65 | 1CC5A6862B681A0500F7419A /* DataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataService.swift; sourceTree = ""; }; 66 | 1CC5A6882B681A5400F7419A /* Queries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Queries.swift; sourceTree = ""; }; 67 | 1CC5A68A2B681B1500F7419A /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = ""; }; 68 | 1CC5A68C2B68219A00F7419A /* MapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButton.swift; sourceTree = ""; }; 69 | 1CC5A68E2B68222100F7419A /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = ""; }; 70 | 1CC5A6902B68224A00F7419A /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 71 | 1CC5A6922B68227400F7419A /* FormButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormButton.swift; sourceTree = ""; }; 72 | 1CC5A6942B68242800F7419A /* PlacesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesView.swift; sourceTree = ""; }; 73 | 1CC5A6962B68245F00F7419A /* PlaceType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaceType+Extensions.swift"; sourceTree = ""; }; 74 | 1CC5A6982B68261700F7419A /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; 75 | 1CC5A69A2B6826C400F7419A /* VehicleMessageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleMessageService.swift; sourceTree = ""; }; 76 | 1CC5A69C2B6826D200F7419A /* Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscriptions.swift; sourceTree = ""; }; 77 | 1CC5A69E2B682A0900F7419A /* Message+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Extensions.swift"; sourceTree = ""; }; 78 | 1CC5A6A02B682FBA00F7419A /* CarPlayMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayMapView.swift; sourceTree = ""; }; 79 | 1CC5A6A42B68338900F7419A /* CarPlaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlaySceneDelegate.swift; sourceTree = ""; }; 80 | 1CC5A6A62B68340900F7419A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 81 | 1CC5A6AB2B6841BE00F7419A /* mobile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = mobile.entitlements; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 1C9F4B202B65D8D20013245C /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 1C30CF7A2D120B19003B08E8 /* AWSAPIPlugin in Frameworks */, 90 | 1C30CF7C2D120B19003B08E8 /* Amplify in Frameworks */, 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | /* End PBXFrameworksBuildPhase section */ 95 | 96 | /* Begin PBXGroup section */ 97 | 1C9F4B1A2B65D8D10013245C = { 98 | isa = PBXGroup; 99 | children = ( 100 | 1C9F4B252B65D8D20013245C /* mobile */, 101 | 1C9F4B242B65D8D20013245C /* Products */, 102 | ); 103 | sourceTree = ""; 104 | }; 105 | 1C9F4B242B65D8D20013245C /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 1C9F4B232B65D8D20013245C /* mobile.app */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 1C9F4B252B65D8D20013245C /* mobile */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 1CC5A6AB2B6841BE00F7419A /* mobile.entitlements */, 117 | 1CC5A6A62B68340900F7419A /* Info.plist */, 118 | 1C9F4B592B65DAE20013245C /* Services */, 119 | 1C9F4B582B65D9ED0013245C /* Views */, 120 | 1C9F4B3F2B65D98E0013245C /* amplify */, 121 | 1C9F4B262B65D8D20013245C /* mobileApp.swift */, 122 | 1CC5A6A42B68338900F7419A /* CarPlaySceneDelegate.swift */, 123 | 1C9F4B2A2B65D8D30013245C /* Assets.xcassets */, 124 | 1C9F4B2C2B65D8D30013245C /* Preview Content */, 125 | ); 126 | path = mobile; 127 | sourceTree = ""; 128 | }; 129 | 1C9F4B2C2B65D8D30013245C /* Preview Content */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 1C9F4B2D2B65D8D30013245C /* Preview Assets.xcassets */, 133 | ); 134 | path = "Preview Content"; 135 | sourceTree = ""; 136 | }; 137 | 1C9F4B3F2B65D98E0013245C /* amplify */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 1C9F4B402B65D98E0013245C /* Location.swift */, 141 | 1C9F4B412B65D98E0013245C /* Weather+Schema.swift */, 142 | 1C9F4B422B65D98E0013245C /* schema.graphql */, 143 | 1C9F4B432B65D98E0013245C /* AmplifyModels.swift */, 144 | 1C9F4B442B65D98E0013245C /* Message.swift */, 145 | 1C9F4B452B65D98E0013245C /* Place.swift */, 146 | 1C9F4B462B65D98E0013245C /* Place+Schema.swift */, 147 | 1C9F4B472B65D98E0013245C /* PlaceType.swift */, 148 | 1C9F4B482B65D98E0013245C /* Location+Schema.swift */, 149 | 1C9F4B492B65D98E0013245C /* Weather.swift */, 150 | 1C9F4B4A2B65D98E0013245C /* amplifyconfiguration.json */, 151 | 1C9F4B4B2B65D98E0013245C /* Message+Schema.swift */, 152 | ); 153 | path = amplify; 154 | sourceTree = SOURCE_ROOT; 155 | }; 156 | 1C9F4B582B65D9ED0013245C /* Views */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 1C9F4B282B65D8D20013245C /* ContentView.swift */, 160 | 1CC5A68C2B68219A00F7419A /* MapButton.swift */, 161 | 1CC5A6942B68242800F7419A /* PlacesView.swift */, 162 | 1C9F4B5A2B65DB220013245C /* MapView.swift */, 163 | 1CC5A6902B68224A00F7419A /* LoadingView.swift */, 164 | 1CC5A6982B68261700F7419A /* MessagesView.swift */, 165 | 1CC5A6A02B682FBA00F7419A /* CarPlayMapView.swift */, 166 | 1CC5A6922B68227400F7419A /* FormButton.swift */, 167 | 1CC5A68E2B68222100F7419A /* WeatherView.swift */, 168 | ); 169 | path = Views; 170 | sourceTree = ""; 171 | }; 172 | 1C9F4B592B65DAE20013245C /* Services */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 1CC5A6862B681A0500F7419A /* DataService.swift */, 176 | 1CC5A6962B68245F00F7419A /* PlaceType+Extensions.swift */, 177 | 1CC5A68A2B681B1500F7419A /* LocationService.swift */, 178 | 1CC5A69A2B6826C400F7419A /* VehicleMessageService.swift */, 179 | 1CC5A69C2B6826D200F7419A /* Subscriptions.swift */, 180 | 1CC5A6882B681A5400F7419A /* Queries.swift */, 181 | 1CC5A69E2B682A0900F7419A /* Message+Extensions.swift */, 182 | ); 183 | path = Services; 184 | sourceTree = ""; 185 | }; 186 | /* End PBXGroup section */ 187 | 188 | /* Begin PBXNativeTarget section */ 189 | 1C9F4B222B65D8D20013245C /* mobile */ = { 190 | isa = PBXNativeTarget; 191 | buildConfigurationList = 1C9F4B312B65D8D30013245C /* Build configuration list for PBXNativeTarget "mobile" */; 192 | buildPhases = ( 193 | 1C9F4B1F2B65D8D20013245C /* Sources */, 194 | 1C9F4B202B65D8D20013245C /* Frameworks */, 195 | 1C9F4B212B65D8D20013245C /* Resources */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | ); 201 | name = mobile; 202 | packageProductDependencies = ( 203 | 1C30CF792D120B19003B08E8 /* AWSAPIPlugin */, 204 | 1C30CF7B2D120B19003B08E8 /* Amplify */, 205 | ); 206 | productName = mobile; 207 | productReference = 1C9F4B232B65D8D20013245C /* mobile.app */; 208 | productType = "com.apple.product-type.application"; 209 | }; 210 | /* End PBXNativeTarget section */ 211 | 212 | /* Begin PBXProject section */ 213 | 1C9F4B1B2B65D8D10013245C /* Project object */ = { 214 | isa = PBXProject; 215 | attributes = { 216 | BuildIndependentTargetsInParallel = 1; 217 | LastSwiftUpdateCheck = 1520; 218 | LastUpgradeCheck = 1520; 219 | TargetAttributes = { 220 | 1C9F4B222B65D8D20013245C = { 221 | CreatedOnToolsVersion = 15.2; 222 | }; 223 | }; 224 | }; 225 | buildConfigurationList = 1C9F4B1E2B65D8D10013245C /* Build configuration list for PBXProject "mobile" */; 226 | compatibilityVersion = "Xcode 14.0"; 227 | developmentRegion = en; 228 | hasScannedForEncodings = 0; 229 | knownRegions = ( 230 | en, 231 | Base, 232 | ); 233 | mainGroup = 1C9F4B1A2B65D8D10013245C; 234 | packageReferences = ( 235 | 1C30CF782D120B19003B08E8 /* XCRemoteSwiftPackageReference "amplify-swift" */, 236 | ); 237 | productRefGroup = 1C9F4B242B65D8D20013245C /* Products */; 238 | projectDirPath = ""; 239 | projectRoot = ""; 240 | targets = ( 241 | 1C9F4B222B65D8D20013245C /* mobile */, 242 | ); 243 | }; 244 | /* End PBXProject section */ 245 | 246 | /* Begin PBXResourcesBuildPhase section */ 247 | 1C9F4B212B65D8D20013245C /* Resources */ = { 248 | isa = PBXResourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 1C9F4B2E2B65D8D30013245C /* Preview Assets.xcassets in Resources */, 252 | 1C9F4B4E2B65D98E0013245C /* schema.graphql in Resources */, 253 | 1C9F4B2B2B65D8D30013245C /* Assets.xcassets in Resources */, 254 | 1C9F4B562B65D98E0013245C /* amplifyconfiguration.json in Resources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXResourcesBuildPhase section */ 259 | 260 | /* Begin PBXSourcesBuildPhase section */ 261 | 1C9F4B1F2B65D8D20013245C /* Sources */ = { 262 | isa = PBXSourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | 1C9F4B4F2B65D98E0013245C /* AmplifyModels.swift in Sources */, 266 | 1CC5A6972B68245F00F7419A /* PlaceType+Extensions.swift in Sources */, 267 | 1CC5A68D2B68219A00F7419A /* MapButton.swift in Sources */, 268 | 1CC5A6872B681A0500F7419A /* DataService.swift in Sources */, 269 | 1CC5A6952B68242800F7419A /* PlacesView.swift in Sources */, 270 | 1C9F4B292B65D8D20013245C /* ContentView.swift in Sources */, 271 | 1CC5A6A52B68338900F7419A /* CarPlaySceneDelegate.swift in Sources */, 272 | 1CC5A69D2B6826D200F7419A /* Subscriptions.swift in Sources */, 273 | 1C9F4B4C2B65D98E0013245C /* Location.swift in Sources */, 274 | 1C9F4B552B65D98E0013245C /* Weather.swift in Sources */, 275 | 1CC5A69F2B682A0900F7419A /* Message+Extensions.swift in Sources */, 276 | 1CC5A6A12B682FBA00F7419A /* CarPlayMapView.swift in Sources */, 277 | 1CC5A6892B681A5400F7419A /* Queries.swift in Sources */, 278 | 1C9F4B5B2B65DB220013245C /* MapView.swift in Sources */, 279 | 1C9F4B502B65D98E0013245C /* Message.swift in Sources */, 280 | 1C9F4B532B65D98E0013245C /* PlaceType.swift in Sources */, 281 | 1CC5A69B2B6826C400F7419A /* VehicleMessageService.swift in Sources */, 282 | 1C9F4B542B65D98E0013245C /* Location+Schema.swift in Sources */, 283 | 1CC5A68B2B681B1500F7419A /* LocationService.swift in Sources */, 284 | 1C9F4B272B65D8D20013245C /* mobileApp.swift in Sources */, 285 | 1C9F4B572B65D98E0013245C /* Message+Schema.swift in Sources */, 286 | 1C9F4B512B65D98E0013245C /* Place.swift in Sources */, 287 | 1CC5A6932B68227400F7419A /* FormButton.swift in Sources */, 288 | 1CC5A68F2B68222100F7419A /* WeatherView.swift in Sources */, 289 | 1C9F4B522B65D98E0013245C /* Place+Schema.swift in Sources */, 290 | 1CC5A6992B68261700F7419A /* MessagesView.swift in Sources */, 291 | 1CC5A6912B68224A00F7419A /* LoadingView.swift in Sources */, 292 | 1C9F4B4D2B65D98E0013245C /* Weather+Schema.swift in Sources */, 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | /* End PBXSourcesBuildPhase section */ 297 | 298 | /* Begin XCBuildConfiguration section */ 299 | 1C9F4B2F2B65D8D30013245C /* Debug */ = { 300 | isa = XCBuildConfiguration; 301 | buildSettings = { 302 | ALWAYS_SEARCH_USER_PATHS = NO; 303 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 304 | CLANG_ANALYZER_NONNULL = YES; 305 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 306 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_ENABLE_OBJC_WEAK = YES; 310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 311 | CLANG_WARN_BOOL_CONVERSION = YES; 312 | CLANG_WARN_COMMA = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | COPY_PHASE_STRIP = NO; 333 | DEBUG_INFORMATION_FORMAT = dwarf; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | ENABLE_TESTABILITY = YES; 336 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu17; 338 | GCC_DYNAMIC_NO_PIC = NO; 339 | GCC_NO_COMMON_BLOCKS = YES; 340 | GCC_OPTIMIZATION_LEVEL = 0; 341 | GCC_PREPROCESSOR_DEFINITIONS = ( 342 | "DEBUG=1", 343 | "$(inherited)", 344 | ); 345 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 346 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 347 | GCC_WARN_UNDECLARED_SELECTOR = YES; 348 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 349 | GCC_WARN_UNUSED_FUNCTION = YES; 350 | GCC_WARN_UNUSED_VARIABLE = YES; 351 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 352 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 353 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 354 | MTL_FAST_MATH = YES; 355 | ONLY_ACTIVE_ARCH = YES; 356 | SDKROOT = iphoneos; 357 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 358 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 359 | }; 360 | name = Debug; 361 | }; 362 | 1C9F4B302B65D8D30013245C /* Release */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | ALWAYS_SEARCH_USER_PATHS = NO; 366 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 367 | CLANG_ANALYZER_NONNULL = YES; 368 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 369 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 370 | CLANG_ENABLE_MODULES = YES; 371 | CLANG_ENABLE_OBJC_ARC = YES; 372 | CLANG_ENABLE_OBJC_WEAK = YES; 373 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 374 | CLANG_WARN_BOOL_CONVERSION = YES; 375 | CLANG_WARN_COMMA = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 378 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 379 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 380 | CLANG_WARN_EMPTY_BODY = YES; 381 | CLANG_WARN_ENUM_CONVERSION = YES; 382 | CLANG_WARN_INFINITE_RECURSION = YES; 383 | CLANG_WARN_INT_CONVERSION = YES; 384 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 386 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 387 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 388 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 389 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 390 | CLANG_WARN_STRICT_PROTOTYPES = YES; 391 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 392 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 393 | CLANG_WARN_UNREACHABLE_CODE = YES; 394 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 395 | COPY_PHASE_STRIP = NO; 396 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 397 | ENABLE_NS_ASSERTIONS = NO; 398 | ENABLE_STRICT_OBJC_MSGSEND = YES; 399 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 400 | GCC_C_LANGUAGE_STANDARD = gnu17; 401 | GCC_NO_COMMON_BLOCKS = YES; 402 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 403 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 404 | GCC_WARN_UNDECLARED_SELECTOR = YES; 405 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 406 | GCC_WARN_UNUSED_FUNCTION = YES; 407 | GCC_WARN_UNUSED_VARIABLE = YES; 408 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 409 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 410 | MTL_ENABLE_DEBUG_INFO = NO; 411 | MTL_FAST_MATH = YES; 412 | SDKROOT = iphoneos; 413 | SWIFT_COMPILATION_MODE = wholemodule; 414 | VALIDATE_PRODUCT = YES; 415 | }; 416 | name = Release; 417 | }; 418 | 1C9F4B322B65D8D30013245C /* Debug */ = { 419 | isa = XCBuildConfiguration; 420 | buildSettings = { 421 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 422 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 423 | CODE_SIGN_ENTITLEMENTS = mobile/mobile.entitlements; 424 | CODE_SIGN_IDENTITY = "Apple Development"; 425 | CODE_SIGN_STYLE = Automatic; 426 | CURRENT_PROJECT_VERSION = 1; 427 | DEVELOPMENT_ASSET_PATHS = "\"mobile/Preview Content\""; 428 | DEVELOPMENT_TEAM = ""; 429 | ENABLE_PREVIEWS = YES; 430 | GENERATE_INFOPLIST_FILE = YES; 431 | INFOPLIST_FILE = mobile/Info.plist; 432 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Allow the app to use the user's location?"; 433 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 434 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 435 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 436 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 437 | LD_RUNPATH_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "@executable_path/Frameworks", 440 | ); 441 | MARKETING_VERSION = 1.0; 442 | PRODUCT_BUNDLE_IDENTIFIER = com.aws.samples.mobile; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | PROVISIONING_PROFILE_SPECIFIER = ""; 445 | SWIFT_EMIT_LOC_STRINGS = YES; 446 | SWIFT_VERSION = 5.0; 447 | TARGETED_DEVICE_FAMILY = "1,2"; 448 | }; 449 | name = Debug; 450 | }; 451 | 1C9F4B332B65D8D30013245C /* Release */ = { 452 | isa = XCBuildConfiguration; 453 | buildSettings = { 454 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 455 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 456 | CODE_SIGN_ENTITLEMENTS = mobile/mobile.entitlements; 457 | CODE_SIGN_IDENTITY = "Apple Development"; 458 | CODE_SIGN_STYLE = Automatic; 459 | CURRENT_PROJECT_VERSION = 1; 460 | DEVELOPMENT_ASSET_PATHS = "\"mobile/Preview Content\""; 461 | DEVELOPMENT_TEAM = ""; 462 | ENABLE_PREVIEWS = YES; 463 | GENERATE_INFOPLIST_FILE = YES; 464 | INFOPLIST_FILE = mobile/Info.plist; 465 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Allow the app to use the user's location?"; 466 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 467 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 468 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 469 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 470 | LD_RUNPATH_SEARCH_PATHS = ( 471 | "$(inherited)", 472 | "@executable_path/Frameworks", 473 | ); 474 | MARKETING_VERSION = 1.0; 475 | PRODUCT_BUNDLE_IDENTIFIER = com.aws.samples.mobile; 476 | PRODUCT_NAME = "$(TARGET_NAME)"; 477 | PROVISIONING_PROFILE_SPECIFIER = ""; 478 | SWIFT_EMIT_LOC_STRINGS = YES; 479 | SWIFT_VERSION = 5.0; 480 | TARGETED_DEVICE_FAMILY = "1,2"; 481 | }; 482 | name = Release; 483 | }; 484 | /* End XCBuildConfiguration section */ 485 | 486 | /* Begin XCConfigurationList section */ 487 | 1C9F4B1E2B65D8D10013245C /* Build configuration list for PBXProject "mobile" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 1C9F4B2F2B65D8D30013245C /* Debug */, 491 | 1C9F4B302B65D8D30013245C /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | 1C9F4B312B65D8D30013245C /* Build configuration list for PBXNativeTarget "mobile" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | 1C9F4B322B65D8D30013245C /* Debug */, 500 | 1C9F4B332B65D8D30013245C /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | /* End XCConfigurationList section */ 506 | 507 | /* Begin XCRemoteSwiftPackageReference section */ 508 | 1C30CF782D120B19003B08E8 /* XCRemoteSwiftPackageReference "amplify-swift" */ = { 509 | isa = XCRemoteSwiftPackageReference; 510 | repositoryURL = "https://github.com/aws-amplify/amplify-swift"; 511 | requirement = { 512 | kind = upToNextMajorVersion; 513 | minimumVersion = 2.45.2; 514 | }; 515 | }; 516 | /* End XCRemoteSwiftPackageReference section */ 517 | 518 | /* Begin XCSwiftPackageProductDependency section */ 519 | 1C30CF792D120B19003B08E8 /* AWSAPIPlugin */ = { 520 | isa = XCSwiftPackageProductDependency; 521 | package = 1C30CF782D120B19003B08E8 /* XCRemoteSwiftPackageReference "amplify-swift" */; 522 | productName = AWSAPIPlugin; 523 | }; 524 | 1C30CF7B2D120B19003B08E8 /* Amplify */ = { 525 | isa = XCSwiftPackageProductDependency; 526 | package = 1C30CF782D120B19003B08E8 /* XCRemoteSwiftPackageReference "amplify-swift" */; 527 | productName = Amplify; 528 | }; 529 | /* End XCSwiftPackageProductDependency section */ 530 | }; 531 | rootObject = 1C9F4B1B2B65D8D10013245C /* Project object */; 532 | } 533 | -------------------------------------------------------------------------------- /mobile/mobile.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/mobile.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "ac7439ef6e211b020a2cf0fce95d2080bfaa57c86c791bd33ffc85482994d172", 3 | "pins" : [ 4 | { 5 | "identity" : "amplify-swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/aws-amplify/amplify-swift", 8 | "state" : { 9 | "revision" : "0a78846e5162a31040e54058bb58af54a60e7938", 10 | "version" : "2.45.2" 11 | } 12 | }, 13 | { 14 | "identity" : "amplify-swift-utils-notifications", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", 17 | "state" : { 18 | "revision" : "959eec669ba97c7d923b963c3e66ca8a0b2737f6", 19 | "version" : "1.1.1" 20 | } 21 | }, 22 | { 23 | "identity" : "aws-crt-swift", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/awslabs/aws-crt-swift.git", 26 | "state" : { 27 | "revision" : "3f844bef042cc0a4c3381f7090414ce3f9a7e935", 28 | "version" : "0.37.0" 29 | } 30 | }, 31 | { 32 | "identity" : "aws-sdk-swift", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/awslabs/aws-sdk-swift.git", 35 | "state" : { 36 | "revision" : "c6c1064da9bfccb119a7a8ab9ba636fb3bbfa6f5", 37 | "version" : "1.0.47" 38 | } 39 | }, 40 | { 41 | "identity" : "smithy-swift", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/smithy-lang/smithy-swift", 44 | "state" : { 45 | "revision" : "3cd9f181b3ba8ff71da43bf53c09f8de6790a4ad", 46 | "version" : "0.96.0" 47 | } 48 | }, 49 | { 50 | "identity" : "sqlite.swift", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/stephencelis/SQLite.swift.git", 53 | "state" : { 54 | "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", 55 | "version" : "0.15.3" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-log", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-log.git", 62 | "state" : { 63 | "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", 64 | "version" : "1.4.4" 65 | } 66 | } 67 | ], 68 | "version" : 3 69 | } 70 | -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/mobile/mobile/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/poi.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "poi.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "poi-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "poi-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/poi.imageset/poi-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/mobile/mobile/Assets.xcassets/poi.imageset/poi-1.png -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/poi.imageset/poi-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/mobile/mobile/Assets.xcassets/poi.imageset/poi-2.png -------------------------------------------------------------------------------- /mobile/mobile/Assets.xcassets/poi.imageset/poi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-fullstack-swift-apple-carplay-example/8ee1a01d01718334bd4476ecb54ec41ec2fafa9b/mobile/mobile/Assets.xcassets/poi.imageset/poi.png -------------------------------------------------------------------------------- /mobile/mobile/CarPlaySceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import CarPlay 2 | 3 | class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { 4 | 5 | var carWindow: CPWindow? 6 | var interfaceController: CPInterfaceController? 7 | 8 | // services to receive location and messages 9 | var locationService: LocationService? 10 | var vehicleMessageService: VehicleMessageService? 11 | 12 | // templates for each screen type 13 | var mapTemplate: CPMapTemplate? 14 | var coffeeTemplate: CPListTemplate? 15 | var fuelTemplate: CPListTemplate? 16 | var foodTemplate: CPListTemplate? 17 | var weatherTemplate: CPInformationTemplate? 18 | 19 | var vehicleMessageDisplayed = false 20 | 21 | func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController, to window: CPWindow) { 22 | 23 | print("Connected to CarPlay.") 24 | 25 | self.interfaceController = interfaceController 26 | self.carWindow = window 27 | 28 | // initialize the templates that display each screen 29 | initTemplates() 30 | 31 | window.rootViewController = CarPlayMapView() 32 | interfaceController.setRootTemplate(mapTemplate!, animated: true, completion: nil) 33 | 34 | // initiate the services that provide data 35 | self.locationService = LocationService() 36 | self.locationService?.delegate = self 37 | 38 | self.vehicleMessageService = VehicleMessageService() 39 | self.vehicleMessageService?.delegate = self 40 | } 41 | 42 | func getMapBarButtons() -> [CPBarButton] { 43 | 44 | // buttons for Weather and Places in the app main navbar 45 | var buttons: [CPBarButton] = [CPBarButton]() 46 | 47 | buttons.append(CPBarButton(image: UIImage(systemName: "mappin.circle.fill")!, handler: { item in 48 | print("places clicked") 49 | self.interfaceController?.pushTemplate(self.getPlacesGridTemplate(), animated: true, completion: nil) 50 | })) 51 | 52 | buttons.append(CPBarButton(image: UIImage(systemName: "cloud.fill")!, handler: { item in 53 | print("weather clicked") 54 | self.interfaceController?.pushTemplate(self.weatherTemplate!, animated: true, completion: nil) 55 | })) 56 | 57 | return buttons 58 | } 59 | 60 | func getPlacesGridTemplate() -> CPGridTemplate { 61 | 62 | // buttons and actions displayed when user selects the Places icon in the main navbar 63 | var gridButtons: [CPGridButton] = [CPGridButton]() 64 | 65 | gridButtons.append(CPGridButton(titleVariants: ["Coffee"], image: UIImage(named: "poi", in: Bundle.main, compatibleWith: self.carWindow?.rootViewController?.traitCollection)!, handler: { item in 66 | self.interfaceController?.pushTemplate(self.coffeeTemplate!, animated: true, completion: nil) 67 | })) 68 | 69 | gridButtons.append(CPGridButton(titleVariants: ["Food"], image: UIImage(named: "poi", in: Bundle.main, compatibleWith: self.carWindow?.rootViewController?.traitCollection)!, handler: { item in 70 | self.interfaceController?.pushTemplate(self.foodTemplate!, animated: true, completion: nil) 71 | })) 72 | 73 | gridButtons.append(CPGridButton(titleVariants: ["Fuel"], image: UIImage(named: "poi", in: Bundle.main, compatibleWith: self.carWindow?.rootViewController?.traitCollection)!, handler: { item in 74 | self.interfaceController?.pushTemplate(self.fuelTemplate!, animated: true, completion: nil) 75 | })) 76 | 77 | return CPGridTemplate(title: "Places", gridButtons: gridButtons) 78 | } 79 | 80 | 81 | func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnect interfaceController: CPInterfaceController, from window: CPWindow) { 82 | 83 | print("Disconnected from CarPlay.") 84 | self.vehicleMessageService?.cancelSubscription() 85 | self.interfaceController = nil 86 | } 87 | 88 | func initTemplates() { 89 | 90 | // initialize the templates with their title and empty data 91 | self.mapTemplate = CPMapTemplate() 92 | self.mapTemplate?.trailingNavigationBarButtons.append(contentsOf: getMapBarButtons()) 93 | 94 | self.coffeeTemplate = CPListTemplate(title: "Coffee", sections: [CPListSection(items: [])]) 95 | 96 | self.fuelTemplate = CPListTemplate(title: "Fuel", sections: [CPListSection(items: [])]) 97 | 98 | self.foodTemplate = CPListTemplate(title: "Food", sections: [CPListSection(items: [])]) 99 | 100 | self.weatherTemplate = CPInformationTemplate(title: "Weather", layout: CPInformationTemplateLayout.twoColumn, items: [], actions: []) 101 | 102 | } 103 | 104 | func getPlaces (template: CPListTemplate, latitude: Double, longitude: Double, placeType: PlaceType) async { 105 | 106 | // call the Data Service to retrieve the requested PlaceTypes for the user's current location 107 | // and update the provided places template 108 | 109 | var listItems: [CPListItem] = [CPListItem]() 110 | 111 | Task { 112 | // call the Data Service to retrieve the weather for the user's current location and update the weatherTemplate 113 | do { 114 | let places = try await DataService().getPlaces(placeType: placeType, latitude: latitude, longitude: longitude, maxResults: 3) 115 | 116 | for place in places { 117 | let item = CPListItem(text: place.name, detailText: place.address) 118 | 119 | item.handler = { item, completion in 120 | self.interfaceController?.popToRootTemplate(animated: true) {_, _ in 121 | 122 | // display an alert to select this destination 123 | // future functionality would be to initiate navigation directions 124 | let alert = CPNavigationAlert( 125 | titleVariants: [place.name], 126 | subtitleVariants: [place.address], 127 | image: nil, 128 | primaryAction: CPAlertAction(title: "Go", style: CPAlertAction.Style.default, handler: {_ in }), 129 | secondaryAction: nil, 130 | duration: TimeInterval(20)) 131 | 132 | self.mapTemplate?.present(navigationAlert: alert, animated: true) 133 | } 134 | } 135 | 136 | listItems.append(item) 137 | } 138 | 139 | template.updateSections([CPListSection(items: listItems)]) 140 | } catch { 141 | print("Error fetching places: \(error)") 142 | } 143 | } 144 | } 145 | 146 | func getWeather (template: CPInformationTemplate, latitude: Double, longitude: Double, city: String) async { 147 | 148 | Task { 149 | // call the Data Service to retrieve the weather for the user's current location and update the weatherTemplate 150 | do { 151 | let result = try await DataService().getWeather(latitude: latitude, longitude: longitude) 152 | template.items.removeAll() 153 | template.items.append(CPInformationItem(title: "City", detail: city)) 154 | template.items.append(CPInformationItem(title: "Temperature", detail: String(result.temperature))) 155 | template.items.append(CPInformationItem(title: "Air Quality Index", detail: String(result.aqIndex))) 156 | } catch { 157 | print("Error fetching weather: \(error)") 158 | } 159 | } 160 | } 161 | } 162 | 163 | extension CarPlaySceneDelegate: LocationServiceDelegate { 164 | 165 | // event fired from the location service every time the user's location changes by 1/2 mile 166 | // update template content based on the user's new location 167 | 168 | func locationService(latitude: Double, longitude: Double, city: String) { 169 | Task { 170 | await getWeather(template: self.weatherTemplate!, latitude: latitude, longitude: longitude, city: city) 171 | await getPlaces(template: self.coffeeTemplate!, latitude: latitude, longitude: longitude, placeType: PlaceType.coffee) 172 | await getPlaces(template: self.fuelTemplate!, latitude: latitude, longitude: longitude, placeType: PlaceType.fuel) 173 | await getPlaces(template: self.foodTemplate!, latitude: latitude, longitude: longitude, placeType: PlaceType.food) 174 | } 175 | } 176 | } 177 | 178 | extension CarPlaySceneDelegate: VehicleMessageServiceDelegate { 179 | 180 | // event fired when a new message is received from the vehicle message subscription 181 | // display the message as an Alert template 182 | 183 | func vehicleMessageService(message: String) { 184 | 185 | print("CarPlay received message from Cloud: \(message)") 186 | 187 | if (!self.vehicleMessageDisplayed){ 188 | 189 | let okAction = CPAlertAction(title: "OK", style: CPAlertAction.Style.default, handler: {item in 190 | print("OK button pressed") 191 | self.interfaceController?.dismissTemplate(animated: true, completion: nil) 192 | self.vehicleMessageDisplayed = false 193 | }) 194 | 195 | let actionTemplate = CPActionSheetTemplate(title: "New Message", message: message, actions: [okAction]) 196 | 197 | self.interfaceController?.presentTemplate(actionTemplate,animated: true, completion: nil) 198 | 199 | self.vehicleMessageDisplayed = true 200 | } 201 | } 202 | } 203 | 204 | -------------------------------------------------------------------------------- /mobile/mobile/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | AWS 7 | UIApplicationSceneManifest 8 | 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | UIWindowSceneSessionRoleApplication 14 | 15 | 16 | UISceneConfigurationName 17 | iPhone Configuration 18 | 19 | 20 | CPTemplateApplicationSceneSessionRoleApplication 21 | 22 | 23 | UISceneClassName 24 | CPTemplateApplicationScene 25 | UISceneConfigurationName 26 | Default Configuration 27 | UISceneDelegateClassName 28 | $(PRODUCT_MODULE_NAME).CarPlaySceneDelegate 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /mobile/mobile/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mobile/mobile/Services/DataService.swift: -------------------------------------------------------------------------------- 1 | import Amplify 2 | 3 | // functions that interact with the AppSync cloud api 4 | class DataService { 5 | 6 | // function to call Appsync API to retrieve current location based on provided latitude and longitude 7 | func getCity(latitude: Double, longitude: Double) async throws-> Location { 8 | 9 | do { 10 | let result = try await Amplify.API.query(request: .getCity(latitude: latitude, longitude: longitude)) 11 | 12 | switch result { 13 | case .success(let item): 14 | return item 15 | case .failure(let error): 16 | throw error 17 | } 18 | 19 | } catch { 20 | throw error 21 | } 22 | } 23 | 24 | // function to call AppSync API and retrieve the weather based on provided latitude and longitude 25 | func getWeather(latitude: Double, longitude: Double) async throws -> Weather { 26 | 27 | do { 28 | let result = try await Amplify.API.query(request: .getWeather(latitude: latitude, longitude: longitude)) 29 | 30 | switch result { 31 | case .success(let item): 32 | return item 33 | case .failure(let error): 34 | throw error 35 | } 36 | 37 | } catch { 38 | throw error 39 | } 40 | } 41 | 42 | // function to call Appsync API to retrieve points of interest for a PlaceType based on provided latitude and longitude 43 | // passing in a PlaceType of city will return the current city based on provided latitude and longitude 44 | func getPlaces(placeType: PlaceType, latitude: Double, longitude: Double, maxResults: Int) async throws-> [Place] { 45 | 46 | do { 47 | let result = try await Amplify.API.query(request: .getPlaces(placeType: placeType, latitude: latitude, longitude: longitude, maxResults: maxResults)) 48 | 49 | switch result { 50 | case .success(let item): 51 | return item 52 | case .failure(let error): 53 | throw error 54 | } 55 | 56 | } catch { 57 | throw error 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mobile/mobile/Services/LocationService.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Combine 3 | import Amplify 4 | 5 | // delegate to provide location data to Carplay as Carply does not support Swift based ObservedObject 6 | // the iPhone app utilises ObservedObject bindings 7 | protocol LocationServiceDelegate: AnyObject { 8 | func locationService(latitude: Double, longitude: Double, city: String) 9 | } 10 | 11 | // the Location service utilizes CoreLocation to monitor the movement of the user 12 | // updates are published every 1/2 mile (800 meters) 13 | class LocationService: NSObject, ObservableObject { 14 | 15 | weak var delegate:LocationServiceDelegate? 16 | 17 | override init() { 18 | super.init() 19 | self.locationManager.delegate = self 20 | self.locationManager.desiredAccuracy = kCLLocationAccuracyBest 21 | self.locationManager.distanceFilter = 800 22 | self.locationManager.requestWhenInUseAuthorization() 23 | self.locationManager.startUpdatingLocation() 24 | } 25 | 26 | // variables the LocationManager publishes top consumers 27 | @Published var locationStatus: CLAuthorizationStatus? { 28 | willSet { 29 | objectWillChange.send() 30 | } 31 | } 32 | 33 | @Published var lastLocation: CLLocation? { 34 | willSet { 35 | objectWillChange.send() 36 | } 37 | } 38 | 39 | @Published var latitude: Double = 0 40 | @Published var longitude: Double = 0 41 | @Published var city = "" 42 | 43 | var statusString: String { 44 | guard let status = locationStatus else { 45 | return "unknown" 46 | } 47 | 48 | switch status { 49 | case .notDetermined: return "notDetermined" 50 | case .authorizedWhenInUse: return "authorizedWhenInUse" 51 | case .authorizedAlways: return "authorizedAlways" 52 | case .restricted: return "restricted" 53 | case .denied: return "denied" 54 | default: return "unknown" 55 | } 56 | 57 | } 58 | 59 | let objectWillChange = PassthroughSubject() 60 | 61 | private let locationManager = CLLocationManager() 62 | 63 | func stopUpdatingLocation(){ 64 | self.locationManager.stopUpdatingLocation() 65 | } 66 | } 67 | 68 | // delegate to handle events fired by CoreLocation 69 | extension LocationService: CLLocationManagerDelegate { 70 | 71 | // event received when the user's Location authorization status changes 72 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus){ 73 | self.locationStatus = status 74 | } 75 | 76 | // event received when the user's Location changes 77 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 78 | guard let location = locations.last else { return } 79 | 80 | self.lastLocation = location 81 | self.latitude = location.coordinate.latitude 82 | self.longitude = location.coordinate.longitude 83 | 84 | Task { 85 | do { 86 | let result = try await DataService().getCity(latitude: self.latitude, longitude: self.longitude) 87 | self.city = result.name 88 | self.delegate?.locationService(latitude: self.latitude, longitude: self.longitude, city: self.city) 89 | } catch { 90 | print("Error fetching location: \(error)") 91 | } 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /mobile/mobile/Services/Message+Extensions.swift: -------------------------------------------------------------------------------- 1 | // extension to the VehicleMessage to make it Identifiable and bindable to a view List 2 | extension Message: Identifiable { 3 | } 4 | -------------------------------------------------------------------------------- /mobile/mobile/Services/PlaceType+Extensions.swift: -------------------------------------------------------------------------------- 1 | extension PlaceType: Identifiable { 2 | 3 | public var id: String { 4 | return self.rawValue 5 | } 6 | 7 | public static var poiPlaces: [PlaceType] { 8 | return [.fuel, .coffee, .food] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mobile/mobile/Services/Queries.swift: -------------------------------------------------------------------------------- 1 | import Amplify 2 | 3 | // GraphQL queries to retrieve Weather and Places from the AppSync API 4 | extension GraphQLRequest { 5 | 6 | static func getCity(latitude: Double, longitude: Double) -> GraphQLRequest { 7 | let operationName = "getCity" 8 | let document = """ 9 | query \(operationName) { 10 | \(operationName)(latitude: \(latitude), longitude: \(longitude)) { 11 | name 12 | latitude 13 | longitude 14 | } 15 | } 16 | """ 17 | 18 | return GraphQLRequest( 19 | document: document, 20 | responseType: Location.self, 21 | decodePath: operationName) 22 | } 23 | 24 | static func getWeather(latitude: Double, longitude: Double) -> GraphQLRequest { 25 | let operationName = "getWeather" 26 | let document = """ 27 | query \(operationName) { 28 | \(operationName)(latitude: \(latitude), longitude: \(longitude)) { 29 | aqIndex 30 | temperature 31 | latitude 32 | longitude 33 | } 34 | } 35 | """ 36 | 37 | return GraphQLRequest( 38 | document: document, 39 | responseType: Weather.self, 40 | decodePath: operationName) 41 | } 42 | 43 | static func getPlaces(placeType: PlaceType, latitude: Double, longitude: Double, maxResults: Int) -> GraphQLRequest<[Place]> { 44 | let operationName = "getPlaces" 45 | let document = """ 46 | query \(operationName) { 47 | \(operationName)(placeType: \(placeType), latitude: \(latitude), longitude: \(longitude), maxResults: \(maxResults)) { 48 | placeType 49 | name 50 | address 51 | latitude 52 | longitude 53 | } 54 | } 55 | """ 56 | 57 | return GraphQLRequest<[Place]>( 58 | document: document, 59 | responseType: [Place].self, 60 | decodePath: operationName) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mobile/mobile/Services/Subscriptions.swift: -------------------------------------------------------------------------------- 1 | import Amplify 2 | 3 | // Appsync GraphQL subscription request for vehicle messages 4 | extension GraphQLRequest { 5 | 6 | static func onCreateMessage(recipient: String) -> GraphQLRequest { 7 | let operationName = "onCreateMessage" 8 | let document = """ 9 | subscription \(operationName) { 10 | \(operationName)(recipient: "\(recipient)") { 11 | id 12 | text 13 | recipient 14 | timestamp 15 | } 16 | } 17 | """ 18 | 19 | return GraphQLRequest( 20 | document: document, 21 | responseType: Message.self, 22 | decodePath: operationName) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /mobile/mobile/Services/VehicleMessageService.swift: -------------------------------------------------------------------------------- 1 | import Amplify 2 | import Foundation 3 | 4 | // service to subscribe to vehicle messages 5 | 6 | // protocol is used to publish messages to CarPlay as CarPlay does not support the ObservableObject construct 7 | protocol VehicleMessageServiceDelegate: AnyObject { 8 | func vehicleMessageService(message: String) 9 | } 10 | 11 | class VehicleMessageService: NSObject, ObservableObject { 12 | 13 | // in a production app this will come from the authenticated username or vehicle registration 14 | // for this sample we will hard code the vehicle identifier 15 | let recipient = "Vehicle1" 16 | 17 | weak var delegate:VehicleMessageServiceDelegate? 18 | var subscription: AmplifyAsyncThrowingSequence> = AmplifyAsyncThrowingSequence>() 19 | 20 | override init() { 21 | super.init() 22 | startSubscription() 23 | } 24 | 25 | // ObservedObject for iOS views to subscribe to for new messages as they are received from the Cloud 26 | // messages is an array of all messages received from the subscription 27 | @Published var messages: [Message] = [Message]() 28 | 29 | func startSubscription() { 30 | subscription = Amplify.API.subscribe(request: .onCreateMessage(recipient: recipient)) 31 | Task { 32 | do { 33 | for try await subscriptionEvent in subscription { 34 | switch subscriptionEvent { 35 | case .connection(let subscriptionConnectionState): 36 | print("Subscription connect state is \(subscriptionConnectionState)") 37 | case .data(.success(let createdItem)): 38 | print("Successfully received message from subscription") 39 | DispatchQueue.main.async { 40 | self.messages.append(createdItem) 41 | self.delegate?.vehicleMessageService(message: createdItem.text) 42 | } 43 | case .data(.failure(let error)): 44 | print("Failed subscription result with \(error.errorDescription)") 45 | } 46 | } 47 | } catch { 48 | print("Subscription has terminated with \(error)") 49 | } 50 | } 51 | } 52 | 53 | func cancelSubscription() { 54 | print("Cancelling subscription") 55 | subscription.cancel(); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /mobile/mobile/Views/CarPlayMapView.swift: -------------------------------------------------------------------------------- 1 | import MapKit 2 | 3 | class CarPlayMapView: UIViewController { 4 | var mapView: MKMapView? 5 | 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | print("loading the carplay mapview") 10 | 11 | let region = MKCoordinateRegion( 12 | center: CLLocationCoordinate2D(latitude: 0, longitude: 0), 13 | span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5) 14 | ) 15 | 16 | self.mapView = MKMapView(frame: view.bounds) 17 | self.mapView!.setRegion(region, animated: true) 18 | self.mapView!.showsUserLocation = true 19 | self.mapView!.setUserTrackingMode(.follow, animated: true) 20 | self.mapView!.overrideUserInterfaceStyle = .light 21 | 22 | self.mapView!.translatesAutoresizingMaskIntoConstraints = false 23 | self.view.addSubview(self.mapView!) 24 | 25 | self.mapView!.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 26 | self.mapView!.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true 27 | self.mapView!.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 28 | self.mapView!.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mobile/mobile/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | MapView() 6 | } 7 | } 8 | 9 | #Preview { 10 | ContentView() 11 | } 12 | -------------------------------------------------------------------------------- /mobile/mobile/Views/FormButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // button object for the modal sheets 4 | struct FormButton: View { 5 | 6 | var label: String 7 | var action: () -> Void 8 | 9 | var body: some View { 10 | Button(action: action) { 11 | Text(label) 12 | .font(.custom("button", fixedSize: 16)) 13 | .padding() 14 | .background(Color.blue) 15 | .cornerRadius(10) 16 | .foregroundColor(.white) 17 | .padding(2) 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /mobile/mobile/Views/LoadingView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // activity indicator with a label to display while an api call is waiting for results 4 | struct LoadingView: View { 5 | 6 | var label = "Loading..." 7 | var fontSize: CGFloat = 24 8 | 9 | var body: some View { 10 | VStack(alignment: .center, spacing: 20) { 11 | Text(label) 12 | .padding(.top, 75) 13 | .font(Font.system(size:fontSize)) 14 | ProgressView() 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /mobile/mobile/Views/MapButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // buttons with system images displayed on the MapView 4 | struct MapButton: View { 5 | 6 | var image: String 7 | var action: () -> Void 8 | 9 | var body: some View { 10 | Button(action: action) { 11 | Image(systemName: image) 12 | .font(.title) 13 | .frame(width: 60, height: 60, alignment: .center) 14 | .background(Color.gray) 15 | .clipShape(Circle()) 16 | .foregroundColor(.white) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mobile/mobile/Views/MapView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MapKit 3 | 4 | // the main iOS view that displays the Map, the user's current location and action buttons 5 | struct MapView: View { 6 | 7 | // set the initial map region to user location 8 | @State private var position: MapCameraPosition = .userLocation(fallback: .automatic) 9 | 10 | // subscribe to the user's current location 11 | @ObservedObject var locationService = LocationService() 12 | 13 | // subscribe to vehicle messages from the Cloud 14 | @ObservedObject var vehicleMessageService = VehicleMessageService() 15 | 16 | // state variables to control the visibility of modal sheets 17 | @State private var showWeatherView = false 18 | @State private var showPOIView = false 19 | @State private var showMessagesView = false 20 | 21 | var body: some View { 22 | ZStack { 23 | Map(position: $position){ 24 | } 25 | VStack { 26 | Spacer() 27 | Text(locationService.city) 28 | .font(.title) 29 | .padding(.bottom, 20) 30 | HStack { 31 | MapButton(image: "cloud.fill", action: {showWeatherView.toggle()}) 32 | .sheet(isPresented: $showWeatherView) { 33 | WeatherView( 34 | showView: $showWeatherView, 35 | latitude: locationService.latitude, 36 | longitude: locationService.longitude, 37 | city: locationService.city 38 | ) 39 | } 40 | MapButton(image: "mappin.circle", action: {showPOIView.toggle()}) 41 | .sheet(isPresented: $showPOIView) { 42 | PlacesView( 43 | showView: $showPOIView, 44 | latitude: locationService.latitude, 45 | longitude: locationService.longitude 46 | ) 47 | } 48 | MapButton(image: "text.bubble", action: {showMessagesView.toggle()}) 49 | .sheet(isPresented: $showMessagesView) { 50 | MessagesView( 51 | showView: $showMessagesView, 52 | messages: $vehicleMessageService.messages 53 | ) 54 | } 55 | } 56 | .onDisappear { 57 | vehicleMessageService.cancelSubscription() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mobile/mobile/Views/MessagesView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // sheet view that displays cloud subscription messages received from the AppSync API 4 | struct MessagesView: View { 5 | 6 | @Binding var showView: Bool 7 | @Binding var messages: [Message] 8 | 9 | var body: some View { 10 | Text("Messages") 11 | .padding(20) 12 | .font(.title) 13 | .frame(maxWidth: .infinity, alignment: .leading) 14 | VStack(alignment: .center, spacing: 20) { 15 | if messages.isEmpty { 16 | Text("You have no messages") 17 | .padding(.top, 100) 18 | .font(Font.system(size:20)) 19 | } else { 20 | List { 21 | ForEach (messages) { item in 22 | Text(item.text) 23 | } 24 | } 25 | } 26 | Spacer() 27 | HStack { 28 | FormButton(label: "Dismiss", action: {showView.toggle()}) 29 | if !messages.isEmpty { 30 | FormButton(label: "Clear", action: {messages.removeAll()}) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /mobile/mobile/Views/PlacesView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // sheet view that displays local points of interest based on the user's current location 4 | struct PlacesView: View { 5 | 6 | struct PlaceItem: Identifiable { 7 | let id = UUID() 8 | let name: String 9 | let address: String 10 | } 11 | 12 | @Binding var showView: Bool 13 | var latitude: Double 14 | var longitude: Double 15 | 16 | @State private var places: [PlaceItem] = [] 17 | @State private var selectedPlaceType: PlaceType = PlaceType.fuel 18 | @State private var isFetching = true 19 | 20 | var body: some View { 21 | Text("Places") 22 | .padding(20) 23 | .font(.title) 24 | .frame(maxWidth: .infinity, alignment: .leading) 25 | 26 | VStack(alignment: .center, spacing: 20) { 27 | HStack (spacing: 20){ 28 | ForEach(PlaceType.poiPlaces) { type in 29 | let label = type.rawValue.capitalized 30 | 31 | if selectedPlaceType == type { 32 | PlaceTypeText(label: label) 33 | } else { 34 | Button(label, action: { 35 | Task { 36 | await fetch(placeType: type) 37 | } 38 | }) 39 | } 40 | } 41 | } 42 | if isFetching { 43 | LoadingView(label: "Searching nearby places", fontSize: 18) 44 | } else { 45 | List { 46 | ForEach (places) { item in 47 | VStack(alignment: .leading) { 48 | Text(item.name) 49 | Text(item.address) 50 | .font(Font.system(size:14)) 51 | } 52 | } 53 | } 54 | } 55 | Spacer() 56 | FormButton(label: "Dismiss", action: {showView.toggle()} 57 | ).onAppear { 58 | Task { 59 | await fetch(placeType: selectedPlaceType) 60 | } 61 | } 62 | } 63 | } 64 | 65 | // view to display the selected place type 66 | struct PlaceTypeText: View { 67 | 68 | var label: String 69 | 70 | var body: some View { 71 | Text(label) 72 | .font(.custom("button", fixedSize: 16)) 73 | .padding(10) 74 | .background(Color.blue) 75 | .cornerRadius(10) 76 | .foregroundColor(.white) 77 | } 78 | } 79 | 80 | // function to call the Data Service and retrieve the closest places based on the requested PlaceType and the user's location 81 | func fetch(placeType: PlaceType) async { 82 | 83 | self.selectedPlaceType = placeType 84 | 85 | places.removeAll() 86 | 87 | isFetching = true 88 | 89 | do { 90 | let result = try await DataService().getPlaces(placeType: placeType, latitude: latitude, longitude: longitude, maxResults: 5) 91 | for p in result { 92 | places.append(PlaceItem(name: p.name, address: p.address)) 93 | } 94 | } catch { 95 | print("Error fetching places: \(error)") 96 | } 97 | 98 | isFetching = false 99 | } 100 | } 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /mobile/mobile/Views/WeatherView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // sheet view that displays the weather based on the user's current location 4 | struct WeatherView: View { 5 | 6 | @Binding var showView: Bool 7 | 8 | var latitude: Double 9 | var longitude: Double 10 | var city: String 11 | 12 | @State var aqIndex: Double = 0 13 | @State var temperature: Double = 0 14 | @State var isFetching: Bool = true 15 | 16 | var body: some View { 17 | Text("Weather") 18 | .padding(20) 19 | .font(.title) 20 | .frame(maxWidth: .infinity, alignment: .leading) 21 | VStack(alignment: .center, spacing: 20) { 22 | if (isFetching) { 23 | LoadingView(label: "Determining the weather") 24 | } else { 25 | Text(city) 26 | .font(.largeTitle) 27 | .padding(.top, 75) 28 | Text("Temperature") 29 | .font(.title) 30 | .padding(.top, 50) 31 | Text(String(temperature)) 32 | .font(.title) 33 | Text("Air Quality Index") 34 | .font(.title) 35 | .padding(.top, 10) 36 | Text(String(aqIndex)) 37 | .font(.title) 38 | } 39 | Spacer() 40 | FormButton(label: "Dismiss", action: {showView.toggle()}) 41 | } 42 | .onAppear { 43 | Task { 44 | await fetch() 45 | } 46 | } 47 | } 48 | 49 | // function to call the Data Service and retrieve the weather for the user's current location 50 | func fetch() async { 51 | 52 | isFetching = true 53 | 54 | do { 55 | let result = try await DataService().getWeather(latitude: latitude, longitude: longitude) 56 | aqIndex = result.aqIndex 57 | temperature = result.temperature 58 | } catch { 59 | print("Error fetching weather: \(error)") 60 | } 61 | 62 | isFetching = false 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /mobile/mobile/mobile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.carplay-maps 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/mobile/mobileApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Amplify 3 | import AWSAPIPlugin 4 | 5 | @main 6 | struct mobileApp: App { 7 | 8 | init() { 9 | configureAmplify() 10 | } 11 | 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | 19 | func configureAmplify() { 20 | 21 | do { 22 | try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) 23 | try Amplify.configure() 24 | print("Initialized Amplify"); 25 | } catch { 26 | // simplified error handling for the tutorial 27 | print("Could not initialize Amplify: \(error)") 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /mobile/scripts/amplify-config.sh: -------------------------------------------------------------------------------- 1 | ENDPOINT=$1 2 | API_KEY=$2 3 | REGION=$3 4 | 5 | cat << EOF > amplify/amplifyconfiguration.json 6 | { 7 | "UserAgent": "aws-amplify-cli/2.0", 8 | "Version": "1.0", 9 | "api": { 10 | "plugins": { 11 | "awsAPIPlugin": { 12 | "swiftcarplaylocation": { 13 | "endpointType": "GraphQL", 14 | "endpoint": "$ENDPOINT", 15 | "region": "$REGION", 16 | "authorizationType": "API_KEY", 17 | "apiKey": "$API_KEY" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | EOF 24 | --------------------------------------------------------------------------------