Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
FR: Add RemoteConfig Decoder and Encoder
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/feature_request.md
-->

### Feature proposal

* Firebase Component: RemoteConfig

```swift
struct Configuration: Codable {
        
    var signinMethods: [String]
}
```
## Decoder
Would be great to have a Decoder like `Firestore.Decoder` to do something like this:
```swift
let remoteConfig = RemoteConfig.remoteConfig()
let decoder = RemoteConfig.Decoder()
let configuration = try! decoder.decode(Configuration.self, from: remoteConfig)
```
`RemoteConfig.Decoder` should be able to decode 'RemoteConfigValue' as well:
```swift
let remoteConfigValue = RemoteConfig.remoteConfig().configValue(forKey: "authentication")
let decoder = RemoteConfig.Decoder()
let configuration = try! decoder.decode(Configuration.self, from: remoteConfigValue)
```
## Encoder
Would be great to have also an Encoder to set defaults:

```swift
let remoteConfig = RemoteConfig.remoteConfig()
let configuration = Configuration(signiInMethods: ["email"])
let encoder = RemoteConfig.Encoder()
let defaults = try! encoder.encode(configuration)
remoteConfig.setDefaults(defaults)
```
or better, like for Firestore `ref.setValue(from: Encodable)`
```swift
let remoteConfig = RemoteConfig.remoteConfig()
let configuration = Configuration(signiInMethods: ["email"])
try! remoteConfig.setDefaults(from: configuration)
```


Discussion / hints:
This quickstart sequence should transform to a single line with Codable - https://github.com/firebase/quickstart-ios/blob/6c420f893cea50d319931451d9fe110c534f9fcf/config/ConfigExample/RemoteConfigViewController.swift#L128-L151
#9084 now has an initial implementation of the 2nd(RCValue JSON decoding) and 3rd(setDefaults from Encodable) requests.

I'm not sure about the first one. It seems that RC does not have any APIs to get the full configuration, but I'm not sure why that is.  So I'm not sure that we should add only a Codable version to do that.

I suppose a workaround is for an app to do everything via a single JSON entry?

@karenyz or anyone else: Any thoughts/guidance on the above questions?
I talked with the RC team and it turns out that Android already has an API that returns the full config as a dictionary. See https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#getAll%28%29

So it looks like we should implement a `getAll` for Apple and then we can implement the third Codable API requested here on top of it.

Fix patch:
diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml
index 494023e4b2e..3e5ab820b40 100644
--- a/.github/workflows/remoteconfig.yml
+++ b/.github/workflows/remoteconfig.yml
@@ -32,18 +32,17 @@ jobs:
       run: scripts/setup_bundler.sh
     - name: Install Secret GoogleService-Info.plist
       run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/RemoteConfigSwiftAPI/GoogleService-Info.plist.gpg \
-          FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret"
+          FirebaseRemoteConfigSwift/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret"
     - name: Generate Access Token for RemoteConfigConsoleAPI in IntegrationTests
       if: matrix.target == 'iOS'
       run: ([ -z $plist_secret ] || scripts/generate_access_token.sh "$plist_secret" scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg
-          FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json)
-    - name: BuildAndUnitTest # can be replaced with pod lib lint with CocoaPods 1.10
-      run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig ${{ matrix.target }} unit
+          FirebaseRemoteConfigSwift/Tests/AccessToken.json)
     - name: Fake Console API Tests
       run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig iOS fakeconsole
     - name: IntegrationTest
       if: matrix.target == 'iOS'
-      run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig iOS integration)
+      # No retry to avoid exhausting AccessToken quota.
+      run: ([ -z $plist_secret ] || scripts/build.sh RemoteConfig iOS integration)
 
   pod-lib-lint:
     # Don't run on private repo unless it is a PR.
@@ -53,13 +52,14 @@ jobs:
     strategy:
       matrix:
         target: [ios, tvos, macos, watchos]
+        podspec: [FirebaseRemoteConfig.podspec, FirebaseRemoteConfigSwift.podspec --skip-tests]
     steps:
     - uses: actions/checkout@v2
     - name: Setup Bundler
       run: scripts/setup_bundler.sh
     - name: Build and test
       run: |
-       scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --skip-tests --platforms=${{ matrix.target }}
+       scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb ${{ matrix.podspec }} --platforms=${{ matrix.target }}
 
   spm:
     # Don't run on private repo unless it is a PR.
@@ -74,6 +74,8 @@ jobs:
       run: scripts/setup_spm_tests.sh
     - name: iOS Unit Tests
       run: scripts/third_party/travis/retry.sh ./scripts/build.sh RemoteConfigUnit iOS spm
+    - name: Fake Console tests
+      run: scripts/third_party/travis/retry.sh ./scripts/build.sh RemoteConfigFakeConsole iOS spm
 
   spm-cron:
     # Don't run on private repo.
diff --git a/.gitignore b/.gitignore
index d4d23b600d3..3e13cbb630b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,7 @@ FirebaseAuth/Tests/Sample/SwiftApiTests/Credentials.swift
 
 FirebaseDatabase/Tests/Resources/GoogleService-Info.plist
 
-FirebaseRemoteConfig/Tests/SwiftAPI/Resources/GoogleService-Info.plist
-FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/GoogleService-Info.plist
-FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/SecondApp-GoogleService-Info.plist
+FirebaseRemoteConfig/Tests/Sample/GoogleService-Info.plist
 
 # FirebaseStorage integration tests GoogleService-Info.plist
 FirebaseStorage/Tests/Integration/Resources/GoogleService-Info.plist
diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec
index 6dc476dc5cf..d4bb5657ead 100644
--- a/FirebaseRemoteConfig.podspec
+++ b/FirebaseRemoteConfig.podspec
@@ -52,6 +52,11 @@ app update.
 
   s.test_spec 'unit' do |unit_tests|
     unit_tests.scheme = { :code_coverage => true }
+    unit_tests.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target,
+      :tvos => tvos_deployment_target
+    }
     # TODO(dmandar) - Update or delete the commented files.
     unit_tests.source_files =
         'FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m',
@@ -79,40 +84,4 @@ app update.
     unit_tests.requires_arc = true
   end
 
-  # Run Swift API tests on a real backend.
-  s.test_spec 'swift-api-tests' do |swift_api|
-    swift_api.scheme = { :code_coverage => true }
-    swift_api.platforms = {
-      :ios => ios_deployment_target,
-      :osx => osx_deployment_target,
-      :tvos => tvos_deployment_target
-    }
-    swift_api.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift',
-                             'FirebaseRemoteConfig/Tests/FakeUtils/*.[hm]',
-                             'FirebaseRemoteConfig/Tests/FakeUtils/*.swift'
-    swift_api.requires_app_host = true
-    swift_api.pod_target_xcconfig = {
-      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h'
-    }
-    swift_api.dependency 'OCMock'
-  end
-
-  # Run Swift API tests and tests requiring console changes on a Fake Console.
-  s.test_spec 'fake-console-tests' do |fake_console|
-    fake_console.scheme = { :code_coverage => true }
-    fake_console.platforms = {
-      :ios => ios_deployment_target,
-      :osx => osx_deployment_target,
-      :tvos => tvos_deployment_target
-    }
-    fake_console.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift',
-                                      'FirebaseRemoteConfig/Tests/FakeUtils/*.[hm]',
-                                      'FirebaseRemoteConfig/Tests/FakeUtils/*.swift',
-                                      'FirebaseRemoteConfig/Tests/FakeConsole/*.swift'
-    fake_console.requires_app_host = true
-    fake_console.pod_target_xcconfig = {
-      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h'
-    }
-    fake_console.dependency 'OCMock'
-  end
 end
diff --git a/FirebaseRemoteConfig/Tests/FakeUtils/GoogleService-Info.plist b/FirebaseRemoteConfig/Tests/Sample/GoogleService-Info.plist
similarity index 100%
rename from FirebaseRemoteConfig/Tests/FakeUtils/GoogleService-Info.plist
rename to FirebaseRemoteConfig/Tests/Sample/GoogleService-Info.plist
diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj
index de938307e72..7a0067afc18 100644
--- a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj
+++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj
@@ -17,7 +17,7 @@
 		5BE818FB23271579004DE6BA /* RemoteConfigSampleAppUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BE818FA23271579004DE6BA /* RemoteConfigSampleAppUITests.m */; };
 		D5C72761A16C64F0DB522FE9 /* Pods_RemoteConfigSampleAppUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1244CD4C99B6BF70752BE1B6 /* Pods_RemoteConfigSampleAppUITests.framework */; };
 		DE71200824C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE71200724C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist */; };
