Skip to content

Commit

Permalink
Merge branch 'combine-main'
Browse files Browse the repository at this point in the history
  • Loading branch information
peterfriese committed Apr 19, 2021
2 parents 8652585 + a284f94 commit 78a82e3
Show file tree
Hide file tree
Showing 60 changed files with 7,332 additions and 20 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/combine.yml
@@ -0,0 +1,53 @@
# 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.

name: combine

on:
pull_request:
paths:
# Combine sources
- 'FirebaseCombineSwift/**'

# Podspec
- 'FirebaseCombineSwift.podspec'

# This workflow
- '.github/workflows/combine.yml'

# Rebuild on Ruby infrastructure changes.
- 'Gemfile'

schedule:
# Run every day at 11pm (PST) - cron uses UTC times
- cron: '0 7 * * *'

jobs:
xcodebuild:
# Don't run on private repo unless it is a PR.
if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'
runs-on: macos-latest

strategy:
matrix:
target: [iOS]

steps:
- uses: actions/checkout@v2

- name: Setup build
run: scripts/install_prereqs.sh CombineSwift ${{ matrix.target }} xcodebuild

- name: Build and test
run: scripts/third_party/travis/retry.sh scripts/build.sh CombineSwift ${{ matrix.target }} xcodebuild
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FirestoreTestingSupportTests"
BuildableName = "FirestoreTestingSupportTests"
BlueprintName = "FirestoreTestingSupportTests"
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">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
28 changes: 13 additions & 15 deletions FirebaseAuth/Tests/Sample/SwiftApiTests/Credentials.swift
@@ -1,18 +1,16 @@
/*
* 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.
*/
// 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 Foundation

Expand Down
57 changes: 57 additions & 0 deletions FirebaseAuthTestingSupport.podspec
@@ -0,0 +1,57 @@
Pod::Spec.new do |s|
s.name = 'FirebaseAuthTestingSupport'
s.version = '1.0.0'
s.summary = 'Firebase SDKs testing support types and utilities.'

s.description = <<-DESC
Type declarations and utilities needed for unit testing of the code dependent on Firebase SDKs
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
}

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.requires_arc = true

base_dir = 'FirebaseTestingSupport/Auth/'

s.source_files = [
base_dir + 'Sources/**/*.{m,mm,h}',
]

s.public_header_files = base_dir + '**/*.h'

s.dependency 'FirebaseAuth', '~> 7.7'

s.pod_target_xcconfig = {
'GCC_C_LANGUAGE_STANDARD' => 'c99',
'OTHER_CFLAGS' => '-fno-autolink',
'HEADER_SEARCH_PATHS' =>
'"${PODS_TARGET_SRCROOT}" '
}

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 = [
base_dir + 'Tests/**/*.swift'
]
end
end
85 changes: 85 additions & 0 deletions FirebaseCombineSwift.podspec
@@ -0,0 +1,85 @@
Pod::Spec.new do |s|
s.name = 'FirebaseCombineSwift'
s.version = '7.6.0'
s.summary = 'Swift extensions with Combine support for Firebase'

s.description = <<-DESC
Combine Publishers for Firebase.
DESC

s.homepage = 'https://firebase.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.social_media_url = 'https://twitter.com/Firebase'

s.swift_version = '5.0'

ios_deployment_target = '13.0'
osx_deployment_target = '10.15'
tvos_deployment_target = '13.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

source = 'FirebaseCombineSwift/Sources/'
s.exclude_files = [
source + 'Core/**/*.swift',
]
s.source_files = [
source + '**/*.swift',
source + '**/*.m',
]
s.public_header_files = [
source + '**/*.h',
]

s.framework = 'Foundation'
s.ios.framework = 'UIKit'
s.osx.framework = 'AppKit'
s.tvos.framework = 'UIKit'

s.dependency 'FirebaseCore', '~> 7.6'
s.dependency 'FirebaseAuth', '~> 7.6'
s.dependency 'FirebaseFunctions', '~> 7.6'
s.dependency 'FirebaseStorage', '~> 7.6'
s.dependency 'FirebaseStorageSwift', '~> 7.6-beta'

s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"',
}

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 = [
'FirebaseCombineSwift/Tests/Unit/**/*.swift',
'FirebaseCombineSwift/Tests/Unit/**/*.h',
'SharedTestUtilities/FIROptionsMock.[mh]',
'SharedTestUtilities/FIRComponentTestUtilities.[mh]',
]
unit_tests.exclude_files = 'FirebaseCombineSwift/Tests/Unit/**/*Template.swift'
unit_tests.requires_app_host = true
unit_tests.pod_target_xcconfig = {
'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseCombineSwift/Tests/Unit/FirebaseCombine-unit-Bridging-Header.h'
}
unit_tests.dependency 'OCMock'
unit_tests.dependency 'FirebaseAuthTestingSupport'
unit_tests.dependency 'FirebaseFunctionsTestingSupport'
end
end
5 changes: 5 additions & 0 deletions FirebaseCombineSwift/CHANGELOG.md
@@ -0,0 +1,5 @@
# Unreleased

