diff --git a/.github/workflows/practical_examples.yml b/.github/workflows/practical_examples.yml index 58bae46..8b7c97d 100644 --- a/.github/workflows/practical_examples.yml +++ b/.github/workflows/practical_examples.yml @@ -39,6 +39,6 @@ jobs: run: | set -o pipefail && \ xcodebuild -scheme BoardApplication \ - build -destination "name=iPhone 12" \ + build -destination "name=iPhone 15" \ -clonedSourcePackagesDirPath SourcePackages \ | bundle exec xcpretty diff --git a/.github/workflows/sources.yml b/.github/workflows/sources.yml index 07e2a4a..4d8a9ba 100644 --- a/.github/workflows/sources.yml +++ b/.github/workflows/sources.yml @@ -36,17 +36,7 @@ jobs: - name: Build and Test run: | set -o pipefail && \ - xcodebuild -scheme EasyFirebaseAuth \ - clean build \ - -destination "name=iPhone 12" \ - | bundle exec xcpretty && \ - xcodebuild -scheme EasyFirebaseFirestore \ - clean build test \ - -destination "name=iPhone 12" \ - | bundle exec xcpretty && \ - xcodebuild -scheme EasyFirebaseStorage \ - clean build test \ - -destination "name=iPhone 12" \ - | bundle exec xcpretty + swift build && \ + swift test - name: Kill firebase_emulator process run: kill `cat /tmp/firebase_emulator_pid.pid` &>/dev/null diff --git a/.gitignore b/.gitignore index 043c850..9d4de2a 100644 --- a/.gitignore +++ b/.gitignore @@ -96,4 +96,6 @@ firestore-debug.log ui-debug.log # fvm -.fvm/ \ No newline at end of file +.fvm/ + +.index-build/ \ No newline at end of file diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..50a9f23 --- /dev/null +++ b/.swift-format @@ -0,0 +1,69 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": true, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineLength": 100, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "tabWidth": 8, + "version": 1 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..33f4b2e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "editor.formatOnSave": true, + "swift.diagnostics": true, + "apple-swift-format.enable": true, + "swift.autoGenerateLaunchConfigurations": true, + "swift.buildArguments": [], + "swift.testEnvironmentVariables": {}, + "[swift]": { + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + "editor.formatOnSaveMode": "file", + "editor.defaultFormatter": "vknabel.vscode-apple-swift-format", + "cSpell.words": ["firestore"], + "makefile.configureOnOpen": false, + "swift.sourcekit-lsp.serverArguments": [ + "-Xswiftc", + "-sdk", + "-Xswiftc", + "/Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator18.0.sdk", + ] + } + \ No newline at end of file diff --git a/Examples/Example/AuthExample/AppDelegate.swift b/Examples/Example/AuthExample/AppDelegate.swift index 92f274a..46cb046 100644 --- a/Examples/Example/AuthExample/AppDelegate.swift +++ b/Examples/Example/AuthExample/AppDelegate.swift @@ -5,33 +5,39 @@ // Created by Fumiya Tanaka on 2022/01/01. // -import UIKit import FirebaseCore +import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { FirebaseApp.configure() return true } // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + return UISceneConfiguration( + name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + func application( + _ application: UIApplication, didDiscardSceneSessions sceneSessions: Set + ) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - } - diff --git a/Examples/Example/AuthExample/SceneDelegate.swift b/Examples/Example/AuthExample/SceneDelegate.swift index 3ed0cf8..b5d1884 100644 --- a/Examples/Example/AuthExample/SceneDelegate.swift +++ b/Examples/Example/AuthExample/SceneDelegate.swift @@ -11,12 +11,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene( + _ scene: UIScene, willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard (scene as? UIWindowScene) != nil else { return } } func sceneDidDisconnect(_ scene: UIScene) { @@ -47,6 +49,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } - } - diff --git a/Examples/Example/AuthExample/SignInWithAppleViewController.swift b/Examples/Example/AuthExample/SignInWithAppleViewController.swift index 8f129cd..80bd523 100644 --- a/Examples/Example/AuthExample/SignInWithAppleViewController.swift +++ b/Examples/Example/AuthExample/SignInWithAppleViewController.swift @@ -5,9 +5,9 @@ // Created by Fumiya Tanaka on 2022/01/01. // -import UIKit -import EasyFirebaseSwiftAuth import AuthenticationServices +import EasyFirebaseAuth +import UIKit class SignInWithAppleViewController: UIViewController { @@ -40,7 +40,7 @@ class SignInWithAppleViewController: UIViewController { private func startSignInWithApple() { // Show Authentication Alert // NOTE: Assign `delegate` before proceed. -// appleAuthClient.delegate = self + // appleAuthClient.delegate = self // `with` parameter is optional appleAuthClient.startSignInWithAppleFlow(with: nil) } diff --git a/Examples/Example/AuthExample/ViewController.swift b/Examples/Example/AuthExample/ViewController.swift index f188f12..72434af 100644 --- a/Examples/Example/AuthExample/ViewController.swift +++ b/Examples/Example/AuthExample/ViewController.swift @@ -14,6 +14,4 @@ class ViewController: UIViewController { // Do any additional setup after loading the view. } - } - diff --git a/Examples/Example/Example.xcodeproj/project.pbxproj b/Examples/Example/Example.xcodeproj/project.pbxproj index 945c12b..f167868 100644 --- a/Examples/Example/Example.xcodeproj/project.pbxproj +++ b/Examples/Example/Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -22,7 +22,9 @@ D33159D1263D409D0061211B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D33159CF263D409D0061211B /* Main.storyboard */; }; D33159D3263D409E0061211B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D33159D2263D409E0061211B /* Assets.xcassets */; }; D33159D6263D409E0061211B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D33159D4263D409E0061211B /* LaunchScreen.storyboard */; }; - D3B4383D277FFA3C000D6ED2 /* ViewController+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B4383C277FFA3C000D6ED2 /* ViewController+Combine.swift */; }; + D3A38E072CB9676F000B7EE1 /* EasyFirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = D3A38E062CB9676F000B7EE1 /* EasyFirebaseAuth */; }; + D3A38E092CB9676F000B7EE1 /* EasyFirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = D3A38E082CB9676F000B7EE1 /* EasyFirebaseFirestore */; }; + D3A38E0B2CB9676F000B7EE1 /* EasyFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = D3A38E0A2CB9676F000B7EE1 /* EasyFirebaseStorage */; }; D3B4383F277FFABB000D6ED2 /* ViewController+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B4383E277FFABB000D6ED2 /* ViewController+Filter.swift */; }; D3B43841277FFAE7000D6ED2 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B43840277FFAE7000D6ED2 /* Model.swift */; }; D3B43843277FFB5A000D6ED2 /* ViewController+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B43842277FFB5A000D6ED2 /* ViewController+Async.swift */; }; @@ -56,7 +58,6 @@ D33159D2263D409E0061211B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D33159D5263D409E0061211B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; D33159D7263D409E0061211B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D3B4383C277FFA3C000D6ED2 /* ViewController+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Combine.swift"; sourceTree = ""; }; D3B4383E277FFABB000D6ED2 /* ViewController+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Filter.swift"; sourceTree = ""; }; D3B43840277FFAE7000D6ED2 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; D3B43842277FFB5A000D6ED2 /* ViewController+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Async.swift"; sourceTree = ""; }; @@ -77,6 +78,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D3A38E0B2CB9676F000B7EE1 /* EasyFirebaseStorage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,6 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D3A38E092CB9676F000B7EE1 /* EasyFirebaseFirestore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,6 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D3A38E072CB9676F000B7EE1 /* EasyFirebaseAuth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -143,7 +147,6 @@ D33159CB263D409D0061211B /* SceneDelegate.swift */, D33159CD263D409D0061211B /* ViewController.swift */, D3B43842277FFB5A000D6ED2 /* ViewController+Async.swift */, - D3B4383C277FFA3C000D6ED2 /* ViewController+Combine.swift */, D3B4383E277FFABB000D6ED2 /* ViewController+Filter.swift */, D33159D2263D409E0061211B /* Assets.xcassets */, D33159D4263D409E0061211B /* LaunchScreen.storyboard */, @@ -193,6 +196,7 @@ ); name = StorageExample; packageProductDependencies = ( + D3A38E0A2CB9676F000B7EE1 /* EasyFirebaseStorage */, ); productName = StorageExample; productReference = D32E5BE527DCD65200088B60 /* StorageExample.app */; @@ -212,6 +216,7 @@ ); name = FirestoreExample; packageProductDependencies = ( + D3A38E082CB9676F000B7EE1 /* EasyFirebaseFirestore */, ); productName = Example; productReference = D33159C6263D409D0061211B /* FirestoreExample.app */; @@ -231,6 +236,7 @@ ); name = AuthExample; packageProductDependencies = ( + D3A38E062CB9676F000B7EE1 /* EasyFirebaseAuth */, ); productName = AuthExample; productReference = D3B43848277FFBE5000D6ED2 /* AuthExample.app */; @@ -266,6 +272,7 @@ ); mainGroup = D33159BD263D409D0061211B; packageReferences = ( + D3A38E052CB9676F000B7EE1 /* XCLocalSwiftPackageReference "../../../EasyFirebaseSwift" */, ); productRefGroup = D33159C7263D409D0061211B /* Products */; projectDirPath = ""; @@ -335,7 +342,6 @@ D3B43843277FFB5A000D6ED2 /* ViewController+Async.swift in Sources */, D3B4383F277FFABB000D6ED2 /* ViewController+Filter.swift in Sources */, D3B43841277FFAE7000D6ED2 /* Model.swift in Sources */, - D3B4383D277FFA3C000D6ED2 /* ViewController+Combine.swift in Sources */, D33159CC263D409D0061211B /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -421,7 +427,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -451,7 +457,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -590,7 +596,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 2X6AD4W323; INFOPLIST_FILE = "$(SRCROOT)/FirestoreExample/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -611,7 +617,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 2X6AD4W323; INFOPLIST_FILE = "$(SRCROOT)/FirestoreExample/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -639,7 +645,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -669,7 +675,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -723,6 +729,28 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + D3A38E052CB9676F000B7EE1 /* XCLocalSwiftPackageReference "../../../EasyFirebaseSwift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../EasyFirebaseSwift; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D3A38E062CB9676F000B7EE1 /* EasyFirebaseAuth */ = { + isa = XCSwiftPackageProductDependency; + productName = EasyFirebaseAuth; + }; + D3A38E082CB9676F000B7EE1 /* EasyFirebaseFirestore */ = { + isa = XCSwiftPackageProductDependency; + productName = EasyFirebaseFirestore; + }; + D3A38E0A2CB9676F000B7EE1 /* EasyFirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + productName = EasyFirebaseStorage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = D33159BE263D409D0061211B /* Project object */; } diff --git a/Examples/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..84d6c90 --- /dev/null +++ b/Examples/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "012ca0c5bc83f92a514b00387cb75910945effabdc7a099ca8b2a185bbe34d76", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d", + "version" : "10.19.2" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "eca84fd638116dd6adb633b5a3f31cc7befcbb7d", + "version" : "10.29.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "fe727587518729046fc1465625b9afd80b5ab361", + "version" : "10.28.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6", + "version" : "7.13.3" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", + "version" : "1.62.2" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" + } + } + ], + "version" : 3 +} diff --git a/Examples/Example/FirestoreExample/AppDelegate.swift b/Examples/Example/FirestoreExample/AppDelegate.swift index f35384e..688b5c5 100644 --- a/Examples/Example/FirestoreExample/AppDelegate.swift +++ b/Examples/Example/FirestoreExample/AppDelegate.swift @@ -5,33 +5,39 @@ // Created by Fumiya Tanaka on 2021/05/01. // -import UIKit import FirebaseCore +import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { FirebaseApp.configure() return true } // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + return UISceneConfiguration( + name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + func application( + _ application: UIApplication, didDiscardSceneSessions sceneSessions: Set + ) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - } - diff --git a/Examples/Example/FirestoreExample/Model.swift b/Examples/Example/FirestoreExample/Model.swift index 1bef7dd..bd5db11 100644 --- a/Examples/Example/FirestoreExample/Model.swift +++ b/Examples/Example/FirestoreExample/Model.swift @@ -5,10 +5,10 @@ // Created by Fumiya Tanaka on 2022/01/01. // -import Foundation +import EasyFirebaseFirestore import FirebaseFirestore -import EasyFirebaseSwiftFirestore import FirebaseFirestoreSwift +import Foundation struct Model: FirestoreModel { diff --git a/Examples/Example/FirestoreExample/SceneDelegate.swift b/Examples/Example/FirestoreExample/SceneDelegate.swift index ba965b2..6e4d45d 100644 --- a/Examples/Example/FirestoreExample/SceneDelegate.swift +++ b/Examples/Example/FirestoreExample/SceneDelegate.swift @@ -11,12 +11,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene( + _ scene: UIScene, willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard (scene as? UIWindowScene) != nil else { return } } func sceneDidDisconnect(_ scene: UIScene) { @@ -47,6 +49,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } - } - diff --git a/Examples/Example/FirestoreExample/ViewController+Async.swift b/Examples/Example/FirestoreExample/ViewController+Async.swift index e02ad05..bf0700e 100644 --- a/Examples/Example/FirestoreExample/ViewController+Async.swift +++ b/Examples/Example/FirestoreExample/ViewController+Async.swift @@ -6,15 +6,19 @@ // import Foundation +import FirebaseFirestore extension ViewController { func create_async(message: String) async throws { let newModel = Model( - ref: nil, + ref: Model.generateDocumentReference( + firestore: Firestore.firestore(), + id: savedDocumentId + ), createdAt: nil, updatedAt: nil, message: message ) - _ = try await client.create(newModel, documentId: savedDocumentId) + try await client.write(newModel) } } diff --git a/Examples/Example/FirestoreExample/ViewController+Combine.swift b/Examples/Example/FirestoreExample/ViewController+Combine.swift deleted file mode 100644 index 8505768..0000000 --- a/Examples/Example/FirestoreExample/ViewController+Combine.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ViewController+Combine.swift -// Example -// -// Created by Fumiya Tanaka on 2022/01/01. -// - -import Foundation -import FirebaseFirestore -import EasyFirebaseSwiftFirestore - -extension ViewController { - func create_combine(message: String) { - let newModel = Model( - ref: nil, - createdAt: nil, - updatedAt: nil, - message: message - ) - newModel.write(for: .createWithDocumentId(savedDocumentId)).sink { completion in - print(completion) - } receiveValue: { } - .store(in: &cancellables) - } - - func fetch_combine() { - guard let ref = model.ref else { - return - } - Model.single(for: .fetch(ref: ref)).sink { completion in - switch completion { - case .failure(let error): - print(error) - case .finished: - break - } - } receiveValue: { model in - print(model.message) - } - .store(in: &cancellables) - } -} diff --git a/Examples/Example/FirestoreExample/ViewController+Filter.swift b/Examples/Example/FirestoreExample/ViewController+Filter.swift index f08f3c5..0c6ed5f 100644 --- a/Examples/Example/FirestoreExample/ViewController+Filter.swift +++ b/Examples/Example/FirestoreExample/ViewController+Filter.swift @@ -5,29 +5,25 @@ // Created by Fumiya Tanaka on 2022/01/01. // +import EasyFirebaseFirestore import Foundation -import EasyFirebaseSwiftFirestore extension ViewController { - func filter() { + func filter() async { let equalFilter = FirestoreEqualFilter( fieldPath: "message", value: "Update Text" ) - client.listen( - filter: [equalFilter], - order: [], - limit: nil) - { (models: [Model]) in - let messageChecking = models.allSatisfy { model in - model.message == "Update Text" + let stream: AsyncThrowingStream<[Model], any Error> = await client.listen(filter: [equalFilter]) + var iterator = stream.makeAsyncIterator() + do { + while let model = try await iterator.next() { + if model.isEmpty == false && model.allSatisfy({ $0.message == "Update Text" }) { + print("Update Text is included") + } } - // Do All models have `Update Text`? - // fail if condition is false. - assert(messageChecking) - } failure: { error in + } catch { print(error) } - } } diff --git a/Examples/Example/FirestoreExample/ViewController.swift b/Examples/Example/FirestoreExample/ViewController.swift index c6740cc..b533a37 100644 --- a/Examples/Example/FirestoreExample/ViewController.swift +++ b/Examples/Example/FirestoreExample/ViewController.swift @@ -5,13 +5,13 @@ // Created by Fumiya Tanaka on 2021/05/01. // -import EasyFirebaseSwiftFirestore -import FirebaseFirestore import Combine +import EasyFirebaseFirestore +import FirebaseFirestore import UIKit class ViewController: UIViewController { - + let client = FirestoreClient() var cancellables: Set = [] @@ -23,21 +23,32 @@ class ViewController: UIViewController { } @IBOutlet private weak var label: UILabel! - + override func viewDidLoad() { super.viewDidLoad() - create_combine(message: "Test") + Task { + try await client.write(Model(message: "Test")) + } snapshots() - create_combine(message: getNewMessage()) + super.viewDidLoad() + Task { + let message = getNewMessage() + try await client.write(Model(message: message)) + } } @IBAction func update() { - model.message = getNewMessage() - client.update(model, success: { }, failure: { _ in }) + Task { + model.message = getNewMessage() + try await client.write(model) + } } @IBAction func update_combine() { - create_combine(message: getNewMessage()) + Task { + model.message = getNewMessage() + try await client.write(model) + } } @IBAction func update_async() { @@ -59,27 +70,21 @@ class ViewController: UIViewController { func get() { // Get single Document - client.get(uid: savedDocumentId) { (model: Model) in + Task { + let model: Model = try await client.get(documentId: savedDocumentId) print(model.message) - } failure: { error in - print(error) } } func snapshots() { - client.listen( - filter: [], - order: [], - limit: nil - ) { (models: [Model]) in - for model in models { - if model.id == self.savedDocumentId { + Task { + let stream: AsyncThrowingStream<[Model], any Error> = await client.listen() + var iterator = stream.makeAsyncIterator() + while let models = try await iterator.next() { + if let model = models.first(where: { $0.id == savedDocumentId }) { self.model = model } } - } failure: { error in - print(error) } } } - diff --git a/Examples/Example/StorageExample/AppDelegate.swift b/Examples/Example/StorageExample/AppDelegate.swift index 4a692bd..28d0ec8 100644 --- a/Examples/Example/StorageExample/AppDelegate.swift +++ b/Examples/Example/StorageExample/AppDelegate.swift @@ -5,32 +5,39 @@ // Created by Fumiya Tanaka on 2022/03/12. // -import UIKit import FirebaseCore +import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { FirebaseApp.configure() return true } // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + return UISceneConfiguration( + name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + func application( + _ application: UIApplication, didDiscardSceneSessions sceneSessions: Set + ) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - } - diff --git a/Examples/Example/StorageExample/SceneDelegate.swift b/Examples/Example/StorageExample/SceneDelegate.swift index 5fb468f..1e2fe42 100644 --- a/Examples/Example/StorageExample/SceneDelegate.swift +++ b/Examples/Example/StorageExample/SceneDelegate.swift @@ -11,12 +11,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene( + _ scene: UIScene, willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard (scene as? UIWindowScene) != nil else { return } } func sceneDidDisconnect(_ scene: UIScene) { @@ -47,6 +49,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } - } - diff --git a/Examples/Example/StorageExample/ViewController.swift b/Examples/Example/StorageExample/ViewController.swift index 6d20630..3333451 100644 --- a/Examples/Example/StorageExample/ViewController.swift +++ b/Examples/Example/StorageExample/ViewController.swift @@ -5,12 +5,12 @@ // Created by Fumiya Tanaka on 2022/03/12. // -import UIKit -import Photos -import PhotosUI import Combine +import EasyFirebaseStorage import FirebaseStorage -import EasyFirebaseSwiftStorage +import Photos +import PhotosUI +import UIKit class ViewController: UIViewController { @@ -100,7 +100,10 @@ extension ViewController: PHPickerViewControllerDelegate { @available(iOS, introduced: 14, unavailable) extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] + ) { picker.dismiss(animated: true, completion: nil) let image = info[.originalImage] as? UIImage self.image = image diff --git a/Examples/Practical/BoardApplication/BoardApplication/BoardApplicationApp.swift b/Examples/Practical/BoardApplication/BoardApplication/BoardApplicationApp.swift index f9e0b6c..ed23a2f 100644 --- a/Examples/Practical/BoardApplication/BoardApplication/BoardApplicationApp.swift +++ b/Examples/Practical/BoardApplication/BoardApplication/BoardApplicationApp.swift @@ -1,10 +1,10 @@ -import SwiftUI import FirebaseCore +import SwiftUI @main struct BoardApplicationApp: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { @@ -14,7 +14,10 @@ struct BoardApplicationApp: App { } class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { FirebaseApp.configure() return true } diff --git a/Examples/Practical/BoardApplication/BoardApplication/ContentView.swift b/Examples/Practical/BoardApplication/BoardApplication/ContentView.swift index a3898dc..4cbafab 100644 --- a/Examples/Practical/BoardApplication/BoardApplication/ContentView.swift +++ b/Examples/Practical/BoardApplication/BoardApplication/ContentView.swift @@ -1,5 +1,5 @@ -import SwiftUI import Foundation +import SwiftUI struct ContentView: View { diff --git a/Examples/Practical/BoardApplication/BoardApplication/Message.swift b/Examples/Practical/BoardApplication/BoardApplication/Message.swift index 3237fcf..191d14b 100644 --- a/Examples/Practical/BoardApplication/BoardApplication/Message.swift +++ b/Examples/Practical/BoardApplication/BoardApplication/Message.swift @@ -1,7 +1,7 @@ -import Foundation import EasyFirebaseSwift import FirebaseFirestore import FirebaseFirestoreSwift +import Foundation struct Message: FirestoreModel { diff --git a/Examples/Practical/BoardApplication/BoardApplication/Model.swift b/Examples/Practical/BoardApplication/BoardApplication/Model.swift index 4b4c8d6..0ec2e1e 100644 --- a/Examples/Practical/BoardApplication/BoardApplication/Model.swift +++ b/Examples/Practical/BoardApplication/BoardApplication/Model.swift @@ -1,6 +1,6 @@ import Combine -import Foundation import EasyFirebaseSwift +import Foundation @MainActor class Model: ObservableObject { @@ -16,7 +16,8 @@ class Model: ObservableObject { let action = FirestoreModelTypeAction .snapshots(SnapshotInputParameter.Default()) Message.multiple(for: action, client: firestoreClient) - .sink { _ in } receiveValue: { messages in + .sink { _ in + } receiveValue: { messages in self.list = messages } .store(in: &cancellables) diff --git a/Package.swift b/Package.swift index 94d8bc2..6c06a12 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "EasyFirebase", platforms: [ - .iOS(.v15), + .iOS(.v15), .macOS(.v13), ], products: [ @@ -15,12 +15,12 @@ let package = Package( ), .library( name: "EasyFirebaseFirestore", - targets: ["EasyFirebaseFirestore"] + targets: ["EasyFirebaseFirestore", "EasyFirebaseFirestoreTests"] ), .library( name: "EasyFirebaseStorage", targets: ["EasyFirebaseStorage"] - ) + ), ], dependencies: [ .package( @@ -46,7 +46,7 @@ let package = Package( .product( name: "FirebaseFirestoreSwift", package: "firebase-ios-sdk" - ) + ), ], path: "Sources/Firestore" ), @@ -71,23 +71,23 @@ let package = Package( .product( name: "FirebaseStorage", package: "firebase-ios-sdk" - ) + ), ], path: "Sources/TestCore" ), .testTarget( - name: "FirestoreTests", + name: "EasyFirebaseFirestoreTests", dependencies: [ .target(name: "EasyFirebaseFirestore"), - .target(name: "TestCore") + .target(name: "TestCore"), ] ), .testTarget( - name: "StorageTests", + name: "EasyFirebaseStorageTests", dependencies: [ .target(name: "EasyFirebaseStorage"), - .target(name: "TestCore") + .target(name: "TestCore"), ] - ) + ), ] ) diff --git a/Sources/Auth/AppleAuthClient.swift b/Sources/Auth/AppleAuthClient.swift index d983eed..4970a24 100644 --- a/Sources/Auth/AppleAuthClient.swift +++ b/Sources/Auth/AppleAuthClient.swift @@ -1,27 +1,27 @@ // // AppleAuthClient.swift -// +// // // Created by Fumiya Tanaka on 2021/05/02. // -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif -import Foundation +import AuthenticationServices import Combine import CryptoKit -import AuthenticationServices import FirebaseAuth +import Foundation + +#if canImport(UIKit) + import UIKit +#elseif canImport(AppKit) + import AppKit +#endif public enum AppleAuthClientError: Error { case failedToCastCredential case emptyIdToken } - public class AppleAuthClient: NSObject { // Unhashed nonce. @@ -83,16 +83,19 @@ public class AppleAuthClient: NSObject { private func randomNonceString(length: Int = 32) -> String { precondition(length > 0) let charset: [Character] = - Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") + Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") var result = "" var remainingLength = length while remainingLength > 0 { - let randoms: [UInt8] = (0 ..< 16).map { _ in + let randoms: [UInt8] = (0..<16).map { _ in var random: UInt8 = 0 let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random) if errorCode != errSecSuccess { - assert(false, "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)") + assert( + false, + "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)" + ) } return random } @@ -114,33 +117,40 @@ public class AppleAuthClient: NSObject { } #if canImport(UIKit) -extension AppleAuthClient: ASAuthorizationControllerPresentationContextProviding { - public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }), - let delegate = scene.delegate as? UIWindowSceneDelegate, - let window = delegate.window as? UIWindow else { - assert(false) - return UIWindow() + extension AppleAuthClient: ASAuthorizationControllerPresentationContextProviding { + public func presentationAnchor(for controller: ASAuthorizationController) + -> ASPresentationAnchor + { + guard + let scene = UIApplication.shared.connectedScenes.first(where: { + $0.activationState == .foregroundActive + }), + let delegate = scene.delegate as? UIWindowSceneDelegate, + let window = delegate.window as? UIWindow + else { + assert(false) + return UIWindow() + } + return window } - return window } -} #elseif canImport(AppKit) -extension AppleAuthClient: ASAuthorizationControllerPresentationContextProviding { - public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - guard let window = NSApplication.shared.keyWindow else { - assert(false) - return NSWindow() + extension AppleAuthClient: ASAuthorizationControllerPresentationContextProviding { + public func presentationAnchor(for controller: ASAuthorizationController) + -> ASPresentationAnchor + { + guard let window = NSApplication.shared.keyWindow else { + assert(false) + return NSWindow() + } + return window } - return window } -} #endif -public extension AppleAuthClient { - class Delegator: NSObject, ASAuthorizationControllerDelegate { - +extension AppleAuthClient { + public class Delegator: NSObject, ASAuthorizationControllerDelegate { public init( nonce: String, @@ -166,18 +176,24 @@ public extension AppleAuthClient { credentialRelay.eraseToAnyPublisher() } - public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + public func authorizationController( + controller: ASAuthorizationController, didCompleteWithError error: Error + ) { errorRelay.send(error) } - public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { // MARK: STEP2: Handle Response and Create Credential for FirebaseAuth - guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { + guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential + else { errorRelay.send(AppleAuthClientError.failedToCastCredential) return } guard let appleIdToken = credential.identityToken, - let idTokenString = String(data: appleIdToken, encoding: .utf8) + let idTokenString = String(data: appleIdToken, encoding: .utf8) else { errorRelay.send(AppleAuthClientError.emptyIdToken) return diff --git a/Sources/Auth/FirebaseAuthClient.swift b/Sources/Auth/FirebaseAuthClient.swift index cc64a18..128cf51 100644 --- a/Sources/Auth/FirebaseAuthClient.swift +++ b/Sources/Auth/FirebaseAuthClient.swift @@ -1,223 +1,97 @@ // // FirebaseAuthClient.swift -// +// // // Created by Fumiya Tanaka on 2021/05/02. // -import Foundation -import FirebaseAuth import Combine +import FirebaseAuth +import Foundation public enum FirebaseAuthClientError: Error { case noAuthData - case failedToLinkDueToNoCurrentUser + case currentUserNotFound case noEmailToLink } public class FirebaseAuthClient { - private let auth: Auth - private let userSubject: CurrentValueSubject = .init(nil) - + internal let auth: Auth + public var uid: String? { auth.currentUser?.uid } - + + public let stream: AsyncStream? + public var currentUser: FirebaseAuth.User? { - userSubject.value - } - - public var user: AnyPublisher { - userSubject.dropFirst().eraseToAnyPublisher() + auth.currentUser } - public init(auth: Auth = Auth.auth()) { + public init( + auth: Auth = Auth.auth(), + listensToStateChange: Bool = true + ) { self.auth = auth - - auth.addStateDidChangeListener { [weak self] (_, user) in - self?.userSubject.send(user) + if listensToStateChange { + stream = .init { continuation in + let listener = auth.addStateDidChangeListener { (_, user) in + continuation.yield(user) + } + continuation.onTermination = { _ in + auth.removeStateDidChangeListener(listener) + } + } + } else { + stream = nil } } - public func signIn(with credential: AuthCredential) -> AnyPublisher { - Future { [weak self] promise in - self?.auth.signIn(with: credential, completion: { (data, error) in - if let error = error { - promise(.failure(error)) - return - } - guard let data = data else { - promise(.failure(FirebaseAuthClientError.noAuthData)) - return - } - promise(.success(data.user)) - }) - }.eraseToAnyPublisher() + public func signIn( + with credential: AuthCredential + ) async throws -> FirebaseAuth.User { + let data = try await auth.signIn(with: credential) + return data.user } - - public func getAppleCredential(idToken token: String, nonce: String?) -> AuthCredential { - let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: token, rawNonce: nonce) + + internal func getAppleCredential( + idToken token: String, + nonce: String? + ) -> AuthCredential { + let credential = OAuthProvider.credential( + withProviderID: "apple.com", + idToken: token, + rawNonce: nonce + ) return credential } - - public func link(with credential: AuthCredential) -> AnyPublisher { - Future { [weak self] promise in - guard let user = self?.auth.currentUser else { - promise(.failure(FirebaseAuthClientError.failedToLinkDueToNoCurrentUser)) - return - } - user.link(with: credential, completion: { (result, error) in - if let error = error { - promise(.failure(error)) - return - } - if let user = result?.user { - promise(.success(user)) - } else { - promise(.failure(FirebaseAuthClientError.noAuthData)) - } - }) + + public func link(with credential: AuthCredential) async throws -> FirebaseAuth.User { + guard let currentUser else { + throw FirebaseAuthClientError.currentUserNotFound } - .eraseToAnyPublisher() + let data = try await currentUser.link(with: credential) + return data.user } - - public func signInWithApple(idToken token: String, nonce: String?) -> AnyPublisher { + + public func signInWithApple(idToken token: String, nonce: String?) async throws + -> FirebaseAuth.User + { let credential = getAppleCredential(idToken: token, nonce: nonce) - return signIn(with: credential) - } - - public func signInAnonymously() -> AnyPublisher { - Future { [weak self] promise in - self?.auth.signInAnonymously { (result, error) in - if let error = error { - promise(.failure(error)) - return - } - if let user = result?.user { - promise(.success(user)) - } - } - }.eraseToAnyPublisher() + return try await signIn(with: credential) } - public func createUserWithEmailAndPassword( - email: String, - password: String, - needVerification: Bool, - actionCodeSettings: ActionCodeSettings? - ) -> AnyPublisher { - Future { [weak self] promise in - self?.auth.createUser(withEmail: email, password: password, completion: { result, error in - if let error = error { - promise(.failure(error)) - return - } - guard let result = result else { - promise(.failure(FirebaseAuthClientError.noAuthData)) - return - } - guard needVerification else { - promise(.success(result.user)) - return - } - // TODO: want to write a cleaner code - if let actionCodeSettings = actionCodeSettings { - result.user.sendEmailVerification(with: actionCodeSettings) { error in - if let error = error { - promise(.failure(error)) - return - } - promise(.success(result.user)) - } - } else { - result.user.sendEmailVerification { error in - if let error = error { - promise(.failure(error)) - return - } - promise(.success(result.user)) - } - } - }) - }.eraseToAnyPublisher() + public func signInAnonymously() async throws -> FirebaseAuth.User { + let data = try await auth.signInAnonymously() + return data.user } - public func sendSignInLink( - email: String, - actionCodeSettings: ActionCodeSettings, - shouldSaveEmail: Bool = false - ) -> AnyPublisher { - Future { [weak self] promise in - self?.auth.sendSignInLink( - toEmail: email, - actionCodeSettings: actionCodeSettings - ) { error in - if let error = error { - promise(.failure(error)) - return - } - if shouldSaveEmail { - let key = "EasyFirebaseSwift.sendSignInLink.Email" - UserDefaults.standard.set(email, forKey: key) - } - promise(.success(())) - } - }.eraseToAnyPublisher() + public func signOut() async throws { + try auth.signOut() } - public func signInWithLink( - link: String, - email: String?, - shouldUseSavedEmail: Bool = false - ) -> AnyPublisher { - Future { [weak self] promise in - var email: String? = email - if shouldUseSavedEmail { - let key = "EasyFirebaseSwift.sendSignInLink.Email" - email = UserDefaults.standard.object(forKey: key) as? String - } - guard let email = email else { - promise(.failure(FirebaseAuthClientError.noEmailToLink)) - return - } - self?.auth.signIn( - withEmail: email, - link: link - ) { result, error in - if let error = error { - promise(.failure(error)) - return - } - guard let user = result?.user else { - promise(.failure(FirebaseAuthClientError.noAuthData)) - return - } - promise(.success(user)) - } - }.eraseToAnyPublisher() - } - - public func signOut() -> AnyPublisher { - Future { [weak self] promise in - do { - try self?.auth.signOut() - promise(.success(())) - } catch { - promise(.failure(error)) - } - }.eraseToAnyPublisher() - } - - public func delete() -> AnyPublisher { - Future { [weak self] promise in - self?.auth.currentUser?.delete(completion: { error in - if let error = error { - promise(.failure(error)) - } else { - promise(.success(())) - } - }) - }.eraseToAnyPublisher() + public func delete() async throws { + try await auth.currentUser?.delete() } } diff --git a/Sources/Firestore/Combine/CombineCore.swift b/Sources/Firestore/Combine/CombineCore.swift deleted file mode 100644 index a375601..0000000 --- a/Sources/Firestore/Combine/CombineCore.swift +++ /dev/null @@ -1,268 +0,0 @@ -// -// CombineCore.swift -// -// -// Created by Fumiya Tanaka on 2021/06/23. -// - -import Foundation -import FirebaseFirestore -import Combine - -public enum FirestoreModelCombine { -} - -// MARK: Single Write -public extension FirestoreModelCombine { - final class WriteSubscription: Combine.Subscription where SubscriberType.Input == DocumentReference, SubscriberType.Failure == Swift.Error { - - private var subscriber: SubscriberType? - private let model: Model - private let action: FirestoreModelAction - private let client: FirestoreClient - - public init(subscriber: SubscriberType, model: Model, action: FirestoreModelAction, firestoreClient client: FirestoreClient) { - self.subscriber = subscriber - self.model = model - self.action = action - self.client = client - - switch action { - case .create: - create(model: model) - - case .createWithDocumentId(let id): - create(model: model, documentId: id) - - case .update: - update(model: model) - - case .delete: - guard let ref = model.ref else { - assertionFailure() - return - } - delete(ref: ref) - } - - } - public func request(_ demand: Subscribers.Demand) { - } - - public func cancel() { - subscriber = nil - } - - private func create(model: Model, documentId: String? = nil) { - client.create(model, documentId: documentId) { [weak self] ref in - _ = self?.subscriber?.receive(ref) - self?.subscriber?.receive(completion: .finished) - } failure: { [weak self] error in - self?.subscriber?.receive(completion: .failure(error)) - } - } - - private func update(model: Model) { - client.update(model) { [weak self] in - self?.subscriber?.receive(completion: .finished) - } failure: { [weak self] error in - self?.subscriber?.receive(completion: .finished) - } - - } - - private func delete(ref: DocumentReference) { - client.delete(id: ref.documentID, type: Model.self) { [weak self] error in - if let error = error { - self?.subscriber?.receive(completion: .failure(error)) - } else { - self?.subscriber?.receive(completion: .finished) - } - } - - } - } - - struct WritePublisher: Combine.Publisher { - public typealias Output = DocumentReference - - public typealias Failure = Error - - let model: Model - let action: FirestoreModelAction - let firestoreClient: FirestoreClient - - init(model: Model, action: FirestoreModelAction, firestoreClient: FirestoreClient = FirestoreClient()) { - self.model = model - self.action = action - self.firestoreClient = firestoreClient - } - - public func receive(subscriber: S) where S : Subscriber, Error == S.Failure, DocumentReference == S.Input { - let subscription = WriteSubscription( - subscriber: subscriber, - model: model, - action: action, - firestoreClient: firestoreClient - ) - subscriber.receive(subscription: subscription) - } - } -} - -// MARK: Single Fetch -public extension FirestoreModelCombine { - final class FetchSubscription: Combine.Subscription where SubscriberType.Input == Model, SubscriberType.Failure == Swift.Error { - - private var subscriber: SubscriberType? - private let action: FirestoreModelTypeAction - private let client: FirestoreClient - - public init(subscriber: SubscriberType, action: FirestoreModelTypeAction, firestoreClient client: FirestoreClient) { - self.subscriber = subscriber - self.action = action - self.client = client - - switch action { - case let .fetch(ref): - fetch(ref: ref) - - case let .snapshot(ref): - snapshot(ref: ref) - - default: - assertionFailure() - break - } - - } - - public func request(_ demand: Subscribers.Demand) { - } - - public func cancel() { - subscriber = nil - } - - private func snapshot(ref: DocumentReference) { - client.listen(ref: ref) { [weak self] (model: Model) in - _ = self?.subscriber?.receive(model) - } failure: { [weak self] error in - self?.subscriber?.receive(completion: .failure(error)) - } - } - - private func fetch(ref: DocumentReference) { - client.get(uid: ref.documentID) { [weak self] (model: Model) in - _ = self?.subscriber?.receive(model) - } failure: { [weak self] error in - self?.subscriber?.receive(completion: .failure(error)) - } - } - } - - final class CollectionSubscription: Combine.Subscription where SubscriberType.Input == [Model], SubscriberType.Failure == Swift.Error { - - private var subscriber: SubscriberType? - private let action: FirestoreModelTypeAction - private let client: FirestoreClient - - public init(subscriber: SubscriberType, action: FirestoreModelTypeAction, firestoreClient client: FirestoreClient) { - self.subscriber = subscriber - self.action = action - self.client = client - - switch action { - case let .snapshots(input): - if let input = input as? SnapshotInputParameter.Default { - snapshots( - filter: input.filter, - order: input.order, - limit: input.limit - ) - } else if let input = input as? SnapshotInputParameter.Query { - snapshots( - query: input.ref, - includeCache: input.includeCache - ) - } - - default: - assertionFailure() - break - } - - } - - public func request(_ demand: Subscribers.Demand) { - } - - public func cancel() { - subscriber = nil - } - - private func snapshots( - filter: [FirestoreQueryFilter], - order: [FirestoreQueryOrder], - limit: Int? - ) { - client.listen( - filter: filter, - order: order, - limit: limit) { [weak self] (models: [Model]) in - _ = self?.subscriber?.receive(models) - } failure: { [weak self] error in - self?.subscriber?.receive(completion: .failure(error)) - } - } - - private func snapshots( - query: Query, - includeCache: Bool - ) { - client.listen(ref: query, includeCache: includeCache) { [weak self] (models: [Model]) in - _ = self?.subscriber?.receive(models) - } failure: { [weak self] error in - self?.subscriber?.receive(completion: .failure(error)) - } - } - } - - struct FetchPublisher: Combine.Publisher { - public typealias Output = Model - - public typealias Failure = Error - - let action: FirestoreModelTypeAction - let firestoreClient: FirestoreClient - - init(action: FirestoreModelTypeAction, firestoreClient: FirestoreClient = FirestoreClient()) { - self.action = action - self.firestoreClient = firestoreClient - } - - public func receive(subscriber: S) where S : Subscriber, Error == S.Failure, Model == S.Input { - let subscription = FetchSubscription(subscriber: subscriber, action: action, firestoreClient: firestoreClient) - subscriber.receive(subscription: subscription) - } - } - - struct CollectionPublisher: Combine.Publisher { - public typealias Output = [Model] - - public typealias Failure = Error - - let action: FirestoreModelTypeAction - let firestoreClient: FirestoreClient - - init(action: FirestoreModelTypeAction, firestoreClient: FirestoreClient = FirestoreClient()) { - self.action = action - self.firestoreClient = firestoreClient - } - - public func receive(subscriber: S) where S : Subscriber, Error == S.Failure, [Model] == S.Input { - let subscription = CollectionSubscription(subscriber: subscriber, action: action, firestoreClient: firestoreClient) - subscriber.receive(subscription: subscription) - } - } -} diff --git a/Sources/Firestore/Combine/FirestoreModel+Combine.swift b/Sources/Firestore/Combine/FirestoreModel+Combine.swift deleted file mode 100644 index a0f6bfd..0000000 --- a/Sources/Firestore/Combine/FirestoreModel+Combine.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// FirestoreModel+Combine.swift -// -// -// Created by Fumiya Tanaka on 2021/05/03. -// - -import Foundation -import FirebaseFirestore -import Combine - -public protocol CombineCompatible { } - -public enum FirestoreModelAction { - case create - case createWithDocumentId(String) - case update - case delete -} - -public protocol SnapshotInputParameterType { -} - -public enum SnapshotInputParameter { - public struct Default: SnapshotInputParameterType { - public init( - filter: [FirestoreQueryFilter] = [], - order: [FirestoreQueryOrder] = [], - limit: Int? = nil - ) { - self.filter = filter - self.order = order - self.limit = limit - } - - public let filter: [FirestoreQueryFilter] - public let order: [FirestoreQueryOrder] - public let limit: Int? - } - - public struct Query: SnapshotInputParameterType { - public init( - ref: FirebaseFirestore.Query, - includeCache: Bool = false - ) { - self.ref = ref - self.includeCache = includeCache - } - - public let ref: FirebaseFirestore.Query - public let includeCache: Bool - } -} - -public enum FirestoreModelTypeAction { - case snapshot(ref: DocumentReference) - case snapshots(SnapshotInputParameterType) - case fetch(ref: DocumentReference) - case query(query: Query) -} - -public extension CombineCompatible where Self: FirestoreModel { - func write( - for action: FirestoreModelAction, - client: FirestoreClient = FirestoreClient() - ) -> FirestoreModelCombine.WritePublisher { - FirestoreModelCombine.WritePublisher( - model: self, - action: action, - firestoreClient: client - ) - } - - static func single( - for action: FirestoreModelTypeAction, - client: FirestoreClient = FirestoreClient() - ) -> FirestoreModelCombine.FetchPublisher { - FirestoreModelCombine.FetchPublisher( - action: action, - firestoreClient: client - ) - } - - static func multiple( - for action: FirestoreModelTypeAction, - client: FirestoreClient = FirestoreClient() - ) -> FirestoreModelCombine.CollectionPublisher { - FirestoreModelCombine.CollectionPublisher( - action: action, - firestoreClient: client - ) - } -} diff --git a/Sources/Firestore/Concurrency/FirestoreClient+Concurrency.swift b/Sources/Firestore/Concurrency/FirestoreClient+Concurrency.swift deleted file mode 100644 index 0bb3d0d..0000000 --- a/Sources/Firestore/Concurrency/FirestoreClient+Concurrency.swift +++ /dev/null @@ -1,260 +0,0 @@ -// -// FirestoreClient+Concurrency.swift -// FirestoreClient+Concurrency -// -// Created by Fumiya Tanaka on 2021/09/04. -// - -import Foundation -import FirebaseFirestore -import FirebaseFirestoreSwift - - -// MARK: async/await functions -@available(iOS 15, *) -@available(macOS 12, *) -public extension FirestoreClient { - - func writeTransaction( - _ model: Model, - fieldPath: WritableKeyPath, - fieldValue: FieldValue, - handler: @escaping ((FieldValue, FieldValue) -> FieldValue) - ) async throws -> DocumentReference { - let reference = try await withCheckedThrowingContinuation { continuation in - self.writeTransaction( - model, - fieldPath: fieldPath, - fieldValue: fieldValue, - handler: handler, - success: { reference in - continuation.resume(returning: reference) - }, - failure: { error in - continuation.resume(throwing: error) - }) - } - return reference - } - - func create( - _ model: Model, - documentId: String? = nil - ) async throws -> DocumentReference { - try await withCheckedThrowingContinuation({ continuation in - create( - model, - documentId: documentId) { reference in - continuation.resume(returning: reference) - } failure: { error in - continuation.resume(throwing: error) - } - }) - } - - /// Update document's data or Create new document if `model.ref` is nil. - func write( - _ model: Model, - documentId: String? = nil - ) async throws { - try await withCheckedThrowingContinuation({ continuation in - write( - model, - documentId: documentId) { - continuation.resume() - } failure: { error in - continuation.resume(throwing: error) - } - }) - } - - func update( - _ model: Model - ) async throws { - try await withCheckedThrowingContinuation({ continuation in - update(model) { - continuation.resume() - } failure: { error in - continuation.resume(throwing: error) - } - }) - } - - func get( - uid: String, - includeCache: Bool = true - ) async throws -> Model { - let model = try await withCheckedThrowingContinuation({ continuation in - self.get( - uid: uid, - includeCache: includeCache) { (model: Model) in - continuation.resume(returning: model) - } failure: { error in - continuation.resume(throwing: error) - } - }) - return model - } - - - func get( - filter: [FirestoreQueryFilter], - includeCache: Bool = true, - order: [FirestoreQueryOrder], - limit: Int? - ) async throws -> [Model] { - let models: [Model] = try await withCheckedThrowingContinuation({ continuation in - get( - filter: filter, - includeCache: includeCache, - order: order, - limit: limit - ) { models in - continuation.resume(returning: models) - } failure: { error in - continuation.resume(throwing: error) - } - }) - return models - } - - func delete(_ model: Model) async throws { - try await withCheckedThrowingContinuation({ continuation in - delete(model) { - continuation.resume(returning: ()) - } failure: { error in - continuation.resume(throwing: error) - } - }) - } - - func delete( - id: String, - type: Model.Type - ) async throws { - let _: Void = try await withCheckedThrowingContinuation({ continuation in - delete(id: id, type: type) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: ()) - } - } - }) - } -} - -// MARK: SubCollection -@available(iOS 15, *) -@available(macOS 12, *) -public extension FirestoreClient { - - func create( - _ model: Model, - documentId: String? = nil, - parent parentUid: String, - superParent superParentUid: String? - ) async throws -> DocumentReference { - let ref: DocumentReference = try await withCheckedThrowingContinuation { continuation in - create( - model, - documentId: documentId, - parent: parentUid, - superParent: superParentUid) { ref in - continuation.resume(returning: ref) - } failure: { error in - continuation.resume(throwing: error) - } - } - return ref - } - - func update( - _ model: Model, - parent parentUid: String, - superParent superParentUid: String? - ) async throws { - let _: Void = try await withCheckedThrowingContinuation { continuation in - update( - model, - parent: parentUid, - superParent: superParentUid) { - continuation.resume(returning: ()) - } failure: { error in - continuation.resume(throwing: error) - } - } - } - - func get( - parent parentUid: String, - superParent superParentUid: String?, - filter: [FirestoreQueryFilter], - includeCache: Bool = true, - order: [FirestoreQueryOrder], - limit: Int? - ) async throws -> [Model] { - let models: [Model] = try await withCheckedThrowingContinuation { continuation in - get( - parent: parentUid, - superParent: superParentUid, - filter: filter, - includeCache: includeCache, - order: order, - limit: limit - ) { models in - continuation.resume(returning: models) - } failure: { error in - continuation.resume(throwing: error) - } - } - return models - } - - func get( - parent parentUid: String, - superParent superParentUid: String?, - docId: String, - includeCache: Bool = true - ) async throws -> Model { - let model: Model = try await withCheckedThrowingContinuation { continuation in - get( - parent: parentUid, - superParent: superParentUid, - docId: docId) { model in - continuation.resume(returning: model) - } failure: { error in - continuation.resume(throwing: error) - } - } - return model - } -} - - -// MARK: CollectionGroup -@available(iOS 15, *) -@available(macOS 12, *) -public extension FirestoreClient { - func getCollectionGroup( - collectionName: String, - filter: FirestoreQueryFilter, - includeCache: Bool, - order: [FirestoreQueryOrder], - limit: Int? - ) async throws -> [Model] { - let models: [Model] = try await withCheckedThrowingContinuation({ continuation in - getCollectionGroup( - collectionName: collectionName, - filter: filter, - includeCache: includeCache, - order: order, - limit: limit) { models in - continuation.resume(returning: models) - } failure: { error in - continuation.resume(throwing: error) - } - }) - return models - } -} diff --git a/Sources/Firestore/FirestoreClient.swift b/Sources/Firestore/FirestoreClient.swift index 4fdbaa9..6fe535d 100644 --- a/Sources/Firestore/FirestoreClient.swift +++ b/Sources/Firestore/FirestoreClient.swift @@ -4,57 +4,53 @@ // Created by Fumiya Tanaka on 2020/11/11. // -import Foundation import FirebaseFirestore import FirebaseFirestoreSwift +import Foundation -public protocol FirestoreModel: Codable, CombineCompatible { +public protocol FirestoreModel: Codable, Identifiable { static var collectionName: String { get } var id: String? { get } var ref: DocumentReference? { get set } var createdAt: Timestamp? { get set } var updatedAt: Timestamp? { get set } - - static func buildRef(id: String) -> DocumentReference + static func generateDocumentReference(firestore: Firestore?, id: String?) -> DocumentReference } -public extension FirestoreModel { - var id: String? { +extension FirestoreModel { + public var id: String? { ref?.documentID } - static func buildRef(id: String) -> DocumentReference { - Firestore.firestore().collection(Self.collectionName).document(id) + public static func generateDocumentReference(firestore: Firestore?, id: String?) -> DocumentReference { + let firestore = firestore ?? Firestore.firestore() + if let id { + return firestore.collection(Self.collectionName).document(id) + } + return firestore.collection(Self.collectionName).document() } } -public protocol SubCollectionModel { - static var parentModelType: FirestoreModel.Type { get } +public protocol SubCollectionModel: FirestoreModel { + static var parentCollectionName: String { get } } public protocol FirestoreQueryFilter { var fieldPath: String? { get } - - func build(from: Query) -> Query - func build(type: Model.Type) -> Query -} -public protocol FirestoreQueryOrder { - var fieldPath: String { get } - var isAscending: Bool { get } - + func build(type: Model.Type) -> Query func build(from: Query) -> Query } -public struct DefaultFirestoreQueryOrder: FirestoreQueryOrder { +public struct FirestoreQueryOrder { public var fieldPath: String public var isAscending: Bool - + public init(fieldPath: String, isAscending: Bool) { self.fieldPath = fieldPath self.isAscending = isAscending } - + public func build(from: Query) -> Query { from.order(by: fieldPath, descending: !isAscending) } @@ -75,17 +71,19 @@ public struct FirestoreRangeFilter: FirestoreQueryFilter { guard let fieldPath = fieldPath, maxValue > minValue else { return from } - return from + return + from .whereField(fieldPath, isGreaterThan: minValue) .whereField(fieldPath, isLessThan: maxValue) } - public func build(type: Model.Type) -> Query where Model : FirestoreModel { + public func build(type: Model.Type) -> Query where Model: FirestoreModel { let from = Firestore.firestore().collection(type.collectionName) guard let fieldPath = fieldPath, maxValue > minValue else { return from } - return from + return + from .whereField(fieldPath, isGreaterThan: minValue) .whereField(fieldPath, isLessThan: maxValue) } @@ -94,21 +92,21 @@ public struct FirestoreRangeFilter: FirestoreQueryFilter { public struct FirestoreEqualFilter: FirestoreQueryFilter { public var fieldPath: String? public var value: Any? - + public init(fieldPath: String?, value: Any?) { self.fieldPath = fieldPath self.value = value } - + public func build(from: Query) -> Query { - + guard let fieldPath = fieldPath else { return from } return from.whereField(fieldPath, isEqualTo: value as Any) } - public func build(type: Model.Type) -> Query where Model : FirestoreModel { + public func build(type: Model.Type) -> Query where Model: FirestoreModel { let from = Firestore.firestore().collection(type.collectionName) guard let fieldPath = fieldPath else { return from @@ -129,13 +127,12 @@ public struct FirestoreContainFilter: FirestoreQueryFilter { public func build(from: Query) -> Query { guard let fieldPath = fieldPath, !value.isEmpty else { - assertionFailure("Invalid Data") return from } return from.whereField(fieldPath, in: value) } - public func build(type: Model.Type) -> Query where Model : FirestoreModel { + public func build(type: Model.Type) -> Query where Model: FirestoreModel { let from = Firestore.firestore().collection(type.collectionName) guard let fieldPath = fieldPath, !value.isEmpty else { return from @@ -144,297 +141,205 @@ public struct FirestoreContainFilter: FirestoreQueryFilter { } } -public enum FirestoreClientError: Error { +public enum EasyFirebaseFirestoreError: Error { // Decode/Encode case failedToDecode(data: [String: Any]?) - + // Ref - case alreadyExistsDocumentReferenceInCreateModel - case notExistsDocumentReferenceInUpdateModel - + case alreadyExists(ref: DocumentReference) + case notFound(ref: DocumentReference) + // Timestamp - case occureTimestampExceptionInCreateModel - case occureTimestampExceptionInUpdateModel + case invalidTimestamp(createdAt: Timestamp?, updatedAt: Timestamp?) + + case refNotExists +} + +public actor FirestoreClient { + + public let firestore = Firestore.firestore() + public private(set) var documentListeners: [DocumentReference: ListenerRegistration] = [:] + public private(set) var queryListeners: [Query: ListenerRegistration] = [:] + + public init() {} + } +// MARK: - FirestoreModel +extension FirestoreClient { + + // MARK: Write -public class FirestoreClient { - - private let firestore = Firestore.firestore() - private var documentListeners: [DocumentReference: ListenerRegistration] = [:] - private var queryListeners: [Query: ListenerRegistration] = [:] - - public init() { } - public func writeTransaction( _ model: Model, fieldPath: WritableKeyPath, fieldValue: FieldValue, - handler: @escaping ((FieldValue, FieldValue)) -> FieldValue, - success: @escaping (DocumentReference) -> Void, - failure: @escaping (Error) -> Void - ) { + beforeCommit: @escaping ((old: FieldValue, new: FieldValue)) -> FieldValue + ) async throws { var model = model guard let ref = model.ref else { - return + throw EasyFirebaseFirestoreError.refNotExists } - firestore.runTransaction { (transaction, errorPointeer) -> Any? in + _ = try await firestore.runTransaction { transaction, errorPointeer in do { let snapshot = try transaction.getDocument(ref) let data = try snapshot.data(as: Model.self) let currentFieldValue = data[keyPath: fieldPath] - let newFieldValue = handler((currentFieldValue, fieldValue)) + let newFieldValue = beforeCommit((currentFieldValue, fieldValue)) model[keyPath: fieldPath] = newFieldValue try transaction.setData(from: model, forDocument: ref) } catch { errorPointeer?.pointee = error as NSError } - return nil - } completion: { (_, error) in - if let error = error { - failure(error) - return - } - success(ref) - } - - } - - public func create( - _ model: Model, - documentId: String? = nil, - success: @escaping (DocumentReference) -> Void, - failure: @escaping (Error) -> Void - ) { - do { - if model.ref != nil { - failure(FirestoreClientError.alreadyExistsDocumentReferenceInCreateModel) - return - } - - if model.createdAt != nil || model.updatedAt != nil { - failure(FirestoreClientError.occureTimestampExceptionInCreateModel) - return - } - - let ref: DocumentReference - - if let documentId = documentId { - ref = firestore.collection(Model.collectionName).document(documentId) - } else { - ref = firestore.collection(Model.collectionName).document() - } - - try ref.setData(from: model, merge: false) { error in - if let error = error { - failure(error) - return - } - success(ref) - } - } catch { - failure(error) + return } } - + /// Update document's data or Create new document if `model.ref` is nil. + @discardableResult public func write( _ model: Model, - documentId: String? = nil, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void - ) { + newDocumentIdIfNotExists: String? = nil + ) async throws -> DocumentReference { let ref: DocumentReference - - if let _ref = model.ref { - ref = _ref + if let existingRef = model.ref { + ref = existingRef } else { - if let documentId = documentId { - ref = firestore.collection(Model.collectionName).document(documentId) - } else { - ref = firestore.collection(Model.collectionName).document() - } - } - - do { - try ref.setData(from: model, merge: true) { error in - if let error = error { - failure(error) - return - } - success() - } - } catch { - failure(error) + ref = Model.generateDocumentReference(firestore: firestore, id: newDocumentIdIfNotExists) } - - } - - public func update( - _ model: Model, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void - ) { - do { - guard let ref = model.ref else { - failure(FirestoreClientError.notExistsDocumentReferenceInUpdateModel) - return - } - - if model.createdAt == nil { - failure(FirestoreClientError.occureTimestampExceptionInUpdateModel) - return - } - - try ref.setData(from: model, merge: true) { error in - if let error = error { - failure(error) - return + + return try await withCheckedThrowingContinuation { continuation in + do { + try ref.setData(from: model, merge: true) { error in + if let error { + continuation.resume(throwing: error) + return + } + continuation.resume(returning: ref) } - success() + } catch { + continuation.resume(throwing: error) } - - } catch { - failure(error) } } - - public func listen( - uid: String, - includeCache: Bool = true, - success: @escaping (Model) -> Void, - failure: @escaping (Error) -> Void - ) { - let ref = firestore.collection(Model.collectionName) - .document(uid) - let listener = ref - .addSnapshotListener { (snapshot, error) in - if let error = error { - failure(error) - return - } - guard let snapshot = snapshot else { - return - } - if snapshot.metadata.isFromCache, includeCache == false { - return - } - do { - let model: Model = try FirestoreClient.putSnaphotTogether(snapshot) - success(model) - } catch { - failure(error) - } + + // MARK: Get + public func get( + documentId: String, + includeCache: Bool = true + ) async throws -> Model { + let ref = Model.generateDocumentReference(firestore: firestore, id: documentId) + let snapshot = try await ref.getDocument(source: includeCache ? .default : .server) + + guard snapshot.exists else { + throw EasyFirebaseFirestoreError.notFound(ref: ref) } - documentListeners[ref]?.remove() - documentListeners[ref] = listener + + return try FirestoreClient.putSnaphotTogether(snapshot) } - - public func listen( - filter: [FirestoreQueryFilter], + + public func get( + filter: [FirestoreQueryFilter] = [], includeCache: Bool = true, - order: [FirestoreQueryOrder], - limit: Int?, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { let query = createQuery(modelType: Model.self, filter: filter) .build(order: order, limit: limit) - let listener = query - .addSnapshotListener { (snapshots, error) in + let snapshot = try await query.getDocuments(source: includeCache ? .default : .server) + return try FirestoreClient.putSnaphotsTogether(snapshot) + } + + // MARK: Listen + + public func listen( + documentId: String, + includeCache: Bool = true + ) -> AsyncThrowingStream { + let ref = Model.generateDocumentReference(firestore: firestore, id: documentId) + documentListeners[ref]?.remove() + return AsyncThrowingStream { [weak self] continuation in + let listener = ref.addSnapshotListener( + includeMetadataChanges: includeCache + ) { snapshot, error in if let error = error { - failure(error) + continuation.yield(with: .failure(error)) return } - guard let snapshots = snapshots else { + guard let snapshot = snapshot else { return } - if snapshots.metadata.isFromCache, includeCache == false { + let isCache = snapshot.metadata.isFromCache + if isCache, !includeCache { return } do { - let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) + let model = try snapshot.data(as: Model.self) + continuation.yield(model) } catch { - failure(error) + continuation.yield(with: .failure(error)) } } - queryListeners[query]?.remove() - queryListeners[query] = listener - } - - public func get( - uid: String, - includeCache: Bool = true, - success: @escaping (Model) -> Void, - failure: @escaping (Error) -> Void - ) { - firestore.collection(Model.collectionName).document(uid).getDocument { (snapshot, error) in - if let error = error { - failure(error) - return - } - guard let snapshot = snapshot else { - return - } - if snapshot.metadata.isFromCache, includeCache == false { - return + continuation.onTermination = { _ in + listener.remove() } - do { - let model: Model = try FirestoreClient.putSnaphotTogether(snapshot) - success(model) - } catch { - failure(error) + Task { + await self?.documentListeners[ref]?.remove() + await self?.setListener(key: ref, value: listener) } } } - - public func get( - filter: [FirestoreQueryFilter], + + public func listen( + filter: [FirestoreQueryFilter] = [], includeCache: Bool = true, - order: [FirestoreQueryOrder], - limit: Int?, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { - createQuery(modelType: Model.self, filter: filter) - .build(order: order, limit: limit) - .getDocuments { (snapshots, error) in + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) -> AsyncThrowingStream<[Model], Error> { + let query = createQuery(modelType: Model.self, filter: filter) + .build( + order: order, + limit: limit + ) + queryListeners[query]?.remove() + return AsyncThrowingStream { [weak self] continuation in + let listener = query.addSnapshotListener { (snapshots, error) in if let error = error { - failure(error) + continuation.yield(with: .failure(error)) return } guard let snapshots = snapshots else { return } - if snapshots.metadata.isFromCache, includeCache == false { + if !includeCache, snapshots.metadata.isFromCache { + // Ignore this event if `includeCache` is `false` and the source is from cache. return } do { let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) + continuation.yield(models) } catch { - failure(error) + continuation.yield(with: .failure(error)) } } + continuation.onTermination = { _ in + listener.remove() + } + Task { + await self?.queryListeners[query]?.remove() + await self?.setListener(key: query, value: listener) + } + } } - - public func delete( - _ model: Model, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void - ) { + + // MARK: Delete + + public func delete(_ model: Model) async throws { guard let ref = model.ref else { - return - } - ref.delete { (error) in - if let error = error { - failure(error) - return - } - success() + throw EasyFirebaseFirestoreError.refNotExists } + try await ref.delete() } - + private func createQuery( modelType: Model.Type, filter: [FirestoreQueryFilter] @@ -445,473 +350,306 @@ public class FirestoreClient { } return query } - + + // MARK: Stop Listener + public func stopListening(type: Model.Type) { let query = firestore.collection(type.collectionName) queryListeners[query]?.remove() } - + /// Not applicable to SubCollectionModel public func stopListening(type: Model.Type, documentID: String) { let ref = firestore.collection(type.collectionName).document(documentID) stopListening(ref: ref) } - + public func stopListening(ref: DocumentReference) { documentListeners[ref]?.remove() } - + /// If you want to stop listening to SubCollectionModel, please use this method public func stopListeningAll() { documentListeners.forEach({ $0.value.remove() }) queryListeners.forEach({ $0.value.remove() }) } - - public func delete( - id: String, - type: Model.Type, - completion: ((Error?) -> Void)? = nil - ) { - firestore.collection(Model.collectionName).document(id).delete(completion: completion) - } - + // MARK: Internal - func listen( - ref: DocumentReference, - includeCache: Bool = true, - success: @escaping (Model) -> Void, - failure: @escaping (Error) -> Void + internal func setListener( + key ref: DocumentReference, value documentListener: ListenerRegistration ) { - let listener = ref.addSnapshotListener { snapshot, error in - if let error = error { - failure(error) - return - } - guard let snapshot = snapshot else { - return - } - if snapshot.metadata.isFromCache, includeCache == false { - return - } - do { - let model: Model = try FirestoreClient.putSnaphotTogether(snapshot) - success(model) - } catch { - failure(error) - } - } - documentListeners[ref]?.remove() - documentListeners[ref] = listener + documentListeners[ref] = documentListener } - - func listen( - ref: Query, - includeCache: Bool = true, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { - let listener = ref.addSnapshotListener { snapshots, error in - if let error = error { - failure(error) - return - } - guard let snapshots = snapshots else { - return - } - if snapshots.metadata.isFromCache, includeCache == false { - return - } - do { - let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) - } catch { - failure(error) - } - } - queryListeners[ref]?.remove() - queryListeners[ref] = listener + + internal func setListener(key query: Query, value documentListener: ListenerRegistration) { + queryListeners[query] = documentListener } } -// MARK: SubCollection +// MARK: - SubCollectionModel extension FirestoreClient { - - public func create( - _ model: Model, - documentId: String? = nil, - parent parentUid: String, - superParent superParentUid: String?, - success: @escaping (DocumentReference) -> Void, - failure: @escaping (Error) -> Void - ) { - do { - - if model.ref != nil { - failure(FirestoreClientError.alreadyExistsDocumentReferenceInCreateModel) - return - } - - let ref: DocumentReference - - if let superParentUid = superParentUid, let superParentType = Model.parentModelType as? SubCollectionModel.Type { - let superCollectionName = superParentType.parentModelType.collectionName - let parentCollectionName = Model.parentModelType.collectionName - let collectionName = Model.collectionName - - if let documentId = documentId { - ref = firestore.collection(Model.parentModelType.collectionName).document(parentUid) - .collection(Model.collectionName) - .document(documentId) - } else { - ref = firestore.collection(superCollectionName).document(superParentUid) - .collection(parentCollectionName) - .document(parentUid) - .collection(collectionName) - .document() - } - } else { - - if let documentId = documentId { - ref = firestore.collection(Model.parentModelType.collectionName).document(parentUid) - .collection(Model.collectionName) - .document(documentId) - } else { - ref = firestore.collection(Model.parentModelType.collectionName).document(parentUid) - .collection(Model.collectionName) - .document() - } - } - - if model.updatedAt != nil || model.createdAt != nil { - failure(FirestoreClientError.occureTimestampExceptionInCreateModel) - return - } - - try ref.setData(from: model, merge: false) { (error) in - if let error = error { - failure(error) - return + + public func create(_ model: Model) async throws { + guard let ref = model.ref else { + throw EasyFirebaseFirestoreError.refNotExists + } + + if model.updatedAt != nil || model.createdAt != nil { + throw EasyFirebaseFirestoreError.invalidTimestamp( + createdAt: model.createdAt, + updatedAt: model.updatedAt + ) + } + + return try await withCheckedThrowingContinuation { continuation in + do { + try ref.setData(from: model, merge: false) { (error) in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume(returning: ()) } - success(ref) + } catch { + continuation.resume(throwing: error) } - } catch { - failure(error) } } - - public func update( - _ model: Model, - parent parentUid: String, - superParent superParentUid: String?, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void - ) { - do { - var model = model - guard var ref = model.ref else { - failure(FirestoreClientError.notExistsDocumentReferenceInUpdateModel) - return - } - - if model.updatedAt == nil || model.createdAt == nil { - failure(FirestoreClientError.occureTimestampExceptionInCreateModel) - return - } - if let superParentUid = superParentUid, let superParentType = Model.parentModelType as? SubCollectionModel.Type { - let superCollectionName = superParentType.parentModelType.collectionName - let parentCollectionName = Model.parentModelType.collectionName - let collectionName = Model.collectionName - - let documentId = ref.documentID - ref = firestore - .collection(superCollectionName) - .document(superParentUid) - .collection(parentCollectionName) - .document(parentUid) - .collection(collectionName) - .document(documentId) - } else { - - let documentId = ref.documentID - ref = firestore - .collection(Model.parentModelType.collectionName) - .document(parentUid) - .collection(Model.collectionName) - .document(documentId) - } + public func update(_ model: Model) async throws { + guard let ref = model.ref else { + throw EasyFirebaseFirestoreError.refNotExists + } - model.updatedAt = nil - - try ref.setData(from: model, merge: true) { (error) in - if let error = error { - failure(error) - return + if model.updatedAt == nil || model.createdAt == nil { + throw EasyFirebaseFirestoreError.invalidTimestamp( + createdAt: model.createdAt, + updatedAt: model.updatedAt + ) + } + + return try await withCheckedThrowingContinuation { continuation in + do { + try ref.setData(from: model, merge: false) { (error) in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume(returning: ()) } - success() + } catch { + continuation.resume(throwing: error) } - } catch { - failure(error) } } - - public func get( - parent parentUid: String, - superParent superParentUid: String?, - filter: [FirestoreQueryFilter], + + public func get( + parent parentDocumentId: String, + filter: [FirestoreQueryFilter] = [], includeCache: Bool = true, - order: [FirestoreQueryOrder], - limit: Int?, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { - createQueryOfSubCollection( - parent: parentUid, - superParent: superParentUid, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + let snapshot = try await createQueryOfSubCollection( + parent: parentDocumentId, modelType: Model.self, - filter: filter + filter: filter, + order: order, + limit: limit ) - .build(order: order, limit: limit) - .addSnapshotListener { (snapshots, error) in - if let error = error { - failure(error) - return - } - guard let snapshots = snapshots else { - return - } - if snapshots.metadata.isFromCache, includeCache == false { - return - } - do { - let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) - } catch { - failure(error) - } - } + .getDocuments() + + return try FirestoreClient.putSnaphotsTogether(snapshot) } - - public func get( + + public func get( + documentId: String, parent parentUid: String, - superParent superParentUid: String?, - docId: String, - includeCache: Bool = true, - success: @escaping (Model) -> Void, - failure: @escaping (Error) -> Void - ) { - let ref: DocumentReference - if let superParent = superParentUid, let superParentType = Model.parentModelType as? SubCollectionModel.Type { - ref = firestore - .collection(superParentType.parentModelType.collectionName) - .document(superParent) - .collection(Model.parentModelType.collectionName) - .document(parentUid) - .collection(Model.collectionName) - .document(docId) - } else { - ref = firestore - .collection(Model.parentModelType.collectionName) - .document(parentUid) - .collection(Model.collectionName) - .document(docId) - } - ref.getDocument { (snapshot, error) in - if let error = error { - failure(error) - return - } - guard let snapshot = snapshot else { - return - } - if snapshot.metadata.isFromCache, includeCache == false { - return - } - do { - let model: Model = try FirestoreClient.putSnaphotTogether(snapshot) - success(model) - } catch { - failure(error) - } - } + includeCache: Bool = true + ) async throws -> Model { + let collectionName = Model.parentCollectionName + let ref: DocumentReference = firestore.collection(collectionName) + .document(parentUid) + .collection(Model.collectionName) + .document(documentId) + + let snapshot = try await ref.getDocument() + return try FirestoreClient.putSnaphotTogether(snapshot) + } - + public func listen( - parent parentUID: String, - uid: String, + parentDocumentId parentUID: String, + documentId: String, + filter: [FirestoreQueryFilter], includeCache: Bool = true, - success: @escaping (Model) -> Void, - failure: @escaping (Error) -> Void - ) { - let ref = firestore.collection(Model.parentModelType.collectionName).document(parentUID).collection(Model.collectionName).document(uid) - let listener = ref.addSnapshotListener { (snapshot, error) in - if let error = error { - failure(error) - return - } - guard let snapshot = snapshot else { - return + order: [FirestoreQueryOrder], + limit: Int? + ) -> AsyncThrowingStream { + let ref = firestore.collection(Model.parentCollectionName).document(parentUID) + .collection(Model.collectionName).document(documentId) + return AsyncThrowingStream { [weak self] continuation in + let listener = ref.addSnapshotListener( + includeMetadataChanges: includeCache + ) { snapshot, error in + if let error = error { + continuation.yield(with: .failure(error)) + return + } + guard let snapshot = snapshot else { + return + } + let isCache = snapshot.metadata.isFromCache + if isCache, !includeCache { + return + } + do { + let model = try snapshot.data(as: Model.self) + continuation.yield(model) + } catch { + continuation.yield(with: .failure(error)) + } } - if snapshot.metadata.isFromCache, includeCache == false { - return + continuation.onTermination = { _ in + listener.remove() } - do { - let model: Model = try FirestoreClient.putSnaphotTogether(snapshot) - success(model) - } catch { - failure(error) + Task { + await self?.documentListeners[ref]?.remove() + await self?.setListener(key: ref, value: listener) } } - documentListeners[ref]?.remove() - documentListeners[ref] = listener + } - + public func listen( parent parentUid: String, superParent superParentUid: String?, filter: [FirestoreQueryFilter], includeCache: Bool = true, order: [FirestoreQueryOrder], - limit: Int?, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { + limit: Int? + ) -> AsyncThrowingStream<[Model], any Error> { let query = createQueryOfSubCollection( parent: parentUid, - superParent: superParentUid, modelType: Model.self, - filter: filter + filter: filter, + order: order, + limit: limit ) - .build(order: order, limit: limit) - - let listener = query - .addSnapshotListener { (snapshots, error) in + + return AsyncThrowingStream { [weak self] continuation in + let listener = query.addSnapshotListener { (snapshots, error) in if let error = error { - failure(error) + continuation.yield(with: .failure(error)) return } guard let snapshots = snapshots else { return } - if snapshots.metadata.isFromCache, includeCache == false { + if !includeCache, snapshots.metadata.isFromCache { + // Ignore this event if `includeCache` is `false` and the source is from cache. return } do { let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) + continuation.yield(models) } catch { - failure(error) + continuation.yield(with: .failure(error)) } } - queryListeners[query]?.remove() - queryListeners[query] = listener + continuation.onTermination = { _ in + listener.remove() + } + Task { + await self?.queryListeners[query]?.remove() + await self?.setListener(key: query, value: listener) + } + } } - - private func createQueryOfSubCollection - ( + + private func createQueryOfSubCollection( parent parentUid: String, - superParent superParentUid: String?, modelType: Model.Type, - filter: [FirestoreQueryFilter] + filter: [FirestoreQueryFilter], + order: [FirestoreQueryOrder], + limit: Int? ) -> Query { - var query: Query - if let superParentUid = superParentUid, - let superParentType = Model.parentModelType as? SubCollectionModel.Type { - let superCollectionName = superParentType.parentModelType.collectionName - let parentCollectionName = Model.parentModelType.collectionName - let collectionName = Model.collectionName - query = firestore - .collection(superCollectionName) - .document(superParentUid) - .collection(parentCollectionName) - .document(parentUid) - .collection(collectionName) - } else { - query = firestore - .collection(modelType.parentModelType.collectionName) - .document(parentUid) - .collection(modelType.collectionName) - } + var query: Query = + firestore + .collection(modelType.parentCollectionName) + .document(parentUid) + .collection(modelType.collectionName) + for element in filter { - query = element.build(from: query) + query = element.build(from: query).build(order: order, limit: limit) } return query } } -// MARK: CollectionGroup +// MARK: - CollectionGroup extension FirestoreClient { public func getCollectionGroup( - collectionName: String, - filter: FirestoreQueryFilter, - includeCache: Bool, - order: [FirestoreQueryOrder], - limit: Int?, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { - createQuery( + filter: [FirestoreQueryFilter] = [], + includeCache: Bool = true, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + let collectionName = Model.collectionName + let snapshots = try await createQuery( from: firestore.collectionGroup(collectionName), - filter: [filter] + filter: filter ) .build(order: order, limit: limit) - .getDocuments { (snapshots, error) in - if let error = error { - failure(error) - return - } - guard let snapshots = snapshots else { - return - } - if snapshots.metadata.isFromCache, includeCache == false { - return - } - do { - let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) - } catch { - failure(error) - } - } + .getDocuments(source: includeCache ? .default : .server) + + let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) + return models } - + public func listenCollectionGroup( collectionName: String, - filter: FirestoreQueryFilter, - includeCache: Bool, - order: [FirestoreQueryOrder], - limit: Int?, - success: @escaping ([Model]) -> Void, - failure: @escaping (Error) -> Void - ) { - + filter: [FirestoreQueryFilter] = [], + includeCache: Bool = true, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) -> AsyncThrowingStream<[Model], Error> { + let query = createQuery( from: firestore.collectionGroup(collectionName), - filter: [filter] + filter: filter ).build(order: order, limit: limit) - let listener = query.addSnapshotListener { (snapshots, error) in - if let error = error { - failure(error) - return - } - guard let snapshots = snapshots else { - return + return AsyncThrowingStream { [weak self] continuation in + let listener = query.addSnapshotListener { (snapshots, error) in + if let error = error { + continuation.yield(with: .failure(error)) + return + } + guard let snapshots = snapshots else { + return + } + if !includeCache, snapshots.metadata.isFromCache { + // Ignore this event if `includeCache` is `false` and the source is from cache. + return + } + do { + let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) + continuation.yield(models) + } catch { + continuation.yield(with: .failure(error)) + } } - if snapshots.metadata.isFromCache, includeCache == false { - return + continuation.onTermination = { _ in + listener.remove() } - do { - let models: [Model] = try FirestoreClient.putSnaphotsTogether(snapshots) - success(models) - } catch { - failure(error) + Task { + await self?.queryListeners[query]?.remove() + await self?.setListener(key: query, value: listener) } } - queryListeners[query]?.remove() - queryListeners[query] = listener } - + private func createQuery(from ref: Query, filter: [FirestoreQueryFilter]) -> Query { var query: Query = ref for element in filter { @@ -935,10 +673,12 @@ extension Query { } } -// MARK: internal common methods +// MARK: - Internal common methods extension FirestoreClient { - - static func putSnaphotsTogether(_ snapshots: QuerySnapshot) throws -> [Model] { + + static func putSnaphotsTogether(_ snapshots: QuerySnapshot) throws + -> [Model] + { let documents = snapshots.documents let models = try documents.map { document -> Model in let model = try document.data(as: Model.self) @@ -946,19 +686,11 @@ extension FirestoreClient { } return models } - - static func putSnaphotTogether(_ snapshot: DocumentSnapshot) throws -> Model { - let model = try snapshot.data(as: Model.self) - return model - } -} -// MARK: Utility -public extension FirestoreClient { - func updateDocumentID(of model: Model, newId: String) throws -> Model { - var model = model - let parent = model.ref?.parent - model.ref = parent?.document(newId) + static func putSnaphotTogether(_ snapshot: DocumentSnapshot) throws + -> Model + { + let model = try snapshot.data(as: Model.self) return model } } diff --git a/Sources/Storage/StorageClient.swift b/Sources/Storage/StorageClient.swift index ababd76..8ae25f9 100644 --- a/Sources/Storage/StorageClient.swift +++ b/Sources/Storage/StorageClient.swift @@ -1,13 +1,13 @@ // // StorageClient.swift -// +// // // Created by Fumiya Tanaka on 2022/01/15. // import Combine -import Foundation import FirebaseStorage +import Foundation public protocol Folder { var name: String { get set } @@ -183,7 +183,7 @@ public class Resource { } public func download( - maxSize: Int64 = 1024 * 1024 * 10 // 10MB + maxSize: Int64 = 1024 * 1024 * 10 // 10MB ) -> AnyPublisher { let subject: CurrentValueSubject = .init( .init( @@ -252,9 +252,9 @@ public class Resource { } } -public extension Resource { +extension Resource { - struct Metadata { + public struct Metadata { public init(contentType: Resource.Metadata.ContentType) { self.contentType = contentType } @@ -300,7 +300,7 @@ public extension Resource { } } - struct Task { + public struct Task { public init( status: Resource.Status, resource: Resource @@ -313,7 +313,7 @@ public extension Resource { public let resource: Resource } - enum Status { + public enum Status { case progress(Double) case success case fail(Error) @@ -331,7 +331,7 @@ public class StorageClient { public static let shared: StorageClient = StorageClient() - private init() { } + private init() {} public func cancel() { uploads.forEach { task in diff --git a/Sources/TestCore/FirebaseTestHepler.swift b/Sources/TestCore/FirebaseTestHepler.swift index cbc33ac..b1644b4 100644 --- a/Sources/TestCore/FirebaseTestHepler.swift +++ b/Sources/TestCore/FirebaseTestHepler.swift @@ -1,15 +1,15 @@ // // FirebaseTestsHepler.swift -// +// // // Created by Fumiya Tanaka on 2022/01/16. // -import Foundation -import FirebaseCore import FirebaseAuth +import FirebaseCore import FirebaseFirestore import FirebaseStorage +import Foundation // Reference // https://techblog.sgr-ksmt.dev/2019/09/28/180821/ diff --git a/Tests/FirestoreTests/FirestoreClientTests.swift b/Tests/EasyFirebaseFirestoreTests/FirestoreClientTests.swift similarity index 56% rename from Tests/FirestoreTests/FirestoreClientTests.swift rename to Tests/EasyFirebaseFirestoreTests/FirestoreClientTests.swift index 0e3b735..9c229ca 100644 --- a/Tests/FirestoreTests/FirestoreClientTests.swift +++ b/Tests/EasyFirebaseFirestoreTests/FirestoreClientTests.swift @@ -1,18 +1,20 @@ // // FirestoreClientTests.swift -// +// // // Created by Fumiya Tanaka on 2022/01/16. // -import XCTest import Combine import FirebaseFirestore import FirebaseFirestoreSwift +import XCTest + @testable import EasyFirebaseFirestore @testable import TestCore struct TestModel: FirestoreModel { + // MARK: Protocol Requirement static let collectionName: String = "tests" @DocumentID @@ -36,28 +38,11 @@ class FirestoreClientTests: XCTestCase { client = FirestoreClient() } - @available(iOS 15, *) - @available(macOS 12, *) func test_create_async() async throws { let newModel = TestModel(message: "Async Test") - let ref = try await client.create(newModel) - let fetched: TestModel = try await client.get(uid: ref.documentID) + let ref = try await client.write(newModel) + let documentId = ref.documentID + let fetched: TestModel = try await client.get(documentId: documentId) XCTAssertEqual(newModel.message, fetched.message) } - - func test_create_combine() { - let newModel = TestModel(message: "Combine Test") - let exp = XCTestExpectation(description: "Test_Create_Combine") - newModel.write(for: .create, client: client) - .sink { completion in - switch completion { - case .finished: - exp.fulfill() - case .failure(let error): - XCTFail(error.localizedDescription) - } - } receiveValue: { _ in } - .store(in: &cancellables) - wait(for: [exp], timeout: 5) - } } diff --git a/Tests/StorageTests/StorageClientTests.swift b/Tests/EasyFirebaseStorageTests/StorageClientTests.swift similarity index 99% rename from Tests/StorageTests/StorageClientTests.swift rename to Tests/EasyFirebaseStorageTests/StorageClientTests.swift index 567f6fe..f2c3d10 100644 --- a/Tests/StorageTests/StorageClientTests.swift +++ b/Tests/EasyFirebaseStorageTests/StorageClientTests.swift @@ -1,13 +1,14 @@ // // StorageClientTests.swift -// +// // // Created by Fumiya Tanaka on 2022/01/16. // -import XCTest import Combine import FirebaseStorage +import XCTest + @testable import EasyFirebaseStorage @testable import TestCore