diff --git a/Package.swift b/Package.swift index d8fad28..c97f7e7 100644 --- a/Package.swift +++ b/Package.swift @@ -57,9 +57,18 @@ let package = Package( .target(name: "ShellExecutor", dependencies: [], path: "Tools/ShellExecutor"), .executableTarget( name: "SetupDevEnv", - dependencies: ["ShellExecutor"], + dependencies: [ + "ShellExecutor", + .target(name: "TemplateProject", condition: .when(platforms: [.macOS])), + ], path: "Tools/SetupDevEnv" ), + .target( + name: "TemplateProject", + dependencies: [], + path: "Tools/TemplateProject", + resources: [.copy("Resources")] + ), .testTarget( name: "FirebaseDataConnectUnit", dependencies: ["FirebaseDataConnect"], diff --git a/README.md b/README.md index 565bc0b..7358ad1 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,196 @@ -# Firebase Data Connect iOS Open Source Development - -This repository contains the source code of Firebase Data Connect Swift SDKs for development on iOS and other Apple platforms. - -Firebase Data Connect (https://firebase.google.com/docs/data-connect) lets you build applications with CloudSQL for PostgreSQL. - -Firebase is an app development platform with tools to help you build, grow, and -monetize your app. More information about Firebase can be found on the -[official Firebase website](https://firebase.google.com). - -## Installation - -This process shows you how to setup Firebase Data Connect tools with Xcode. - -* Get the Firebase Data Connect iOS SDK. - ``` - git clone https://github.com/firebase/data-connect-ios-sdk.git - ``` -* Add it as a local package dependency in your Xcode project - * From your Xcode project, select `File -> Add Package Dependencies -> Add Local`. - * Select the `data-connect-ios-sdk` folder containing the cloned SDK. - * Add `FirebaseDataConnect` to your app target. -* Add and Edit Xcode Scheme - * From your list of schemes, select `New Scheme`. - * Xcode should show you `Start FDC Tools` as a potential scheme. Select that. -* Adjust working directory - * Edit the `Start FDC Tools` Scheme from `Edit Scheme... -> Start FDC Tools`. - * Go to `Run -> Options -> Working Directory` - * Select `Custom working directory` and pick your Xcode project folder - * This ensures that Firebase Data Connect tools start in the correct folder each time. -* Run the `Start FDC Tools` target selecting `My Mac` as the device. -* This should start the tools in a separate browser window (or tab). -* Now that you have the tools, try out our [Codelab](https://firebase.google.com/codelabs/firebase-dataconnect-ios#0) -* Learn more about designing schemas, queries and mutations - [Design Schemas](https://firebase.google.com/docs/data-connect/schemas-guide) - -* Generated SDKs are Swift packages. Add the generated SDKs to your project as a Local Package dependency and start using them in your app. - * Generated APIs support `@Observable` (Observation framework) for easy integration into SwiftUI apps. - * UIKit apps can make use of the underlying `Combine` publishers. - * All Query and Mutation APIs are `async` calls and can participate in Swift concurrency. -* Since you cloned the `data-connect-ios-sdk`, be sure to reference your local folder to generate SDKs. - * In the `connector.yaml`, configure the Swift SDK generation to point to the local path where you cloned the Data Connect iOS SDK using the `coreSdkPackageLocation` parameter. Example - - ```yaml - generate: - swiftSdk: - outputDir: ../../dataconnect-generated/swift - package: DefaultConnector #change this to your project specific name - coreSdkPackageLocation: /Users/abc/Code/data-connect-ios-sdk - ``` - -There are other paths to get setup with Firebase Data Connect tools such as from the Firebase Console. To learn more visit - [Quick Start](https://firebase.google.com/docs/data-connect/quickstart). - -## Sample App and Code Lab - -* [Codelab](https://firebase.google.com/codelabs/firebase-dataconnect-ios#0) - -* Comprehensive Sample app - [FriendlyFlix](https://github.com/firebase/data-connect-ios-sdk/tree/main/Examples/FriendlyFlix) - -### Swift Package Manager -Instructions for [Swift Package Manager](https://swift.org/package-manager/) support can be -found in the [SwiftPackageManager.md](SwiftPackageManager.md) Markdown file. - -## Contributing - -See [Contributing](CONTRIBUTING.md) for more information on contributing to the Firebase Data Connect -iOS SDK. - -## License - -The contents of this repository are licensed under the -[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0). - -Your use of Firebase is governed by the -[Terms of Service for Firebase Services](https://firebase.google.com/terms/). +# Firebase Data Connect for Swift + +**Connect your Swift & SwiftUI apps directly to a managed Google CloudSQL (PostgreSQL) database.** + +This repository contains the official open-source Swift SDK for [Firebase Data Connect](https://firebase.google.com/docs/data-connect), a service that lets you build modern, data-driven applications on Apple platforms (iOS, macOS, etc.) with the power and scalability of a SQL database. + +This SDK is perfect for those: +* Who need a robust SQL database for their app but want to avoid writing and managing a separate backend. +* Looking for a type-safe, async-first library to integrate a PostgreSQL database into their applications. + +--- + +## ✨ Why Use Firebase Data Connect? + +* **Use power of SQL:** Get the power of a managed PostgreSQL database without the hassle of managing servers. Focus on your app's frontend experience. +* **Type-Safe & Modern Swift:** Interact with your database using a strongly-typed, auto-generated Swift SDK. It's built with modern `async/await` for clean, concurrent code. +* **Built for SwiftUI:** `@Observable Queries` automatically update your SwiftUI views when data changes, making it incredibly simple to build reactive UIs. +* **Full CRUD Operations:** Define your data models and operations using GraphQL, and Data Connect generates the Swift code to query, insert, update, and delete data. +* **Local Emulator:** Develop and test your entire application locally with the FDC emulator for a fast and efficient development cycle. + +--- + +## 🚀 Getting Started + +This guide will walk you through setting up a new iOS (or other Apple platform) project with Firebase Data Connect. + +### Prerequisites + +* Xcode 16.2 or later +* iOS 15.0 or later +* A Google account for the FDC tools + +### Step 1: Get the SDK & Tools + +First, clone this repository to your local machine. This contains both the core SDK and the command-line tools needed for code generation. + +```bash +git clone https://github.com/firebase/data-connect-ios-sdk.git +``` + +### **Step 2: Configure Your Xcode Project** + +1. **Add Local Package:** In Xcode, open your app project and navigate to **File \> Add Package Dependencies...**. +2. In the package prompt, click **"Add Local..."** and select the `data-connect-ios-sdk` folder you just cloned. +3. Add the `FirebaseDataConnect` library to your app's primary target. + +### **Step 3: Run the Data Connect Tools** + +The Data Connect tools run on your Mac to provide a local development emulator and code generation service. + +1. **Add New Scheme:** In Xcode's scheme menu, select **New Scheme...**. Choose the **`Start FDC Tools`** target and click OK. +2. **Set Working Directory:** Edit the new `Start FDC Tools` scheme. Go to **Run \> Options** and check **"Use custom working directory"**. Set this to the root folder of your Xcode project. +3. **Run the Tools:** Select the `Start FDC Tools` scheme with **My Mac** as the destination and click Run (â–ļ). This will open the FDC tools in your web browser. + +### **Step 4: Generate Your Type-Safe Swift SDK** + +The tools will generate a custom Swift package based on your database schema. + +1. **Sign In:** In the FDC tools web UI, sign in with your Google account. + +![FDC Extension](docs/resources/Extension_SignIn.png) + +2. **Start Emulator:** Click the button to start the local FDC Emulator. + +![Start Emulator](docs/resources/StartEmulatorButton.png) + +3. **Generate Code:** The tools will automatically detect the GraphQL schema (`.gql` files) in your project's `dataconnect` subfolder and generate a new Swift package in a `dataconnect-generated` folder. +4. **Reference cloned SDK:** From the FDC tools, modify the `dataconnect/default/connector.yaml` file to specify the location of the cloned `data-connect-ios-sdk` by updating the `coreSdkPackageLocation` property. +5. **Add Generated SDK to Xcode:** Back in Xcode, add another local package (**File \> Add Package Dependencies... \> Add Local...**). This time, select the new Swift package inside the `dataconnect-generated` folder. Add this new library (e.g., `ItemData`) to your app target. +6. **Add GoogleService-Info.plist to Xcode:** From Xcode. add a file to your project (**File \> Add Files to ...**). Select the file `GoogleService-Info.plist` thats in your Xcode project folder and add it as a `Reference`. + +### **Step 5: Initialize Firebase in Your App** + +In your main app file (the one with `@main`), initialize Firebase and configure Data Connect to use the local emulator. + + +```swift +// MyApp.swift +import SwiftUI +import Firebase +import FirebaseDataConnect +import ItemData // The name of your generated SDK package + +@main +struct MyApp: App { + init() { + // 1. Configure Firebase + FirebaseApp.configure() + + // 2. Point Data Connect to the local emulator + DataConnect.itemsConnector.useEmulator() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +### **Step 6: Execute a Mutation (Create Data)** + +Now you can write data to the database from any SwiftUI view. + +```swift +// ContentView.swift +import SwiftUI +import FirebaseDataConnect +import ItemData + +struct ContentView: View { + var body: some View { + VStack { + Button("Create Item") { + Task { + do { + let itemID = UUID() + let itemName = "Item-\(Int.random(in: 1...1000))" + let result = try await DataConnect.itemsConnector.createItemMutation.execute(id: itemID, name: itemName) + print("✅ Successfully created item: \(result)") + } catch { + print("❌ Error creating item: \(error)") + } + } + } + } + } +} +``` + +*Note:* You may need to enable `App Sandbox` -> `Outgoing Connections (Client)` for your Xcode app target to run it from iPhone simulator. + + +### **Step 7: Execute a Query (Read & Display Data)** + +Use a `QueryRef` to fetch data and automatically bind it to your SwiftUI view. + + +```swift +// ContentView.swift +import SwiftUI +import FirebaseDataConnect +import ItemData + +struct ContentView: View { + // A reference to our query + @State var itemsQueryRef = DataConnect.itemsConnector.listItemsQuery.ref() + + var body: some View { + VStack { + // (Create Button from previous step) + + // The List will update when the query data changes + if let items = itemsQueryRef.data?.items { + List(items) { item in + Text(item.name) + } + } + + Button("Refresh List") { + Task { + // Manually refetch the data + _ = try? await itemsQueryRef.execute() + } + } + } + .task { + // Fetch initial data when the view appears + _ = await itemsQueryRef.execute() + } + } +} +``` + +**That's it\!** You've connected your app to a local SQL database, created a new record, and displayed a list of records in your UI. + +--- + +## **📚 Next Steps** + +* **Stretch Goal:** Modify the `schema.gql`, `queries.gql` and `mutations.gql` to add a `price` field to the `Item` entity. The generated SDK should get automatically created. *Hint:* See comments in the files. +* **Schema Design:** Try creating your own Schema and Queries. Learn more about designing schemas, queries, and mutations in the [official documentation](https://firebase.google.com/docs/data-connect/schemas-guide). +* **Codelab:** Follow our detailed [Firebase Data Connect for iOS Codelab](https://firebase.google.com/codelabs/firebase-dataconnect-ios#0). +* **Sample App:** Explore a complete sample application, [FriendlyFlix](https://github.com/firebase/data-connect-ios-sdk/tree/main/Examples/FriendlyFlix), to see more advanced usage patterns. +* **Go to Production:** When you're ready to deploy, visit the [Firebase Console](https://console.firebase.google.com) to connect to a live CloudSQL (PostgreSQL) instance. + +--- + +## **🤝 Contributing** + +Please see the [Contributing](https://www.google.com/search?q=CONTRIBUTING.md) guide for more information. + +## **📄 License** + +This repository is licensed under the [Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0). Your use of Firebase is governed by the [Terms of Service for Firebase Services](https://firebase.google.com/terms/). diff --git a/Tools/SetupDevEnv/SetupDevEnv.swift b/Tools/SetupDevEnv/SetupDevEnv.swift index ee5e606..90932bd 100644 --- a/Tools/SetupDevEnv/SetupDevEnv.swift +++ b/Tools/SetupDevEnv/SetupDevEnv.swift @@ -14,7 +14,10 @@ import Foundation -import ShellExecutor +#if os(macOS) + import ShellExecutor + import TemplateProject +#endif // port on which FDC tools (code-server) listen let FDC_TOOLS_PORT: UInt = 9394 @@ -25,7 +28,9 @@ struct SetupDevEnv { static func main() { #if os(macOS) let currentDirectoryPath = FileManager.default.currentDirectoryPath - print("Attempting to start Data Connect Tools in Directory: \(currentDirectoryPath)") + print( + "â„šī¸ Attempting to start Firebase Data Connect Tools in Directory: \(currentDirectoryPath)" + ) let executor = ShellExecutor() @@ -38,6 +43,19 @@ struct SetupDevEnv { print("❌ Error killing process \(error)") } + if !CommandLine.arguments.contains("--skip-template-project") { + do { + let templateManager = TemplateProjectManager() + try templateManager + .copyTemplateProject(to: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) + + } catch { + print("❌ Error copying template project: \(error)") + } + } else { + print("â„šī¸ Skipping copying template project because --skip-template-project was provided") + } + do { let commandToRun = "curl -sL https://firebase.tools/dataconnect | bash" try executor.run(commandToRun) diff --git a/Tools/TemplateProject/Resources/demo-iosproject/.firebaserc b/Tools/TemplateProject/Resources/demo-iosproject/.firebaserc new file mode 100644 index 0000000..2604ff0 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/.firebaserc @@ -0,0 +1,8 @@ +{ + "projects": { + "default": "demo-iosproject" + }, + "targets": {}, + "etags": {}, + "dataconnectEmulatorConfig": {} +} diff --git a/Tools/TemplateProject/Resources/demo-iosproject/.gitignore b/Tools/TemplateProject/Resources/demo-iosproject/.gitignore new file mode 100644 index 0000000..b17f631 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/.gitignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# dataconnect generated files +.dataconnect diff --git a/Tools/TemplateProject/Resources/demo-iosproject/.vscode/settings.json b/Tools/TemplateProject/Resources/demo-iosproject/.vscode/settings.json new file mode 100644 index 0000000..32cfc61 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.watcherExclude": { + "**/target": true + } +} \ No newline at end of file diff --git a/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/dataconnect.yaml b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/dataconnect.yaml new file mode 100644 index 0000000..de8e3ef --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/dataconnect.yaml @@ -0,0 +1,14 @@ + + + +specVersion: "v1" +serviceId: "demo-iosproject" +location: "us-central1" +schema: + source: "./schema" + datasource: + postgresql: + database: "demo-iosproject" + cloudSql: + instanceId: "demo-iosproject" +connectorDirs: ["./default"] diff --git a/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/connector.yaml b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/connector.yaml new file mode 100644 index 0000000..ea5759e --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/connector.yaml @@ -0,0 +1,10 @@ + + +connectorId: "Items" +authMode: "PUBLIC" + +generate: + swiftSdk: + outputDir: "../../dataconnect-generated/" + package: "ItemData" #Swift Package Name for Generated SDK +# coreSdkPackageLocation: "/path/to/cloned/data-connect-ios-sdk" diff --git a/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/mutations.gql b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/mutations.gql new file mode 100644 index 0000000..3409299 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/mutations.gql @@ -0,0 +1,20 @@ + + + + +# **IMPORTANT** +# Before deploying to server, be sure to change the auth level to something higher than PUBLIC + + +# Mutation with price field +# mutation CreateItem($id: UUID!, $name: String!, $desc: String, $price: Float!) @auth(level: PUBLIC) { + + +mutation CreateItem($id: UUID!, $name: String!, $desc: String ) @auth(level: PUBLIC) { + item_upsert(data: { + id: $id, + name: $name, + desc: $desc, + # price: $price + }) +} diff --git a/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/queries.gql b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/queries.gql new file mode 100644 index 0000000..5cd50a9 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/queries.gql @@ -0,0 +1,24 @@ + + + + +# **IMPORTANT** +# Before deploying to server, be sure to change the auth level to something higher than PUBLIC + +query GetItem($id: UUID!) @auth(level: PUBLIC) { + item(id: $id) { + id + name + desc + #price + } +} + +query ListItems @auth(level: PUBLIC) { + items { + id + name + #price #Uncomment this to include price in the query + } +} + diff --git a/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/schema/schema.gql b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/schema/schema.gql new file mode 100644 index 0000000..5fcb163 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/dataconnect/schema/schema.gql @@ -0,0 +1,8 @@ + + +type Item @table { + id: UUID! + name: String! + desc: String + #price: Float! +} diff --git a/Tools/TemplateProject/Resources/demo-iosproject/firebase.json b/Tools/TemplateProject/Resources/demo-iosproject/firebase.json new file mode 100644 index 0000000..73f5997 --- /dev/null +++ b/Tools/TemplateProject/Resources/demo-iosproject/firebase.json @@ -0,0 +1,5 @@ +{ + "dataconnect": { + "source": "dataconnect" + } +} diff --git a/Tools/TemplateProject/TemplateProject.swift b/Tools/TemplateProject/TemplateProject.swift new file mode 100644 index 0000000..7d6d79a --- /dev/null +++ b/Tools/TemplateProject/TemplateProject.swift @@ -0,0 +1,98 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// This struct is for internal Firebase Data Connect use only +public struct TemplateProjectManager { + public init() {} + + /// Copies a folder named "Templates" from the package's resource bundle + /// to the current working directory. + public func copyTemplateProject(to directoryURL: URL? = nil) throws { + #if os(macOS) + let fileManager = FileManager.default + + guard let workingDirectoryURL = directoryURL else { + print("❌ Invalid target directory URL. Cannot copy template project.") + return + } + + guard !containsDataConnectProject(folderURL: workingDirectoryURL) else { + print( + "â„šī¸ Working directory already contains a dataconnect project. Skipping copy of template project." + ) + return + } + + let resourceFolderName = "dataconnect" + let destinationURL = workingDirectoryURL.appendingPathComponent(resourceFolderName) + + // 3. Find the URL for the resource folder within the compiled tool's bundle + guard let sourceURL = Bundle.module.url( + forResource: resourceFolderName, + withExtension: nil, + subdirectory: "Resources/demo-iosproject" + ) else { + throw NSError(domain: "StartFDCTools", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Could not find '\(resourceFolderName)' in the resource bundle.", + ]) + } + + // 4. Perform the copy operation + try fileManager.copyItem(at: sourceURL, to: destinationURL) + + // Copy the firebase.json + if let sourceJsonUrl = Bundle.module.url( + forResource: "firebase", + withExtension: "json", + subdirectory: "Resources/demo-iosproject" + ) { + let destinationJson = workingDirectoryURL.appendingPathComponent("firebase.json") + try fileManager.copyItem(at: sourceJsonUrl, to: destinationJson) + } + + // Copy the GoogleServices-Info.plist + if let sourcePlistUrl = Bundle.module.url( + forResource: "GoogleService-Info", + withExtension: "plist", + subdirectory: "Resources/demo-iosproject" + ) { + let destinationPlist = workingDirectoryURL + .appendingPathComponent("GoogleService-Info.plist") + try fileManager.copyItem(at: sourcePlistUrl, to: destinationPlist) + } + #endif + } + + // Looks for dataconnect.yaml file within the specified folder recursively + func containsDataConnectProject(folderURL: URL) -> Bool { + #if os(macOS) + let fileManager = FileManager.default + if let enumerator = fileManager.enumerator( + at: folderURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles, .skipsPackageDescendants] + ) { + for case let itemURL as URL in enumerator { + if itemURL.lastPathComponent.contains("dataconnect.yaml") { + print("Found existing dataconnect.yaml \(itemURL)") + return true + } + } + } + #endif + return false + } +} diff --git a/docs/resources/ConnectorYamlLocation.png b/docs/resources/ConnectorYamlLocation.png new file mode 100644 index 0000000..0381f41 Binary files /dev/null and b/docs/resources/ConnectorYamlLocation.png differ diff --git a/docs/resources/EmulatorPort.png b/docs/resources/EmulatorPort.png new file mode 100644 index 0000000..5f64626 Binary files /dev/null and b/docs/resources/EmulatorPort.png differ diff --git a/docs/resources/Extension_SignIn.png b/docs/resources/Extension_SignIn.png new file mode 100644 index 0000000..5b48bea Binary files /dev/null and b/docs/resources/Extension_SignIn.png differ diff --git a/docs/resources/NetworkCapabilities.png b/docs/resources/NetworkCapabilities.png new file mode 100644 index 0000000..ffce40a Binary files /dev/null and b/docs/resources/NetworkCapabilities.png differ diff --git a/docs/resources/StartEmulatorButton.png b/docs/resources/StartEmulatorButton.png new file mode 100644 index 0000000..f379e22 Binary files /dev/null and b/docs/resources/StartEmulatorButton.png differ