-		DE71200A24C9C1F100C28FE2 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE71200924C9C1F000C28FE2 /* GoogleService-Info.plist */; };
+		DEB1E8792785018F0054A548 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEB1E8782785018F0054A548 /* GoogleService-Info.plist */; };
 		F149334D7027F03ADF9FF9FC /* Pods_RemoteConfigSampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D431F3C235B420372A7847C /* Pods_RemoteConfigSampleApp.framework */; };
 /* End PBXBuildFile section */
 
@@ -53,7 +53,7 @@
 		5CC24231A56D124F27682F1C /* Pods-RemoteConfigSampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleApp.debug.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp.debug.xcconfig"; sourceTree = "<group>"; };
 		DA0A0BDC31A76C0D70249412 /* Pods-RemoteConfigSampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleApp.release.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp.release.xcconfig"; sourceTree = "<group>"; };
 		DE71200724C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "SecondApp-GoogleService-Info.plist"; path = "../../Unit/SecondApp-GoogleService-Info.plist"; sourceTree = "<group>"; };
-		DE71200924C9C1F000C28FE2 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../FakeUtils/GoogleService-Info.plist"; sourceTree = "<group>"; };
+		DEB1E8782785018F0054A548 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; };
 		DEB267EAC843EF4BD455C9EC /* Pods-RemoteConfigSampleAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleAppUITests.debug.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleAppUITests/Pods-RemoteConfigSampleAppUITests.debug.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -120,7 +120,7 @@
 		5BE818E023271577004DE6BA /* RemoteConfigSampleApp */ = {
 			isa = PBXGroup;
 			children = (
-				DE71200924C9C1F000C28FE2 /* GoogleService-Info.plist */,
+				DEB1E8782785018F0054A548 /* GoogleService-Info.plist */,
 				DE71200724C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist */,
 				5B1A9B12232846F3001809E9 /* FRCLog.h */,
 				5B1A9B13232846F3001809E9 /* FRCLog.m */,
@@ -231,7 +231,7 @@
 			files = (
 				5BE818EB23271579004DE6BA /* Assets.xcassets in Resources */,
 				DE71200824C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist in Resources */,
-				DE71200A24C9C1F100C28FE2 /* GoogleService-Info.plist in Resources */,
+				DEB1E8792785018F0054A548 /* GoogleService-Info.plist in Resources */,
 				5B1A9B1723284735001809E9 /* LaunchScreen.xib in Resources */,
 				5BE818E923271577004DE6BA /* Main.storyboard in Resources */,
 			);
diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/APITestBase.swift b/FirebaseRemoteConfig/Tests/SwiftAPI/APITestBase.swift
deleted file mode 100644
index 13e6f3b5c3b..00000000000
--- a/FirebaseRemoteConfig/Tests/SwiftAPI/APITestBase.swift
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import FirebaseCore
-@testable import FirebaseRemoteConfig
-
-import XCTest
-
-class APITestBase: XCTestCase {
-  static var useFakeConfig: Bool!
-  static var mockedFetch: Bool!
-  var app: FirebaseApp!
-  var config: RemoteConfig!
-  var fakeConsole: FakeConsole!
-
-  override class func setUp() {
-    if FirebaseApp.app() == nil {
-      FirebaseApp.configure()
-      APITests.mockedFetch = false
-    }
-    useFakeConfig = FirebaseApp.app()!.options.projectID == "FakeProject"
-  }
-
-  override func setUp() {
-    super.setUp()
-    app = FirebaseApp.app()
-    config = RemoteConfig.remoteConfig(app: app!)
-    let settings = RemoteConfigSettings()
-    settings.minimumFetchInterval = 0
-    config.configSettings = settings
-    if APITests.useFakeConfig {
-      if !APITests.mockedFetch {
-        APITests.mockedFetch = true
-        config.configFetch = FetchMocks.mockFetch(config.configFetch)
-      }
-      fakeConsole = FakeConsole()
-      config.configFetch.fetchSession = URLSessionMock(with: fakeConsole)
-    }
-
-    // Uncomment for verbose debug logging.
-    // FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
-  }
-
-  override func tearDown() {
-    if APITests.useFakeConfig {
-      fakeConsole.empty()
-    }
-    app = nil
-    config = nil
-    super.tearDown()
-  }
-}
diff --git a/FirebaseRemoteConfigSwift.podspec b/FirebaseRemoteConfigSwift.podspec
new file mode 100644
index 00000000000..d46517b3635
--- /dev/null
+++ b/FirebaseRemoteConfigSwift.podspec
@@ -0,0 +1,85 @@
+Pod::Spec.new do |s|
+  s.name                    = 'FirebaseRemoteConfigSwift'
+  s.version                 = '8.11.0-beta'
+  s.summary                 = 'Swift Extensions for Firebase Remote Config'
+
+  s.description      = <<-DESC
+Firebase Remote Config is a cloud service that lets you change the
+appearance and behavior of your app without requiring users to download an
+app update.
+                       DESC
+
+
+  s.homepage                = 'https://developers.google.com/'
+  s.license                 = { :type => 'Apache', :file => 'LICENSE' }
+  s.authors                 = 'Google, Inc.'
+
+  s.source                  = {
+    :git => 'https://github.com/Firebase/firebase-ios-sdk.git',
+    :tag => 'CocoaPods-' + s.version.to_s
+  }
+
+  s.swift_version           = '5.0'
+
+  ios_deployment_target = '10.0'
+  osx_deployment_target = '10.12'
+  tvos_deployment_target = '10.0'
+  watchos_deployment_target = '6.0'
+
+  s.ios.deployment_target = ios_deployment_target
+  s.osx.deployment_target = osx_deployment_target
+  s.tvos.deployment_target = tvos_deployment_target
+  s.watchos.deployment_target = watchos_deployment_target
+
+  s.cocoapods_version       = '>= 1.4.0'
+  s.prefix_header_file      = false
+
+  s.source_files = [
+    'FirebaseRemoteConfigSwift/Sources/*.swift',
+  ]
+
+  s.dependency 'FirebaseRemoteConfig', '~> 8.11'
+  s.dependency 'FirebaseSharedSwift', '~> 8.11-beta'
+
+  # Run Swift API tests on a real backend.
+  s.test_spec 'swift-api-tests' do |swift_api|
+    swift_api.scheme = { :code_coverage => true }
+    swift_api.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target,
+      :tvos => tvos_deployment_target
+    }
+    swift_api.source_files = ['FirebaseRemoteConfigSwift/Tests/SwiftAPI/*.swift',
+                              'FirebaseRemoteConfigSwift/Tests/FakeUtils/*.swift',
+                              'FirebaseRemoteConfigSwift/Tests/ObjC/*.[hm]',
+                             ]
+    swift_api.requires_app_host = true
+    swift_api.pod_target_xcconfig = {
+      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h',
+      'OTHER_SWIFT_FLAGS' => '$(inherited) -D USE_REAL_CONSOLE',
+      'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
+    }
+    swift_api.dependency 'OCMock'
+  end
+
+  # Run Swift API tests and tests requiring console changes on a Fake Console.
+  s.test_spec 'fake-console-tests' do |fake_console|
+    fake_console.scheme = { :code_coverage => true }
+    fake_console.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target,
+      :tvos => tvos_deployment_target
+    }
+    fake_console.source_files = ['FirebaseRemoteConfigSwift/Tests/SwiftAPI/*.swift',
+                                 'FirebaseRemoteConfigSwift/Tests/FakeUtils/*.swift',
+                                 'FirebaseRemoteConfigSwift/Tests/FakeConsole/*.swift',
+                                 'FirebaseRemoteConfigSwift/Tests/ObjC/*.[hm]',
+                                ]
+    fake_console.requires_app_host = true
+    fake_console.pod_target_xcconfig = {
+      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h',
+      'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
+    }
+    fake_console.dependency 'OCMock'
+  end
+end
diff --git a/FirebaseRemoteConfigSwift/CHANGELOG.md b/FirebaseRemoteConfigSwift/CHANGELOG.md
new file mode 100644
index 00000000000..c5dcb9f5000
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/CHANGELOG.md
@@ -0,0 +1,5 @@
+# 8.12.0-beta
+- Initial public beta release with Codable support. See example usage in
+  https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/Codable.swift
+  and
+  https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/Value.swift. (#6883)
diff --git a/FirebaseRemoteConfigSwift/Sources/Codable.swift b/FirebaseRemoteConfigSwift/Sources/Codable.swift
new file mode 100644
index 00000000000..193ebf17fe2
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Sources/Codable.swift
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import FirebaseRemoteConfig
+import FirebaseSharedSwift
+
+public enum RemoteConfigValueCodableError: Error {
+  case unsupportedType(String)
+}
+
+public extension RemoteConfigValue {
+  /// Extracts a RemoteConfigValue JSON-encoded object and decodes it to the requested type.
+  ///
+  /// - Parameter asType: The type to decode the JSON-object to
+  func decoded<Value: Decodable>(asType: Value.Type = Value.self) throws -> Value {
+    if asType == Date.self {
+      throw RemoteConfigValueCodableError
+        .unsupportedType("Date type is not currently supported for " +
+          " Remote Config Value decoding. Please file a feature request")
+    }
+    return try FirebaseDataDecoder()
+      .decode(Value.self, from: FirebaseRemoteConfigValueDecoderHelper(value: self))
+  }
+}
+
+public enum RemoteConfigCodableError: Error {
+  case invalidSetDefaultsInput(String)
+}
+
+public extension RemoteConfig {
+  /// Decodes a struct from the respective Remote Config values.
+  ///
+  /// - Parameter asType: The type to decode to.
+  func decoded<Value: Decodable>(asType: Value.Type = Value.self) throws -> Value {
+    let keys = allKeys(from: RemoteConfigSource.default) + allKeys(from: RemoteConfigSource.remote)
+    let config = keys.reduce(into: [String: FirebaseRemoteConfigValueDecoderHelper]()) {
+      $0[$1] = FirebaseRemoteConfigValueDecoderHelper(value: configValue(forKey: $1))
+    }
+    return try FirebaseDataDecoder().decode(Value.self, from: config)
+  }
+
+  /// Sets config defaults from an encodable struct.
+  ///
+  /// - Parameter value: The object to use to set the defaults.
+  func setDefaults<Value: Encodable>(from value: Value) throws {
+    guard let encoded = try FirebaseDataEncoder().encode(value) as? [String: NSObject] else {
+      throw RemoteConfigCodableError.invalidSetDefaultsInput(
+        "The setDefaults input: \(value), must be a Struct that encodes to a Dictionary"
+      )
+    }
+    setDefaults(encoded)
+  }
+}
diff --git a/FirebaseRemoteConfigSwift/Sources/FirebaseRemoteConfigValueDecoderHelper.swift b/FirebaseRemoteConfigSwift/Sources/FirebaseRemoteConfigValueDecoderHelper.swift
new file mode 100644
index 00000000000..44d892ad4c5
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Sources/FirebaseRemoteConfigValueDecoderHelper.swift
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import FirebaseRemoteConfig
+import FirebaseSharedSwift
+
+/// Implement the FirebaseRemoteConfigValueDecoding protocol for the shared Firebase decoder to
+/// decode Remote Config Values. It returns the four different kinds of values from
+/// a RemoteConfigValue object.
+struct FirebaseRemoteConfigValueDecoderHelper: FirebaseRemoteConfigValueDecoding {
+  let value: RemoteConfigValue
+
+  func numberValue() -> NSNumber {
+    return value.numberValue
+  }
+
+  func boolValue() -> Bool {
+    return value.boolValue
+  }
+
+  func stringValue() -> String {
+    return value.stringValue ?? ""
+  }
+
+  func dataValue() -> Data {
+    return value.dataValue
+  }
+
+  func jsonValue() -> [String: AnyHashable]? {
+    guard let value = value.jsonValue as? [String: AnyHashable] else {
+      // nil is the historical behavior for failing to extract JSON.
+      return nil
+    }
+    return value
+  }
+}
diff --git a/FirebaseRemoteConfigSwift/Sources/Value.swift b/FirebaseRemoteConfigSwift/Sources/Value.swift
new file mode 100644
index 00000000000..0e81056511d
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Sources/Value.swift
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import FirebaseRemoteConfig
+
+/// Implements subscript overloads to enable Remote Config values to be accessed
+/// in a type-safe way directly from the current config.
+public extension RemoteConfig {
+  /// Return a typed RemoteConfigValue for a key.
+  /// - Parameter key: A Remote Config key.
+  /// - Returns: A typed RemoteConfigValue.
+  subscript<T: Decodable>(decodedValue key: String) -> T? {
+    return try? configValue(forKey: key).decoded()
+  }
+
+  /// Return a Dictionary for a RemoteConfig JSON key.
+  /// - Parameter key: A Remote Config key.
+  /// - Returns: A Dictionary representing a RemoteConfig JSON value.
+  subscript(jsonValue key: String) -> [String: AnyHashable]? {
+    guard let value = configValue(forKey: key).jsonValue as? [String: AnyHashable] else {
+      // nil is the historical behavior for failing to extract JSON.
+      return nil
+    }
+    return value
+  }
+}
diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json b/FirebaseRemoteConfigSwift/Tests/AccessToken.json
similarity index 100%
rename from FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json
rename to FirebaseRemoteConfigSwift/Tests/AccessToken.json
diff --git a/FirebaseRemoteConfig/Tests/FakeConsole/FakeConsoleTests.swift b/FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift
similarity index 100%
rename from FirebaseRemoteConfig/Tests/FakeConsole/FakeConsoleTests.swift
rename to FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift
diff --git a/FirebaseRemoteConfig/Tests/FakeUtils/FakeConsole.swift b/FirebaseRemoteConfigSwift/Tests/FakeUtils/FakeConsole.swift
similarity index 95%
rename from FirebaseRemoteConfig/Tests/FakeUtils/FakeConsole.swift
rename to FirebaseRemoteConfigSwift/Tests/FakeUtils/FakeConsole.swift
index 34285c00e76..17795a56edc 100644
--- a/FirebaseRemoteConfig/Tests/FakeUtils/FakeConsole.swift
+++ b/FirebaseRemoteConfigSwift/Tests/FakeUtils/FakeConsole.swift
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#if SWIFT_PACKAGE
+  import RemoteConfigFakeConsoleObjC
+#endif
+
 class FakeConsole {
   var config = [String: AnyHashable]()
   private var last = [String: AnyHashable]()
diff --git a/FirebaseRemoteConfig/Tests/FakeUtils/URLSessionPartialMock.swift b/FirebaseRemoteConfigSwift/Tests/FakeUtils/URLSessionPartialMock.swift
similarity index 97%
rename from FirebaseRemoteConfig/Tests/FakeUtils/URLSessionPartialMock.swift
rename to FirebaseRemoteConfigSwift/Tests/FakeUtils/URLSessionPartialMock.swift
index 68407005d96..93a8047f7d0 100644
--- a/FirebaseRemoteConfig/Tests/FakeUtils/URLSessionPartialMock.swift
+++ b/FirebaseRemoteConfigSwift/Tests/FakeUtils/URLSessionPartialMock.swift
@@ -14,6 +14,10 @@
 
 import Foundation
 
+#if SWIFT_PACKAGE
+  import RemoteConfigFakeConsoleObjC
+#endif
+
 // Create a partial mock by subclassing the URLSessionDataTask.
 class URLSessionDataTaskMock: URLSessionDataTask {
   private let closure: () -> Void
diff --git a/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h b/FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h
similarity index 92%
rename from FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h
rename to FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h
index 44253f8db0c..0522aff9de9 100644
--- a/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h
+++ b/FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h
@@ -14,4 +14,4 @@
 
 #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
-#import "FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h"
+#import "FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h"
diff --git a/FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h b/FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h
similarity index 100%
rename from FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h
rename to FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h
diff --git a/FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.m b/FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.m
similarity index 95%
rename from FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.m
rename to FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.m
index 368044805ae..01c111cde12 100644
--- a/FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.m
+++ b/FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.m
@@ -15,7 +15,7 @@
 #import <OCMock/OCMock.h>
 
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
-#import "FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h"
+#import "FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h"
 
 @interface RCNConfigFetch (ExposedForTest)
 - (void)refreshInstallationsTokenWithCompletionHandler:
diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md b/FirebaseRemoteConfigSwift/Tests/README.md
similarity index 75%
rename from FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md
rename to FirebaseRemoteConfigSwift/Tests/README.md
index a08e32a04c3..f063716e688 100644
--- a/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md
+++ b/FirebaseRemoteConfigSwift/Tests/README.md
@@ -1,10 +1,16 @@
-# Remote Config Console API
+# Remote Config Swift Tests
 
-[`RemoteConfigConsole.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift)
+Currently the Remote Config tests run in two configurations:
+1. Fake Console - mocks the console to run tests with a dummy GoogleService-Info.plist.
+2. Remote Config Console API - relies on generating an access token to use a real Firebase project.
+
+## Remote Config Console API
+
+[`RemoteConfigConsole.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift)
 provides a simple API for interacting with an app's Remote Config on the
 Firebase console.
 
-## Local Development Setup
+### Local Development Setup
 1. Create a Firebase project on the Firebase Console and download
 the  `GoogleService-Info.plist`.
 2. Navigate to your project's settings. Click on the **Service accounts** tab and
@@ -12,7 +18,7 @@ then download a private key by clicking the blue button that says "Generate new
 Rename it `ServiceAccount.json`.
 3. Within the `firebase-ios-sdk`, run:
 ```bash
-./scripts/generate_access_token.sh local_dev PATH/TO/ServiceAccount.json FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json
+./scripts/generate_access_token.sh local_dev PATH/TO/ServiceAccount.json FirebaseRemoteConfigSwift/Tests/AccessToken.json
 ```
 4. Generate the `FirebaseRemoteConfig` project:
 ```bash
@@ -24,12 +30,12 @@ Xcode project.
 🚀 Everything is ready to go! Run the tests in the `swift-api-tests` target.
 
 
-## How it works
+### How it works
 
 While the `RemoteConfigConsole` API basically just makes simple network calls,
 we need to include an `access token` so our requests do the proper "handshake" with the Firebase console.
 
-### Firebase Service Account Private Key
+#### Firebase Service Account Private Key
 This private key is needed to create an access token with the valid parameters
 that authorizes our requests to programmatically make changes to remote config on the Firebase console.
 
@@ -37,7 +43,7 @@ The private key can be located on the Firebase console and navigate to your proj
 click on the **Service accounts** tab and then generate the private key by clicking
 the blue button that says "Generate new private key".
 
-### Create the Access Token
+#### Create the Access Token
 We use Google's [Auth Library for Swift](https://github.com/googleapis/google-auth-library-swift)
 to generate the access token. There are a few example use cases provided. We use the
 [`TokenSource`](https://github.com/googleapis/google-auth-library-swift/blob/master/Sources/Examples/TokenSource/main.swift)
@@ -48,7 +54,7 @@ Firebase project's service account key is stored. This is set in the
 [`generate_access_token.sh`](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/generate_access_token.sh)
 script.
 
-### Remote Config API Tests
-There is a [section](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift#L210)
-of tests in [`APITests.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift)
+#### Remote Config API Tests
+There is a [section](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift#L210)
+of tests in [`APITests.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift)
 showcasing the  `RemoteConfigConsole` in action.
diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITestBase.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITestBase.swift
new file mode 100644
index 00000000000..e192fcfa77b
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITestBase.swift
@@ -0,0 +1,102 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import FirebaseCore
+import FirebaseRemoteConfig
+
+#if SWIFT_PACKAGE
+  import RemoteConfigFakeConsoleObjC
+#endif
+
+import XCTest
+
+class APITestBase: XCTestCase {
+  static var useFakeConfig: Bool!
+  static var mockedFetch: Bool!
+  var app: FirebaseApp!
+  var config: RemoteConfig!
+  var console: RemoteConfigConsole!
+  var fakeConsole: FakeConsole!
+
+  override class func setUp() {
+    if FirebaseApp.app() == nil {
+      #if USE_REAL_CONSOLE
+        useFakeConfig = false
+        FirebaseApp.configure()
+      #else
+        useFakeConfig = true
+        let options = FirebaseOptions(googleAppID: "1:123:ios:123abc",
+                                      gcmSenderID: "correct_gcm_sender_id")
+        options.apiKey = "A23456789012345678901234567890123456789"
+        options.projectID = "Fake Project"
+        FirebaseApp.configure(options: options)
+        APITests.mockedFetch = false
+      #endif
+    }
+  }
+
+  override func setUpWithError() throws {
+    try super.setUpWithError()
+    app = FirebaseApp.app()
+    config = RemoteConfig.remoteConfig(app: app!)
+    let settings = RemoteConfigSettings()
+    settings.minimumFetchInterval = 0
+    config.configSettings = settings
+
+    let jsonData = try JSONSerialization.data(
+      withJSONObject: Constants.jsonValue
+    )
+    guard let jsonValue = String(data: jsonData, encoding: .ascii) else {
+      fatalError("Failed to make json Value from jsonData")
+    }
+
+    if APITests.useFakeConfig {
+      if !APITests.mockedFetch {
+        APITests.mockedFetch = true
+        config.configFetch = FetchMocks.mockFetch(config.configFetch)
+      }
+      fakeConsole = FakeConsole()
+      config.configFetch.fetchSession = URLSessionMock(with: fakeConsole)
+
+      fakeConsole.config = [Constants.key1: Constants.value1,
+                            Constants.jsonKey: jsonValue,
+                            Constants.nonJsonKey: Constants.nonJsonValue,
+                            Constants.stringKey: Constants.stringValue,
+                            Constants.intKey: String(Constants.intValue),
+                            Constants.floatKey: String(Constants.floatValue),
+                            Constants.decimalKey: "\(Constants.decimalValue)",
+                            Constants.trueKey: String(true),
+                            Constants.falseKey: String(false),
+                            Constants.dataKey: String(decoding: Constants.dataValue, as: UTF8.self)]
+    } else {
+      console = RemoteConfigConsole()
+      console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi)
+    }
+
+    // Uncomment for verbose debug logging.
+    // FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
+  }
+
+  override func tearDown() {
+    if APITests.useFakeConfig {
+      fakeConsole.empty()
+    } else {
+      console.removeRemoteConfigValue(forKey: Constants.sith)
+      console.removeRemoteConfigValue(forKey: Constants.jedi)
+    }
+    app = nil
+    config = nil
+    super.tearDown()
+  }
+}
diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift
similarity index 91%
rename from FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift
rename to FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift
index 9b841c24a97..a9e3bba1b0e 100644
--- a/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift
+++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift
@@ -17,40 +17,7 @@ import FirebaseCore
 
 import XCTest
 
-/// String constants used for testing.
-private enum Constants {
-  static let key1 = "Key1"
-  static let jedi = "Jedi"
-  static let sith = "Sith_Lord"
-  static let value1 = "Value1"
-  static let obiwan = "Obi-Wan"
-  static let yoda = "Yoda"
-  static let darthSidious = "Darth Sidious"
-}
-
 class APITests: APITestBase {
-  var console: RemoteConfigConsole!
-
-  override func setUp() {
-    super.setUp()
-    if APITests.useFakeConfig {
-      fakeConsole.config = [Constants.key1: Constants.value1]
-    } else {
-      console = RemoteConfigConsole()
-      console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi)
-    }
-  }
-
-  override func tearDown() {
-    super.tearDown()
-
-    // If using RemoteConfigConsole, reset remote config values.
-    if !APITests.useFakeConfig {
-      console.removeRemoteConfigValue(forKey: Constants.sith)
-      console.removeRemoteConfigValue(forKey: Constants.jedi)
-    }
-  }
-
   func testFetchThenActivate() {
     let expectation = self.expectation(description: #function)
     config.fetch { status, error in
diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/AsyncAwaitTests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/AsyncAwaitTests.swift
similarity index 85%
rename from FirebaseRemoteConfig/Tests/SwiftAPI/AsyncAwaitTests.swift
rename to FirebaseRemoteConfigSwift/Tests/SwiftAPI/AsyncAwaitTests.swift
index 7c711523596..ccaa474170b 100644
--- a/FirebaseRemoteConfig/Tests/SwiftAPI/AsyncAwaitTests.swift
+++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/AsyncAwaitTests.swift
@@ -17,42 +17,9 @@ import FirebaseCore
 
 import XCTest
 
-/// String constants used for testing.
-private enum Constants {
-  static let key1 = "Key1"
-  static let jedi = "Jedi"
-  static let sith = "Sith_Lord"
-  static let value1 = "Value1"
-  static let obiwan = "Obi-Wan"
-  static let yoda = "Yoda"
-  static let darthSidious = "Darth Sidious"
-}
-
 #if compiler(>=5.5) && canImport(_Concurrency)
   @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
   class AsyncAwaitTests: APITestBase {
-    var console: RemoteConfigConsole!
-
-    override func setUp() {
-      super.setUp()
-      if APITests.useFakeConfig {
-        fakeConsole.config = [Constants.key1: Constants.value1]
-      } else {
-        console = RemoteConfigConsole()
-        console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi)
-      }
-    }
-
-    override func tearDown() {
-      super.tearDown()
-
-      // If using RemoteConfigConsole, reset remote config values.
-      if !APITests.useFakeConfig {
-        console.removeRemoteConfigValue(forKey: Constants.sith)
-        console.removeRemoteConfigValue(forKey: Constants.jedi)
-      }
-    }
-
     func testFetchThenActivate() async throws {
       let status = try await config.fetch()
       XCTAssertEqual(status, RemoteConfigFetchStatus.success)
@@ -73,6 +40,12 @@ private enum Constants {
       XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
     }
 
+    func testFetchAndActivateGenericValue() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
+    }
+
     // Contrast with testChangedActivateWillNotFlag in FakeConsole.swift.
     func testUnchangedActivateWillFlag() async throws {
       let status = try await config.fetch()
diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Codable.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Codable.swift
new file mode 100644
index 00000000000..67de76394ae
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Codable.swift
@@ -0,0 +1,174 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import FirebaseRemoteConfig
+import FirebaseRemoteConfigSwift
+
+import XCTest
+
+#if compiler(>=5.5) && canImport(_Concurrency)
+  @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
+  class CodableTests: APITestBase {
+    // MARK: - Test decoding Remote Config JSON values
+
+    // Contrast this test with the subsequent one to see the value of the Codable API.
+    func testFetchAndActivateWithoutCodable() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let dict = try XCTUnwrap(config[Constants.jsonKey].jsonValue as? [String: AnyHashable])
+      XCTAssertEqual(dict["recipeName"], "PB&J")
+      XCTAssertEqual(dict["ingredients"], ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(dict["cookTime"], 7)
+      XCTAssertEqual(
+        config[Constants.jsonKey].jsonValue as! [String: AnyHashable],
+        Constants.jsonValue
+      )
+    }
+
+    struct Recipe: Decodable {
+      var recipeName: String
+      var ingredients: [String]
+      var cookTime: Int
+    }
+
+    func testFetchAndActivateWithCodable() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let recipe = try XCTUnwrap(config[Constants.jsonKey].decoded(asType: Recipe.self))
+      XCTAssertEqual(recipe.recipeName, "PB&J")
+      XCTAssertEqual(recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(recipe.cookTime, 7)
+    }
+
+    func testFetchAndActivateWithCodableAlternativeAPI() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let recipe: Recipe = try XCTUnwrap(config[Constants.jsonKey].decoded())
+      XCTAssertEqual(recipe.recipeName, "PB&J")
+      XCTAssertEqual(recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(recipe.cookTime, 7)
+    }
+
+    func testFetchAndActivateWithCodableBadJson() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      do {
+        _ = try config[Constants.nonJsonKey].decoded(asType: Recipe.self)
+      } catch let DecodingError.typeMismatch(_, context) {
+        XCTAssertEqual(context.debugDescription,
+                       "Expected to decode Dictionary<String, Any> but found " +
+                         "FirebaseRemoteConfigValueDecoderHelper instead.")
+        return
+      }
+      XCTFail("Failed to catch trying to decode non-JSON key as JSON")
+    }
+
+    // MARK: - Test setting Remote Config defaults via an encodable struct
+
+    struct DataTestDefaults: Encodable {
+      var bool: Bool
+      var int: Int32
+      var long: Int64
+      var string: String
+    }
+
+    func testSetEncodeableDefaults() throws {
+      let data = DataTestDefaults(
+        bool: true,
+        int: 2,
+        long: 9_876_543_210,
+        string: "four"
+      )
+      try config.setDefaults(from: data)
+      let boolValue = try XCTUnwrap(config.defaultValue(forKey: "bool")).numberValue.boolValue
+      XCTAssertTrue(boolValue)
+      let intValue = try XCTUnwrap(config.defaultValue(forKey: "int")).numberValue.intValue
+      XCTAssertEqual(intValue, 2)
+      let longValue = try XCTUnwrap(config.defaultValue(forKey: "long")).numberValue.int64Value
+      XCTAssertEqual(longValue, 9_876_543_210)
+      let stringValue = try XCTUnwrap(config.defaultValue(forKey: "string")).stringValue
+      XCTAssertEqual(stringValue, "four")
+    }
+
+    func testSetEncodeableDefaultsInvalid() throws {
+      do {
+        _ = try config.setDefaults(from: 7)
+      } catch let RemoteConfigCodableError.invalidSetDefaultsInput(message) {
+        XCTAssertEqual(message,
+                       "The setDefaults input: 7, must be a Struct that encodes to a Dictionary")
+        return
+      }
+      XCTFail("Failed to catch trying to encode an invalid input to setDefaults.")
+    }
+
+    // MARK: - Test extracting config to an decodable struct.
+
+    struct MyConfig: Decodable {
+      var Recipe: Recipe
+      var notJSON: String
+      var myInt: Int
+      var myFloat: Float
+      var myDecimal: Decimal
+      var myTrue: Bool
+      var myData: Data
+    }
+
+    func testExtractConfig() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let myConfig: MyConfig = try config.decoded()
+      XCTAssertEqual(myConfig.notJSON, Constants.nonJsonValue)
+      XCTAssertEqual(myConfig.myInt, Constants.intValue)
+      XCTAssertEqual(myConfig.myTrue, true)
+      XCTAssertEqual(myConfig.myFloat, Constants.floatValue)
+      XCTAssertEqual(myConfig.myDecimal, Constants.decimalValue)
+      XCTAssertEqual(myConfig.myData, Constants.dataValue)
+      XCTAssertEqual(myConfig.Recipe.recipeName, "PB&J")
+      XCTAssertEqual(myConfig.Recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(myConfig.Recipe.cookTime, 7)
+    }
+
+    // Additional fields in config are ignored.
+    func testExtractConfigExtra() async throws {
+      guard APITests.useFakeConfig else { return }
+      fakeConsole.config["extra"] = "extra Value"
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let myConfig: MyConfig = try config.decoded()
+      XCTAssertEqual(myConfig.notJSON, Constants.nonJsonValue)
+      XCTAssertEqual(myConfig.Recipe.recipeName, "PB&J")
+      XCTAssertEqual(myConfig.Recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(myConfig.Recipe.cookTime, 7)
+    }
+
+    // Failure if requested field does not exist.
+    func testExtractConfigMissing() async throws {
+      struct MyConfig: Decodable {
+        var missing: String
+        var Recipe: String
+        var notJSON: String
+      }
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      do {
+        let _: MyConfig = try config.decoded()
+      } catch let DecodingError.keyNotFound(codingKey, context) {
+        XCTAssertEqual(codingKey.stringValue, "missing")
+        print(codingKey, context)
+        return
+      }
+      XCTFail("Failed to throw on missing field")
+    }
+  }
+#endif
diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Constants.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Constants.swift
new file mode 100644
index 00000000000..e9f435d8f03
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Constants.swift
@@ -0,0 +1,46 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// String constants used for testing.
+enum Constants {
+  static let key1 = "Key1"
+  static let jedi = "Jedi"
+  static let sith = "Sith_Lord"
+  static let value1 = "Value1"
+  static let obiwan = "Obi-Wan"
+  static let yoda = "Yoda"
+  static let darthSidious = "Darth Sidious"
+
+  static let stringKey = "myString"
+  static let stringValue = "string contents"
+  static let intKey = "myInt"
+  static let intValue: Int = 123
+  static let floatKey = "myFloat"
+  static let floatValue: Float = 42.75
+  static let doubleValue: Double = 42.75
+  static let decimalKey = "myDecimal"
+  static let decimalValue: Decimal = 375.785
+  static let trueKey = "myTrue"
+  static let falseKey = "myFalse"
+  static let dataKey = "myData"
+  static let dataValue: Data = "data".data(using: .utf8)!
+  static let jsonKey = "Recipe"
+  static let jsonValue: [String: AnyHashable] = ["recipeName": "PB&J",
+                                                 "ingredients": ["bread", "peanut butter", "jelly"],
+                                                 "cookTime": 7]
+  static let nonJsonKey = "notJSON"
+  static let nonJsonValue = "notJSON"
+}
diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift
similarity index 100%
rename from FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift
rename to FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift
diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Value.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Value.swift
new file mode 100644
index 00000000000..44c13f44dc7
--- /dev/null
+++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/Value.swift
@@ -0,0 +1,197 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import FirebaseRemoteConfig
+import FirebaseRemoteConfigSwift
+
+import XCTest
+
+#if compiler(>=5.5) && canImport(_Concurrency)
+  @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
+  class ValueTests: APITestBase {
+    func testFetchAndActivateAllTypes() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(config[Constants.stringKey].stringValue, Constants.stringValue)
+      XCTAssertEqual(config[Constants.intKey].numberValue.intValue, Constants.intValue)
+      XCTAssertEqual(config[Constants.intKey].numberValue.int8Value, Int8(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.int16Value, Int16(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.int32Value, Int32(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.int64Value, Int64(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uintValue, UInt(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint8Value, UInt8(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint16Value, UInt16(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint32Value, UInt32(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint64Value, UInt64(Constants.intValue))
+      XCTAssertEqual(
+        config[Constants.floatKey].numberValue.decimalValue,
+        Decimal(Constants.doubleValue)
+      )
+      XCTAssertEqual(config[Constants.floatKey].numberValue.floatValue, Constants.floatValue)
+      XCTAssertEqual(config[Constants.floatKey].numberValue.doubleValue, Constants.doubleValue)
+      XCTAssertEqual(config[Constants.trueKey].boolValue, true)
+      XCTAssertEqual(config[Constants.falseKey].boolValue, false)
+      XCTAssertEqual(
+        config[Constants.stringKey].dataValue,
+        Constants.stringValue.data(using: .utf8)
+      )
+      XCTAssertEqual(
+        config[Constants.jsonKey].jsonValue as! [String: AnyHashable],
+        Constants.jsonValue
+      )
+    }
+
+    func testStrongTypingViaSubscriptApi() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(config[decodedValue: Constants.stringKey], Constants.stringValue)
+      XCTAssertEqual(config[decodedValue: Constants.intKey], Constants.intValue)
+      XCTAssertEqual(config[decodedValue: Constants.floatKey], Constants.floatValue)
+      XCTAssertEqual(config[decodedValue: Constants.floatKey], Constants.doubleValue)
+      XCTAssertEqual(config[decodedValue: Constants.trueKey], true)
+      XCTAssertEqual(config[decodedValue: Constants.falseKey], false)
+      XCTAssertEqual(
+        config[decodedValue: Constants.stringKey],
+        Constants.stringValue.data(using: .utf8)
+      )
+      XCTAssertEqual(try XCTUnwrap(config[jsonValue: Constants.jsonKey]), Constants.jsonValue)
+    }
+
+    func testStrongTypingViaDecoder() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(
+        try config[Constants.stringKey].decoded(asType: String.self),
+        Constants.stringValue
+      )
+      XCTAssertEqual(try config[Constants.intKey].decoded(asType: Int.self), Constants.intValue)
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int8.self),
+        Int8(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int16.self),
+        Int16(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int32.self),
+        Int32(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int64.self),
+        Int64(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt.self),
+        UInt(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt8.self),
+        UInt8(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt16.self),
+        UInt16(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt32.self),
+        UInt32(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt64.self),
+        UInt64(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.floatKey].decoded(asType: Decimal.self),
+        Decimal(Constants.doubleValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.floatKey].decoded(asType: Float.self),
+        Constants.floatValue
+      )
+      XCTAssertEqual(
+        try config[Constants.floatKey].decoded(asType: Double.self),
+        Constants.doubleValue
+      )
+      XCTAssertEqual(try config[Constants.trueKey].decoded(asType: Bool.self), true)
+      XCTAssertEqual(try config[Constants.falseKey].decoded(asType: Bool.self), false)
+      XCTAssertEqual(
+        try config[Constants.stringKey].decoded(asType: Data.self),
+        Constants.stringValue.data(using: .utf8)
+      )
+    }
+
+    func testStrongTypingViaDecoderAlternateDecoderApi() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let myString: String = try config[Constants.stringKey].decoded()
+      XCTAssertEqual(myString, Constants.stringValue)
+      let myInt: Int = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt, Constants.intValue)
+      let myInt8: Int8 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt8, Int8(Constants.intValue))
+      let myInt16: Int16 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt16, Int16(Constants.intValue))
+      let myInt32: Int32 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt32, Int32(Constants.intValue))
+      let myInt64: Int64 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt64, Int64(Constants.intValue))
+      let myUInt: UInt = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt, UInt(Constants.intValue))
+      let myUInt8: UInt8 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt8, UInt8(Constants.intValue))
+      let myUInt16: UInt16 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt16, UInt16(Constants.intValue))
+      let myUInt32: UInt32 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt32, UInt32(Constants.intValue))
+      let myUInt64: UInt64 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt64, UInt64(Constants.intValue))
+      let myDecimal: Decimal = try config[Constants.floatKey].decoded()
+      XCTAssertEqual(myDecimal, Decimal(Constants.doubleValue))
+      let myFloat: Float = try config[Constants.floatKey].decoded()
+      XCTAssertEqual(myFloat, Constants.floatValue)
+      let myDouble: Double = try config[Constants.floatKey].decoded()
+      XCTAssertEqual(myDouble, Constants.doubleValue)
+      let myTrue: Bool = try config[Constants.trueKey].decoded()
+      XCTAssertEqual(myTrue, true)
+      let myFalse: Bool = try config[Constants.falseKey].decoded()
+      XCTAssertEqual(myFalse, false)
+      let myData: Data = try config[Constants.stringKey].decoded()
+      XCTAssertEqual(myData, Constants.stringValue.data(using: .utf8))
+    }
+
+    func testStringFails() {
+      XCTAssertEqual(config[decodedValue: "UndefinedKey"], "")
+    }
+
+    func testJSONFails() {
+      XCTAssertNil(config[jsonValue: "UndefinedKey"])
+      XCTAssertNil(config[jsonValue: Constants.stringKey])
+    }
+
+    func testDateDecodingNotYetSupported() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      do {
+        let _: Date = try config[Constants.stringKey].decoded()
+      } catch let RemoteConfigValueCodableError.unsupportedType(message) {
+        XCTAssertEqual(message,
+                       "Date type is not currently supported for  Remote Config Value decoding. " +
+                         "Please file a feature request")
+        return
+      }
+      XCTFail("Failed to throw unsupported Date error.")
+    }
+  }
+#endif
diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec
index d86aaa09327..dca3c7c4420 100644
--- a/FirebaseSharedSwift.podspec
+++ b/FirebaseSharedSwift.podspec
@@ -4,9 +4,11 @@ Pod::Spec.new do |s|
   s.summary                 = 'Shared Swift Extensions for Firebase'
 
   s.description      = <<-DESC
-This pod is for Firebase internal use and not supported for independent use.
+This pod provides capabilities like Codable support that is shared by multiple
+Firebase products. FirebaseSharedSwift is not supported for non-Firebase usage.
                        DESC
 
+
   s.homepage                = 'https://developers.google.com/'
   s.license                 = { :type => 'Apache', :file => 'FirebaseSharedSwift/LICENSE' }
   s.authors                 = 'Google, Inc.'
@@ -36,12 +38,13 @@ This pod is for Firebase internal use and not supported for independent use.
   ]
 
   s.test_spec 'unit' do |unit_tests|
-    unit_tests.scheme = { :code_coverage => true }
     unit_tests.platforms = {
       :ios => ios_deployment_target,
       :osx => osx_deployment_target,
       :tvos => tvos_deployment_target
     }
-    unit_tests.source_files = 'FirebaseSharedSwift/Tests/**/*.swift'
+    unit_tests.source_files = [
+      'FirebaseSharedSwift/Tests/**/*.swift',
+    ]
   end
 end
diff --git a/FirebaseSharedSwift/Sources/FirebaseRemoteConfigValueDecoding.swift b/FirebaseSharedSwift/Sources/FirebaseRemoteConfigValueDecoding.swift
new file mode 100644
index 00000000000..da6c4467842
--- /dev/null
+++ b/FirebaseSharedSwift/Sources/FirebaseRemoteConfigValueDecoding.swift
@@ -0,0 +1,24 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Conform to this protocol for the Firebase Decoder to extract values as from a RemoteConfigValue object.
+public protocol FirebaseRemoteConfigValueDecoding {
+  func numberValue() -> NSNumber
+  func boolValue() -> Bool
+  func stringValue() -> String
+  func dataValue() -> Data
+  func jsonValue() -> [String: AnyHashable]?
+}
diff --git a/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift
index c5e69ac6e89..03788f712a4 100644
--- a/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift
+++ b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift
@@ -1251,9 +1251,15 @@ fileprivate class __JSONDecoder : Decoder {
                                         DecodingError.Context(codingPath: self.codingPath,
                                                               debugDescription: "Cannot get keyed decoding container -- found null value instead."))
     }
-
-    guard let topContainer = self.storage.topContainer as? [String : Any] else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
+    var topContainer : [String : Any]
+    if let rcValue = self.storage.topContainer as? FirebaseRemoteConfigValueDecoding,
+       let top = rcValue.jsonValue() {
+      topContainer = top
+    } else {
+      guard let top = self.storage.topContainer as? [String : Any] else {
+        throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
+      }
+      topContainer = top
     }
 
     let container = _JSONKeyedDecodingContainer<Key>(referencing: self, wrapping: topContainer)
@@ -2118,6 +2124,9 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
     guard !(value is NSNull) else { return nil }
 
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.boolValue()
+    }
     if let number = value as? NSNumber {
       // TODO: Add a flag to coerce non-boolean numbers into Bools?
       if number === kCFBooleanTrue as NSNumber {
@@ -2136,13 +2145,25 @@ extension __JSONDecoder {
     throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
   }
 
-  fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
-    guard !(value is NSNull) else { return nil }
+  fileprivate func rcValNumberAdaptor(_ value: Any) -> Any {
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.numberValue()
+    }
+    return value
+  }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
+  fileprivate func getNumber(_ value: Any, as type: Any.Type) throws -> NSNumber {
+    let val = rcValNumberAdaptor(value)
+    guard let number = val as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
+      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: val)
     }
+    return number
+  }
 
+  fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
+    guard !(value is NSNull) else { return nil }
+
+    let number = try getNumber(value, as: type)
     let int = number.intValue
     guard NSNumber(value: int) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2154,10 +2175,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int8 = number.int8Value
     guard NSNumber(value: int8) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2169,10 +2187,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int16 = number.int16Value
     guard NSNumber(value: int16) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2184,10 +2199,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int32 = number.int32Value
     guard NSNumber(value: int32) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2199,10 +2211,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int64 = number.int64Value
     guard NSNumber(value: int64) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2214,10 +2223,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint = number.uintValue
     guard NSNumber(value: uint) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2229,10 +2235,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint8 = number.uint8Value
     guard NSNumber(value: uint8) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2244,10 +2247,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint16 = number.uint16Value
     guard NSNumber(value: uint16) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2259,10 +2259,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint32 = number.uint32Value
     guard NSNumber(value: uint32) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2274,10 +2271,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint64 = number.uint64Value
     guard NSNumber(value: uint64) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2289,7 +2283,8 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Float.Type) throws -> Float? {
     guard !(value is NSNull) else { return nil }
 
-    if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
+    let val = rcValNumberAdaptor(value)
+    if let number = val as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
       // We are willing to return a Float by losing precision:
       // * If the original value was integral,
       //   * and the integral value was > Float.greatestFiniteMagnitude, we will fail
@@ -2318,7 +2313,7 @@ extension __JSONDecoder {
        overflow = true
        */
 
-    } else if let string = value as? String,
+    } else if let string = val as? String,
               case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy {
       if string == posInfString {
         return Float.infinity
@@ -2335,7 +2330,8 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Double.Type) throws -> Double? {
     guard !(value is NSNull) else { return nil }
 
-    if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
+    let val = rcValNumberAdaptor(value)
+    if let number = val as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
       // We are always willing to return the number as a Double:
       // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double
       // * If it was a Float or Double, you will get back the precise value
@@ -2353,7 +2349,7 @@ extension __JSONDecoder {
        overflow = true
        */
 
-    } else if let string = value as? String,
+    } else if let string = val as? String,
               case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy {
       if string == posInfString {
         return Double.infinity
@@ -2370,6 +2366,9 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: String.Type) throws -> String? {
     guard !(value is NSNull) else { return nil }
 
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.stringValue()
+    }
     guard let string = value as? String else {
       throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
     }
@@ -2424,6 +2423,10 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Data.Type) throws -> Data? {
     guard !(value is NSNull) else { return nil }
 
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.dataValue()
+    }
+
     switch self.options.dataDecodingStrategy {
     case .deferredToData:
       self.storage.push(container: value)
@@ -2451,11 +2454,13 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Decimal.Type) throws -> Decimal? {
     guard !(value is NSNull) else { return nil }
 
+    let val = rcValNumberAdaptor(value)
+
     // Attempt to bridge from NSDecimalNumber.
-    if let decimal = value as? Decimal {
+    if let decimal = val as? Decimal {
       return decimal
     } else {
-      let doubleValue = try self.unbox(value, as: Double.self)!
+      let doubleValue = try self.unbox(val, as: Double.self)!
       return Decimal(doubleValue)
     }
   }
diff --git a/Package.swift b/Package.swift
index e08c35942a0..d6905ec2178 100644
--- a/Package.swift
+++ b/Package.swift
@@ -123,6 +123,10 @@ let package = Package(
       name: "FirebaseRemoteConfig",
       targets: ["FirebaseRemoteConfig"]
     ),
+    .library(
+      name: "FirebaseRemoteConfigSwift-Beta",
+      targets: ["FirebaseRemoteConfigSwift"]
+    ),
     .library(
       name: "FirebaseStorage",
       targets: ["FirebaseStorage"]
@@ -938,6 +942,8 @@ let package = Package(
       ]
     ),
 
+    // MARK: - Firebase Remote Config
+
     .target(
       name: "FirebaseRemoteConfig",
       dependencies: [
@@ -973,6 +979,40 @@ let package = Package(
         .headerSearchPath("../../.."),
       ]
     ),
+    .target(
+      name: "FirebaseRemoteConfigSwift",
+      dependencies: [
+        "FirebaseRemoteConfig",
+        "FirebaseSharedSwift",
+      ],
+      path: "FirebaseRemoteConfigSwift/Sources"
+    ),
+    .testTarget(
+      name: "RemoteConfigFakeConsole",
+      dependencies: ["FirebaseRemoteConfigSwift",
+                     "RemoteConfigFakeConsoleObjC"],
+      path: "FirebaseRemoteConfigSwift/Tests",
+      exclude: [
+        "AccessToken.json",
+        "README.md",
+        "ObjC/",
+      ],
+      cSettings: [
+        .headerSearchPath("../../"),
+      ]
+    ),
+    .target(
+      name: "RemoteConfigFakeConsoleObjC",
+      dependencies: ["OCMock"],
+      path: "FirebaseRemoteConfigSwift/Tests/ObjC",
+      publicHeadersPath: ".",
+      cSettings: [
+        .headerSearchPath("../../../"),
+      ]
+    ),
+
+    // MARK: - Firebase Storage
+
     .target(
       name: "FirebaseStorage",
       dependencies: [
diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift
index b0f0627c577..df02083b964 100755
--- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift
+++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift
@@ -23,6 +23,7 @@ import Foundation
 public let shared = Manifest(
   version: "8.11.0",
   pods: [
+    Pod("FirebaseSharedSwift"),
     Pod("FirebaseCoreDiagnostics", zip: true),
     Pod("FirebaseCore", zip: true),
     Pod("FirebaseInstallations", zip: true),
@@ -32,7 +33,7 @@ public let shared = Manifest(
     Pod("FirebaseABTesting", zip: true),
     Pod("FirebaseAppCheck", isBeta: true, zip: true),
     Pod("FirebaseRemoteConfig", zip: true),
-    Pod("FirebaseSharedSwift"),
+    Pod("FirebaseRemoteConfigSwift", isBeta: true),
     Pod("FirebaseAppDistribution", isBeta: true, platforms: ["ios"], zip: true),
     Pod("FirebaseAuth", zip: true),
     Pod("FirebaseCrashlytics", zip: true),
diff --git a/SharedTestUtilities/FIROptionsMock.m b/SharedTestUtilities/FIROptionsMock.m
index 5b809ddcef8..304382d9b47 100644
--- a/SharedTestUtilities/FIROptionsMock.m
+++ b/SharedTestUtilities/FIROptionsMock.m
@@ -18,7 +18,8 @@
 #import "SharedTestUtilities/FIROptionsMock.h"
 
 NSString *const kAndroidClientID = @"correct_android_client_id";
-NSString *const kAPIKey = @"correct_api_key";
+// FIS requires 39 characters starting with A.
+NSString *const kAPIKey = @"A23456789012345678901234567890123456789";
 NSString *const kCustomizedAPIKey = @"customized_api_key";
 NSString *const kClientID = @"correct_client_id";
 NSString *const kTrackingID = @"correct_tracking_id";
@@ -31,7 +32,7 @@
 NSString *const kNewDeepLinkURLScheme = @"newdeeplinkurlfortest";
 
 NSString *const kBundleID = @"com.google.FirebaseSDKTests";
-NSString *const kProjectID = @"abc-xyz-123";
+NSString *const kProjectID = @"Mocked Project ID";
 
 @interface FIROptionsMock ()
 
diff --git a/SwiftDashboard.md b/SwiftDashboard.md
index d873d47f358..3ccd7c61000 100644
--- a/SwiftDashboard.md
+++ b/SwiftDashboard.md
@@ -9,11 +9,11 @@ tasks for additional Swift improvements.
 
 |                       | AB  | An     | ApC    | ApD    | Aut    | Cor    | Crs    | DB     | Fst    | Fn     | IAM    | Ins    | Msg    | MLM    | Prf    | RC     |    Str |
 |   :---                | :--- | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: |
-| **Swift Library**     | ❌   |   ✔   | ❌     |❌     | ❌     | ❌     | ❌      |  ✔     |  ✔    | 1      |  ✔     | ❌    | ❌     | ✔      | ❌     | ❌    | ✔     |
+| **Swift Library**     | ❌   |   ✔   | ❌     |❌     | ❌     | ❌     | ❌      |  ✔     |  ✔    | 1      |  ✔     | ❌    | ❌     | ✔      | ❌     |  ✔    | ✔     |
 | **API Tests**         | ❌   |  ❌    |  ✔    |❌     | ❌     | ✔       | ❌     | 3      | 2     |  ✔     | 2      | ✔      | 2     | 2      | ❌    |  ✔     | ✔    |
-| **async/await**       | ❌   |  n/a   |  ✔    |❌     | ❌     |  ✔      | ❌     | 3     | ✔     |  ✔     | ❌     | ❌    | ✔     | ❌    | ❌     |  ✔    | ✔    |
-| **Swift Errors**      |  ❌  |  ❌    | ❌    |❌     | 4      | ❌     | ❌     | ❌     | ❌    | ❌     | ❌     | ❌    | ❌     | ✔      | ❌     | ❌   | 5   |
-| **Codable**           | n/a  | n/a     | n/a   |n/a    | n/a     | n/a    |n/a     |  ✔     |  ✔     | 1      | n/a     | n/a   | ❌     | n/a    | n/a    |  ❌  |n/a   |
+| **async/await**       | ❌   |  n/a   |  ✔    |❌     | ❌     |  ✔      | ❌     | 3     |  ✔     |  ✔     | ❌     | ❌    | ✔     | ❌    | ❌     |  ✔    | ✔    |
+| **Swift Errors**      |  ❌  |  ❌    | ❌    |❌     | 4      | ❌     | ❌     | ❌     | ❌    | ❌     | ❌     | ❌    | ❌     | ✔      | ❌     |  ✔   | 5   |
+| **Codable**           | n/a  | n/a     | n/a   |n/a    | n/a     | n/a    |n/a     |  ✔     |  ✔     | 1      | n/a     | n/a   | ❌     | n/a    | n/a    |   ✔  |n/a   |
 | **SwiftUI Lifecycle** | n/a  |  ❌    | n/a    |❌     | ❌     | n/a    |n/a     | n/a    | n/a    | n/a     | n/a    | n/a   | ❌     | n/a    | n/a    | n/a   |n/a  |
 | **SwiftUI Interop**   | ❌   |  ✔     | ❌     |❌    | ❌     | ❌     |❌      | ❌     | ❌    | ❌     | ✔      | ❌    | ❌     | ❌    | ❌     | ❌    |n/a  |
 | **Property Wrappers** |  ❌  |  ❌    | ❌    |❌     | ❌     | ❌     | ❌     | ❌     | 6     | ❌     | ❌     | ❌    | ❌     | ❌    | ❌     | ❌   |❌    |
diff --git a/scripts/build.sh b/scripts/build.sh
index 92d6dd2cfd5..7ad77f3e2cf 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -484,48 +484,33 @@ case "$product-$platform-$method" in
       test
     ;;
 
-  RemoteConfig-*-unit)
-    pod_gen FirebaseRemoteConfig.podspec --platforms="${gen_platform}"
-    RunXcodebuild \
-      -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \
-      -scheme "FirebaseRemoteConfig-Unit-unit" \
-      "${xcb_flags[@]}" \
-      build \
-      test
-    ;;
-
   RemoteConfig-*-fakeconsole)
-    pod_gen FirebaseRemoteConfig.podspec --platforms="${gen_platform}"
-
-    # Add GoogleService-Info.plist to generated Test Wrapper App.
-    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfig/Pods/Pods.xcodeproj \
-      AppHost-FirebaseRemoteConfig-Unit-Tests \
-      ../../../FirebaseRemoteConfig/Tests/FakeUtils/GoogleService-Info.plist
+    pod_gen FirebaseRemoteConfigSwift.podspec --platforms="${gen_platform}"
 
     RunXcodebuild \
-      -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \
-      -scheme "FirebaseRemoteConfig-Unit-fake-console-tests" \
+      -workspace 'gen/FirebaseRemoteConfigSwift/FirebaseRemoteConfigSwift.xcworkspace' \
+      -scheme "FirebaseRemoteConfigSwift-Unit-fake-console-tests" \
       "${xcb_flags[@]}" \
       build \
       test
     ;;
 
   RemoteConfig-*-integration)
-    pod_gen FirebaseRemoteConfig.podspec --platforms="${gen_platform}"
+    pod_gen FirebaseRemoteConfigSwift.podspec --platforms="${gen_platform}"
 
     # Add GoogleService-Info.plist to generated Test Wrapper App.
-    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfig/Pods/Pods.xcodeproj \
-      AppHost-FirebaseRemoteConfig-Unit-Tests \
-      ../../../FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist
+    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfigSwift/Pods/Pods.xcodeproj \
+      AppHost-FirebaseRemoteConfigSwift-Unit-Tests \
+      ../../../FirebaseRemoteConfigSwift/Tests/SwiftAPI/GoogleService-Info.plist
 
     # Add AccessToken to generated Test Wrapper App.
-    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfig/Pods/Pods.xcodeproj \
-      AppHost-FirebaseRemoteConfig-Unit-Tests \
-      ../../../FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json
+    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfigSwift/Pods/Pods.xcodeproj \
+      AppHost-FirebaseRemoteConfigSwift-Unit-Tests \
+      ../../../FirebaseRemoteConfigSwift/Tests/AccessToken.json
 
     RunXcodebuild \
-      -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \
-      -scheme "FirebaseRemoteConfig-Unit-swift-api-tests" \
+      -workspace 'gen/FirebaseRemoteConfigSwift/FirebaseRemoteConfigSwift.xcworkspace' \
+      -scheme "FirebaseRemoteConfigSwift-Unit-swift-api-tests" \
       "${xcb_flags[@]}" \
       build \
       test
diff --git a/scripts/health_metrics/file_patterns.json b/scripts/health_metrics/file_patterns.json
index ad57963df40..0aab11a0bb7 100644
--- a/scripts/health_metrics/file_patterns.json
+++ b/scripts/health_metrics/file_patterns.json
@@ -171,7 +171,7 @@
   },
   {
     "sdk": "remoteconfig",
-    "podspecs": ["FirebaseRemoteConfig.podspec"],
+    "podspecs": ["FirebaseRemoteConfig.podspec", "FirebaseRemoteConfigSwift.podspec"],
     "filePatterns": [
       "^FirebaseRemoteConfig.*",
       "Interop/Analytics/Public/[^/]+\\.h",


Test patch:
diff --git a/scripts/spm_test_schemes/RemoteConfigFakeConsole.xcscheme b/scripts/spm_test_schemes/RemoteConfigFakeConsole.xcscheme
new file mode 100644
index 00000000000..c05be8c9294
--- /dev/null
+++ b/scripts/spm_test_schemes/RemoteConfigFakeConsole.xcscheme
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1320"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "RemoteConfigFakeConsole"
+               BuildableName = "RemoteConfigFakeConsole"
+               BlueprintName = "RemoteConfigFakeConsole"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "RemoteConfigFakeConsole"
+               BuildableName = "RemoteConfigFakeConsole"
+               BlueprintName = "RemoteConfigFakeConsole"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "RemoteConfigFakeConsole"
+            BuildableName = "RemoteConfigFakeConsole"
+            BlueprintName = "RemoteConfigFakeConsole"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>