- [feature] Added Combine support for Cloud Functions for Firebase
- [feature] WIP: Added Combine support for Firebase Auth
- [feature] WIP: Added Combine support for Firebase Storage
62 changes: 62 additions & 0 deletions FirebaseCombineSwift/DECISIONS.md
@@ -0,0 +1,62 @@
# Decisions

This file documents some of the decisions we made when developing Combine support for Firebase.

# Module structure

## Discussion
The general idea is to keep all Combine-related code in a separate module (`FirebaseCombineSwift`, to match the naming scheme used for `FirebaseFirestoreSwift` and `FirebaseStorageSwift`).

By using the `#if canImport(moduleName)` directive, we can make sure to only enable the publishers for a module that developers have imported into a build target.


# Implementing Publishers

## Custom Publishers vs. wrapping in Futures / using PassthroughSubject

Instead of implementing [custom publishers](https://thoughtbot.com/blog/lets-build-a-custom-publisher-in-combine), which [Apple discourages developers from doing](https://developer.apple.com/documentation/combine/publisher), we make use of [`PassthroughSubject`](https://developer.apple.com/documentation/combine/passthroughsubject) (for publishers that emit a stream of events), and [`Future`](https://developer.apple.com/documentation/combine/future) for one-shot calls that produce a single value.

## Using capture lists

After discussing internally, we came to the conclusion that the outer closure in the following piece of code is non-escaping, hence there is no benefit to weakly capture `self`. As the inner closure does't refer to `self`, the reference does not outlive the current call stack.

It is thus safe to not use `[weak self]` in this instance.

```swift
extension Auth {
public func createUser(withEmail email: String,
password: String) -> Future<AuthDataResult, Error> {
Future<AuthDataResult, Error> { /* [weak self] <-- not required */ promise in
self?.createUser(withEmail: email, password: password) { authDataResult, error in
if let error = error {
promise(.failure(error))
} else if let authDataResult = authDataResult {
promise(.success(authDataResult))
}
}
}
}
}
```

# Method naming

## Discussion
* Methods that might send a **stream of events** over time will receive a `Publisher` suffix, in line with Apple's own APIs. Any `add` prefix will be removed. This helps to clarify that the user is not _adding_ something that they will have to remove later on ([as is required](https://firebase.google.com/docs/auth/ios/start#listen_for_authentication_state) in most of Firebase's existing APIs). Instead, the result of the publisher needs to be handled just like any other publisher (i.e. be kept in a set of `Cancellable`s).

Examples:
* `addStateDidChangeListener` -> `authStateDidChangePublisher`
* `addSnapshotListener` -> `snapshotPublisher`

* Methods that **return a result once** will not receive a suffix. This effectively means that these methods are overloads to their existing counterparts that take a closure. To silence any `Result of call to xzy is unused` warnings, these methods need to be prefixed with `@discardableresult`. This shouldn't be a problem, since the Future that is created inside those functions is called immediately and will be disposed of by the runtime upon returning from the inner closure.

Examples:
* `signIn` -> `signIn`
* `createUser` -> `createUser`

## Options considered
Using the same method and parameter names for one-shot asynchronous methods results in both methods to be shown in close proximity when invoking code completion

![image](https://user-images.githubusercontent.com/232107/99672274-76f05680-2a73-11eb-880a-3563f293de7d.png)

To achieve the same for methods that return a stream of events, we'd have to name those `addXzyListener`. This would be in contrast to Apple's naming scheme (e.g. `dataTask(with:completionHandler)` -> `dataTaskPublisher(for:)`
37 changes: 37 additions & 0 deletions FirebaseCombineSwift/DEVELOPING.md
@@ -0,0 +1,37 @@
# Developing

This is a quick overview to help you get started contributing to Firebase Combine.

## Prerequisites

* Xcode 12.x (or later)
* CocoaPods 1.10.x (or later)
* [CocoaPods Generate](https://github.com/square/cocoapods-generate)

## Setting up your development environment

* Check out firebase-ios-sdk
* Install utilities

```bash
$ ./scripts/setup_check.sh
$ ./scripts/setup_bundler.sh
```

## Generating the development project

For developing _Firebase Combine_, you'll need a development project that imports the relevant pods.

Run the following command to generate and open the development project:

```bash
$ pod gen FirebaseCombineSwift.podspec --local-sources=./ --auto-open --platforms=ios
```

## Checking in code

Before checking in your code, make sure to check your code against the coding styleguide by running the following command:

```bash
$ ./scripts/check.sh --allow-dirty
```

0 comments on commit 78a82e3

Please sign in to comment.